Changeset 3477523
- Timestamp:
- 03/08/2026 05:43:06 PM (4 weeks ago)
- Location:
- muse-ai
- Files:
-
- 5 added
- 2 edited
-
tags/0.6.0 (added)
-
tags/0.6.0/muse-ai.php (added)
-
tags/0.6.0/readme.txt (added)
-
tags/0.6.0/skiv.php (added)
-
trunk/muse-ai.php (modified) (1 diff)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/skiv.php (added)
Legend:
- Unmodified
- Added
- Removed
-
muse-ai/trunk/muse-ai.php
r3362100 r3477523 1 1 <?php 2 /** 3 * Plugin Name: muse.ai 4 * Description: Enable oEmbed and shortcode support for muse.ai video embedding. 5 * Version: 0.5.1 6 * Author: muse.ai 7 * Author URI: https://muse.ai 8 */ 9 10 add_action('init', 'museai_init'); 11 add_action('elementor/widgets/register', 'museai_elementor'); 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 } 153 154 function museai_init() { 155 wp_oembed_add_provider('#https://muse.ai/(v|vc|vd|vt)/.+#', 'https://muse.ai/oembed', true); 156 wp_enqueue_script('museai-embed-player', 'https://muse.ai/static/js/embed-player.min.js'); 157 } 158 159 function museai_shortcode_video( $atts = [] ) { 160 $embed_id = bin2hex(random_bytes(16)); 161 162 // Sanitize and validate all inputs with proper whitelisting. 163 $video_id = preg_replace('/[^a-z0-9]/i', '', $atts['id'] ?? ''); 164 $width = preg_replace('/[^0-9%]/', '', $atts['width'] ?? '100%'); 165 $volume = max(0, min(100, (int) preg_replace('/[^0-9]/', '', $atts['volume'] ?? '50'))); 166 $autoplay = (int) preg_replace('/[^01]/', '', $atts['autoplay'] ?? '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')); 185 $loop = (int) preg_replace('/[^01]/', '', $atts['loop'] ?? '0'); 186 $resume = (int) preg_replace('/[^01]/', '', $atts['resume'] ?? '0'); 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 253 $download = (int) preg_replace('/[^01]/', '', $atts['download'] ?? '0'); 254 $playlist = preg_replace('/[^a-z0-9,]/i', '', $atts['playlist'] ?? ''); 255 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); 312 313 $out = sprintf( 314 '<div id="museai-player-%s"></div><script>MusePlayer(%s);</script>', 315 esc_attr($embed_id), 316 $config_json 317 ); 318 return $out; 319 } 320 321 function museai_elementor( $widgets_manager ) { 322 class Elementor_Museai_Widget extends \Elementor\Widget_Base { 323 public function get_name() { 324 return 'muse.ai'; 325 } 326 public function get_title() { 327 return esc_html__( 'muse.ai', 'elementor-oembed-widget' ); 328 } 329 public function get_icon() { 330 return 'eicon-youtube'; 331 } 332 public function get_custom_help_url() { 333 return 'https://wordpress.org/plugins/muse-ai/'; 334 } 335 public function get_categories() { 336 return [ 'general' ]; 337 } 338 public function get_keywords() { 339 return [ 'embed', 'url', 'link', 'video' ]; 340 } 341 protected function register_controls() { 342 $this->start_controls_section( 343 'content_section', 344 [ 345 'label' => esc_html__( 'Content', 'elementor-museai-widget' ), 346 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, 347 ] 348 ); 349 $this->add_control( 350 'video_id', 351 [ 352 'label' => esc_html__( 'Video ID', 'elementor-museai-widget' ), 353 'type' => \Elementor\Controls_Manager::TEXT, 354 'input_type' => 'text', 355 'placeholder' => esc_html__( 'A1b2C3d', 'elementor-oembed-widget' ), 356 ] 357 ); 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( 370 'title', 371 [ 372 'label' => esc_html__( 'Title', 'elementor-museai-widget' ), 373 'type' => \Elementor\Controls_Manager::SWITCHER, 374 'label_on' => 'yes', 375 'label_off' => 'no', 376 'default' => 'yes', 377 ] 378 ); 379 $this->add_control( 380 'links', 381 [ 382 'label' => esc_html__( 'Link to video page', 'elementor-museai-widget' ), 383 'type' => \Elementor\Controls_Manager::SWITCHER, 384 'label_on' => 'yes', 385 'label_off' => 'no', 386 'default' => 'yes', 387 ] 388 ); 389 $this->add_control( 390 'search', 391 [ 392 'label' => esc_html__( 'Search', 'elementor-museai-widget' ), 393 'type' => \Elementor\Controls_Manager::SWITCHER, 394 'label_on' => 'show', 395 'label_off' => 'hide', 396 'default' => 'yes', 397 ] 398 ); 399 $this->add_control( 400 'autoplay', 401 [ 402 'label' => esc_html__( 'Autoplay', 'elementor-museai-widget' ), 403 'type' => \Elementor\Controls_Manager::SWITCHER, 404 'label_on' => 'yes', 405 'label_off' => 'no', 406 'default' => 'no', 407 ] 408 ); 409 $this->add_control( 410 'mute', 411 [ 412 'label' => esc_html__( 'Mute', 'elementor-museai-widget' ), 413 'type' => \Elementor\Controls_Manager::SWITCHER, 414 'label_on' => 'yes', 415 'label_off' => 'no', 416 'default' => 'no', 417 ] 418 ); 419 $this->end_controls_section(); 420 } 421 protected function render() { 422 $settings = $this->get_settings_for_display(); 423 $embed_id = bin2hex(random_bytes(16)); 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); 481 482 $out = sprintf( 483 '<div id="museai-player-%s"></div><script>MusePlayer(%s);</script>', 484 esc_attr($embed_id), 485 $config_json 486 ); 487 echo $out; 488 } 489 } 490 491 $widgets_manager->register( new Elementor_Museai_Widget() ); 492 } 2 // Backward compatibility: loads the main plugin file (skiv.php). 3 require_once __DIR__ . '/skiv.php'; -
muse-ai/trunk/readme.txt
r3362100 r3477523 1 === muse.aivideo embedding ===1 === skiv video embedding === 2 2 Contributors: museai 3 Tags: muse.ai, muse ai, video search, embed videos, video player, upload video3 Tags: skiv, skiv.com, video search, embed videos, video player 4 4 Requires at least: 4.7 5 5 Tested up to: 6.8.2 6 Stable tag: 0. 5.17 Requires PHP: 7 .06 Stable tag: 0.6.0 7 Requires PHP: 7 8 8 License: GPLv2 or later 9 9 10 This plugin enables muse.ai oEmbed links, and adds shortcodes to easily embed videos hosted on muse.ai.10 This plugin enables skiv.com oEmbed links, and adds shortcodes to easily embed videos hosted on skiv.com. 11 11 12 12 == Description == 13 13 14 This plugin simplifies the embedding of videos hosted on [ muse.ai](https://muse.ai) platform.14 This plugin simplifies the embedding of videos hosted on [skiv.com](https://skiv.com) platform. 15 15 16 16 It does three things: 17 - whitelists muse.aias an oEmbed provider (which lets you embed videos simply by pasting links),17 - whitelists skiv.com as an oEmbed provider (which lets you embed videos simply by pasting links), 18 18 - adds shortcodes as an alternative method of embedding that gives you a bit more control, 19 - adds an Elementor widget for embedding muse.aivideos.19 - adds an Elementor widget for embedding skiv.com videos. 20 20 21 The shortcodes are essentially a wrapper around muse.ai [embed library](https://muse.ai/docs#embed-player).21 The shortcodes are essentially a wrapper around skiv.com [embed library](https://skiv.com/docs#embed-player). 22 22 23 23 To embed videos using oEmbed, simply paste a video link into a separate line in your post or page. For example: 24 24 25 `https:// muse.ai/v/VBdrD8v`25 `https://skiv.com/v/VBdrD8v` 26 26 27 27 If you would like more control, you can use shortcodes. For example: … … 30 30 31 31 == Changelog == 32 33 = 0.6.0 = 34 * Rebranded from muse.ai to skiv.com. 35 * Main plugin file renamed from muse-ai.php to skiv.php. 32 36 33 37 = 0.5.1 =
Note: See TracChangeset
for help on using the changeset viewer.