This commit is contained in:
Markos Gogoulos
2026-02-12 16:33:41 +02:00
parent 4656ba1176
commit c6be65edd6
9 changed files with 76 additions and 69 deletions

View File

@@ -39,38 +39,36 @@ class text_filter extends \core_filters\text_filter {
$pattern_tag = '/\[mediacms:([a-zA-Z0-9]+)\]/';
$newtext = preg_replace_callback($pattern_tag, [$this, 'callback_tag'], $newtext);
// 2. Handle Auto-convert URLs if enabled
if (get_config('filter_mediacms', 'enableautoconvert')) {
// First, protect text-only links from being converted
// by temporarily replacing them with placeholders
$textlink_placeholders = [];
$textlink_pattern = '/<a\s+[^>]*data-mediacms-textlink=["\']true["\'][^>]*>.*?<\/a>/is';
// 2. Auto-convert MediaCMS URLs to embedded players
// First, protect text-only links from being converted
// by temporarily replacing them with placeholders
$textlink_placeholders = [];
$textlink_pattern = '/<a\s+[^>]*data-mediacms-textlink=["\']true["\'][^>]*>.*?<\/a>/is';
$newtext = preg_replace_callback($textlink_pattern, function($matches) use (&$textlink_placeholders) {
$placeholder = '###MEDIACMS_TEXTLINK_' . count($textlink_placeholders) . '###';
$textlink_placeholders[$placeholder] = $matches[0];
return $placeholder;
}, $newtext);
$newtext = preg_replace_callback($textlink_pattern, function($matches) use (&$textlink_placeholders) {
$placeholder = '###MEDIACMS_TEXTLINK_' . count($textlink_placeholders) . '###';
$textlink_placeholders[$placeholder] = $matches[0];
return $placeholder;
}, $newtext);
// Regex for MediaCMS view URLs: https://domain/view?m=TOKEN
// We need to be careful to match the configured domain
$parsed_url = parse_url($mediacmsurl);
$host = preg_quote($parsed_url['host'] ?? '', '/');
$scheme = preg_quote($parsed_url['scheme'] ?? 'https', '/');
// Regex for MediaCMS view URLs: https://domain/view?m=TOKEN
// We need to be careful to match the configured domain
$parsed_url = parse_url($mediacmsurl);
$host = preg_quote($parsed_url['host'] ?? '', '/');
$scheme = preg_quote($parsed_url['scheme'] ?? 'https', '/');
// Allow http or https, and optional path prefix
$path_prefix = preg_quote(rtrim($parsed_url['path'] ?? '', '/'), '/');
// Allow http or https, and optional path prefix
$path_prefix = preg_quote(rtrim($parsed_url['path'] ?? '', '/'), '/');
// Pattern: https://HOST/PREFIX/view?m=TOKEN
// Also handle /embed?m=TOKEN
$pattern_url = '/(' . $scheme . ':\/\/' . $host . $path_prefix . '\/(view|embed)\?m=([a-zA-Z0-9]+)(?:&[^\s<]*)?)/';
// Pattern: https://HOST/PREFIX/view?m=TOKEN
// Also handle /embed?m=TOKEN
$pattern_url = '/(' . $scheme . ':\/\/' . $host . $path_prefix . '\/(view|embed)\?m=([a-zA-Z0-9]+)(?:&[^\s<]*)?)/';
$newtext = preg_replace_callback($pattern_url, [$this, 'callback_url'], $newtext);
$newtext = preg_replace_callback($pattern_url, [$this, 'callback_url'], $newtext);
// Restore protected text-only links
foreach ($textlink_placeholders as $placeholder => $original) {
$newtext = str_replace($placeholder, $original, $newtext);
}
// Restore protected text-only links
foreach ($textlink_placeholders as $placeholder => $original) {
$newtext = str_replace($placeholder, $original, $newtext);
}
return $newtext;
@@ -113,7 +111,7 @@ class text_filter extends \core_filters\text_filter {
parse_str($parsed_url['query'], $query_params);
// Extract embed-related parameters
$supported_params = ['showTitle', 'showRelated', 'showUserAvatar', 'linkTitle', 't'];
$supported_params = ['showTitle', 'showRelated', 'showUserAvatar', 'linkTitle', 't', 'width', 'height'];
foreach ($supported_params as $param) {
if (isset($query_params[$param])) {
$embed_params[$param] = $query_params[$param];
@@ -130,8 +128,9 @@ class text_filter extends \core_filters\text_filter {
private function generate_iframe($token, $embed_params = []) {
global $CFG, $COURSE;
$width = get_config('filter_mediacms', 'iframewidth') ?: 960;
$height = get_config('filter_mediacms', 'iframeheight') ?: 540;
// Use width/height from embed params if provided, otherwise use hardcoded defaults
$width = isset($embed_params['width']) ? $embed_params['width'] : 960;
$height = isset($embed_params['height']) ? $embed_params['height'] : 540;
$courseid = $COURSE->id ?? 0;
// Build launch URL parameters
@@ -142,9 +141,11 @@ class text_filter extends \core_filters\text_filter {
'height' => $height
];
// Add embed parameters if provided
// Add other embed parameters if provided (excluding width/height as they're already set)
foreach ($embed_params as $key => $value) {
$launch_params[$key] = $value;
if ($key !== 'width' && $key !== 'height') {
$launch_params[$key] = $value;
}
}
$launchurl = new moodle_url('/filter/mediacms/launch.php', $launch_params);

View File

@@ -87,14 +87,13 @@ $startTime = optional_param('t', '', PARAM_TEXT);
// Get configuration
$mediacmsurl = get_config('filter_mediacms', 'mediacmsurl');
$ltitoolid = get_config('filter_mediacms', 'ltitoolid');
$defaultwidth = get_config('filter_mediacms', 'iframewidth') ?: 960;
$defaultheight = get_config('filter_mediacms', 'iframeheight') ?: 540;
// Use hardcoded defaults if width/height not provided
if (empty($width)) {
$width = $defaultwidth;
$width = 960;
}
if (empty($height)) {
$height = $defaultheight;
$height = 540;
}
if (empty($mediacmsurl)) {
@@ -213,6 +212,12 @@ if ($linkTitle !== '') {
if ($startTime !== '') {
$hidden_fields .= '<input type="hidden" name="embed_start_time" value="' . htmlspecialchars($startTime, ENT_QUOTES) . '" />';
}
if ($width) {
$hidden_fields .= '<input type="hidden" name="embed_width" value="' . htmlspecialchars($width, ENT_QUOTES) . '" />';
}
if ($height) {
$hidden_fields .= '<input type="hidden" name="embed_height" value="' . htmlspecialchars($height, ENT_QUOTES) . '" />';
}
$content = str_replace('</form>', $hidden_fields . '</form>', $content);

View File

@@ -32,29 +32,4 @@ if ($ADMIN->fulltree) {
0,
$ltioptions
));
// Dimensions
$settings->add(new admin_setting_configtext(
'filter_mediacms/iframewidth',
get_string('iframewidth', 'filter_mediacms'),
get_string('iframewidth_desc', 'filter_mediacms'),
'960',
PARAM_INT
));
$settings->add(new admin_setting_configtext(
'filter_mediacms/iframeheight',
get_string('iframeheight', 'filter_mediacms'),
get_string('iframeheight_desc', 'filter_mediacms'),
'540',
PARAM_INT
));
// Auto-convert
$settings->add(new admin_setting_configcheckbox(
'filter_mediacms/enableautoconvert',
get_string('enableautoconvert', 'filter_mediacms'),
get_string('enableautoconvert_desc', 'filter_mediacms'),
1
));
}

View File

@@ -5,6 +5,6 @@ define("tiny_mediacms/commands",["exports","core/str","./common","./iframeembed"
* @module tiny_mediacms/commands
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getSetup=void 0,_iframeembed=(obj=_iframeembed)&&obj.__esModule?obj:{default:obj};const isIframe=node=>"iframe"===node.nodeName.toLowerCase()||node.classList&&node.classList.contains("tiny-iframe-responsive")||node.classList&&node.classList.contains("tiny-mediacms-iframe-wrapper"),setupIframeOverlays=(editor,handleIframeAction)=>{const processIframes=()=>{const editorBody=editor.getBody();if(!editorBody)return;editorBody.querySelectorAll("iframe").forEach((iframe=>{var _iframe$parentElement;if(null!==(_iframe$parentElement=iframe.parentElement)&&void 0!==_iframe$parentElement&&_iframe$parentElement.classList.contains("tiny-mediacms-iframe-wrapper"))return;if(iframe.hasAttribute("data-mce-object")||iframe.hasAttribute("data-mce-placeholder"))return;const wrapper=editor.getDoc().createElement("div");wrapper.className="tiny-mediacms-iframe-wrapper",wrapper.setAttribute("contenteditable","false");const editBtn=editor.getDoc().createElement("button");editBtn.className="tiny-mediacms-edit-btn",editBtn.setAttribute("type","button"),editBtn.setAttribute("title","Edit media embed options"),editBtn.textContent="EDIT",iframe.parentNode.insertBefore(wrapper,iframe),wrapper.appendChild(iframe),wrapper.appendChild(editBtn)}))},handleOverlayClick=e=>{const editBtn=e.target.closest(".tiny-mediacms-edit-btn");if(!editBtn)return;e.preventDefault(),e.stopPropagation();const wrapper=editBtn.closest(".tiny-mediacms-iframe-wrapper");if(!wrapper)return;wrapper.querySelector("iframe")&&(editor.selection.select(wrapper),handleIframeAction())};editor.on("init",(()=>{(()=>{const editorDoc=editor.getDoc();if(!editorDoc)return;if(editorDoc.getElementById("tiny-mediacms-overlay-styles"))return;const style=editorDoc.createElement("style");style.id="tiny-mediacms-overlay-styles",style.textContent="\n .tiny-mediacms-iframe-wrapper {\n display: inline-block;\n position: relative;\n line-height: 0;\n vertical-align: top;\n margin-top: 40px;\n }\n .tiny-mediacms-iframe-wrapper iframe {\n display: block;\n }\n .tiny-mediacms-edit-btn {\n position: absolute;\n top: -15px;\n left: 50%;\n transform: translateX(-50%);\n background: rgba(0, 0, 0, 0.7);\n color: #ffffff;\n border: none;\n border-radius: 3px;\n cursor: pointer;\n z-index: 10;\n padding: 4px 12px;\n margin: 0;\n font-size: 12px;\n font-weight: bold;\n text-decoration: none;\n box-shadow: 0 2px 4px rgba(0,0,0,0.3);\n transition: background 0.15s, box-shadow 0.15s;\n display: inline-block;\n box-sizing: border-box;\n }\n .tiny-mediacms-edit-btn:hover {\n background: rgba(0, 0, 0, 0.85);\n box-shadow: 0 3px 6px rgba(0,0,0,0.4);\n }\n ",editorDoc.head.appendChild(style)})(),processIframes(),editor.getBody().addEventListener("click",handleOverlayClick)})),editor.on("SetContent",(()=>{processIframes()})),editor.on("PastePostProcess",(()=>{setTimeout(processIframes,100)})),editor.on("Undo Redo",(()=>{processIframes()})),editor.on("Change",(()=>{setTimeout(processIframes,50)})),editor.on("NodeChange",(()=>{processIframes()}))};_exports.getSetup=async()=>{const[iframeButtonText]=await(0,_str.getStrings)(["iframebuttontitle"].map((key=>({key:key,component:_common.component})))),[iframeButtonImage]=await Promise.all([(0,_utils.getButtonImage)("icon",_common.component)]);return editor=>{((editor,iframeButtonText,iframeButtonImage)=>{const handleIframeAction=()=>{new _iframeembed.default(editor).displayDialogue()};editor.ui.registry.addIcon(_common.iframeIcon,iframeButtonImage.html),editor.ui.registry.addToggleButton(_common.iframeButtonName,{icon:_common.iframeIcon,tooltip:iframeButtonText,onAction:handleIframeAction,onSetup:api=>editor.selection.selectorChangedWithUnbind("iframe:not([data-mce-object]):not([data-mce-placeholder]),.tiny-iframe-responsive,.tiny-mediacms-iframe-wrapper",api.setActive).unbind}),editor.ui.registry.addMenuItem(_common.iframeMenuItemName,{icon:_common.iframeIcon,text:iframeButtonText,onAction:handleIframeAction}),editor.ui.registry.addContextToolbar(_common.iframeButtonName,{predicate:isIframe,items:_common.iframeButtonName,position:"node",scope:"node"}),editor.ui.registry.addContextMenu(_common.iframeButtonName,{update:isIframe}),setupIframeOverlays(editor,handleIframeAction)})(editor,iframeButtonText,iframeButtonImage)}}}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getSetup=void 0,_iframeembed=(obj=_iframeembed)&&obj.__esModule?obj:{default:obj};const isIframe=node=>"iframe"===node.nodeName.toLowerCase()||node.classList&&node.classList.contains("tiny-iframe-responsive")||node.classList&&node.classList.contains("tiny-mediacms-iframe-wrapper")||"a"===node.nodeName.toLowerCase()&&"true"===node.getAttribute("data-mediacms-textlink"),setupIframeOverlays=(editor,handleIframeAction)=>{const processIframes=()=>{const editorBody=editor.getBody();if(!editorBody)return;editorBody.querySelectorAll("iframe").forEach((iframe=>{var _iframe$parentElement;if(null!==(_iframe$parentElement=iframe.parentElement)&&void 0!==_iframe$parentElement&&_iframe$parentElement.classList.contains("tiny-mediacms-iframe-wrapper"))return;if(iframe.hasAttribute("data-mce-object")||iframe.hasAttribute("data-mce-placeholder"))return;const wrapper=editor.getDoc().createElement("div");wrapper.className="tiny-mediacms-iframe-wrapper",wrapper.setAttribute("contenteditable","false");const editBtn=editor.getDoc().createElement("button");editBtn.className="tiny-mediacms-edit-btn",editBtn.setAttribute("type","button"),editBtn.setAttribute("title","Edit media embed options"),editBtn.textContent="EDIT",iframe.parentNode.insertBefore(wrapper,iframe),wrapper.appendChild(iframe),wrapper.appendChild(editBtn)}))},handleOverlayClick=e=>{const editBtn=e.target.closest(".tiny-mediacms-edit-btn");if(!editBtn)return;e.preventDefault(),e.stopPropagation();const wrapper=editBtn.closest(".tiny-mediacms-iframe-wrapper");if(!wrapper)return;wrapper.querySelector("iframe")&&(editor.selection.select(wrapper),handleIframeAction())};editor.on("init",(()=>{(()=>{const editorDoc=editor.getDoc();if(!editorDoc)return;if(editorDoc.getElementById("tiny-mediacms-overlay-styles"))return;const style=editorDoc.createElement("style");style.id="tiny-mediacms-overlay-styles",style.textContent="\n .tiny-mediacms-iframe-wrapper {\n display: inline-block;\n position: relative;\n line-height: 0;\n vertical-align: top;\n margin-top: 40px;\n }\n .tiny-mediacms-iframe-wrapper iframe {\n display: block;\n }\n .tiny-mediacms-edit-btn {\n position: absolute;\n top: -15px;\n left: 50%;\n transform: translateX(-50%);\n background: rgba(0, 0, 0, 0.7);\n color: #ffffff;\n border: none;\n border-radius: 3px;\n cursor: pointer;\n z-index: 10;\n padding: 4px 12px;\n margin: 0;\n font-size: 12px;\n font-weight: bold;\n text-decoration: none;\n box-shadow: 0 2px 4px rgba(0,0,0,0.3);\n transition: background 0.15s, box-shadow 0.15s;\n display: inline-block;\n box-sizing: border-box;\n }\n .tiny-mediacms-edit-btn:hover {\n background: rgba(0, 0, 0, 0.85);\n box-shadow: 0 3px 6px rgba(0,0,0,0.4);\n }\n ",editorDoc.head.appendChild(style)})(),processIframes(),editor.getBody().addEventListener("click",handleOverlayClick)})),editor.on("SetContent",(()=>{processIframes()})),editor.on("PastePostProcess",(()=>{setTimeout(processIframes,100)})),editor.on("Undo Redo",(()=>{processIframes()})),editor.on("Change",(()=>{setTimeout(processIframes,50)})),editor.on("NodeChange",(()=>{processIframes()}))};_exports.getSetup=async()=>{const[iframeButtonText]=await(0,_str.getStrings)(["iframebuttontitle"].map((key=>({key:key,component:_common.component})))),[iframeButtonImage]=await Promise.all([(0,_utils.getButtonImage)("icon",_common.component)]);return editor=>{((editor,iframeButtonText,iframeButtonImage)=>{const handleIframeAction=()=>{new _iframeembed.default(editor).displayDialogue()};editor.ui.registry.addIcon(_common.iframeIcon,iframeButtonImage.html),editor.ui.registry.addToggleButton(_common.iframeButtonName,{icon:_common.iframeIcon,tooltip:iframeButtonText,onAction:handleIframeAction,onSetup:api=>{const selector=["iframe:not([data-mce-object]):not([data-mce-placeholder])",".tiny-iframe-responsive",".tiny-mediacms-iframe-wrapper",'a[data-mediacms-textlink="true"]'].join(",");return editor.selection.selectorChangedWithUnbind(selector,api.setActive).unbind}}),editor.ui.registry.addMenuItem(_common.iframeMenuItemName,{icon:_common.iframeIcon,text:iframeButtonText,onAction:handleIframeAction}),editor.ui.registry.addContextToolbar(_common.iframeButtonName,{predicate:isIframe,items:_common.iframeButtonName,position:"node",scope:"node"}),editor.ui.registry.addContextMenu(_common.iframeButtonName,{update:isIframe}),setupIframeOverlays(editor,handleIframeAction)})(editor,iframeButtonText,iframeButtonImage)}}}));
//# sourceMappingURL=commands.min.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -98,6 +98,8 @@ export default class IframeEmbed {
// MediaCMS embed URL: /embed?m=VIDEO_ID&options
if (urlObj.pathname === '/embed' && urlObj.searchParams.has('m')) {
const tParam = urlObj.searchParams.get('t');
const widthParam = urlObj.searchParams.get('width');
const heightParam = urlObj.searchParams.get('height');
return {
baseUrl: baseUrl,
videoId: urlObj.searchParams.get('m'),
@@ -107,6 +109,8 @@ export default class IframeEmbed {
showRelated: urlObj.searchParams.get('showRelated') === '1',
showUserAvatar:
urlObj.searchParams.get('showUserAvatar') === '1',
width: widthParam ? parseInt(widthParam) : null,
height: heightParam ? parseInt(heightParam) : null,
startAt: tParam
? this.secondsToTimeString(parseInt(tParam))
: null,
@@ -207,6 +211,14 @@ export default class IframeEmbed {
);
url.searchParams.set('linkTitle', options.linkTitle ? '1' : '0');
// Add width and height if provided
if (options.width) {
url.searchParams.set('width', options.width.toString());
}
if (options.height) {
url.searchParams.set('height', options.height.toString());
}
// Add start time if enabled
if (options.startAtEnabled && options.startAt) {
const seconds = this.timeStringToSeconds(options.startAt);
@@ -338,8 +350,8 @@ export default class IframeEmbed {
return {
url: href,
width: 560,
height: 315,
width: parsed?.width || 560,
height: parsed?.height || 315,
showTitle: parsed?.showTitle ?? true,
linkTitle: parsed?.linkTitle ?? true,
showRelated: parsed?.showRelated ?? true,
@@ -359,10 +371,14 @@ export default class IframeEmbed {
const style = this.selectedIframe.getAttribute('style') || '';
const isResponsive = style.includes('aspect-ratio');
// Prioritize width/height from URL params, fallback to iframe attributes
const width = parsed?.width || parseInt(this.selectedIframe.getAttribute('width')) || 560;
const height = parsed?.height || parseInt(this.selectedIframe.getAttribute('height')) || 315;
return {
url: src,
width: this.selectedIframe.getAttribute('width') || 560,
height: this.selectedIframe.getAttribute('height') || 315,
width: width,
height: height,
showTitle: parsed?.showTitle ?? true,
linkTitle: parsed?.linkTitle ?? true,
showRelated: parsed?.showRelated ?? true,

View File

@@ -87,6 +87,8 @@ class OIDCLoginView(View):
embed_show_user_avatar = request.GET.get('embed_show_user_avatar') or request.POST.get('embed_show_user_avatar')
embed_link_title = request.GET.get('embed_link_title') or request.POST.get('embed_link_title')
embed_start_time = request.GET.get('embed_start_time') or request.POST.get('embed_start_time')
embed_width = request.GET.get('embed_width') or request.POST.get('embed_width')
embed_height = request.GET.get('embed_height') or request.POST.get('embed_height')
if not all([target_link_uri, iss, client_id]):
return JsonResponse({'error': 'Missing required OIDC parameters'}, status=400)
@@ -132,6 +134,10 @@ class OIDCLoginView(View):
state_data['embed_link_title'] = embed_link_title
if embed_start_time:
state_data['embed_start_time'] = embed_start_time
if embed_width:
state_data['embed_width'] = embed_width
if embed_height:
state_data['embed_height'] = embed_height
# Encode as base64 URL-safe string
state = base64.urlsafe_b64encode(json.dumps(state_data).encode()).decode().rstrip('=')
@@ -202,11 +208,15 @@ class LaunchView(View):
'embed_show_user_avatar': 'showUserAvatar',
'embed_link_title': 'linkTitle',
'embed_start_time': 't',
'embed_width': 'width',
'embed_height': 'height',
'showTitle': 'showTitle',
'showRelated': 'showRelated',
'showUserAvatar': 'showUserAvatar',
'linkTitle': 'linkTitle',
't': 't',
'width': 'width',
'height': 'height',
}
for key, param_name in param_mapping.items():
@@ -254,7 +264,7 @@ class LaunchView(View):
media_token_from_state = state_data.get('media_token')
# Extract embed parameters from state
for key in ['embed_show_title', 'embed_show_related', 'embed_show_user_avatar', 'embed_link_title', 'embed_start_time']:
for key in ['embed_show_title', 'embed_show_related', 'embed_show_user_avatar', 'embed_link_title', 'embed_start_time', 'embed_width', 'embed_height']:
if key in state_data:
embed_params_from_state[key] = state_data[key]
except Exception: