diff --git a/.github/workflows/test-php.yml b/.github/workflows/test-php.yml index 9d019b4d..134fd5f2 100644 --- a/.github/workflows/test-php.yml +++ b/.github/workflows/test-php.yml @@ -28,7 +28,7 @@ jobs: strategy: fail-fast: false matrix: - php-versions: [ '7.4', '8.0', '8.1', '8.2', '8.3' ] + php-versions: [ '8.0', '8.1', '8.2', '8.3' ] services: database: image: mysql:latest @@ -41,7 +41,7 @@ jobs: - name: Setup PHP version uses: shivammathur/setup-php@v2 with: - php-version: ${{ matrix.php-version }} + php-version: ${{ matrix.php-versions }} extensions: simplexml, mysql tools: phpunit-polyfills:1.1 - name: Checkout source code diff --git a/composer.lock b/composer.lock index d743021e..18aefafa 100644 --- a/composer.lock +++ b/composer.lock @@ -904,11 +904,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.31", + "version": "2.1.32", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ead89849d879fe203ce9292c6ef5e7e76f867b96", - "reference": "ead89849d879fe203ce9292c6ef5e7e76f867b96", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e126cad1e30a99b137b8ed75a85a676450ebb227", + "reference": "e126cad1e30a99b137b8ed75a85a676450ebb227", "shasum": "" }, "require": { @@ -953,7 +953,7 @@ "type": "github" } ], - "time": "2025-10-10T14:14:11+00:00" + "time": "2025-11-11T15:18:17+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/inc/admin.php b/inc/admin.php index 6f9f8884..0b5b705c 100755 --- a/inc/admin.php +++ b/inc/admin.php @@ -575,11 +575,14 @@ public function inline_bootstrap_script() { /** * Add settings links in the plugin listing page. * - * @param string[] $links Old plugin links. + * @param string[]|mixed $links Old plugin links. * - * @return string[] Altered links. + * @return string[]|mixed Altered links. */ public function add_action_links( $links ) { + if ( ! is_array( $links ) ) { + return $links; + } return array_merge( $links, [ diff --git a/inc/app_replacer.php b/inc/app_replacer.php index 6ed4dbc2..48be9566 100644 --- a/inc/app_replacer.php +++ b/inc/app_replacer.php @@ -654,21 +654,24 @@ public function url_has_dam_flag( $url ) { /** * Get the optimized image url for the image url. * - * @param string $url The image URL. - * @param mixed $width The image width. - * @param mixed $height The image height. - * @param array $resize The resize properties. + * @param string $url The image URL. + * @param mixed $width The image width. + * @param mixed $height The image height. + * @param array|mixed $resize The resize properties. * * @return string */ protected function get_optimized_image_url( $url, $width, $height, $resize = [] ) { $width = is_int( $width ) ? $width : 'auto'; $height = is_int( $height ) ? $height : 'auto'; + // If the image is already using Optimole URL, we extract the source to rebuild it. + $url = $this->get_unoptimized_url( $url ); + $optimized_image = Optimole::image( $url, $this->settings->get( 'cache_buster' ) ) ->width( $width ) ->height( $height ); - if ( ! empty( $resize['type'] ) ) { + if ( is_array( $resize ) && ! empty( $resize['type'] ) ) { $optimized_image->resize( $resize['type'], $resize['gravity'] ?? Position::CENTER, $resize['enlarge'] ?? false ); } diff --git a/inc/lazyload_replacer.php b/inc/lazyload_replacer.php index 15fd4534..1284a71e 100644 --- a/inc/lazyload_replacer.php +++ b/inc/lazyload_replacer.php @@ -115,6 +115,11 @@ public static function get_background_lazyload_selectors() { return self::$background_lazyload_selectors; } + if ( self::instance()->settings === null ) { + self::$background_lazyload_selectors = []; + + return self::$background_lazyload_selectors; + } if ( self::instance()->settings->get( 'bg_replacer' ) === 'disabled' ) { self::$background_lazyload_selectors = []; return self::$background_lazyload_selectors; diff --git a/inc/tag_replacer.php b/inc/tag_replacer.php index 75372f50..78cc5f87 100644 --- a/inc/tag_replacer.php +++ b/inc/tag_replacer.php @@ -809,15 +809,18 @@ public function change_url_for_size( $original_url, $width, $height, $dpr = 1 ) /** * Replace image URLs in the srcset attributes and in case there is a resize in action, also replace the sizes. * - * @param array $sources Array of image sources. - * @param array{0: int, 1: int}|int[] $size_array Array of width and height values in pixels (in that order). - * @param string $image_src The 'src' of the image. - * @param array $image_meta The image meta data as returned by 'wp_get_attachment_metadata()'. - * @param int $attachment_id Image attachment ID or 0. + * @param array|mixed $sources Array of image sources. + * @param array{0: int, 1: int}|int[] $size_array Array of width and height values in pixels (in that order). + * @param string $image_src The 'src' of the image. + * @param array $image_meta The image meta data as returned by 'wp_get_attachment_metadata()'. + * @param int $attachment_id Image attachment ID or 0. * - * @return array + * @return array|mixed */ public function filter_srcset_attr( $sources = [], $size_array = [], $image_src = '', $image_meta = [], $attachment_id = 0 ) { + if ( ! is_array( $sources ) ) { + return $sources; + } if ( Optml_Media_Offload::is_uploaded_image( $image_src ) ) { return $sources; } diff --git a/inc/traits/dam_offload_utils.php b/inc/traits/dam_offload_utils.php index 08509975..4f455363 100644 --- a/inc/traits/dam_offload_utils.php +++ b/inc/traits/dam_offload_utils.php @@ -3,6 +3,16 @@ trait Optml_Dam_Offload_Utils { use Optml_Normalizer; + /** + * Check if this contains the DAM flag. + * + * @param string $url The URL to check. + * + * @return bool + */ + private function is_dam_url( $url ) { + return strpos( $url, Optml_Dam::URL_DAM_FLAG ) !== false; + } /** * Checks that the attachment is a DAM image. * @@ -239,6 +249,31 @@ private function is_completed_offload( $id ) { return false; } + /** + * Get the attachment ID from optimole ID. + * + * @param string $optimole_id The optimole ID. + * + * @return int + */ + private function get_attachement_id_from_optimole_id( string $optimole_id ): int { + global $wpdb; + + $id = $wpdb->get_var( + $wpdb->prepare( + " + SELECT post_id + FROM {$wpdb->postmeta} + WHERE meta_key = %s + AND meta_value = %s + LIMIT 1 + ", + Optml_Dam::OM_DAM_IMPORTED_FLAG, + $optimole_id + ) + ); + return empty( $id ) ? 0 : (int) $id; + } /** * Get the attachment ID from URL. * @@ -253,6 +288,19 @@ private function attachment_url_to_post_id( $input_url ) { return (int) $cached; } + if ( Optml_Media_Offload::is_uploaded_image( $input_url ) ) { + // The DAM are stored as attachments of format /id:/ + $pattern = '#/' . Optml_Media_Offload::KEYS['uploaded_flag'] . '([^/]+)#'; + if ( preg_match( $pattern, $input_url, $m ) ) { + $attachment_id = $this->get_attachement_id_from_optimole_id( $m[1] ); + if ( $attachment_id !== 0 ) { + Optml_Attachment_Cache::set_cached_attachment_id( $input_url, $attachment_id ); + + return $attachment_id; + } + } + } + $url = $this->strip_image_size( $input_url ); $attachment_id = attachment_url_to_postid( $url ); diff --git a/inc/traits/normalizer.php b/inc/traits/normalizer.php index e1bd1522..a0c2f713 100644 --- a/inc/traits/normalizer.php +++ b/inc/traits/normalizer.php @@ -53,6 +53,10 @@ public function get_unoptimized_url( $url ) { } // If the url is an uploaded image, return the url if ( Optml_Media_Offload::is_uploaded_image( $url ) ) { + $pattern = '#/id:([^/]+)/((?:https?|http)://\S+)#'; + if ( preg_match( $pattern, $url, $matches ) ) { + $url = $matches[0]; + } return $url; } diff --git a/inc/url_replacer.php b/inc/url_replacer.php index f6eca2a4..1540f90c 100644 --- a/inc/url_replacer.php +++ b/inc/url_replacer.php @@ -409,16 +409,6 @@ private function get_dam_url( Image $image ) { return $url; } - /** - * Check if this contains the DAM flag. - * - * @param string $url The URL to check. - * - * @return bool - */ - private function is_dam_url( $url ) { - return strpos( $url, Optml_Dam::URL_DAM_FLAG ) !== false; - } /** * Check if the URL is offloaded. diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index deb5805c..a002da25 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -516,12 +516,6 @@ parameters: count: 1 path: inc/app_replacer.php - - - message: '#^Method Optml_App_Replacer\:\:get_optimized_image_url\(\) has parameter \$resize with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: inc/app_replacer.php - - message: '#^Method Optml_App_Replacer\:\:get_upload_resource\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -2790,12 +2784,6 @@ parameters: count: 1 path: inc/tag_replacer.php - - - message: '#^Method Optml_Tag_Replacer\:\:filter_srcset_attr\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: inc/tag_replacer.php - - message: '#^Method Optml_Tag_Replacer\:\:init\(\) has no return type specified\.$#' identifier: missingType.return diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 3e4a50c0..ea778477 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -66,6 +66,10 @@ function( $message ) { } } require dirname( dirname( __FILE__ ) ) . '/optimole-wp.php'; + + // Prevent cache clearing actions during tests to avoid errors from cache compatibility classes + // This filter prevents optml_settings_updated and optml_clear_cache from being triggered + add_filter( 'optml_dont_trigger_settings_updated', '__return_true' ); } tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' ); @@ -83,6 +87,7 @@ function( $message ) { // Activate Optimole plugin activate_plugin( 'optimole-wp/optimole-wp.php' ); + // Set up the current logged in user global $current_user; diff --git a/tests/test-generic.php b/tests/test-generic.php index 538fcc59..fef9a2a9 100644 --- a/tests/test-generic.php +++ b/tests/test-generic.php @@ -44,4 +44,157 @@ function test_domain_hash() { $this->assertEquals( $this->to_domain_hash("//www.domain.com/"), $value ); $this->assertNotEquals( $this->to_domain_hash("https://something.com/"), $value ); } + + /** + * Test get_unoptimized_url with non-Optimole URLs. + */ + function test_get_unoptimized_url_non_optimole() { + // Initialize config for testing + $settings = new Optml_Settings(); + $settings->update( 'service_data', [ + 'cdn_key' => 'test123', + 'cdn_secret' => '12345', + 'whitelist' => [ 'example.com', 'example.org' ], + ] ); + Optml_Config::init( [ + 'key' => 'test123', + 'secret' => '12345', + ] ); + + // Non-Optimole URLs should be returned as-is + $url = 'http://example.org/wp-content/uploads/image.jpg'; + $this->assertEquals( $url, $this->get_unoptimized_url( $url ) ); + + $url = 'https://example.com/image.png'; + $this->assertEquals( $url, $this->get_unoptimized_url( $url ) ); + + $url = '/wp-content/uploads/image.jpg'; + $this->assertEquals( $url, $this->get_unoptimized_url( $url ) ); + } + + /** + * Test get_unoptimized_url with Optimole URLs (regular images). + */ + function test_get_unoptimized_url_optimole_regular() { + // Initialize config for testing + $settings = new Optml_Settings(); + $settings->update( 'service_data', [ + 'cdn_key' => 'test123', + 'cdn_secret' => '12345', + 'whitelist' => [ 'example.com', 'example.org' ], + ] ); + Optml_Config::init( [ + 'key' => 'test123', + 'secret' => '12345', + ] ); + + // Optimole URL with regular image - should extract original URL after second 'http' + $optimole_url = 'https://test123.i.optimole.com/cb:test/w:800/h:600/q:mauto/http://example.org/wp-content/uploads/image.jpg'; + $expected = 'http://example.org/wp-content/uploads/image.jpg'; + $this->assertEquals( $expected, $this->get_unoptimized_url( $optimole_url ) ); + + // Optimole URL with https in original + $optimole_url = 'https://test123.i.optimole.com/cb:test/w:800/h:600/q:mauto/https://example.org/wp-content/uploads/image.jpg'; + $expected = 'https://example.org/wp-content/uploads/image.jpg'; + $this->assertEquals( $expected, $this->get_unoptimized_url( $optimole_url ) ); + + // Optimole URL with query parameters + $optimole_url = 'https://test123.i.optimole.com/cb:test/w:800/h:600/q:mauto/http://example.org/wp-content/uploads/image.jpg?param=value'; + $expected = 'http://example.org/wp-content/uploads/image.jpg?param=value'; + $this->assertEquals( $expected, $this->get_unoptimized_url( $optimole_url ) ); + } + + /** + * Test get_unoptimized_url with uploaded images (offloaded images). + */ + function test_get_unoptimized_url_uploaded_image() { + // Initialize config for testing + $settings = new Optml_Settings(); + $settings->update( 'service_data', [ + 'cdn_key' => 'test123', + 'cdn_secret' => '12345', + 'whitelist' => [ 'example.com', 'example.org' ], + ] ); + Optml_Config::init( [ + 'key' => 'test123', + 'secret' => '12345', + ] ); + + // Uploaded image URL with /id: pattern - should extract original URL + $uploaded_url = 'https://test123.i.optimole.com/cb:test/w:800/h:600/q:mauto/id:abc123/http://example.org/wp-content/uploads/image.jpg'; + $expected = '/id:abc123/http://example.org/wp-content/uploads/image.jpg'; + $result = $this->get_unoptimized_url( $uploaded_url ); + $this->assertStringContainsString( '/id:abc123/', $result ); + $this->assertStringContainsString( 'http://example.org/wp-content/uploads/image.jpg', $result ); + + // Uploaded image URL with https in original + $uploaded_url = 'https://test123.i.optimole.com/cb:test/w:800/h:600/q:mauto/id:xyz789/https://example.org/wp-content/uploads/image.jpg'; + $expected = '/id:xyz789/https://example.org/wp-content/uploads/image.jpg'; + $result = $this->get_unoptimized_url( $uploaded_url ); + $this->assertStringContainsString( '/id:xyz789/', $result ); + $this->assertStringContainsString( 'https://example.org/wp-content/uploads/image.jpg', $result ); + } + + /** + * Test get_unoptimized_url edge cases. + */ + function test_get_unoptimized_url_edge_cases() { + // Initialize config for testing + $settings = new Optml_Settings(); + $settings->update( 'service_data', [ + 'cdn_key' => 'test123', + 'cdn_secret' => '12345', + 'whitelist' => [ 'example.com', 'example.org' ], + ] ); + Optml_Config::init( [ + 'key' => 'test123', + 'secret' => '12345', + ] ); + + // Optimole URL without second 'http' - should return as-is + $optimole_url = 'https://test123.i.optimole.com/cb:test/w:800/h:600/q:mauto'; + $this->assertEquals( $optimole_url, $this->get_unoptimized_url( $optimole_url ) ); + + // Empty string + $this->assertEquals( '', $this->get_unoptimized_url( '' ) ); + + // URL with only one 'http' occurrence + $optimole_url = 'https://test123.i.optimole.com/cb:test/w:800/h:600/q:mauto/image.jpg'; + $this->assertEquals( $optimole_url, $this->get_unoptimized_url( $optimole_url ) ); + + // Uploaded image URL without matching pattern - should return as-is + $uploaded_url = 'https://test123.i.optimole.com/cb:test/w:800/h:600/q:mauto/id:abc123/image.jpg'; + $result = $this->get_unoptimized_url( $uploaded_url ); + // Should return the URL as-is since pattern doesn't match + $this->assertEquals( $uploaded_url, $result ); + } + + /** + * Test get_unoptimized_url with custom domain configuration. + */ + function test_get_unoptimized_url_custom_domain() { + // Initialize config with custom domain + $settings = new Optml_Settings(); + $settings->update( 'service_data', [ + 'cdn_key' => 'test123', + 'cdn_secret' => '12345', + 'whitelist' => [ 'example.com', 'example.org' ], + 'domain' => 'cdn.example.com', + 'is_cname_assigned' => 'yes', + ] ); + Optml_Config::init( [ + 'key' => 'test123', + 'secret' => '12345', + 'domain' => 'cdn.example.com', + ] ); + + // Custom domain Optimole URL + $optimole_url = 'https://cdn.example.com/cb:test/w:800/h:600/q:mauto/http://example.org/wp-content/uploads/image.jpg'; + $expected = 'http://example.org/wp-content/uploads/image.jpg'; + $this->assertEquals( $expected, $this->get_unoptimized_url( $optimole_url ) ); + + // Non-Optimole URL should still return as-is + $url = 'http://example.org/wp-content/uploads/image.jpg'; + $this->assertEquals( $url, $this->get_unoptimized_url( $url ) ); + } } diff --git a/tests/test-lazyload-viewport.php b/tests/test-lazyload-viewport.php index dfeb8c66..93930c70 100644 --- a/tests/test-lazyload-viewport.php +++ b/tests/test-lazyload-viewport.php @@ -489,7 +489,7 @@ private function storeMockProfileData( $deviceType, $above_fold_images = [], $bgSelectors = [], - $lcpData = [], + $lcpData = [] ) { Optml_Manager::instance()->page_profiler->store( $profileId,