diff --git a/.github/workflows/plugin-build.yml b/.github/workflows/plugin-build.yml index f426b74..e7d9674 100644 --- a/.github/workflows/plugin-build.yml +++ b/.github/workflows/plugin-build.yml @@ -2,9 +2,9 @@ name: Plugin Build on: push: - branches: [ master, main ] + branches: [ master, develop ] pull_request: - branches: [ master, main ] + branches: [ master, develop ] jobs: build: diff --git a/.vscode/settings.json b/.vscode/settings.json index 0f52398..ec4b8c8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -40,5 +40,8 @@ "editor.codeActionsOnSave": { "source.fixAll.stylelint": "never" } + }, + "search.exclude": { + "**/pro__premium_only": false } } \ No newline at end of file diff --git a/interactions.php b/interactions.php index 081bfa0..5790750 100644 --- a/interactions.php +++ b/interactions.php @@ -7,7 +7,7 @@ * Author URI: http://gambit.ph * License: GPLv2 or later * Text Domain: interactions - * Version: 1.3.0 + * Version: 1.3.2 * * @fs_premium_only /freemius.php, /freemius/ */ @@ -18,7 +18,7 @@ } defined( 'INTERACT_BUILD' ) || define( 'INTERACT_BUILD', 'free' ); -defined( 'INTERACT_VERSION' ) || define( 'INTERACT_VERSION', '1.3.0' ); +defined( 'INTERACT_VERSION' ) || define( 'INTERACT_VERSION', '1.3.2' ); defined( 'INTERACT_FILE' ) || define( 'INTERACT_FILE', __FILE__ ); /** @@ -31,8 +31,8 @@ function interact_on_activation() { // Run migration if version not set or outdated if ( ! $saved_version || version_compare( $saved_version, INTERACT_VERSION, '<' ) ) { do_action( 'interact/on_plugin_update', $saved_version, INTERACT_VERSION ); - update_option( 'interact_plugin_version', INTERACT_VERSION ); } + update_option( 'interact_plugin_version', INTERACT_VERSION ); } } register_activation_hook( __FILE__, 'interact_on_activation' ); diff --git a/package.json b/package.json index b7d3cc5..6d26698 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "interactions", - "version": "1.3.0", + "version": "1.3.2", "description": "Make your blocks interactive! Effortlessly set triggers that do actions", "author": "Benjamin Intal of Gambit", "private": true, diff --git a/readme.txt b/readme.txt index 481eb62..28b0312 100644 --- a/readme.txt +++ b/readme.txt @@ -1,10 +1,10 @@ -=== Interactions === +=== Interactions - Create Interactive Experiences in the Block Editor === Contributors: bfintal, gambitph Tags: interaction, interactivity, trigger, blocks, gutenberg -Requires at least: 6.6.4 -Tested up to: 6.8.3 +Requires at least: 6.7.4 +Tested up to: 6.9 Requires PHP: 8.0 -Stable tag: 1.3.0 +Stable tag: 1.3.2 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -12,22 +12,23 @@ Add animations and interactivity to your blocks. Choose from ready-made effects == Description == -**Interactions – WordPress Animations, Effects & Functionality for Gutenberg Blocks** +**Interactions – WordPress Animations, Interactive Experiences for Gutenberg Blocks** -Want to make your website feel alive and interactive? **Interactions** is the easiest way to add animations, effects, interactivity, and functional features to WordPress — directly inside the block editor. +[Visit our website](https://wpinteractions.com) to learn more about how Interactions work. + +Want to make your website feel alive and interactive? **Interactions** is the easiest way to add animations, effects, interactivity, and functional features to WordPress — directly inside the block editor. Check our [samples page here](https://wpinteractions.com/samples/) to see a glimpse of what type of interactions you can create. You don't need coding skills or complex tools. With Interactions, you can: -- **Pick from the Interactions Library** – A collection of pre-built animations and effects (like images that move upon scrolling down the page, buttons that glow when hovered, and more). Just click and apply. -- **Build your own custom effects** – Use a simple **Trigger → Action** system. Example: "On scroll → Fade in block", or "On click → Play video". -- **Add functional features** – Securely update post data, handle form submissions, display user info, copy text to clipboard, and more without coding. +- **Pick from the [Interactions Library](https://docs.wpinteractions.com/article/744-how-to-use-interactions-library)** – A collection of pre-built animations and effects (like images that move upon scrolling down the page, buttons that glow when hovered, and more). Just click and apply. [Learn more](https://docs.wpinteractions.com/article/744-how-to-use-interactions-library) +- **Build your own custom effects** – Use a simple **Trigger → Action** system. Example: "On scroll → Fade in block", or "On click → Play video". [Learn more](https://docs.wpinteractions.com/article/577-what-is-wp-interactions-and-how-does-it-work) +- **Add functional features** – Securely update post data, handle form submissions, display user info, copy text to clipboard, and more without coding. [Learn more](https://docs.wpinteractions.com/category/729-interactions) -Whether you want subtle hover effects, attention-grabbing story-telling animations, playful micro-interactions, or powerful functional features, Interactions makes it possible. +Whether you want subtle hover effects, attention-grabbing story-telling animations, playful micro-interactions, or powerful functional features, [Interactions](https://wpinteractions.com) makes it possible. ### 🚀 Features -Create custom interactions easily with a simple Trigger → Action builder. Features include: - +Create [custom interactions](https://docs.wpinteractions.com/article/571-what-are-interactions) easily with a simple Trigger → Action builder. Features include: **Animations & Visual Effects:** @@ -70,6 +71,8 @@ Create custom interactions easily with a simple Trigger → Action builder. Feat ### 💎 What's in Premium? +[Check our pricing page](https://wpinteractions.com/pricing/) to learn more about what's in Interactions premium. + **Advanced Interactions:** - **Scroll Strength** – Measure scroll intensity @@ -98,6 +101,11 @@ Create custom interactions easily with a simple Trigger → Action builder. Feat - **Regular Updates** – New features and improvements - **Commercial License** – Use in client projects +**Source Code:** + +The source code for this plugin is available on GitHub: +https://github.com/gambitph/Interactions + == Installation == 1. Install “Interactions” from the WordPress Plugin Directory, or upload it to `/wp-content/plugins/interactions/`. @@ -133,23 +141,32 @@ The free version includes basic animations and interactions. Premium adds advanc == Screenshots == -1. Interaction Library – Pre-built animations and effects. +1. Adding from the Interaction Library – Pre-built animations and effects. 2. Advanced trigger and action timeline builder – Create custom interactions with flexible logic and multiple steps. +3. Interaction Library contents – Pre-built animations and effects. -== Source == - -The source code for this plugin is available on GitHub: -https://github.com/gambitph/Interactions +== Upgrade Notice == == Changelog == += 1.3.2 = + +* Fixed: Added restrictions for users without unfiltered_html capabilities +* Fixed: Added additional input sanitization + += 1.3.1 = + +* Fixed: Updated readme info +* Fixed: Updated long name in the plugins page +* Fixed: License activation issue + = 1.3.0 = * New: Interaction library -* New: Initial release in the WordPress Plugin Directory! * New: Block name field is now searchable #70 * New: Import / export functionality #71 * New: Box shadow action #81 +* New: 3D Rotate - new transform origin option * Fixed: Hover interaction glitches when hovering too fast #9 * Fixed: On enter viewport doesn't always trigger when on mobile #23 * Fixed: Confetti action - selecting window will no longer show a display target warning message #74 diff --git a/scripts/package.js b/scripts/package.js index 86f080a..a1e5c95 100644 --- a/scripts/package.js +++ b/scripts/package.js @@ -292,6 +292,34 @@ function cleanupEmptyDirectories( dir ) { } } +function updatePluginHeaderVersion( buildDir, suffix ) { + if ( ! suffix ) { + return + } + + const pluginFileName = IS_PREMIUM_BUILD ? 'plugin.php' : 'interactions.php' + const pluginFilePath = path.join( buildDir, pluginFileName ) + + if ( ! fs.existsSync( pluginFilePath ) ) { + return + } + + let content = fs.readFileSync( pluginFilePath, 'utf8' ) + // Append folder suffix to version in plugin header + content = content.replace( + /^(\s*\*\s*Version:\s*)([^\r\n]+)/m, + ( match, prefix, version ) => { + // Only append if suffix is not already present + if ( ! version.includes( suffix ) ) { + return prefix + version + '-' + suffix + } + return match + } + ) + fs.writeFileSync( pluginFilePath, content ) + console.log( `📝 Updated version in ${ pluginFileName } to include suffix: ${ suffix }` ) +} + async function packagePlugin() { console.log( '🚀 Starting plugin packaging...' ) console.log( `📦 Build type: ${ IS_PREMIUM_BUILD ? 'Premium' : 'Free' }` ) @@ -312,6 +340,16 @@ async function packagePlugin() { } } + // Rename interactions.php to plugin.php for premium builds only + if ( IS_PREMIUM_BUILD ) { + const oldPath = path.join( BUILD_DIR, 'interactions.php' ) + const newPath = path.join( BUILD_DIR, 'plugin.php' ) + if ( fs.existsSync( oldPath ) ) { + fs.renameSync( oldPath, newPath ) + console.log( '📝 Renamed interactions.php to plugin.php for premium build' ) + } + } + console.log( '📁 Copying source directories...' ) // Pass isSrcRoot = true for the top-level src folder copyDir( 'src', path.join( BUILD_DIR, 'src' ), true ) @@ -322,6 +360,9 @@ async function packagePlugin() { console.log( '🔒 Adding security index.php files...' ) addSecurityFiles( BUILD_DIR ) + console.log( '📝 Updating plugin header version...' ) + updatePluginHeaderVersion( BUILD_DIR, folderSuffix ) + console.log( '🧹 Cleaning up empty directories...' ) cleanupEmptyDirectories( BUILD_DIR ) diff --git a/src/action-types/abstract-action-type.php b/src/action-types/abstract-action-type.php index e84a7a7..c52efd6 100644 --- a/src/action-types/abstract-action-type.php +++ b/src/action-types/abstract-action-type.php @@ -256,5 +256,155 @@ public function initilize_action( $action, $animation_data ) { return $action; } + + /** + * Sanitizes the action's value before saving. + * + * Override this in a child class to implement specific sanitization. + * + * @param mixed $value The action value to sanitize. + * @return mixed The sanitized action value. + */ + public function sanitize_data_for_saving( $value ) { + // By default, no sanitization is applied. + return $value; + } + + /** + * Remove any `expression(...)` and `javascript:` content from a CSS style string for security. + * + * @param string $string + * @return string + */ + public function sanitize_style_value( $string ) { + if ( ! is_string( $string ) ) { + return $string; + } + // Remove all expression(...) (case-insensitive). + $string = preg_replace( '/expression\s*\((?:[^\(\)]|(?R))*\)/i', '', $string ); + + // Remove all javascript: URIs (case-insensitive). + $string = preg_replace( '/javascript\s*:/i', '', $string ); + + return $string; + } + + /** + * Detect if an HTML tag is considered dangerous (can execute scripts or + * otherwise modify page behavior). + * + * @param string $tag_name + * @return bool + */ + public function is_dangerous_tag( $tag_name ) { + if ( empty( $tag_name ) || ! is_string( $tag_name ) ) { + return false; + } + + $tag_name = strtolower( trim( $tag_name ) ); + + // Tags that can execute scripts or modify page behavior + $dangerous_tags = [ + 'script', + 'iframe', + 'object', + 'embed', + 'applet', + 'meta', + 'link', + 'style', + 'base', + 'form', + ]; + + return in_array( $tag_name, $dangerous_tags, true ); + } + + /** + * Detect if an HTML attribute is considered dangerous (event handlers, + * attributes that can contain JS URIs, form actions, etc.). + * + * @param string $attribute_name + * @return bool + */ + public function is_dangerous_attribute( $attribute_name ) { + if ( empty( $attribute_name ) || ! is_string( $attribute_name ) ) { + return false; + } + + $attribute_name = strtolower( trim( $attribute_name ) ); + + // Event handler attributes (onclick, onerror, onload, etc.) + if ( preg_match( '/^on[a-z]+/', $attribute_name ) ) { + return true; + } + + // Attributes that can contain JavaScript URIs or code + $dangerous_attributes = [ + 'href', + 'src', + 'action', + 'formaction', + 'form', + 'formmethod', + 'formtarget', + ]; + + return in_array( $attribute_name, $dangerous_attributes, true ); + } + + /** + * Validate an HTML snippet for dangerous tags, attributes or protocols. + * Returns true when safe, or a WP_Error describing the violation. + * + * @param string $html + * @return true|WP_Error + */ + public function validate_html_for_saving( $html ) { + if ( ! is_string( $html ) ) { + return new WP_Error( + 'invalid_html', + __( 'HTML must be a string.', 'interactions' ) + ); + } + + // Detect dangerous tags + if ( preg_match_all( '/<\s*([a-z0-9\-]+)/i', $html, $matches ) ) { + foreach ( $matches[1] as $tag ) { + if ( $this->is_dangerous_tag( $tag ) ) { + return new WP_Error( + 'invalid_tag', + sprintf( __( 'The HTML tag "%s" is not allowed.', 'interactions' ), esc_html( $tag ) ) + ); + } + } + } + + // Detect dangerous attributes + if ( preg_match_all( '/<[^>]+>/i', $html, $tagMatches ) ) { + foreach ( $tagMatches[0] as $tagString ) { + if ( preg_match_all( '/([a-zA-Z0-9:\-]+)\s*=\s*(?:"[^"]*"|\'[^\']*\'|[^\s>]+)/i', $tagString, $attrMatches ) ) { + foreach ( $attrMatches[1] as $attr ) { + if ( $this->is_dangerous_attribute( $attr ) ) { + return new WP_Error( + 'invalid_attribute', + sprintf( __( 'The HTML attribute "%s" is not allowed.', 'interactions' ), esc_html( $attr ) ) + ); + } + } + } + } + } + + // Detect disallowed protocols + if ( preg_match( '/javascript:\s*/i', $html ) || preg_match( '/data:\s*text\//i', $html ) ) { + return new WP_Error( + 'invalid_protocol', + __( 'The HTML contains disallowed protocols (javascript: or data:).', 'interactions' ) + ); + } + + return true; + } } } diff --git a/src/action-types/class-action-type-background-color.php b/src/action-types/class-action-type-background-color.php index 4b02e78..ea1225d 100644 --- a/src/action-types/class-action-type-background-color.php +++ b/src/action-types/class-action-type-background-color.php @@ -52,6 +52,13 @@ public function initialize() { // return parent::initilize_action( $action, $animation_data ); // } + + public function sanitize_data_for_saving( $value ) { + if ( is_array( $value ) && isset( $value['color'] ) ) { + $value['color'] = $this->sanitize_style_value( $value['color'] ); + } + return $value; + } } interact_add_action_type( 'backgroundColor', 'Interact_Action_Type_Background_Color' ); diff --git a/src/action-types/class-action-type-background-image.php b/src/action-types/class-action-type-background-image.php index 4187202..62bda13 100644 --- a/src/action-types/class-action-type-background-image.php +++ b/src/action-types/class-action-type-background-image.php @@ -33,6 +33,13 @@ public function initialize() { $this->has_dynamic = false; } + + public function sanitize_data_for_saving( $value ) { + if ( is_array( $value ) && isset( $value['image'] ) ) { + $value['image'] = $this->sanitize_style_value( $value['image'] ); + } + return $value; + } } interact_add_action_type( 'backgroundImage', 'Interact_Action_Type_Background_Image' ); diff --git a/src/action-types/class-action-type-css-rule.php b/src/action-types/class-action-type-css-rule.php index ad8a184..79822fd 100644 --- a/src/action-types/class-action-type-css-rule.php +++ b/src/action-types/class-action-type-css-rule.php @@ -40,6 +40,13 @@ public function initialize() { ], ]; } + + public function sanitize_data_for_saving( $value ) { + if ( is_array( $value ) && isset( $value['value'] ) ) { + $value['value'] = $this->sanitize_style_value( $value['value'] ); + } + return $value; + } } interact_add_action_type( 'cssRule', 'Interact_Action_Type_Css_Rule' ); diff --git a/src/action-types/class-action-type-display.php b/src/action-types/class-action-type-display.php index c3aac36..6f1e839 100644 --- a/src/action-types/class-action-type-display.php +++ b/src/action-types/class-action-type-display.php @@ -55,6 +55,29 @@ public function initialize() { $this->has_duration = false; $this->has_easing = false; } + + public function sanitize_data_for_saving( $value ) { + if ( is_array( $value ) && isset( $value['display'] ) ) { + $allowed_values = [ + 'block', + 'none', + 'inline', + 'inline-block', + 'flex', + 'inline-flex', + 'grid', + 'inline-grid', + 'initial', + 'inherit', + 'revert', + 'unset', + ]; + if ( ! in_array( $value['display'], $allowed_values, true ) ) { + $value['display'] = 'block'; + } + } + return $value; + } } interact_add_action_type( 'display', 'Interact_Action_Type_Display' ); diff --git a/src/action-types/class-action-type-move.php b/src/action-types/class-action-type-move.php index 0b277ed..a17a8fd 100644 --- a/src/action-types/class-action-type-move.php +++ b/src/action-types/class-action-type-move.php @@ -58,6 +58,21 @@ public function initialize() { $this->has_dynamic = false; } + + public function sanitize_data_for_saving( $value ) { + // Ensure x, y, z are sanitized as numeric (including negatives and decimals), otherwise set to null (but leave blank as is) + foreach ( [ 'x', 'y', 'z' ] as $key ) { + if ( isset( $value[ $key ] ) && $value[ $key ] !== '' ) { + // Allow negative/positive/decimal + if ( is_numeric( $value[ $key ] ) ) { + $value[ $key ] = $value[ $key ] + 0; // Cast to int or float + } else { + $value[ $key ] = null; + } + } + } + return $value; + } } interact_add_action_type( 'move', 'Interact_Action_Type_Move' ); diff --git a/src/action-types/class-action-type-opacity.php b/src/action-types/class-action-type-opacity.php index 4a496aa..8284f4c 100644 --- a/src/action-types/class-action-type-opacity.php +++ b/src/action-types/class-action-type-opacity.php @@ -36,6 +36,17 @@ public function initialize() { $this->has_dynamic = false; } + + public function sanitize_data_for_saving( $value ) { + if ( is_array( $value ) && isset( $value['opacity'] ) ) { + if ( is_numeric( $value['opacity'] ) ) { + $value['opacity'] = $value['opacity'] + 0; + } else { + $value['opacity'] = null; + } + } + return $value; + } } interact_add_action_type( 'opacity', 'Interact_Action_Type_Opacity' ); diff --git a/src/action-types/class-action-type-redirect-to-url.php b/src/action-types/class-action-type-redirect-to-url.php index 93360f9..acc7833 100644 --- a/src/action-types/class-action-type-redirect-to-url.php +++ b/src/action-types/class-action-type-redirect-to-url.php @@ -37,6 +37,17 @@ public function initialize() { $this->has_easing = false; $this->has_preview = false; } + + public function sanitize_data_for_saving( $value ) { + if ( is_array( $value ) && isset( $value['url'] ) ) { + if ( is_string( $value['url'] ) ) { + $value['url'] = esc_url( $value['url'] ); + } else { + $value['url'] = null; + } + } + return $value; + } } interact_add_action_type( 'redirectToUrl', 'Interact_Action_Type_Redirect_To_Url' ); diff --git a/src/action-types/class-action-type-rotate.php b/src/action-types/class-action-type-rotate.php index 2a74918..7d914c5 100644 --- a/src/action-types/class-action-type-rotate.php +++ b/src/action-types/class-action-type-rotate.php @@ -63,6 +63,39 @@ public function initialize() { $this->has_dynamic = false; } + + public function sanitize_data_for_saving( $value ) { + if ( is_array( $value ) && isset( $value['rotate'] ) ) { + if ( is_numeric( $value['rotate'] ) ) { + $value['rotate'] = $value['rotate'] + 0; + } else { + $value['rotate'] = null; + } + } + + if ( is_array( $value ) && isset( $value['transformOrigin'] ) ) { + $allowed_transform_origins = [ + 'center', + 'top', + 'right', + 'bottom', + 'left', + 'top left', + 'top right', + 'bottom left', + 'bottom right', + 'custom', + ]; + if ( ! in_array( $value['transformOrigin'], $allowed_transform_origins, true ) ) { + $value['transformOrigin'] = 'center'; + } + } + + if ( isset( $value['customTransformOrigin'] ) ) { + $value['customTransformOrigin'] = $this->sanitize_style_value( $value['customTransformOrigin'] ); + } + return $value; + } } interact_add_action_type( 'rotate', 'Interact_Action_Type_Rotate' ); diff --git a/src/action-types/class-action-type-scale.php b/src/action-types/class-action-type-scale.php index 60748f0..b83b61e 100644 --- a/src/action-types/class-action-type-scale.php +++ b/src/action-types/class-action-type-scale.php @@ -46,6 +46,24 @@ public function initialize() { $this->has_dynamic = false; } + + public function sanitize_data_for_saving( $value ) { + if ( is_array( $value ) && isset( $value['x'] ) ) { + if ( is_numeric( $value['x'] ) ) { + $value['x'] = $value['x'] + 0; + } else { + $value['x'] = null; + } + } + if ( is_array( $value ) && isset( $value['y'] ) ) { + if ( is_numeric( $value['y'] ) ) { + $value['y'] = $value['y'] + 0; + } else { + $value['y'] = null; + } + } + return $value; + } } interact_add_action_type( 'scale', 'Interact_Action_Type_Scale' ); diff --git a/src/action-types/class-action-type-skew.php b/src/action-types/class-action-type-skew.php index 59be79a..3a965f5 100644 --- a/src/action-types/class-action-type-skew.php +++ b/src/action-types/class-action-type-skew.php @@ -46,6 +46,24 @@ public function initialize() { $this->has_dynamic = false; } + + public function sanitize_data_for_saving( $value ) { + if ( is_array( $value ) && isset( $value['x'] ) ) { + if ( is_numeric( $value['x'] ) ) { + $value['x'] = $value['x'] + 0; + } else { + $value['x'] = null; + } + } + if ( is_array( $value ) && isset( $value['y'] ) ) { + if ( is_numeric( $value['y'] ) ) { + $value['y'] = $value['y'] + 0; + } else { + $value['y'] = null; + } + } + return $value; + } } interact_add_action_type( 'skew', 'Interact_Action_Type_Skew' ); diff --git a/src/action-types/class-action-type-text-color.php b/src/action-types/class-action-type-text-color.php index d166032..dcb21ef 100644 --- a/src/action-types/class-action-type-text-color.php +++ b/src/action-types/class-action-type-text-color.php @@ -34,6 +34,13 @@ public function initialize() { $this->has_dynamic = false; } + + public function sanitize_data_for_saving( $value ) { + if ( is_array( $value ) && isset( $value['color'] ) ) { + $value['color'] = $this->sanitize_style_value( $value['color'] ); + } + return $value; + } } interact_add_action_type( 'textColor', 'Interact_Action_Type_Text_Color' ); diff --git a/src/action-types/class-action-type-toggle-class.php b/src/action-types/class-action-type-toggle-class.php index 8b6aa61..873ebf2 100644 --- a/src/action-types/class-action-type-toggle-class.php +++ b/src/action-types/class-action-type-toggle-class.php @@ -35,14 +35,14 @@ public function initialize() { 'name' => 'Action', 'type' => 'select', 'default' => 'add', - 'options' => [ - // Translators: %s is the word 'class'. - [ 'value' => 'add', 'label' => sprintf( __( 'Add %s', 'interactions' ), __( 'class', 'interactions' ) ) ], - // Translators: %s is the word 'class'. - [ 'value' => 'remove', 'label' => sprintf( __( 'Remove %s', 'interactions' ), __( 'class', 'interactions' ) ) ], - // Translators: %s is the word 'class'. - [ 'value' => 'toggle', 'label' => sprintf( __( 'Toggle %s', 'interactions' ), __( 'class', 'interactions' ) ) ], - ] + 'options' => [ + // Translators: %s is the word 'class'. + [ 'value' => 'add', 'label' => sprintf( __( 'Add %s', 'interactions' ), __( 'class', 'interactions' ) ) ], + // Translators: %s is the word 'class'. + [ 'value' => 'remove', 'label' => sprintf( __( 'Remove %s', 'interactions' ), __( 'class', 'interactions' ) ) ], + // Translators: %s is the word 'class'. + [ 'value' => 'toggle', 'label' => sprintf( __( 'Toggle %s', 'interactions' ), __( 'class', 'interactions' ) ) ], + ] ], ]; @@ -50,6 +50,21 @@ public function initialize() { $this->has_duration = false; $this->has_easing = false; } + + public function sanitize_data_for_saving( $value ) { + if ( is_array( $value ) && isset( $value['class'] ) ) { + $value['class'] = sanitize_html_class( $value['class'] ); + } + + if ( is_array( $value ) && isset( $value['action'] ) ) { + $allowed_actions = [ 'add', 'remove', 'toggle' ]; + if ( ! in_array( $value['action'], $allowed_actions, true ) ) { + $value['action'] = 'add'; + } + } + + return $value; + } } interact_add_action_type( 'toggleClass', 'Interact_Action_Type_Toggle_Class' ); diff --git a/src/action-types/class-action-type-toggle-video.php b/src/action-types/class-action-type-toggle-video.php index 5f92293..42c864e 100644 --- a/src/action-types/class-action-type-toggle-video.php +++ b/src/action-types/class-action-type-toggle-video.php @@ -54,6 +54,23 @@ public function initialize() { $this->has_duration = false; $this->has_easing = false; } + + public function sanitize_data_for_saving( $value ) { + if ( is_array( $value ) && isset( $value['mode'] ) ) { + $allowed_modes = [ 'play', 'pause', 'toggle' ]; + if ( ! in_array( $value['mode'], $allowed_modes, true ) ) { + $value['mode'] = 'play'; + } + } + if ( is_array( $value ) && isset( $value['startTime'] ) ) { + if ( is_numeric( $value['startTime'] ) ) { + $value['startTime'] = $value['startTime'] + 0; + } else { + $value['startTime'] = null; + } + } + return $value; + } } interact_add_action_type( 'toggleVideo', 'Interact_Action_Type_Toggle_Video' ); diff --git a/src/action-types/class-action-type-update-attribute.php b/src/action-types/class-action-type-update-attribute.php index 1895fa2..4c342da 100644 --- a/src/action-types/class-action-type-update-attribute.php +++ b/src/action-types/class-action-type-update-attribute.php @@ -30,6 +30,7 @@ public function initialize() { 'name' => 'Attribute name', 'type' => 'text', 'default' => '', + 'restrictedNotice' => __( 'Some attribute names and values are disallowed unless you are an administrator with unfiltered_html capability for security reasons.', 'interactions' ), ], 'value' => [ 'name' => 'Value', @@ -57,6 +58,52 @@ public function initialize() { $this->has_duration = false; $this->has_easing = false; } + + public function sanitize_data_for_saving( $value ) { + // Sanitize action value: ensure $value is an array and attribute/value are strings. + if ( ! is_array( $value ) ) { + return new WP_Error( + 'invalid_structure', + __( 'Value must be an array containing attribute and value keys.', 'interactions' ) + ); + } + + // Sanitize attribute name + if ( isset( $value['attribute'] ) && is_string( $value['attribute'] ) ) { + $value['attribute'] = sanitize_key( $value['attribute'] ); + } + + // Sanitize value if present, and convert to string. + if ( isset( $value['value'] ) ) { + $value['value'] = $this->sanitize_style_value( $value['value'] ); + } + + // Sanitize action field for select option. + if ( isset( $value['action'] ) ) { + $allowed_actions = [ 'update', 'remove', 'toggle' ]; + if ( ! in_array( $value['action'], $allowed_actions, true ) ) { + $value['action'] = 'update'; + } + } + + if ( current_user_can( 'unfiltered_html' ) ) { + return $value; + } + + if ( ! empty( $value['attribute'] ) && $this->is_dangerous_attribute( $value['attribute'] ) ) { + // Only allow dangerous attributes if user has unfiltered_html capability + return new WP_Error( + 'invalid_attribute', + sprintf( + // Translators: %s is the attribute name. + __( 'The attribute "%s" requires administrator privileges with unfiltered_html capability to prevent security vulnerabilities.', 'interactions' ), + esc_html( $value['attribute'] ) + ) + ); + } + + return $value; + } } interact_add_action_type( 'updateAttribute', 'Interact_Action_Type_Update_Attribute' ); diff --git a/src/action-types/class-action-type-visibility.php b/src/action-types/class-action-type-visibility.php index bf551c0..3054db5 100644 --- a/src/action-types/class-action-type-visibility.php +++ b/src/action-types/class-action-type-visibility.php @@ -40,6 +40,16 @@ public function initialize() { $this->has_easing = false; $this->has_dynamic = false; } + + public function sanitize_data_for_saving( $value ) { + if ( is_array( $value ) && isset( $value['visibility'] ) ) { + $allowed_visibilities = [ 'toggle', 'hide', 'show' ]; + if ( ! in_array( $value['visibility'], $allowed_visibilities, true ) ) { + $value['visibility'] = 'toggle'; + } + } + return $value; + } } interact_add_action_type( 'visibility', 'Interact_Action_Type_Visibility' ); diff --git a/src/class-interaction.php b/src/class-interaction.php index 84f7eb6..fbd9c30 100644 --- a/src/class-interaction.php +++ b/src/class-interaction.php @@ -59,13 +59,18 @@ function __construct( $post ) { * @return int|WP_Error */ public static function update( $interaction_data ) { + $sanitized_data = self::secure_interaction_data( $interaction_data ); + if ( is_wp_error( $sanitized_data ) ) { + return $sanitized_data; + } + $post_arr = [ 'ID' => self::get_post_id_from_key( $interaction_data['key'] ), 'post_type' => 'interact-interaction', 'post_name' => $interaction_data['key'], 'post_title' => $interaction_data['title'], // TODO: emojis do not work somehow. - 'post_content' => wp_slash( maybe_serialize( self::secure_interaction_data( $interaction_data ) ) ), + 'post_content' => wp_slash( maybe_serialize( $sanitized_data ) ), 'post_status' => $interaction_data['active'] ? 'publish' : 'interact-inactive', ]; $result = $post_arr['ID'] === 0 ? wp_insert_post( $post_arr ) : wp_update_post( $post_arr ); @@ -78,6 +83,30 @@ public static function update( $interaction_data ) { return $result; } + /** + * Sanitizes the interaction value + * + * @param mixed $value + * @return mixed + */ + public static function sanitize_interaction_value( $value ) { + if ( current_user_can( 'unfiltered_html' ) ) { + return $value; + } + + if ( is_array( $value ) ) { + foreach ( $value as $key => $val ) { + $value[ $key ] = self::sanitize_interaction_value( $val ); + } + } + + if ( is_string( $value ) ) { + $value = wp_kses_post( $value ); + } + + return $value; + } + /** * Runs through all interaction data, and if the action-type has a * $verify_integrity set to true, it will hash and sign the action's @@ -92,8 +121,19 @@ public static function secure_interaction_data( $interaction_data ) { $action_type = $action['type']; $action_config = interact_get_action_type( $action_type ); - $action_value = $action['value']; + // Sanitize the action value for saving. + $action_value = self::sanitize_interaction_value( $action['value'] ); + // Sanitize for specific action type. + $action_value = $action_config->sanitize_data_for_saving( $action_value ); + + // If the action value is a WP_Error, return the error. + if ( is_wp_error( $action_value ) ) { + return $action_value; + } + + $interaction_data['timelines'][ $timeline_index ]['actions'][ $action_index ]['value'] = $action_value; + if ( $action_config->verify_integrity ) { $signature = hash_hmac( 'sha256', wp_json_encode( $action_value ), interact_salt() ); diff --git a/src/editor/components/timeline/property-control.js b/src/editor/components/timeline/property-control.js index f142c55..9b63590 100644 --- a/src/editor/components/timeline/property-control.js +++ b/src/editor/components/timeline/property-control.js @@ -10,6 +10,7 @@ import { TextControl, TextareaControl, ToggleControl, + Tooltip, __experimentalNumberControl as NumberControl, __experimentalHStack as HStack, } from '@wordpress/components' @@ -17,7 +18,7 @@ import { __ } from '@wordpress/i18n' import { clamp } from 'lodash' import ColorControl from '../color-control' import SVGBolt from './images/bolt.svg' -import { plan } from 'interactions' +import { plan, currentUserCanUnfilteredHtml } from 'interactions' const NOOP = () => {} const EMPTYARR = [] @@ -49,6 +50,29 @@ export const PropertyControl = props => { ) : null + // Add a restriction notice. + if ( property.restrictedNotice && ! currentUserCanUnfilteredHtml ) { + help = ( + <> + { help }{ ' ' } + { property.restrictedNotice.replace( /<[^>]+>/g, '' ) } } + > + + { __( 'Has restrictions', 'interactions' ) } + + + + ) + } + // Allow the property to override the config for if the option is dynamic. const { hasDynamic: _propertyHasDynamic = true, @@ -148,7 +172,7 @@ export const PropertyControl = props => { const propsToPass = {} if ( property.type === 'id' ) { const idHelp = __( 'Give this data a unique name to make it available to other actions. Can only be lowercase letters, numbers and underscore.', 'interactions' ) - if ( property.help ) { + if ( help ) { help = <>{ help } { idHelp } } else { help = idHelp diff --git a/src/editor/editor.php b/src/editor/editor.php index 68b98a2..a964602 100644 --- a/src/editor/editor.php +++ b/src/editor/editor.php @@ -76,6 +76,7 @@ public function enqueue_editor() { 'restUrl' => trailingslashit( esc_url_raw( rest_url() ) ), // We need to know how to access the REST API. 'restNonce' => wp_create_nonce( 'wp_rest' ), // This needs to be 'wp_rest' to use the built-in nonce verification. 'srcUrl' => untrailingslashit( plugins_url( '/', INTERACT_FILE ) ), + 'currentUserCanUnfilteredHtml' => current_user_can( 'unfiltered_html' ), ) ); wp_localize_script( 'interact-editor', 'interactions', $args ); } diff --git a/src/freemius.php b/src/freemius.php index 5dec256..f0a1434 100644 --- a/src/freemius.php +++ b/src/freemius.php @@ -601,7 +601,7 @@ function __interact_mask_license_key( $key ) {

- + @@ -610,68 +610,70 @@ function __interact_mask_license_key( $key ) { LINK;