diff options
| author | Rami Jasim <minecraftmrgold@gmail.com> | 2025-10-14 14:08:57 +0000 |
|---|---|---|
| committer | Jan-Hendrik Willms <tleilax+studip@gmail.com> | 2025-10-14 16:08:57 +0200 |
| commit | f65843128420a4da2ebbaf1f5a8958bfac97166e (patch) | |
| tree | 7a72538a33637f3ced32b4c7493cf9c6e43e7439 /lib/classes | |
| parent | 849ed9c3e73d16296ec45b631fa7cd48be449719 (diff) | |
Resolve "CKEditor: Media support aktivieren"
Closes #5651
Merge request studip/studip!4471
Diffstat (limited to 'lib/classes')
| -rw-r--r-- | lib/classes/Markup.php | 26 | ||||
| -rw-r--r-- | lib/classes/htmlpurifier/HTMLPurifier_Injector_OembedToIframe.php | 88 |
2 files changed, 110 insertions, 4 deletions
diff --git a/lib/classes/Markup.php b/lib/classes/Markup.php index b4d1241..f018e57 100644 --- a/lib/classes/Markup.php +++ b/lib/classes/Markup.php @@ -23,6 +23,7 @@ namespace Studip; require_once __DIR__ . '/htmlpurifier/HTMLPurifier_Injector_ClassifyLinks.php'; require_once __DIR__ . '/htmlpurifier/HTMLPurifier_Injector_ClassifyTables.php'; require_once __DIR__ . '/htmlpurifier/HTMLPurifier_Injector_LinkifyEmail.php'; +require_once __DIR__ . '/htmlpurifier/HTMLPurifier_Injector_OembedToIframe.php'; require_once __DIR__ . '/htmlpurifier/HTMLPurifier_Injector_TransformLinks.php'; require_once __DIR__ . '/htmlpurifier/HTMLPurifier_Injector_Unlinkify.php'; @@ -273,7 +274,7 @@ class Markup br caption code[class] - div[class|style] + div[class|style|data-oembed-url] em figure[class|style] figcaption @@ -285,8 +286,10 @@ class Markup h6 hr i + iframe[src|class] img[alt|src|height|width|class|style] li + oembed[url] ol[reversed|start|style] p[style] pre[class] @@ -314,6 +317,8 @@ class Markup $config->set('Attr.EnableID', true); $config->set('Attr.AllowedClasses', [ 'author', + 'ckeditor-embed', + 'ckeditor-embed-container', 'content', 'image', 'image-style-side', @@ -333,6 +338,7 @@ class Markup 'link-extern', 'link-intern', 'math-tex', + 'media', 'table', 'usercode', 'wiki-link' @@ -354,7 +360,7 @@ class Markup 'border-style', 'float', 'border', - 'vertical-align' + 'vertical-align', ]); $config->set('CSS.MaxImgLength', null); @@ -363,12 +369,18 @@ class Markup $config->set('AutoFormat.Custom', [ 'ClassifyLinks', 'ClassifyTables', - 'LinkifyEmail' + 'LinkifyEmail', ]); $config->set('AutoFormat.RemoveSpansWithoutAttributes', true); } else { - $config->set('AutoFormat.Custom', ['TransformLinks']); + $config->set('AutoFormat.Custom', [ + 'TransformLinks', + 'OembedToIframe' + ]); } + // is needed for ckeditor mediaEmbed + $config->set('HTML.SafeIframe', true); + $config->set('URI.SafeIframeRegexp', '#^https?://(www\.)?youtube\.com/embed/#'); // avoid <img src="evil_CSRF_stuff"> $def = $config->getHTMLDefinition(true); @@ -391,10 +403,16 @@ class Markup $def->addElement('figcaption', 'Inline', 'Flow', 'Common'); $def->addElement('figure', 'Block', 'Optional: (figcaption, Flow) | (Flow, figcaption) | Flow', 'Common'); + $def->addElement('oembed', 'Block', 'Flow', 'Common', [ + 'url' => 'URI' + ]); $def->addAttribute('ol', 'reversed', 'Bool'); $def->addAttribute('ol', 'style', 'Text'); $def->addAttribute('ul', 'style', 'Text'); + // is needed for ckeditor mediaEmbed + $def->addAttribute('div', 'data-oembed-url', 'URI'); + $def->addAttribute('iframe', 'class', 'Text'); return new \HTMLPurifier($config); } diff --git a/lib/classes/htmlpurifier/HTMLPurifier_Injector_OembedToIframe.php b/lib/classes/htmlpurifier/HTMLPurifier_Injector_OembedToIframe.php new file mode 100644 index 0000000..501fbad --- /dev/null +++ b/lib/classes/htmlpurifier/HTMLPurifier_Injector_OembedToIframe.php @@ -0,0 +1,88 @@ +<?php + +/** + * Converts oembed tags to embedded content like the ckeditor does in its `previewInData` feature. + * But we only store the oembed into the database and perform the transformation here (with purify) + * + * Basically replaced <oembed url="..."></oembed> with a styled iframe. + * Currently only Youtube embed are supported + * TODO CKEditor also supports several other embed, which could also be implemented + */ +class HTMLPurifier_Injector_OembedToIframe extends HTMLPurifier_Injector +{ + public $name = 'OEmbed'; + public $needed = ['div', 'iframe']; + + /** @var array[] mapping the corresponding embed url to all their valid urls */ + const VALID_URLS = [ + 'https://www.youtube.com/embed/' => [ + // See https://github.com/ckeditor/ckeditor5/blob/master/packages/ckeditor5-media-embed/src/mediaembedediting.ts#L95 + '/^(?:m\.)?youtube\.com\/watch\?v=([\w-]+)(?:&t=(\d+))?/', + '/^(?:m\.)?youtube\.com\/shorts\/([\w-]+)(?:\?t=(\d+))?/', + '/^(?:m\.)?youtube\.com\/v\/([\w-]+)(?:\?t=(\d+))?/', + '/^youtube\.com\/embed\/([\w-]+)(?:\?start=(\d+))?/', + '/^youtu\.be\/([\w-]+)(?:\?t=(\d+))?/' + ] + ]; + + public function handleElement(&$token) + { + if ($token->name !== 'oembed') { + return; + } + + $url = $token->attr['url'] ?? ''; + $embedUrl = self::toEmbedUrl($url); + if (!$embedUrl) { + $token = false; + return; + } + + // See https://github.com/ckeditor/ckeditor5/blob/master/packages/ckeditor5-media-embed/src/mediaembedediting.ts#L108 + // The css is in content.scss and matches the css of ckeditor5 + $token = [ + new HTMLPurifier_Token_Start('div', [ + 'data-oembed-url' => $embedUrl + ]), + new HTMLPurifier_Token_Start('div', [ + 'class' => 'ckeditor-embed-container' + ]), + new HTMLPurifier_Token_Empty('iframe', [ + 'src' => $embedUrl, + 'class' => 'ckeditor-embed', + ]), + new HTMLPurifier_Token_End('div'), + new HTMLPurifier_Token_End('div'), + ]; + } + + /** + * Transforms a valid url into a embed url + * @param string $url + * @return string|null + */ + private static function toEmbedUrl(string $url): ?string + { + if (empty($url)) { + return null; + } + $cleanUrl = preg_replace('/^https?:\/\/(?:www\.)?/', '', $url); + foreach (self::VALID_URLS as $embedUrl => $patterns) { + foreach ($patterns as $pattern) { + if (preg_match($pattern, $cleanUrl, $matches)) { + $videoId = $matches[1]; + $timestamp = !empty($matches[2]) ? $matches[2] : null; + + $full_url = $embedUrl . $videoId; + if ($timestamp) { + $full_url .= '?start=' . $timestamp; + } + + return $full_url; + } + } + } + + return null; + } +} |
