Changeset 3362049
- Timestamp:
- 09/15/2025 08:08:20 PM (7 months ago)
- Location:
- muse-ai
- Files:
-
- 3 added
- 2 edited
-
tags/0.5 (added)
-
tags/0.5/muse-ai.php (added)
-
tags/0.5/readme.txt (added)
-
trunk/muse-ai.php (modified) (5 diffs)
-
trunk/readme.txt (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
muse-ai/trunk/muse-ai.php
r2987496 r3362049 3 3 * Plugin Name: muse.ai 4 4 * Description: Enable oEmbed and shortcode support for muse.ai video embedding. 5 * Version: 0. 45 * Version: 0.5 6 6 * Author: muse.ai 7 7 * Author URI: https://muse.ai … … 11 11 add_action('elementor/widgets/register', 'museai_elementor'); 12 12 add_shortcode('muse-ai', 'museai_shortcode_video'); 13 14 function museai_sanitize_css($css_input) { 15 $css_input = trim($css_input); 16 if (empty($css_input)) { 17 return ''; 18 } 19 20 // Whitelist of safe CSS properties. 21 $allowed_properties = [ 22 'width', 'height', 'max-width', 'max-height', 'min-width', 'min-height', 23 'margin', 'margin-top', 'margin-bottom', 'margin-left', 'margin-right', 24 'padding', 'padding-top', 'padding-bottom', 'padding-left', 'padding-right', 25 'border', 'border-width', 'border-style', 'border-color', 'border-radius', 26 'background', 'background-color', 'background-image', 'background-size', 'background-position', 27 'color', 'font-size', 'font-family', 'font-weight', 'font-style', 28 'text-align', 'text-decoration', 'line-height', 'letter-spacing', 29 'opacity', 'visibility', 'display', 'position', 'top', 'bottom', 'left', 'right', 30 'z-index', 'overflow', 'box-shadow', 'text-shadow' 31 ]; 32 33 // Remove dangerous content. 34 $dangerous_patterns = [ 35 '/javascript:/i', 36 '/expression\s*\(/i', 37 '/url\s*\(\s*["\']?\s*data:/i', 38 '/import/i', 39 '/@import/i', 40 '/behavior:/i', 41 '/binding:/i', 42 '/mozbinding:/i', 43 '/vbscript:/i', 44 '/livescript:/i' 45 ]; 46 47 foreach ($dangerous_patterns as $pattern) { 48 $css_input = preg_replace($pattern, '', $css_input); 49 } 50 51 // Parse CSS declarations. 52 $declarations = explode(';', $css_input); 53 $safe_css = []; 54 55 foreach ($declarations as $declaration) { 56 $declaration = trim($declaration); 57 if (empty($declaration)) continue; 58 59 // Split property and value 60 $parts = explode(':', $declaration, 2); 61 if (count($parts) !== 2) continue; 62 63 $property = trim(strtolower($parts[0])); 64 $value = trim($parts[1]); 65 66 // Check if property is allowed 67 if (!in_array($property, $allowed_properties)) continue; 68 69 // Sanitize value 70 $value = museai_sanitize_css_value($property, $value); 71 if ($value === false) continue; 72 73 $safe_css[] = $property . ': ' . $value; 74 } 75 76 return implode('; ', $safe_css); 77 } 78 79 function museai_sanitize_css_value($property, $value) { 80 // Remove quotes and normalize. 81 $value = trim($value, '"\''); 82 $value = trim($value); 83 84 // Block dangerous patterns in values. 85 $dangerous_patterns = [ 86 '/javascript:/i', 87 '/expression/i', 88 '/url\s*\(\s*["\']?\s*data:/i', 89 '/url\s*\(\s*["\']?\s*javascript:/i', 90 '/vbscript:/i' 91 ]; 92 93 foreach ($dangerous_patterns as $pattern) { 94 if (preg_match($pattern, $value)) { 95 return false; 96 } 97 } 98 99 // Property-specific validation. 100 switch ($property) { 101 case 'width': 102 case 'height': 103 case 'max-width': 104 case 'max-height': 105 case 'min-width': 106 case 'min-height': 107 // Allow px, %, em, rem, vh, vw 108 if (!preg_match('/^(auto|inherit|\d+(\.\d+)?(px|%|em|rem|vh|vw))$/', $value)) { 109 return false; 110 } 111 break; 112 113 case 'color': 114 case 'background-color': 115 case 'border-color': 116 // Allow hex colors, rgb, rgba, named colors 117 if (!preg_match('/^(#[0-9a-f]{3,6}|rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)|rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[\d.]+\s*\)|transparent|inherit|[a-z]+)$/i', $value)) { 118 return false; 119 } 120 break; 121 122 case 'font-size': 123 if (!preg_match('/^(\d+(\.\d+)?(px|pt|em|rem|%)|small|medium|large|x-large|xx-large)$/i', $value)) { 124 return false; 125 } 126 break; 127 128 case 'z-index': 129 if (!preg_match('/^(\d+|auto|inherit)$/', $value)) { 130 return false; 131 } 132 break; 133 134 case 'opacity': 135 if (!preg_match('/^(0(\.\d+)?|1(\.0+)?|\.\d+)$/', $value)) { 136 return false; 137 } 138 break; 139 140 default: 141 // General sanitization - remove suspicious content 142 if (preg_match('/[<>{}\\\\]/', $value)) { 143 return false; 144 } 145 // Limit length 146 if (strlen($value) > 100) { 147 return false; 148 } 149 } 150 151 return $value; 152 } 13 153 14 154 function museai_init() { … … 19 159 function museai_shortcode_video( $atts = [] ) { 20 160 $embed_id = bin2hex(random_bytes(16)); 161 162 // Sanitize and validate all inputs with proper whitelisting. 21 163 $video_id = preg_replace('/[^a-z0-9]/i', '', $atts['id'] ?? ''); 22 164 $width = preg_replace('/[^0-9%]/', '', $atts['width'] ?? '100%'); 23 $volume = (int) preg_replace('/[^0-9]/', '', $atts['volume'] ?? '50');165 $volume = max(0, min(100, (int) preg_replace('/[^0-9]/', '', $atts['volume'] ?? '50'))); 24 166 $autoplay = (int) preg_replace('/[^01]/', '', $atts['autoplay'] ?? '0'); 25 $title = (int) preg_replace('/[^01]/', '', $atts['title'] ?? '1'); 26 $style = preg_replace('/"/', '', $atts['style'] ?? 'full'); 27 $start = (float) preg_replace('/[^0-9.]/', '', $atts['start'] ?? '0'); 167 168 // Handle title parameter: can be 0, 1, or custom string 169 $title_input = trim($atts['title'] ?? '1'); 170 $title_lower = strtolower($title_input); 171 if ($title_lower === '0' || $title_lower === 'false') { 172 $title = 0; 173 } elseif ($title_lower === '1' || $title_lower === 'true') { 174 $title = 1; 175 } else { 176 // Custom title string - sanitize it. 177 $title = substr(sanitize_text_field($title_input), 0, 200); 178 } 179 180 // Whitelist allowed styles. 181 $allowed_styles = ['full', 'minimal', 'audio', 'cover']; 182 $style = in_array($atts['style'] ?? 'full', $allowed_styles) ? ($atts['style'] ?? 'full') : 'full'; 183 184 $start = max(0, (float) preg_replace('/[^0-9.]/', '', $atts['start'] ?? '0')); 28 185 $loop = (int) preg_replace('/[^01]/', '', $atts['loop'] ?? '0'); 29 186 $resume = (int) preg_replace('/[^01]/', '', $atts['resume'] ?? '0'); 30 $align = preg_replace('/"/', '', $atts['align'] ?? ''); 31 $playpos = preg_replace('/"/', '', $atts['cover_play_position'] ?? 'bottom-left'); 32 $cta = addslashes(($atts['cta'] ?? '') ? $atts['cta'] : ''); 33 $css = preg_replace('/"/', '\'', $atts['css'] ?? ''); 34 $subtitles = preg_replace('/"/', '', $atts['subtitles'] ?? ''); 35 $locale = preg_replace('/"/', '', $atts['locale'] ?? ''); 187 188 // Whitelist allowed align values. 189 $allowed_aligns = ['left', 'center', 'right', '']; 190 $align = in_array($atts['align'] ?? '', $allowed_aligns) ? ($atts['align'] ?? '') : ''; 191 192 // Whitelist allowed cover play positions. 193 $allowed_positions = ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'center']; 194 $playpos = in_array($atts['cover_play_position'] ?? 'bottom-left', $allowed_positions) ? ($atts['cover_play_position'] ?? 'bottom-left') : 'bottom-left'; 195 196 // Handle CTA parameter - can be JSON object or empty. 197 $cta_input = trim($atts['cta'] ?? ''); 198 $cta = ''; 199 200 if (!empty($cta_input)) { 201 // Try to decode as JSON. 202 $cta_decoded = json_decode($cta_input, true); 203 204 if (json_last_error() === JSON_ERROR_NONE && is_array($cta_decoded)) { 205 // Sanitize each allowed field. 206 $sanitized_cta = array(); 207 208 // Sanitize time field - must be 'end' or positive number. 209 if (isset($cta_decoded['time'])) { 210 if ($cta_decoded['time'] === 'end') { 211 $sanitized_cta['time'] = 'end'; 212 } elseif (is_numeric($cta_decoded['time']) && $cta_decoded['time'] >= 0) { 213 $sanitized_cta['time'] = (float) $cta_decoded['time']; 214 } 215 } 216 217 // Sanitize text fields - strip dangerous characters. 218 if (isset($cta_decoded['title']) && is_string($cta_decoded['title'])) { 219 $sanitized_cta['title'] = substr(sanitize_text_field($cta_decoded['title']), 0, 200); 220 } 221 222 if (isset($cta_decoded['text']) && is_string($cta_decoded['text'])) { 223 $sanitized_cta['text'] = substr(sanitize_textarea_field($cta_decoded['text']), 0, 500); 224 } 225 226 if (isset($cta_decoded['button']) && is_string($cta_decoded['button'])) { 227 $sanitized_cta['button'] = substr(sanitize_text_field($cta_decoded['button']), 0, 100); 228 } 229 230 // Sanitize link field - must be valid URL. 231 if (isset($cta_decoded['link']) && is_string($cta_decoded['link'])) { 232 $link_trimmed = trim($cta_decoded['link']); 233 if (filter_var($link_trimmed, FILTER_VALIDATE_URL) && 234 (strpos($link_trimmed, 'http://') === 0 || strpos($link_trimmed, 'https://') === 0)) { 235 $sanitized_cta['link'] = esc_url_raw($link_trimmed); 236 } 237 } 238 239 // Only use CTA if we have at least some valid data. 240 if (!empty($sanitized_cta)) { 241 $cta = $sanitized_cta; 242 } 243 } 244 } 245 246 // Sanitize CSS with strict whitelist approach. 247 $css = museai_sanitize_css($atts['css'] ?? ''); 248 249 // Whitelist language codes for subtitles and locale. 250 $subtitles = preg_replace('/[^a-z\-]/', '', strtolower($atts['subtitles'] ?? '')); 251 $locale = preg_replace('/[^a-z\-]/', '', strtolower($atts['locale'] ?? '')); 252 36 253 $download = (int) preg_replace('/[^01]/', '', $atts['download'] ?? '0'); 37 254 $playlist = preg_replace('/[^a-z0-9,]/i', '', $atts['playlist'] ?? ''); 38 255 39 $links = preg_replace('/"/', '', $atts['links'] ?? '1'); 40 $links = $links == '0' || $links == '1' ? (int) $links : sprintf('"%s"', $links); 41 42 $logo = preg_replace('/"/', '', $atts['logo'] ?? '1'); 43 $logo = $logo == '0' || $logo == '1' ? (int) $logo : sprintf('"%s"', $logo); 44 45 $search = preg_replace('/"/', '', $atts['search'] ?? '1'); 46 $search = $search == '0' || $search == '1' ? (int) $search : sprintf('"%s"', $search); 256 // Handle links parameter: can be 0, 1, true, false, or URL. 257 $links_input = trim($atts['links'] ?? '1'); 258 $links_lower = strtolower($links_input); 259 if ($links_lower === '0' || $links_lower === 'false') { 260 $links = 0; 261 } elseif ($links_lower === '1' || $links_lower === 'true') { 262 $links = 1; 263 } elseif (filter_var($links_input, FILTER_VALIDATE_URL) && (strpos($links_input, 'http://') === 0 || strpos($links_input, 'https://') === 0)) { 264 $links = esc_url_raw($links_input); 265 } else { 266 $links = 1; // Default fallback. 267 } 268 269 // Handle logo parameter: can be 0, 1, true, false, or URL. 270 $logo_input = trim($atts['logo'] ?? '1'); 271 $logo_lower = strtolower($logo_input); 272 if ($logo_lower === '0' || $logo_lower === 'false') { 273 $logo = 0; 274 } elseif ($logo_lower === '1' || $logo_lower === 'true') { 275 $logo = 1; 276 } elseif (filter_var($logo_input, FILTER_VALIDATE_URL) && (strpos($logo_input, 'http://') === 0 || strpos($logo_input, 'https://') === 0)) { 277 $logo = esc_url_raw($logo_input); 278 } else { 279 $logo = 1; // Default fallback. 280 } 281 282 // Handle search parameter: only boolean. 283 $search_raw = preg_replace('/[^a-z0-9]/', '', strtolower($atts['search'] ?? '1')); 284 $search = ($search_raw == '0' || $search_raw == '1') ? (int) $search_raw : 1; 285 286 // Build JavaScript configuration array with proper escaping 287 $config = array( 288 'container' => '#museai-player-' . $embed_id, 289 'video' => $video_id, 290 'width' => $width, 291 'links' => $links, 292 'logo' => $logo, 293 'search' => $search, 294 'autoplay' => $autoplay, 295 'volume' => $volume, 296 'title' => $title, 297 'style' => $style, 298 'start' => $start, 299 'loop' => $loop, 300 'resume' => $resume, 301 'align' => $align, 302 'coverPlayPosition' => $playpos, 303 'cta' => $cta, 304 'subtitles' => $subtitles, 305 'locale' => $locale, 306 'download' => $download, 307 'playlist' => $playlist 308 ); 309 310 // Use wp_json_encode for safe JavaScript output 311 $config_json = wp_json_encode($config, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); 47 312 48 313 $out = sprintf( 49 '<div id="museai-player-%s"></div>'. 50 '<script>'. 51 'MusePlayer({'. 52 'container: "#museai-player-%1$s", video: "%s", '. 53 'width: "%s", links: %s, logo: %s, search: %s, autoplay: %s, '. 54 'volume: %s, title: %s, style: "%s", start: %s, loop: %s, '. 55 'resume: %s, align: "%s", coverPlayPosition: "%s", cta: "%s", '. 56 'css: "%s", subtitles: "%s", locale: "%s", download: %s, '. 57 'playlist: "%s"'. 58 '})'. 59 '</script>', 60 $embed_id, 61 $video_id, 62 $width, 63 $links, 64 $logo, 65 $search, 66 $autoplay, 67 $volume, 68 $title, 69 $style, 70 $start, 71 $loop, 72 $resume, 73 $align, 74 $playpos, 75 $cta, 76 $css, 77 $subtitles, 78 $locale, 79 $download, 80 $playlist 314 '<div id="museai-player-%s"></div><script>MusePlayer(%s);</script>', 315 esc_attr($embed_id), 316 $config_json 81 317 ); 82 318 return $out; … … 121 357 ); 122 358 $this->add_control( 359 'logo', 360 [ 361 'label' => esc_html__( 'Logo', 'elementor-museai-widget' ), 362 'type' => \Elementor\Controls_Manager::TEXT, 363 'input_type' => 'text', 364 'placeholder' => esc_html__( 'Custom logo or yes/no', 'elementor-museai-widget' ), 365 'default' => 'yes', 366 'description' => esc_html__( 'Enter "yes" to show default logo, "no" to hide, or custom url to png', 'elementor-museai-widget' ), 367 ] 368 ); 369 $this->add_control( 123 370 'title', 124 371 [ 125 372 'label' => esc_html__( 'Title', 'elementor-museai-widget' ), 126 373 'type' => \Elementor\Controls_Manager::SWITCHER, 127 'label_on' => 'show', 128 'label_off' => 'hide', 129 'default' => 'yes', 130 ] 131 ); 132 $this->add_control( 133 'logo', 134 [ 135 'label' => esc_html__( 'Logo', 'elementor-museai-widget' ), 136 'type' => \Elementor\Controls_Manager::SWITCHER, 137 'label_on' => 'show', 138 'label_off' => 'hide', 374 'label_on' => 'yes', 375 'label_off' => 'no', 139 376 'default' => 'yes', 140 377 ] … … 185 422 $settings = $this->get_settings_for_display(); 186 423 $embed_id = bin2hex(random_bytes(16)); 187 $html = wp_oembed_get( $settings['video_id'] ); 188 189 $video_id = $settings['video_id']; 190 $links = $settings['links'] == 'yes' ? 'true' : 'false'; 191 $logo = $settings['logo'] == 'yes' ? '1' : '0'; 192 $search = $settings['search'] == 'yes' ? '1' : '0'; 193 $title = $settings['title'] == 'yes' ? '1' : '0'; 194 $autoplay = $settings['autoplay'] == 'yes' ? '1' : '0'; 195 $volume = $settings['mute'] == 'yes' ? '0' : '100'; 424 425 // Sanitize video ID - only allow alphanumeric characters. 426 $video_id = preg_replace('/[^a-z0-9]/i', '', $settings['video_id'] ?? ''); 427 428 // Handle links parameter: can be boolean or URL in Elementor. 429 $links_setting = $settings['links'] ?? 'yes'; 430 if ($links_setting === 'yes') { 431 $links = 1; 432 } elseif ($links_setting === 'no') { 433 $links = 0; 434 } elseif (filter_var($links_setting, FILTER_VALIDATE_URL) && (strpos($links_setting, 'http://') === 0 || strpos($links_setting, 'https://') === 0)) { 435 $links = esc_url_raw($links_setting); 436 } else { 437 $links = 1; // Default fallback 438 } 439 440 // Handle logo parameter: can be boolean or URL in Elementor. 441 $logo_setting = $settings['logo'] ?? 'yes'; 442 if ($logo_setting === 'yes') { 443 $logo = 1; 444 } elseif ($logo_setting === 'no') { 445 $logo = 0; 446 } elseif (filter_var($logo_setting, FILTER_VALIDATE_URL) && (strpos($logo_setting, 'http://') === 0 || strpos($logo_setting, 'https://') === 0)) { 447 $logo = esc_url_raw($logo_setting); 448 } else { 449 $logo = 1; // Default fallback 450 } 451 $search = ($settings['search'] ?? 'yes') == 'yes' ? 1 : 0; 452 453 // Handle title parameter: can be boolean or custom string in Elementor. 454 $title_setting = $settings['title'] ?? 'yes'; 455 if ($title_setting === 'yes') { 456 $title = 1; 457 } elseif ($title_setting === 'no') { 458 $title = 0; 459 } else { 460 // Custom title string - sanitize it 461 $title = substr(sanitize_text_field($title_setting), 0, 200); 462 } 463 464 $autoplay = ($settings['autoplay'] ?? 'no') == 'yes' ? 1 : 0; 465 $volume = ($settings['mute'] ?? 'no') == 'yes' ? 0 : 100; 466 467 // Build secure configuration array. 468 $config = array( 469 'container' => '#museai-player-' . $embed_id, 470 'video' => $video_id, 471 'links' => $links, 472 'logo' => $logo, 473 'search' => $search, 474 'autoplay' => $autoplay, 475 'volume' => $volume, 476 'title' => $title 477 ); 478 479 // Use wp_json_encode for safe JavaScript output. 480 $config_json = wp_json_encode($config, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); 196 481 197 482 $out = sprintf( 198 '<div id="museai-player-%s"></div>'. 199 '<script>'. 200 'MusePlayer({'. 201 'container: "#museai-player-%1$s", video: "%s", '. 202 'links: %s, logo: %s, search: %s, autoplay: %s, volume: %s, title: %s'. 203 '})'. 204 '</script>', 205 $embed_id, 206 $video_id, 207 $links, 208 $logo, 209 $search, 210 $autoplay, 211 $volume, 212 $title, 483 '<div id="museai-player-%s"></div><script>MusePlayer(%s);</script>', 484 esc_attr($embed_id), 485 $config_json 213 486 ); 214 487 echo $out; -
muse-ai/trunk/readme.txt
r2987496 r3362049 27 27 If you would like more control, you can use shortcodes. For example: 28 28 29 `[muse-ai id="VBdrD8v" width="100%" title="0" ]`29 `[muse-ai id="VBdrD8v" width="100%" title="0" logo="https://tinyurl.com/yourLogoPNG"]` 30 30 31 31 == Changelog == 32 33 = 0.5 = 34 * Sanitize and escape inputs to prevent CVE-2025-6262. 35 * Improvements to Elementor editor to allow custom logo. 36 * Added experimental extensions fo custom css and cta. 32 37 33 38 = 0.4.1 =
Note: See TracChangeset
for help on using the changeset viewer.