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 }{ ' ' }
+
- + @@ -610,68 +610,70 @@ function __interact_mask_license_key( $key ) { LINK;