mirror of https://github.com/zulip/zulip.git
markdown: Add support for inline video thumbnails.
This commit is contained in:
parent
cb1de9092d
commit
8ef52d55d3
|
@ -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
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>",
|
||||
|
|
Loading…
Reference in New Issue