Plugin Directory

Changeset 3477523


Ignore:
Timestamp:
03/08/2026 05:43:06 PM (4 weeks ago)
Author:
museai
Message:

Release 0.6.0: Rebrand from muse.ai to skiv.com

Location:
muse-ai
Files:
5 added
2 edited

Legend:

Unmodified
Added
Removed
  • muse-ai/trunk/muse-ai.php

    r3362100 r3477523  
    11<?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).
     3require_once __DIR__ . '/skiv.php';
  • muse-ai/trunk/readme.txt

    r3362100 r3477523  
    1 === muse.ai video embedding ===
     1=== skiv video embedding ===
    22Contributors: museai
    3 Tags: muse.ai, muse ai, video search, embed videos, video player, upload video
     3Tags: skiv, skiv.com, video search, embed videos, video player
    44Requires at least: 4.7
    55Tested up to: 6.8.2
    6 Stable tag: 0.5.1
    7 Requires PHP: 7.0
     6Stable tag: 0.6.0
     7Requires PHP: 7
    88License: GPLv2 or later
    99
    10 This plugin enables muse.ai oEmbed links, and adds shortcodes to easily embed videos hosted on muse.ai.
     10This plugin enables skiv.com oEmbed links, and adds shortcodes to easily embed videos hosted on skiv.com.
    1111
    1212== Description ==
    1313
    14 This plugin simplifies the embedding of videos hosted on [muse.ai](https://muse.ai) platform.
     14This plugin simplifies the embedding of videos hosted on [skiv.com](https://skiv.com) platform.
    1515
    1616It does three things:
    17  - whitelists muse.ai as 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),
    1818 - adds shortcodes as an alternative method of embedding that gives you a bit more control,
    19  - adds an Elementor widget for embedding muse.ai videos.
     19 - adds an Elementor widget for embedding skiv.com videos.
    2020
    21 The shortcodes are essentially a wrapper around muse.ai [embed library](https://muse.ai/docs#embed-player).
     21The shortcodes are essentially a wrapper around skiv.com [embed library](https://skiv.com/docs#embed-player).
    2222
    2323To embed videos using oEmbed, simply paste a video link into a separate line in your post or page. For example:
    2424
    25 `https://muse.ai/v/VBdrD8v`
     25`https://skiv.com/v/VBdrD8v`
    2626
    2727If you would like more control, you can use shortcodes. For example:
     
    3030
    3131== 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.
    3236
    3337= 0.5.1 =
Note: See TracChangeset for help on using the changeset viewer.