aboutsummaryrefslogtreecommitdiff
path: root/lib/classes
diff options
context:
space:
mode:
authorRami Jasim <minecraftmrgold@gmail.com>2025-10-14 14:08:57 +0000
committerJan-Hendrik Willms <tleilax+studip@gmail.com>2025-10-14 16:08:57 +0200
commitf65843128420a4da2ebbaf1f5a8958bfac97166e (patch)
tree7a72538a33637f3ced32b4c7493cf9c6e43e7439 /lib/classes
parent849ed9c3e73d16296ec45b631fa7cd48be449719 (diff)
Resolve "CKEditor: Media support aktivieren"
Closes #5651 Merge request studip/studip!4471
Diffstat (limited to 'lib/classes')
-rw-r--r--lib/classes/Markup.php26
-rw-r--r--lib/classes/htmlpurifier/HTMLPurifier_Injector_OembedToIframe.php88
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;
+ }
+}