From f65843128420a4da2ebbaf1f5a8958bfac97166e Mon Sep 17 00:00:00 2001 From: Rami Jasim Date: Tue, 14 Oct 2025 14:08:57 +0000 Subject: Resolve "CKEditor: Media support aktivieren" Closes #5651 Merge request studip/studip!4471 --- lib/classes/Markup.php | 26 ++++++- .../HTMLPurifier_Injector_OembedToIframe.php | 88 ++++++++++++++++++++++ .../assets/javascripts/cke/builtin-plugins.js | 2 + resources/assets/javascripts/cke/classic-editor.js | 5 ++ resources/assets/stylesheets/scss/content.scss | 16 ++++ 5 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 lib/classes/htmlpurifier/HTMLPurifier_Injector_OembedToIframe.php 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 $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 @@ + 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; + } +} diff --git a/resources/assets/javascripts/cke/builtin-plugins.js b/resources/assets/javascripts/cke/builtin-plugins.js index 56b8c38..3fa6ad9 100644 --- a/resources/assets/javascripts/cke/builtin-plugins.js +++ b/resources/assets/javascripts/cke/builtin-plugins.js @@ -23,6 +23,7 @@ import IndentBlock from '@ckeditor/ckeditor5-indent/src/indentblock'; import ItalicPlugin from '@ckeditor/ckeditor5-basic-styles/src/italic'; import LinkPlugin from '@ckeditor/ckeditor5-link/src/link'; import ListProperties from '@ckeditor/ckeditor5-list/src/listproperties'; +import MediaEmbed from '@ckeditor/ckeditor5-media-embed/src/mediaembed'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import RemoveFormat from '@ckeditor/ckeditor5-remove-format/src/removeformat.js'; import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting'; @@ -77,6 +78,7 @@ const builtinPlugins = [ LinkPlugin, ListProperties, Mathematics, + MediaEmbed, Paragraph, RemoveFormat, SourceEditing, diff --git a/resources/assets/javascripts/cke/classic-editor.js b/resources/assets/javascripts/cke/classic-editor.js index 09369f6..7b85736 100644 --- a/resources/assets/javascripts/cke/classic-editor.js +++ b/resources/assets/javascripts/cke/classic-editor.js @@ -9,6 +9,10 @@ export { createClassicEditorFromTextarea }; ClassicEditor.builtinPlugins = builtinPlugins; ClassicEditor.defaultConfig = { ...defaultConfig, + mediaEmbed: { + // Only allow youtube for now + removeProviders: ['dailymotion', 'vimeo', 'spotify', 'instagram', 'twitter', 'googleMaps', 'flickr', 'facebook' ] + }, toolbar: { items: [ 'undo', @@ -39,6 +43,7 @@ ClassicEditor.defaultConfig = { 'alignment:justify', '|', 'link', + 'mediaEmbed', 'insertTable', 'uploadImage', 'codeBlock', diff --git a/resources/assets/stylesheets/scss/content.scss b/resources/assets/stylesheets/scss/content.scss index 9ccccb2..2e319a4 100644 --- a/resources/assets/stylesheets/scss/content.scss +++ b/resources/assets/stylesheets/scss/content.scss @@ -56,3 +56,19 @@ margin-bottom: 0; } } + +.ck-content .ckeditor-embed-container { + position: relative; + padding-bottom: 100%; + height: 0; + padding-bottom: 56.2493%; + + .ckeditor-embed { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + } + +} -- cgit v1.0