mirror of
https://github.com/zulip/zulip.git
synced 2025-11-21 15:09:34 +00:00
markdown: Add support for inline video thumbnails.
This commit is contained in:
@@ -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
|
||||
|
||||
4
web/images/play_button.svg
Normal file
4
web/images/play_button.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="16" cy="16" r="13" fill="black" fill-opacity="0.3"/>
|
||||
<path d="M13.9334 20.9332L20.6667 16.6332C20.9112 16.4776 21.0334 16.2665 21.0334 15.9998C21.0334 15.7332 20.9112 15.5221 20.6667 15.3665L13.9334 11.0665C13.689 10.8887 13.4334 10.8721 13.1667 11.0165C12.9001 11.1609 12.7667 11.3887 12.7667 11.6998V20.2998C12.7667 20.6109 12.9001 20.8387 13.1667 20.9832C13.4334 21.1276 13.689 21.1109 13.9334 20.9332ZM16.0001 29.3332C14.1779 29.3332 12.4556 28.9832 10.8334 28.2832C9.21119 27.5832 7.79453 26.6276 6.58341 25.4165C5.3723 24.2054 4.41675 22.7887 3.71675 21.1665C3.01675 19.5443 2.66675 17.8221 2.66675 15.9998C2.66675 14.1554 3.01675 12.4221 3.71675 10.7998C4.41675 9.17762 5.3723 7.7665 6.58341 6.5665C7.79453 5.3665 9.21119 4.4165 10.8334 3.7165C12.4556 3.0165 14.1779 2.6665 16.0001 2.6665C17.8445 2.6665 19.5779 3.0165 21.2001 3.7165C22.8223 4.4165 24.2334 5.3665 25.4334 6.5665C26.6334 7.7665 27.5834 9.17762 28.2834 10.7998C28.9834 12.4221 29.3334 14.1554 29.3334 15.9998C29.3334 17.8221 28.9834 19.5443 28.2834 21.1665C27.5834 22.7887 26.6334 24.2054 25.4334 25.4165C24.2334 26.6276 22.8223 27.5832 21.2001 28.2832C19.5779 28.9832 17.8445 29.3332 16.0001 29.3332ZM16.0001 27.3332C19.1556 27.3332 21.8334 26.2276 24.0334 24.0165C26.2334 21.8054 27.3334 19.1332 27.3334 15.9998C27.3334 12.8443 26.2334 10.1665 24.0334 7.9665C21.8334 5.7665 19.1556 4.6665 16.0001 4.6665C12.8667 4.6665 10.1945 5.7665 7.98341 7.9665C5.7723 10.1665 4.66675 12.8443 4.66675 15.9998C4.66675 19.1332 5.7723 21.8054 7.98341 24.0165C10.1945 26.2276 12.8667 27.3332 16.0001 27.3332Z" fill="white"/>
|
||||
</svg>
|
||||
|
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:
|
||||
|
||||
23
zerver/tests/fixtures/markdown_test_cases.json
vendored
23
zerver/tests/fixtures/markdown_test_cases.json
vendored
@@ -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>",
|
||||
|
||||
Reference in New Issue
Block a user