diff --git a/inc/manager.php b/inc/manager.php index 587fe756..e855de25 100644 --- a/inc/manager.php +++ b/inc/manager.php @@ -698,7 +698,9 @@ public static function parse_images_from_html( $content ) { if ( OPTML_DEBUG ) { do_action( 'optml_log', 'Content to parse images from: ' . $content ); } - $regex = '/(?:]+?href=["|\'](?P[^\s]+?)["|\'][^>]*?>\s*)?(?P(?:\s*)?]*?\s?(?:' . implode( '|', array_merge( [ 'src' ], Optml_Tag_Replacer::possible_src_attributes() ) ) . ')=["\'\\\\]*?(?P[' . Optml_Config::$chars . ']{10,}).*?>(?:\s*<\/noscript\s*>)?){1}(?:\s*<\/a>)?/ismu'; + // A lazy-load attribute (e.g. data-src) holding a data: placeholder is skipped, so the real src is used. + // A data: src is still matched: the image then belongs to another lazy-load script. + $regex = '/(?:]+?href=["|\'](?P[^\s]+?)["|\'][^>]*?>\s*)?(?P(?:\s*)?]*?(?:\ssrc=["\'\\\\]*?|\s?(?:' . implode( '|', array_merge( [ 'src' ], Optml_Tag_Replacer::possible_src_attributes() ) ) . ')=["\'\\\\]*?(?!data:))(?P[' . Optml_Config::$chars . ']{10,}).*?>(?:\s*<\/noscript\s*>)?){1}(?:\s*<\/a>)?/ismu'; if ( preg_match_all( $regex, $content, $images, PREG_OFFSET_CAPTURE ) ) { if ( OPTML_DEBUG ) { diff --git a/tests/test-dimension-replacement.php b/tests/test-dimension-replacement.php index 32640ad4..6d389b03 100644 --- a/tests/test-dimension-replacement.php +++ b/tests/test-dimension-replacement.php @@ -117,6 +117,17 @@ public function test_existing_attributes_preserved() { } + /** + * A data: placeholder in data-src before src does not stop the real image from being processed. + */ + public function test_dimensions_injected_when_data_src_placeholder_comes_first() { + $html_input = 'Test image'; + + $result = $this->tag_replacer->process_image_tags( $html_input, $this->extract_images_from_html( $html_input ) ); + + $this->assertMatchesRegularExpression( '/width="1920".*height="1080".*data-opt-id=' . $this->tag_replacer->get_id_by_url( self::IMAGE_URL ) . '/', $result ); + } + /** * Helper method to extract images from HTML for testing. * Uses the actual manager method to ensure correct structure. diff --git a/tests/test-lazyload.php b/tests/test-lazyload.php index db132d0a..a7bded32 100644 --- a/tests/test-lazyload.php +++ b/tests/test-lazyload.php @@ -553,4 +553,34 @@ public function test_dam_lazyloading_no_wh_attributes() { $this->assertStringContainsString( $svg, $replaced_content ); remove_filter('optml_lazyload_images_skip','__return_zero'); } + + /** + * A tag whose data-src holds a data: placeholder is lazy-loaded from its real URL. + */ + public function test_lazyload_tag_with_data_uri_data_src() { + $settings = new Optml_Settings(); + $settings->update( 'skip_lazyload_images', 0 ); + // The skip limit is cached in a static on first use, so an earlier test may have cached another value. + $skip_limit = new ReflectionProperty( Optml_Lazyload_Replacer::class, 'skip_lazyload_images' ); + $skip_limit->setAccessible( true ); + $skip_limit->setValue( null, null ); + Optml_Tag_Replacer::$lazyload_skipped_images = 0; + Optml_Manager::instance()->init(); + + $placeholder = 'data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; + $tags = [ + 'Test', + 'Test', + ]; + + foreach ( $tags as $tag ) { + $replaced_content = Optml_Manager::instance()->replace_content( $tag ); + + $this->assertStringContainsString( 'data-opt-src="https://test123.i.optimole.com/', $replaced_content, $tag ); + $this->assertStringContainsString( '/https://example.com/wp-content/uploads/2026/10/photo.jpg"', $replaced_content, $tag ); + $this->assertStringContainsString( 'data-src="' . $placeholder . '"', $replaced_content, $tag ); + } + + $skip_limit->setValue( null, null ); + } } diff --git a/tests/test-replacer.php b/tests/test-replacer.php index 231e4407..701acfbc 100644 --- a/tests/test-replacer.php +++ b/tests/test-replacer.php @@ -850,6 +850,44 @@ public function test_dam_flag_removal() { $this->assertStringContainsString('q:mauto', $replaced_content); } + /** + * A data: placeholder in a lazy-load attribute before src is not taken as the image URL. + */ + public function test_parse_images_skips_data_uri_in_lazy_attribute() { + $placeholder = 'data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; + $real_src = 'https://example.com/wp-content/uploads/2026/10/photo.jpg'; + + $tags = [ + 'Test', + "Test", + 'Test', + 'Test', + '', + ]; + + foreach ( $tags as $tag ) { + $images = Optml_Manager::parse_images_from_html( $tag ); + $this->assertSame( [ $real_src ], $images['img_url'], $tag ); + $this->assertSame( [ $tag ], $images['img_tag'], $tag ); + } + + $placeholder_only = 'Test'; + $this->assertSame( [], Optml_Manager::parse_images_from_html( $placeholder_only ) ); + + $placeholder_and_lazy_src = 'Test'; + $this->assertSame( [ $real_src ], Optml_Manager::parse_images_from_html( $placeholder_and_lazy_src )['img_url'] ); + } + + /** + * A data: src is still matched, so an image owned by another lazy-load script is handled as before. + */ + public function test_parse_images_keeps_data_uri_src() { + $tag = ''; + $images = Optml_Manager::parse_images_from_html( $tag ); + + $this->assertSame( [ 'data:image/gif' ], $images['img_url'] ); + } + /** * Individual cache tokens are stored as the API returned them, which can be a number. * The SDK only accepts a string cache buster.