markdown: Add support for inline video thumbnails.

This commit is contained in:
Aman Agrawal 2022-08-09 09:30:16 +00:00 committed by Tim Abbott
parent cb1de9092d
commit 8ef52d55d3
7 changed files with 138 additions and 15 deletions

View File

@ -1,5 +1,5 @@
[program:go-camo]
command=/usr/local/bin/secret-env-wrapper GOCAMO_HMAC=camo_key <%= @bin %> --listen=<%= @listen_address %>:9292 -H "Strict-Transport-Security: max-age=15768000" -H "X-Frame-Options: DENY" --metrics --verbose
command=/usr/local/bin/secret-env-wrapper GOCAMO_HMAC=camo_key <%= @bin %> --listen=<%= @listen_address %>:9292 -H "Strict-Transport-Security: max-age=15768000" -H "X-Frame-Options: DENY" --metrics --verbose --allow-content-video
environment=HTTP_PROXY="<%= @proxy %>",HTTPS_PROXY="<%= @proxy %>"
priority=15
autostart=true

BIN
web/images/play_button.svg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -118,8 +118,13 @@ export function initialize() {
return true;
}
// Inline image and twitter previews.
if ($target.is("img.message_inline_image") || $target.is("img.twitter-avatar")) {
// Inline image, video and twitter previews.
if (
$target.is("img.message_inline_image") ||
$target.is("video") ||
$target.is(".message_inline_video") ||
$target.is("img.twitter-avatar")
) {
return true;
}

View File

@ -16,10 +16,10 @@ import marked from "../third/marked/lib/marked";
// If we see preview-related syntax in our content, we will need the
// backend to render it.
const preview_regexes = [
// Inline image previews, check for contiguous chars ending in image suffix
// Inline image and video previews, check for contiguous chars ending in image and video suffix
// To keep the below regexes simple, split them out for the end-of-message case
/\S*(?:\.bmp|\.gif|\.jpg|\.jpeg|\.png|\.webp)\)?(\s+|$)/m,
/\S*(?:\.bmp|\.gif|\.jpg|\.jpeg|\.png|\.webp|\.mp4|\.webm)\)?(\s+|$)/m,
// Twitter and youtube links are given previews

View File

@ -525,6 +525,37 @@
float: none;
}
.message_inline_video {
&:hover {
&::after {
transform: scale(1);
transition: transform 0.2s;
}
}
&::after {
content: "";
background-image: url("../images/play_button.svg");
display: block;
width: 32px;
height: 32px;
position: absolute;
/* video width (100px) / 2 - icon width (32px) / 2 */
top: 34px;
/* video height (150px) / 2 - icon height (32px) / 2 */
left: 59px;
border-radius: 100%;
transform: scale(0.8);
}
& video {
display: block;
object-fit: contain;
height: 100%;
width: 100%;
}
}
.youtube-video .fa-play::before,
.embed-video .fa-play::before {
position: absolute;

View File

@ -4,6 +4,7 @@ import cgi
import datetime
import html
import logging
import mimetypes
import re
import time
import urllib
@ -535,6 +536,40 @@ class InlineImageProcessor(markdown.treeprocessors.Treeprocessor):
img.set("src", get_camo_url(url))
class InlineVideoProcessor(markdown.treeprocessors.Treeprocessor):
"""
Rewrite inline video tags to serve external content via Camo.
This rewrites all video, except ones that are served from the current
realm or global STATIC_URL. This is to ensure that each realm only loads
videos that are hosted on that realm or by the global installation,
avoiding information leakage to external domains or between realms. We need
to disable proxying of videos hosted on the same realm, because otherwise
we will break videos in /user_uploads/, which require authorization to
view.
"""
def __init__(self, zmd: "ZulipMarkdown") -> None:
super().__init__(zmd)
self.zmd = zmd
def run(self, root: Element) -> None:
# Get all URLs from the blob
found_videos = walk_tree(root, lambda e: e if e.tag == "video" else None)
for video in found_videos:
url = video.get("src")
assert url is not None
if is_static_or_current_realm_url(url, self.zmd.zulip_realm):
# Don't rewrite videos on our own site (e.g. user uploads).
continue
# Pass down both camo generated URL and the original video URL to the client.
# Camo URL is only used to generate preview of the video. When user plays the
# video, we switch to the source url to fetch the video. This allows playing
# the video with no load on our servers.
video.set("src", get_camo_url(url))
video.set("data-video-original-url", url)
class BacktickInlineProcessor(markdown.inlinepatterns.BacktickInlineProcessor):
"""Return a `<code>` element containing the matching text."""
@ -1155,6 +1190,50 @@ class InlineInterestingLinkProcessor(markdown.treeprocessors.Treeprocessor):
if uncle_link.attrib["href"] not in parent_links:
return insertion_index
def is_video(self, url: str) -> bool:
url_type = mimetypes.guess_type(url)[0]
# Support only video formats (containers) that are supported cross-browser and cross-device. As per
# https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers#index_of_media_container_formats_file_types
# MP4 and WebM are the only formats that are widely supported.
supported_mimetypes = ["video/mp4", "video/webm"]
return url_type in supported_mimetypes
def add_video(
self,
root: Element,
url: str,
title: Optional[str],
class_attr: str = "message_inline_image message_inline_video",
insertion_index: Optional[int] = None,
) -> None:
if insertion_index is not None:
div = Element("div")
root.insert(insertion_index, div)
else:
div = SubElement(root, "div")
div.set("class", class_attr)
# Add `a` tag so that the syntax of video matches with
# other media types and clients don't get confused.
a = SubElement(div, "a")
a.set("href", url)
if title:
a.set("title", title)
video = SubElement(a, "video")
video.set("src", url)
video.set("preload", "metadata")
def handle_video_inlining(
self, root: Element, found_url: ResultWithFamily[Tuple[str, Optional[str]]]
) -> None:
info = self.get_inlining_information(root, found_url)
url = found_url.result[0]
self.add_video(info["parent"], url, info["title"], insertion_index=info["index"])
if info["remove"] is not None:
info["parent"].remove(info["remove"])
def run(self, root: Element) -> None:
# Get all URLs from the blob
found_urls = walk_tree_with_family(root, self.get_url_data)
@ -1207,6 +1286,10 @@ class InlineInterestingLinkProcessor(markdown.treeprocessors.Treeprocessor):
else:
continue
if self.is_video(url):
self.handle_video_inlining(root, found_url)
continue
dropbox_image = self.dropbox_image(url)
if dropbox_image is not None:
class_attr = "message_inline_ref"
@ -2229,6 +2312,7 @@ class ZulipMarkdown(markdown.Markdown):
)
if settings.CAMO_URI:
treeprocessors.register(InlineImageProcessor(self), "rewrite_images_proxy", 10)
treeprocessors.register(InlineVideoProcessor(self), "rewrite_videos_proxy", 10)
return treeprocessors
def build_postprocessors(self) -> markdown.util.Registry:

View File

@ -458,6 +458,19 @@
"input": "https://github.com",
"expected_output": "<p><a href=\"https://github.com\">https://github.com</a></p>"
},
{
"name": "only_inline_video",
"input": "https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_480_1_5MG.mp4",
"expected_output": "<div class=\"message_inline_image message_inline_video\"><a href=\"https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_480_1_5MG.mp4\"><video data-video-original-url=\"https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_480_1_5MG.mp4\" preload=\"metadata\" src=\"https://external-content.zulipcdn.net/external_content/785b3eb6f6165491371895ba42ead0e3661ae44b/68747470733a2f2f66696c652d6578616d706c65732d636f6d2e6769746875622e696f2f75706c6f6164732f323031372f30342f66696c655f6578616d706c655f4d50345f3438305f315f354d472e6d7034\"></video></a></div>",
"backend_only_rendering": true
},
{
"name": "only_named_inline_video",
"input": "[Google link](https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_480_1_5MG.mp4)",
"expected_output": "<p><a href=\"https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_480_1_5MG.mp4\">Google link</a></p>\n<div class=\"message_inline_image message_inline_video\"><a href=\"https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_480_1_5MG.mp4\" title=\"Google link\"><video data-video-original-url=\"https://file-examples-com.github.io/uploads/2017/04/file_example_MP4_480_1_5MG.mp4\" preload=\"metadata\" src=\"https://external-content.zulipcdn.net/external_content/785b3eb6f6165491371895ba42ead0e3661ae44b/68747470733a2f2f66696c652d6578616d706c65732d636f6d2e6769746875622e696f2f75706c6f6164732f323031372f30342f66696c655f6578616d706c655f4d50345f3438305f315f354d472e6d7034\"></video></a></div>",
"backend_only_rendering": true,
"text_content": "Google link\n"
},
{
"name": "link_with_text",
"input": "[hello](https://github.com)",
@ -1186,11 +1199,6 @@
"<p>%s</p>",
"http://fr.wikipedia.org/wiki/Fichier:SMirC-facepalm.svg"
],
[
"https://en.wikipedia.org/wiki/File:Methamphetamine_from_ephedrine_with_HI_en.mov",
"<p>%s</p>",
"https://en.wikipedia.org/wiki/File:Methamphetamine_from_ephedrine_with_HI_en.mov"
],
[
"https://jira.atlassian.com/browse/JRA-31953?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel",
"<p>%s</p>",
@ -1321,11 +1329,6 @@
"<p>hash it %s</p>",
"http://foo.com/blah_(wikipedia)_blah#cite-1"
],
[
"http://technet.microsoft.com/en-us/library/Cc751099.rk20_25_big(l=en-us).mov",
"<p>%s</p>",
"http://technet.microsoft.com/en-us/library/Cc751099.rk20_25_big(l=en-us).mov"
],
[
"https://metacpan.org/module/Image::Resize::OpenCV",
"<p>%s</p>",