From 16143690a3a50eb7108ee0f4c3e29a38144565b2 Mon Sep 17 00:00:00 2001 From: Gabriel de Tassigny Date: Mon, 26 Oct 2026 12:24:19 +0100 Subject: [PATCH 1/8] fix: avoid using null as an array offset in static caches and component parts PHP 8.5 deprecates using null as an array offset. Relationship::get_relationship() and Delivery::get_sized() can be called with a null ID (e.g. when the cloudinary_contextualized_post_id filter returns null), and Component::get_part() is called with a null part by the Opt_Level component. Cast the IDs to int (the Relationship constructor already does so) and skip the build parts lookup for a null part. --- php/class-delivery.php | 2 ++ php/relate/class-relationship.php | 7 ++++--- php/ui/class-component.php | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/php/class-delivery.php b/php/class-delivery.php index 1d920518f..e4de801aa 100644 --- a/php/class-delivery.php +++ b/php/class-delivery.php @@ -541,6 +541,8 @@ public function unsync_size_relationship( $attachment_id ) { */ public function get_sized( $attachment_id ) { static $sizes = array(), $registered_sizes; + + $attachment_id = (int) $attachment_id; if ( ! $registered_sizes ) { $registered_sizes = wp_get_registered_image_subsizes(); } diff --git a/php/relate/class-relationship.php b/php/relate/class-relationship.php index bedeef302..172e4ac14 100644 --- a/php/relate/class-relationship.php +++ b/php/relate/class-relationship.php @@ -270,11 +270,12 @@ public function delete_cache() { public static function get_relationship( $post_id ) { static $cache = array(); - if ( ! isset( $cache[ $post_id ] ) ) { - $cache[ $post_id ] = new self( $post_id ); + $key = (int) $post_id; + if ( ! isset( $cache[ $key ] ) ) { + $cache[ $key ] = new self( $post_id ); } - return $cache[ $post_id ]; + return $cache[ $key ]; } /** diff --git a/php/ui/class-component.php b/php/ui/class-component.php index 71b0396e1..33815e09b 100644 --- a/php/ui/class-component.php +++ b/php/ui/class-component.php @@ -631,7 +631,7 @@ public function get_part( $part ) { 'content' => null, 'name' => $part, ); - if ( isset( $this->build_parts[ $part ] ) ) { + if ( null !== $part && isset( $this->build_parts[ $part ] ) ) { $struct = wp_parse_args( $this->build_parts[ $part ], $struct ); } if ( $this->setting->has_param( 'attributes' . $this->setting->separator . $part ) ) { From 4556084cfb5fbdb5f4bdf39f8b9bc6af39886ba5 Mon Sep 17 00:00:00 2001 From: Gabriel de Tassigny Date: Mon, 26 Oct 2026 12:24:40 +0100 Subject: [PATCH 2/8] fix: stop the string replace output handler from producing output PHP 8.4 deprecates producing output from within a user output handler ("ob_end_flush(): Producing output from user output handler Cloudinary\String_Replace::replace_strings is deprecated"). The double ob_start() is not the cause on its own: the deprecation fires when an error is displayed while the handler runs (e.g. notices raised by cloudinary_string_replace callbacks, with display_errors on), since that display is output produced inside the handler and is discarded anyway. Route the buffers through a dedicated output_handler() that disables display_errors while replacing strings and restores it afterwards. Errors are still logged. Both buffers are kept to preserve the early-flush workaround. --- php/class-string-replace.php | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/php/class-string-replace.php b/php/class-string-replace.php index 905fa9a27..ec41eb2df 100644 --- a/php/class-string-replace.php +++ b/php/class-string-replace.php @@ -164,8 +164,30 @@ public function init() { * Stop the buffer capture and set the output callback. */ public function start_capture() { - ob_start( array( $this, 'replace_strings' ) ); - ob_start( array( $this, 'replace_strings' ) ); // Second call to catch early buffer flushing. + ob_start( array( $this, 'output_handler' ) ); + ob_start( array( $this, 'output_handler' ) ); // Second call to catch early buffer flushing. + } + + /** + * Output buffer callback to replace strings in the captured buffer. + * + * Output produced within an output handler is discarded, and doing so is deprecated as of PHP 8.4. + * Errors displayed while replacing (e.g. notices raised by hooked callbacks) would count as such output, + * so display is disabled for the duration of the handler. Errors are still logged as usual. + * + * @param string $buffer The captured buffer. + * @param int $phase The output handler phase bitmask. + * + * @return string + */ + public function output_handler( $buffer, $phase ) { + $display_errors = ini_set( 'display_errors', '0' ); // phpcs:ignore WordPress.PHP.IniSet.display_errors_Disallowed + $buffer = $this->replace_strings( $buffer, $phase ); + if ( false !== $display_errors ) { + ini_set( 'display_errors', $display_errors ); // phpcs:ignore WordPress.PHP.IniSet.display_errors_Disallowed + } + + return $buffer; } /** @@ -285,7 +307,7 @@ protected function prime_replacements( $content, $context = 'view' ) { * Replace string in HTML. * * @param string|array $content The HTML. - * @param string $context The context to use. + * @param string|int $context The context to use, or the output handler phase when capturing a buffer. * * @return string */ From fdc4836580ff231882be583d9a4e9329e4efbea5 Mon Sep 17 00:00:00 2001 From: Gabriel de Tassigny Date: Mon, 26 Oct 2026 12:24:58 +0100 Subject: [PATCH 3/8] fix: cache attachment size URL lookups under the intended key Delivery::find_attachment_size_urls() reused $key inside the results loop for each attachment URL, so wp_cache_add() stored the results under the last attachment's URL instead of md5( $sql ). When the last wp_get_attachment_url() call failed, the key was false, triggering "WP_Object_Cache::add was called incorrectly. Cache key must be an integer or a non-empty string, boolean given". Use a separate variable for the attachment URL, and skip attachments without a URL rather than mapping them under a false key. --- php/class-delivery.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/php/class-delivery.php b/php/class-delivery.php index e4de801aa..34801a6b6 100644 --- a/php/class-delivery.php +++ b/php/class-delivery.php @@ -955,8 +955,10 @@ public function find_attachment_size_urls() { $this->sync->add_to_sync( $post_id ); } $size = $this->get_sized( $post_id ); - $key = ! empty( $size['sized_url'] ) ? $size['sized_url'] : wp_get_attachment_url( $post_id ); - $cached[ $key ] = (int) $post_id; + $attachment_url = ! empty( $size['sized_url'] ) ? $size['sized_url'] : wp_get_attachment_url( $post_id ); + if ( $attachment_url ) { + $cached[ $attachment_url ] = (int) $post_id; + } } } wp_cache_add( $key, $cached ); From 216dbf2d62821d1cce7c9b5e9bd7e2b528788482 Mon Sep 17 00:00:00 2001 From: Gabriel de Tassigny Date: Mon, 26 Oct 2026 12:51:55 +0100 Subject: [PATCH 4/8] fix: avoid passing null to preg_replace() when getting a parent asset version Media::get_cloudinary_version() passed the parent attachment's version meta straight to preg_replace(). When the parent has no version meta, get_post_meta() returns its null default, triggering "preg_replace(): Passing null to parameter #3 ($subject) of type array|string is deprecated". Default the meta to an empty string, which still resolves to version 1. --- php/class-media.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/php/class-media.php b/php/class-media.php index 9948db77b..65f6daf57 100644 --- a/php/class-media.php +++ b/php/class-media.php @@ -3180,7 +3180,7 @@ public function get_cloudinary_version( $attachment_id ) { $attachment = get_post( $attachment_id ); if ( ! empty( $attachment ) && Assets::POST_TYPE_SLUG === $attachment->post_type && ! empty( $attachment->post_parent ) ) { - $version = (int) preg_replace( '/\D/', '', $this->get_post_meta( (int) $attachment->post_parent, Sync::META_KEYS['version'], true ) ); + $version = (int) preg_replace( '/\D/', '', $this->get_post_meta( (int) $attachment->post_parent, Sync::META_KEYS['version'], true, '' ) ); } } From 6cffc38db865d5cd97c483d5820c1b1ea1a37643 Mon Sep 17 00:00:00 2001 From: Gabriel de Tassigny Date: Mon, 26 Oct 2026 13:03:37 +0100 Subject: [PATCH 5/8] fix: skip unresolved contextualized attachments when finding size URLs The cloudinary_contextualized_post_id filter can return null, e.g. WPML's wpml_object_id for an attachment without a translation in the current language. Delivery::find_attachment_size_urls() then carried on with a null ID, calling create_delivery( null ), which ends in wp_cache_delete( null ) ("WP_Object_Cache::delete was called incorrectly. Cache key must be an integer or a non-empty string, NULL given"), and add_to_sync( null ). Skip the result when the contextualized ID is empty. --- php/class-delivery.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/php/class-delivery.php b/php/class-delivery.php index 34801a6b6..7d5c4ef8b 100644 --- a/php/class-delivery.php +++ b/php/class-delivery.php @@ -944,7 +944,8 @@ public function find_attachment_size_urls() { */ $post_id = apply_filters( 'cloudinary_contextualized_post_id', $result->post_id ); - if ( ! $this->is_deliverable( $post_id ) ) { + // The contextualized post can be missing, e.g. WPML returns null for an attachment without a translation. + if ( empty( $post_id ) || ! $this->is_deliverable( $post_id ) ) { continue; } // If we are here, it means that an attachment in the media library doesn't have a delivery for the url. From edd1ada533807faf37ffb2ed436a2e54623fa1ad Mon Sep 17 00:00:00 2001 From: Gabriel de Tassigny Date: Tue, 27 Oct 2026 10:45:15 +0100 Subject: [PATCH 6/8] fixup! fix: cache attachment size URL lookups under the intended key --- php/class-delivery.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/php/class-delivery.php b/php/class-delivery.php index 7d5c4ef8b..a4eaa7452 100644 --- a/php/class-delivery.php +++ b/php/class-delivery.php @@ -922,13 +922,17 @@ public function find_attachment_size_urls() { $search ); + // Cache for the current request only. Across requests, failed deliveries or syncs need to be retried, + // and the contextualized post IDs can differ (e.g. per WPML language). + static $cache = array(); + $key = md5( $sql ); - $cached = wp_cache_get( $key ); + $cached = isset( $cache[ $key ] ) ? $cache[ $key ] : false; $auto_sync = $this->sync->is_auto_sync_enabled(); if ( false === $cached ) { $cached = array(); - $results = $wpdb->get_results( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared + $results = $wpdb->get_results( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared if ( $results ) { foreach ( $results as $result ) { @@ -962,7 +966,7 @@ public function find_attachment_size_urls() { } } } - wp_cache_add( $key, $cached ); + $cache[ $key ] = $cached; } $this->known = array_merge( $this->known, $cached ); From a544f9bbc2ab341f5527b301d85fc00615dd5b88 Mon Sep 17 00:00:00 2001 From: Gabriel de Tassigny Date: Tue, 27 Oct 2026 10:45:16 +0100 Subject: [PATCH 7/8] fixup! fix: stop the string replace output handler from producing output --- php/class-string-replace.php | 1 + 1 file changed, 1 insertion(+) diff --git a/php/class-string-replace.php b/php/class-string-replace.php index ec41eb2df..fe13b160d 100644 --- a/php/class-string-replace.php +++ b/php/class-string-replace.php @@ -174,6 +174,7 @@ public function start_capture() { * Output produced within an output handler is discarded, and doing so is deprecated as of PHP 8.4. * Errors displayed while replacing (e.g. notices raised by hooked callbacks) would count as such output, * so display is disabled for the duration of the handler. Errors are still logged as usual. + * This doesn't cover callbacks that echo output directly, which still trigger the deprecation. * * @param string $buffer The captured buffer. * @param int $phase The output handler phase bitmask. From 94db0c64947d74d52cfe27cccf0da03365781326 Mon Sep 17 00:00:00 2001 From: Gabriel de Tassigny Date: Tue, 27 Oct 2026 10:45:17 +0100 Subject: [PATCH 8/8] fixup! fix: avoid using null as an array offset in static caches and component parts --- php/ui/class-component.php | 4 ++-- php/ui/component/class-opt-level.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/php/ui/class-component.php b/php/ui/class-component.php index 33815e09b..bea5e9dac 100644 --- a/php/ui/class-component.php +++ b/php/ui/class-component.php @@ -618,7 +618,7 @@ public static function build_tag( $element, $attributes = array(), $state = 'ope /** * Get a build part to construct. * - * @param string|null $part The part name. + * @param string $part The part name. * * @return array */ @@ -631,7 +631,7 @@ public function get_part( $part ) { 'content' => null, 'name' => $part, ); - if ( null !== $part && isset( $this->build_parts[ $part ] ) ) { + if ( isset( $this->build_parts[ $part ] ) ) { $struct = wp_parse_args( $this->build_parts[ $part ], $struct ); } if ( $this->setting->has_param( 'attributes' . $this->setting->separator . $part ) ) { diff --git a/php/ui/component/class-opt-level.php b/php/ui/component/class-opt-level.php index 5ff1eb178..8d32e5c0d 100644 --- a/php/ui/component/class-opt-level.php +++ b/php/ui/component/class-opt-level.php @@ -119,7 +119,7 @@ protected function get_setting_line( $slug ) { protected function get_badge( $status = 'active' ) { $badge = $this->get_part( 'span' ); $badge['attributes']['class'][] = "cld-optimisation-item-{$status}"; - $text = $this->get_part( null ); + $text = $this->get_part( '' ); if ( 'active' === $status ) { $text['content'] = __( 'Activated', 'cloudinary' ); $icon = $this->dashicon( $this->get_part( 'span' ) );