From 16016d72fd5ac39d2e74bced093321f3b90f4d82 Mon Sep 17 00:00:00 2001 From: Gabriel de Tassigny Date: Tue, 27 Jan 2026 12:27:19 +0100 Subject: [PATCH 1/7] Handle elementor background image replacement --- php/class-plugin.php | 4 +- php/integrations/class-elementor.php | 106 +++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 php/integrations/class-elementor.php diff --git a/php/class-plugin.php b/php/class-plugin.php index 2a775b2d..8fc84875 100644 --- a/php/class-plugin.php +++ b/php/class-plugin.php @@ -14,6 +14,7 @@ use Cloudinary\Delivery\Lazy_Load; use Cloudinary\Delivery\Responsive_Breakpoints; use Cloudinary\Assets as CLD_Assets; +use Cloudinary\Integrations\Elementor; use Cloudinary\Integrations\WPML; use Cloudinary\Media\Gallery; use Cloudinary\Sync\Storage; @@ -31,7 +32,7 @@ final class Plugin { * * @since 0.1 * - * @var Admin|CLD_Assets|Connect|Dashboard|Deactivation|Delivery|Extensions|Gallery|Lazy_Load|Media|Meta_Box|Relate|Report|Responsive_Breakpoints|REST_API|State|Storage|SVG|Sync|URL[]|WPML|null + * @var Admin|CLD_Assets|Connect|Dashboard|Deactivation|Delivery|Extensions|Gallery|Lazy_Load|Media|Meta_Box|Relate|Report|Responsive_Breakpoints|REST_API|State|Storage|SVG|Sync|URL[]|WPML|Elementor|null */ public $components; /** @@ -136,6 +137,7 @@ public function plugins_loaded() { $this->components['metabox'] = new Meta_Box( $this ); $this->components['url'] = new URL( $this ); $this->components['wpml'] = new WPML( $this ); + $this->components['elementor'] = new Elementor( $this ); $this->components['special_offer'] = new Special_Offer( $this ); } diff --git a/php/integrations/class-elementor.php b/php/integrations/class-elementor.php new file mode 100644 index 00000000..534a043c --- /dev/null +++ b/php/integrations/class-elementor.php @@ -0,0 +1,106 @@ +plugin->get_component( 'media' ); + $settings = $element->get_settings_for_display(); + + // Define all background image related keys and their specificities. + $background_keys = array( + '_background_image' => array( + 'device' => 'desktop', + 'suffix' => '', + ), + '_background_hover_image' => array( + 'device' => 'desktop', + 'suffix' => ':hover', + ), + '_background_image_tablet' => array( + 'device' => 'tablet', + 'suffix' => '', + ), + '_background_hover_image_tablet' => array( + 'device' => 'tablet', + 'suffix' => ':hover', + ), + '_background_image_mobile' => array( + 'device' => 'mobile', + 'suffix' => '', + ), + '_background_hover_image_mobile' => array( + 'device' => 'mobile', + 'suffix' => ':hover', + ), + ); + + foreach ( $background_keys as $background_key => $background_data ) { + if ( ! isset( $settings[ $background_key ]['url'], $settings[ $background_key ]['id'] ) ) { + continue; + } + + $original_url = $settings[ $background_key ]['url']; + $media_id = $settings[ $background_key ]['id']; + $media_size = isset( $settings[ $background_key ]['size'] ) ? $settings[ $background_key ]['size'] : array(); + + $cloudinary_url = $media->cloudinary_url( $media_id, $media_size ); + + // Skip if the URL is unchanged or the cloudinary URL cannot be generated somehow. + if ( ! $cloudinary_url || $cloudinary_url === $original_url ) { + continue; + } + + // Build the CSS selector and rule. + $css_selector = $post_css->get_element_unique_selector( $element ) . $background_data['suffix']; + $css_rule = array( 'background-image' => "url('$cloudinary_url')" ); + + // Retrieve the specific media query rule for non-desktop devices. + $media_query = null; + $device = $background_data['device']; // either 'desktop', 'tablet' or 'mobile'. + if ( 'desktop' !== $device ) { + $media_query = array( 'max' => $device ); + } + + $post_css->get_stylesheet()->add_rules( $css_selector, $css_rule, $media_query ); + } + } +} From 8239f2ac966d7755bd467e3a98415645a31625d3 Mon Sep 17 00:00:00 2001 From: Gabriel de Tassigny Date: Tue, 27 Jan 2026 12:49:43 +0100 Subject: [PATCH 2/7] Handle more edge cases --- php/integrations/class-elementor.php | 81 ++++++++++++++++------------ 1 file changed, 47 insertions(+), 34 deletions(-) diff --git a/php/integrations/class-elementor.php b/php/integrations/class-elementor.php index 534a043c..6c5967c5 100644 --- a/php/integrations/class-elementor.php +++ b/php/integrations/class-elementor.php @@ -16,6 +16,38 @@ */ class Elementor extends Integrations { + /** + * List of Elementor background image settings keys, along with their device and CSS suffix. + * + * @var array + */ + const ELEMENTOR_BACKGROUND_IMAGES = array( + '_background_image' => array( + 'device' => 'desktop', + 'suffix' => '', + ), + '_background_hover_image' => array( + 'device' => 'desktop', + 'suffix' => ':hover', + ), + '_background_image_tablet' => array( + 'device' => 'tablet', + 'suffix' => '', + ), + '_background_hover_image_tablet' => array( + 'device' => 'tablet', + 'suffix' => ':hover', + ), + '_background_image_mobile' => array( + 'device' => 'mobile', + 'suffix' => '', + ), + '_background_hover_image_mobile' => array( + 'device' => 'mobile', + 'suffix' => ':hover', + ), + ); + /** * Check if the integration can be enabled. * @@ -42,38 +74,16 @@ public function register_hooks() { * @return void */ public function replace_bg_images_in_css( $post_css, $element ) { - $media = $this->plugin->get_component( 'media' ); - $settings = $element->get_settings_for_display(); + $settings = $element->get_settings_for_display(); + $media = $this->plugin->get_component( 'media' ); + $delivery = $this->plugin->get_component( 'delivery' ); - // Define all background image related keys and their specificities. - $background_keys = array( - '_background_image' => array( - 'device' => 'desktop', - 'suffix' => '', - ), - '_background_hover_image' => array( - 'device' => 'desktop', - 'suffix' => ':hover', - ), - '_background_image_tablet' => array( - 'device' => 'tablet', - 'suffix' => '', - ), - '_background_hover_image_tablet' => array( - 'device' => 'tablet', - 'suffix' => ':hover', - ), - '_background_image_mobile' => array( - 'device' => 'mobile', - 'suffix' => '', - ), - '_background_hover_image_mobile' => array( - 'device' => 'mobile', - 'suffix' => ':hover', - ), - ); + if ( ! $media || ! $delivery ) { + return; + } - foreach ( $background_keys as $background_key => $background_data ) { + foreach ( self::ELEMENTOR_BACKGROUND_IMAGES as $background_key => $background_data ) { + // We need to have both URL and ID from the image to proceed. if ( ! isset( $settings[ $background_key ]['url'], $settings[ $background_key ]['id'] ) ) { continue; } @@ -82,13 +92,16 @@ public function replace_bg_images_in_css( $post_css, $element ) { $media_id = $settings[ $background_key ]['id']; $media_size = isset( $settings[ $background_key ]['size'] ) ? $settings[ $background_key ]['size'] : array(); - $cloudinary_url = $media->cloudinary_url( $media_id, $media_size ); - - // Skip if the URL is unchanged or the cloudinary URL cannot be generated somehow. - if ( ! $cloudinary_url || $cloudinary_url === $original_url ) { + // Skip if the media is not deliverable via Cloudinary. + if ( ! $delivery->is_deliverable( $media_id ) ) { continue; } + // If the original URL is already a Cloudinary URL, use it directly; otherwise, generate the Cloudinary URL. + $cloudinary_url = $media->is_cloudinary_url( $original_url ) + ? $original_url + : $media->cloudinary_url( $media_id, $media_size ); + // Build the CSS selector and rule. $css_selector = $post_css->get_element_unique_selector( $element ) . $background_data['suffix']; $css_rule = array( 'background-image' => "url('$cloudinary_url')" ); From e8eae5ec7f06bf1988028651747d30f6c6592ee8 Mon Sep 17 00:00:00 2001 From: Gabriel de Tassigny Date: Tue, 27 Jan 2026 13:08:19 +0100 Subject: [PATCH 3/7] Handle cache flushing for Elementor when cloudinary settings are changed --- php/integrations/class-elementor.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/php/integrations/class-elementor.php b/php/integrations/class-elementor.php index 6c5967c5..d50fd609 100644 --- a/php/integrations/class-elementor.php +++ b/php/integrations/class-elementor.php @@ -64,6 +64,7 @@ public function can_enable() { */ public function register_hooks() { add_action( 'elementor/element/parse_css', array( $this, 'replace_bg_images_in_css' ), 10, 2 ); + add_action( 'cloudinary_flush_cache', array( $this, 'clear_elementor_css_cache' ) ); } /** @@ -116,4 +117,17 @@ public function replace_bg_images_in_css( $post_css, $element ) { $post_css->get_stylesheet()->add_rules( $css_selector, $css_rule, $media_query ); } } + + /** + * Clear Elementor CSS cache. + * This is called when Cloudinary cache is flushed, so that any change in media URLs is reflected in Elementor CSS files. + * + * @return void + */ + public function clear_elementor_css_cache() { + if ( class_exists( 'Elementor\Plugin' ) ) { + $elementor = Plugin::instance(); + $elementor->files_manager->clear_cache(); + } + } } From 959f7a433b463b50581ccd933fa7e7fe453e8ae8 Mon Sep 17 00:00:00 2001 From: Gabriel de Tassigny Date: Tue, 27 Jan 2026 14:02:28 +0100 Subject: [PATCH 4/7] Generate URL even if Elementor one seems to come from Cloudinary The one coming from Elementor doesn't contain all the options from the settings and therefore isn't accurate enough --- php/integrations/class-elementor.php | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/php/integrations/class-elementor.php b/php/integrations/class-elementor.php index d50fd609..01e1fe31 100644 --- a/php/integrations/class-elementor.php +++ b/php/integrations/class-elementor.php @@ -63,7 +63,7 @@ public function can_enable() { * @return void */ public function register_hooks() { - add_action( 'elementor/element/parse_css', array( $this, 'replace_bg_images_in_css' ), 10, 2 ); + add_action( 'elementor/element/parse_css', array( $this, 'replace_background_images_in_css' ), 10, 2 ); add_action( 'cloudinary_flush_cache', array( $this, 'clear_elementor_css_cache' ) ); } @@ -74,7 +74,7 @@ public function register_hooks() { * @param Element_Base $element The Elementor element. * @return void */ - public function replace_bg_images_in_css( $post_css, $element ) { + public function replace_background_images_in_css( $post_css, $element ) { $settings = $element->get_settings_for_display(); $media = $this->plugin->get_component( 'media' ); $delivery = $this->plugin->get_component( 'delivery' ); @@ -84,24 +84,21 @@ public function replace_bg_images_in_css( $post_css, $element ) { } foreach ( self::ELEMENTOR_BACKGROUND_IMAGES as $background_key => $background_data ) { - // We need to have both URL and ID from the image to proceed. - if ( ! isset( $settings[ $background_key ]['url'], $settings[ $background_key ]['id'] ) ) { + // We need to have the ID from the image to proceed. + if ( ! isset( $settings[ $background_key ]['id'] ) ) { continue; } - $original_url = $settings[ $background_key ]['url']; - $media_id = $settings[ $background_key ]['id']; - $media_size = isset( $settings[ $background_key ]['size'] ) ? $settings[ $background_key ]['size'] : array(); + $media_id = $settings[ $background_key ]['id']; + $media_size = isset( $settings[ $background_key ]['size'] ) ? $settings[ $background_key ]['size'] : array(); // Skip if the media is not deliverable via Cloudinary. if ( ! $delivery->is_deliverable( $media_id ) ) { continue; } - // If the original URL is already a Cloudinary URL, use it directly; otherwise, generate the Cloudinary URL. - $cloudinary_url = $media->is_cloudinary_url( $original_url ) - ? $original_url - : $media->cloudinary_url( $media_id, $media_size ); + // Generate the Cloudinary URL. + $cloudinary_url = $media->cloudinary_url( $media_id, $media_size ); // Build the CSS selector and rule. $css_selector = $post_css->get_element_unique_selector( $element ) . $background_data['suffix']; @@ -109,11 +106,11 @@ public function replace_bg_images_in_css( $post_css, $element ) { // Retrieve the specific media query rule for non-desktop devices. $media_query = null; - $device = $background_data['device']; // either 'desktop', 'tablet' or 'mobile'. - if ( 'desktop' !== $device ) { - $media_query = array( 'max' => $device ); + if ( 'desktop' !== $background_data['device'] ) { + $media_query = array( 'max' => $background_data['device'] ); } + // Override the CSS rule in Elementor. $post_css->get_stylesheet()->add_rules( $css_selector, $css_rule, $media_query ); } } From 3163d4725c8884d61627b7bba4de868947698b60 Mon Sep 17 00:00:00 2001 From: Gabriel de Tassigny Date: Thu, 29 Jan 2026 08:31:37 +0100 Subject: [PATCH 5/7] Support background keys without leading underscore --- php/integrations/class-elementor.php | 30 ++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/php/integrations/class-elementor.php b/php/integrations/class-elementor.php index 01e1fe31..2fba25bf 100644 --- a/php/integrations/class-elementor.php +++ b/php/integrations/class-elementor.php @@ -22,27 +22,27 @@ class Elementor extends Integrations { * @var array */ const ELEMENTOR_BACKGROUND_IMAGES = array( - '_background_image' => array( + 'background_image' => array( 'device' => 'desktop', 'suffix' => '', ), - '_background_hover_image' => array( + 'background_hover_image' => array( 'device' => 'desktop', 'suffix' => ':hover', ), - '_background_image_tablet' => array( + 'background_image_tablet' => array( 'device' => 'tablet', 'suffix' => '', ), - '_background_hover_image_tablet' => array( + 'background_hover_image_tablet' => array( 'device' => 'tablet', 'suffix' => ':hover', ), - '_background_image_mobile' => array( + 'background_image_mobile' => array( 'device' => 'mobile', 'suffix' => '', ), - '_background_hover_image_mobile' => array( + 'background_hover_image_mobile' => array( 'device' => 'mobile', 'suffix' => ':hover', ), @@ -84,13 +84,23 @@ public function replace_background_images_in_css( $post_css, $element ) { } foreach ( self::ELEMENTOR_BACKGROUND_IMAGES as $background_key => $background_data ) { - // We need to have the ID from the image to proceed. - if ( ! isset( $settings[ $background_key ]['id'] ) ) { + $background = null; + + if ( isset( $settings[ $background_key ] ) ) { + // Elementor section/column elements store background settings without a leading underscore. + $background = $settings[ $background_key ]; + } elseif ( isset( $settings[ '_' . $background_key ] ) ) { + // Elementor basic elements (e.g. heading) store background settings with a leading underscore. + $background = $settings[ '_' . $background_key ]; + } + + // If this specific background setting is not set, we can skip it and check for the next setting. + if ( empty( $background ) || ! isset( $background['id'] ) ) { continue; } - $media_id = $settings[ $background_key ]['id']; - $media_size = isset( $settings[ $background_key ]['size'] ) ? $settings[ $background_key ]['size'] : array(); + $media_id = $background['id']; + $media_size = isset( $background['size'] ) ? $background['size'] : array(); // Skip if the media is not deliverable via Cloudinary. if ( ! $delivery->is_deliverable( $media_id ) ) { From 0a06c637c85a5594d02296934df219d30d53c3fa Mon Sep 17 00:00:00 2001 From: Gabriel de Tassigny Date: Thu, 29 Jan 2026 09:18:04 +0100 Subject: [PATCH 6/7] Add default suffix for container --- php/integrations/class-elementor.php | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/php/integrations/class-elementor.php b/php/integrations/class-elementor.php index 2fba25bf..05100ab9 100644 --- a/php/integrations/class-elementor.php +++ b/php/integrations/class-elementor.php @@ -84,18 +84,20 @@ public function replace_background_images_in_css( $post_css, $element ) { } foreach ( self::ELEMENTOR_BACKGROUND_IMAGES as $background_key => $background_data ) { - $background = null; + $background = null; + $is_container = false; if ( isset( $settings[ $background_key ] ) ) { // Elementor section/column elements store background settings without a leading underscore. - $background = $settings[ $background_key ]; + $background = $settings[ $background_key ]; + $is_container = true; } elseif ( isset( $settings[ '_' . $background_key ] ) ) { // Elementor basic elements (e.g. heading) store background settings with a leading underscore. $background = $settings[ '_' . $background_key ]; } // If this specific background setting is not set, we can skip it and check for the next setting. - if ( empty( $background ) || ! isset( $background['id'] ) ) { + if ( empty( $background ) || empty( $background['id'] ) ) { continue; } @@ -110,9 +112,18 @@ public function replace_background_images_in_css( $post_css, $element ) { // Generate the Cloudinary URL. $cloudinary_url = $media->cloudinary_url( $media_id, $media_size ); + // If URL generation failed, we should leave the original URL within the CSS. + if ( empty( $cloudinary_url ) ) { + continue; + } + // Build the CSS selector and rule. - $css_selector = $post_css->get_element_unique_selector( $element ) . $background_data['suffix']; - $css_rule = array( 'background-image' => "url('$cloudinary_url')" ); + $unique_selector = $post_css->get_element_unique_selector( $element ); + + // Elementor applies this suffix rule to container background images to avoid conflicts with motion effects backgrounds. + $default_suffix = $is_container ? ':not(.elementor-motion-effects-element-type-background)' : ''; + $css_selector = $unique_selector . $default_suffix . $background_data['suffix']; + $css_rule = array( 'background-image' => "url('$cloudinary_url')" ); // Retrieve the specific media query rule for non-desktop devices. $media_query = null; From f290e728a5a7654e4fcfb7085539012bbfb8dac3 Mon Sep 17 00:00:00 2001 From: Gabriel de Tassigny Date: Thu, 29 Jan 2026 09:31:50 +0100 Subject: [PATCH 7/7] Apply defensive code in case Elementor changes its internal APIs --- php/integrations/class-elementor.php | 60 ++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/php/integrations/class-elementor.php b/php/integrations/class-elementor.php index 05100ab9..a6034b1e 100644 --- a/php/integrations/class-elementor.php +++ b/php/integrations/class-elementor.php @@ -75,6 +75,10 @@ public function register_hooks() { * @return void */ public function replace_background_images_in_css( $post_css, $element ) { + if ( ! method_exists( $element, 'get_settings_for_display' ) ) { + return; + } + $settings = $element->get_settings_for_display(); $media = $this->plugin->get_component( 'media' ); $delivery = $this->plugin->get_component( 'delivery' ); @@ -117,8 +121,11 @@ public function replace_background_images_in_css( $post_css, $element ) { continue; } - // Build the CSS selector and rule. - $unique_selector = $post_css->get_element_unique_selector( $element ); + $unique_selector = $this->find_unique_selector( $post_css, $element ); + // If we can't find a unique selector via Elementor's internal API, we can't do any replacement. + if ( null === $unique_selector ) { + return; + } // Elementor applies this suffix rule to container background images to avoid conflicts with motion effects backgrounds. $default_suffix = $is_container ? ':not(.elementor-motion-effects-element-type-background)' : ''; @@ -131,8 +138,11 @@ public function replace_background_images_in_css( $post_css, $element ) { $media_query = array( 'max' => $background_data['device'] ); } - // Override the CSS rule in Elementor. - $post_css->get_stylesheet()->add_rules( $css_selector, $css_rule, $media_query ); + $success = $this->override_elementor_css_rule( $post_css, $css_selector, $css_rule, $media_query ); + if ( ! $success ) { + // If we couldn't override the CSS rule, likely due to Elementor internal API changes, we should stop further processing. + return; + } } } @@ -148,4 +158,46 @@ public function clear_elementor_css_cache() { $elementor->files_manager->clear_cache(); } } + + /** + * Find the unique selector for an Elementor element. + * Double-checks if the method exists before calling it, to ensure compatibility with different Elementor versions. + * + * @param Post $post_css The post CSS object. + * @param Element_Base $element The Elementor element. + * + * @return string|null + */ + private function find_unique_selector( $post_css, $element ) { + if ( ! method_exists( $element, 'get_unique_selector' ) ) { + return null; + } + + return $post_css->get_element_unique_selector( $element ); + } + + /** + * Override the Elementor CSS rule for a specific selector. + * Double-checks if the method exists before calling it, to ensure compatibility with different Elementor versions. + * + * @param Post $post_css The post CSS object. + * @param string $css_selector The CSS selector. + * @param array $css_rule The CSS rule to apply. + * @param array|null $media_query The media query conditions. Null for default (desktop) styles. + * + * @return bool True if the rule could be overridden, false if the internal Elementor methods aren't available. + */ + private function override_elementor_css_rule( $post_css, $css_selector, $css_rule, $media_query ) { + if ( ! method_exists( $post_css, 'get_stylesheet' ) ) { + return false; + } + + $stylesheet = $post_css->get_stylesheet(); + if ( ! method_exists( $stylesheet, 'add_rules' ) ) { + return false; + } + + $stylesheet->add_rules( $css_selector, $css_rule, $media_query ); + return true; + } }