From 253700312eab34c8853dc3fd73615ec1d4b42a6d Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 1 Aug 2026 17:13:32 +0200 Subject: [PATCH] 117 - Decode HTML entities in extracted thumbnail URLs --- .../Parsers/BelgaArticlePageParser.php | 4 +- .../Parsers/GuardianArticlePageParser.php | 4 +- app/Services/Parsers/VrtArticlePageParser.php | 4 +- .../Parsers/BelgaArticlePageParserTest.php | 30 ++++++++ .../Parsers/GuardianArticlePageParserTest.php | 20 +++++ .../Parsers/VrtArticlePageParserTest.php | 75 +++++++++++++++++++ 6 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 tests/Unit/Services/Parsers/VrtArticlePageParserTest.php diff --git a/app/Services/Parsers/BelgaArticlePageParser.php b/app/Services/Parsers/BelgaArticlePageParser.php index 3c1fdc44..955bd51a 100644 --- a/app/Services/Parsers/BelgaArticlePageParser.php +++ b/app/Services/Parsers/BelgaArticlePageParser.php @@ -110,12 +110,12 @@ public static function extractThumbnail(string $html): ?string { // Try OpenGraph image first if (preg_match('/]+src="([^"]+)"/i', $html, $matches)) { - return $matches[1]; + return html_entity_decode($matches[1], ENT_QUOTES, 'UTF-8'); } return null; diff --git a/app/Services/Parsers/GuardianArticlePageParser.php b/app/Services/Parsers/GuardianArticlePageParser.php index 4d462115..09c7c4fa 100644 --- a/app/Services/Parsers/GuardianArticlePageParser.php +++ b/app/Services/Parsers/GuardianArticlePageParser.php @@ -68,12 +68,12 @@ public static function extractThumbnail(string $html): ?string { // Try OpenGraph image first if (preg_match('/]+src="([^"]+)"/i', $html, $matches)) { - return $matches[1]; + return html_entity_decode($matches[1], ENT_QUOTES, 'UTF-8'); } return null; diff --git a/app/Services/Parsers/VrtArticlePageParser.php b/app/Services/Parsers/VrtArticlePageParser.php index e312bb6f..095c7389 100644 --- a/app/Services/Parsers/VrtArticlePageParser.php +++ b/app/Services/Parsers/VrtArticlePageParser.php @@ -67,11 +67,11 @@ public static function extractFullArticle(string $html): ?string public static function extractThumbnail(string $html): ?string { if (preg_match('/]+src="([^"]+)"/i', $html, $matches)) { - return $matches[1]; + return html_entity_decode($matches[1], ENT_QUOTES, 'UTF-8'); } return null; diff --git a/tests/Unit/Services/Parsers/BelgaArticlePageParserTest.php b/tests/Unit/Services/Parsers/BelgaArticlePageParserTest.php index 83a78baf..4774419d 100644 --- a/tests/Unit/Services/Parsers/BelgaArticlePageParserTest.php +++ b/tests/Unit/Services/Parsers/BelgaArticlePageParserTest.php @@ -242,6 +242,36 @@ public function test_extract_thumbnail_prefers_og_image(): void $this->assertEquals('https://example.com/og-image.jpg', $thumbnail); } + public function test_extract_thumbnail_decodes_html_entities_in_og_image(): void + { + // Real Belga CDN URLs carry query params, so the & in the raw HTML + // must be decoded or the URL Lemmy receives is malformed. See #117. + $html = ''; + + $thumbnail = BelgaArticlePageParser::extractThumbnail($html); + + $this->assertEquals('https://cdn.example.com/img:12345:full?v=6a6c828f&m=cdodnjha', $thumbnail); + $this->assertStringNotContainsString('&', (string) $thumbnail); + } + + public function test_extract_thumbnail_decodes_html_entities_in_img_tag(): void + { + $html = 'test'; + + $thumbnail = BelgaArticlePageParser::extractThumbnail($html); + + $this->assertEquals('https://cdn.example.com/pic.png?a=1&b=2', $thumbnail); + } + + public function test_extract_data_returns_decoded_thumbnail(): void + { + $html = ''; + + $data = BelgaArticlePageParser::extractData($html); + + $this->assertEquals('https://cdn.example.com/x.jpg?v=1&m=2', $data['thumbnail']); + } + public function test_extract_thumbnail_returns_null_when_not_found(): void { $html = '
No images here
'; diff --git a/tests/Unit/Services/Parsers/GuardianArticlePageParserTest.php b/tests/Unit/Services/Parsers/GuardianArticlePageParserTest.php index 1e233706..d280a9f8 100644 --- a/tests/Unit/Services/Parsers/GuardianArticlePageParserTest.php +++ b/tests/Unit/Services/Parsers/GuardianArticlePageParserTest.php @@ -282,4 +282,24 @@ public function test_extract_full_article_with_realistic_guardian_html(): void $this->assertStringContainsString("\n\n", $fullArticle); $this->assertStringNotContainsString('', $fullArticle); } + + public function test_extract_thumbnail_decodes_html_entities(): void + { + // Entity-encoded ampersands make the URL unusable downstream. See #117. + $html = ''; + + $thumbnail = GuardianArticlePageParser::extractThumbnail($html); + + $this->assertEquals('https://i.guim.co.uk/img/media/abc.jpg?width=1200&quality=85', $thumbnail); + $this->assertStringNotContainsString('&', (string) $thumbnail); + } + + public function test_extract_thumbnail_decodes_html_entities_in_img_tag(): void + { + $html = 'test'; + + $thumbnail = GuardianArticlePageParser::extractThumbnail($html); + + $this->assertEquals('https://i.guim.co.uk/img/pic.png?a=1&b=2', $thumbnail); + } } diff --git a/tests/Unit/Services/Parsers/VrtArticlePageParserTest.php b/tests/Unit/Services/Parsers/VrtArticlePageParserTest.php new file mode 100644 index 00000000..6f3c6a61 --- /dev/null +++ b/tests/Unit/Services/Parsers/VrtArticlePageParserTest.php @@ -0,0 +1,75 @@ +'; + + $this->assertEquals( + 'https://images.vrt.be/image.jpg', + VrtArticlePageParser::extractThumbnail($html) + ); + } + + public function test_extract_thumbnail_falls_back_to_img_tag(): void + { + $html = 'test'; + + $this->assertEquals( + 'https://images.vrt.be/body.png', + VrtArticlePageParser::extractThumbnail($html) + ); + } + + public function test_extract_thumbnail_prefers_og_image(): void + { + $html = '' + .'test'; + + $this->assertEquals( + 'https://images.vrt.be/og.jpg', + VrtArticlePageParser::extractThumbnail($html) + ); + } + + public function test_extract_thumbnail_returns_null_when_not_found(): void + { + $this->assertNull(VrtArticlePageParser::extractThumbnail('No images')); + } + + public function test_extract_thumbnail_decodes_html_entities(): void + { + // Entity-encoded ampersands make the URL unusable downstream. See #117. + $html = ''; + + $thumbnail = VrtArticlePageParser::extractThumbnail($html); + + $this->assertEquals('https://images.vrt.be/orig.jpg?crop=1&w=1200', $thumbnail); + $this->assertStringNotContainsString('&', (string) $thumbnail); + } + + public function test_extract_thumbnail_decodes_html_entities_in_img_tag(): void + { + $html = 'test'; + + $this->assertEquals( + 'https://images.vrt.be/pic.png?a=1&b=2', + VrtArticlePageParser::extractThumbnail($html) + ); + } + + public function test_extract_data_returns_decoded_thumbnail(): void + { + $html = ''; + + $data = VrtArticlePageParser::extractData($html); + + $this->assertEquals('https://images.vrt.be/x.jpg?v=1&m=2', $data['thumbnail']); + } +}