Compare commits

..
11 Commits
Author SHA1 Message Date
Jérémy BenoistandGitHub 8ed91105ea Merge pull request #112 from j0k3r/fix/1.x/tidy-php85
Fix bad encoding for `tidy_repair_string`
2026-03-27 20:36:19 +01:00
Jeremy Benoist 952588ee1f Fix bad encoding for tidy_repair_string
Tidy on PHP 8.5 is more restrictive on what can be given as input encoding. Before, it worked as an unknown value was converted to `utf8`.
2026-03-27 20:34:45 +01:00
Jeremy Benoist e4e975db96 Add PHP 8.5 on CI 2026-03-27 20:32:42 +01:00
Jérémy BenoistandGitHub 774363e18d Merge pull request #110 from j0k3r/fix/psr-monolog
[1.x] Allow new psr/log & monolog
2025-09-26 21:40:38 +02:00
Jeremy Benoist e6ca6f117f Drop PHP < 7.2 2025-09-26 18:21:56 +02:00
Jeremy Benoist 0b21d4ab2d Clean 2025-09-26 18:19:16 +02:00
Jeremy Benoist 720f0d5503 Allow new psr/log & monolog 2025-09-26 18:17:03 +02:00
Jérémy BenoistandGitHub b9dde0f4cd Merge pull request #107 from j0k3r/backport/encode
[1.x] Backport character decoding regression
2025-06-03 10:02:58 +02:00
Jeremy Benoist a21742b22a Fix GA 2025-06-03 09:48:20 +02:00
Jan TojnarandJeremy Benoist 40219d4595 Fix character decoding regression when title precedes meta[charset]
Because of PHP 8.2 deprecation, in f14428e4c0, we stopped converting non-ASCII characters to HTML entities. Instead, we started to explicitly insert `meta[charset]` tag at the start of the document.

Later, we discovered that was breaking `html[lang]` so, in efbbc86df9, we made the insertion smarter. One of the improvements was that it would not insert the `meta[charset]` tag when it was already present.

That, however, broke websites that had `title` tag before `meta[charset]`. On those, libxml2 would decode the `title` contents as ISO-8859-1.

We could improve the logic (e.g. check that there is not text content before `meta[charset]`) or insert the tag unconditionally but it will probably be simplest to just go back to converting the non-ASCII characters to entities, just using non-deprecated function variant.
2025-06-03 09:48:20 +02:00
Jan TojnarandJeremy Benoist 7f304d03aa tests: Check encoding was preserved in testHtmlLang
The fix introduced in efbbc86df9 alongside this test also manipulates `meta[charset]` but we were not checking if it does not break encoding.
2025-06-03 09:24:02 +02:00
5 changed files with 39 additions and 93 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ env:
jobs: jobs:
coding-standards: coding-standards:
name: "CS Fixer & PHPStan" name: "CS Fixer & PHPStan"
runs-on: "ubuntu-22.04" runs-on: "ubuntu-latest"
strategy: strategy:
matrix: matrix:
+5 -49
View File
@@ -3,11 +3,9 @@ name: "CI"
on: on:
pull_request: pull_request:
branches: branches:
- "master"
- "1.x" - "1.x"
push: push:
branches: branches:
- "master"
- "1.x" - "1.x"
env: env:
@@ -16,14 +14,11 @@ env:
jobs: jobs:
phpunit: phpunit:
name: "PHPUnit (PHP ${{ matrix.php }})" name: "PHPUnit (PHP ${{ matrix.php }})"
runs-on: "ubuntu-22.04" runs-on: "ubuntu-latest"
strategy: strategy:
matrix: matrix:
php: php:
- "5.6"
- "7.0"
- "7.1"
- "7.2" - "7.2"
- "7.3" - "7.3"
- "7.4" - "7.4"
@@ -32,6 +27,7 @@ jobs:
- "8.2" - "8.2"
- "8.3" - "8.3"
- "8.4" - "8.4"
- "8.5"
steps: steps:
- name: "Checkout" - name: "Checkout"
@@ -66,7 +62,7 @@ jobs:
phpunit-coverage: phpunit-coverage:
name: "PHPUnit coverage (PHP ${{ matrix.php }})" name: "PHPUnit coverage (PHP ${{ matrix.php }})"
runs-on: "ubuntu-22.04" runs-on: "ubuntu-latest"
strategy: strategy:
matrix: matrix:
@@ -117,12 +113,12 @@ jobs:
phpunit-lowest: phpunit-lowest:
name: "PHPUnit lowest deps (PHP ${{ matrix.php }})" name: "PHPUnit lowest deps (PHP ${{ matrix.php }})"
runs-on: "ubuntu-22.04" runs-on: "ubuntu-latest"
strategy: strategy:
matrix: matrix:
php: php:
- "7.2" - "7.4"
steps: steps:
- name: "Checkout" - name: "Checkout"
@@ -155,43 +151,3 @@ jobs:
- name: "Run PHPUnit" - name: "Run PHPUnit"
run: "php vendor/bin/simple-phpunit -v" run: "php vendor/bin/simple-phpunit -v"
phpunit-composerv2:
name: "PHPUnit with Composer v1 (PHP ${{ matrix.php }})"
runs-on: "ubuntu-20.04"
strategy:
matrix:
php:
- "7.4"
steps:
- name: "Checkout"
uses: "actions/checkout@v2"
with:
fetch-depth: 2
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
php-version: "${{ matrix.php }}"
coverage: "none"
tools: composer:v1
extensions: tidy
ini-values: "date.timezone=Europe/Paris"
env:
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: "Remove useless deps"
run: "composer remove friendsofphp/php-cs-fixer --dev --no-progress --no-update"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v1"
with:
composer-options: "--optimize-autoloader --prefer-dist"
- name: "Setup logs"
run: "mkdir -p build/logs"
- name: "Run PHPUnit"
run: "php vendor/bin/simple-phpunit -v"
+3 -3
View File
@@ -24,14 +24,14 @@
"role": "Developer (original JS version)" "role": "Developer (original JS version)"
}], }],
"require": { "require": {
"php": ">=5.6.0", "php": ">=7.2",
"ext-mbstring": "*", "ext-mbstring": "*",
"psr/log": "^1.0", "psr/log": "^1.0.1 || ^2.0 || ^3.0",
"masterminds/html5": "^2.7" "masterminds/html5": "^2.7"
}, },
"require-dev": { "require-dev": {
"friendsofphp/php-cs-fixer": "^2.14", "friendsofphp/php-cs-fixer": "^2.14",
"monolog/monolog": "^1.24|^2.1", "monolog/monolog": "^1.24|^2.1|^3.0",
"symfony/phpunit-bridge": "^4.4|^5.3|^6.0|^7.0" "symfony/phpunit-bridge": "^4.4|^5.3|^6.0|^7.0"
}, },
"suggest": { "suggest": {
+9 -36
View File
@@ -118,7 +118,7 @@ class Readability implements LoggerAwareInterface
'enclose-text' => true, 'enclose-text' => true,
'merge-divs' => true, 'merge-divs' => true,
// 'merge-spans' => true, // 'merge-spans' => true,
'input-encoding' => '????', 'input-encoding' => 'utf8',
'output-encoding' => 'utf8', 'output-encoding' => 'utf8',
'hide-comments' => true, 'hide-comments' => true,
]; ];
@@ -183,10 +183,7 @@ class Readability implements LoggerAwareInterface
$this->loadHtml(); $this->loadHtml();
} }
/** public function setLogger(LoggerInterface $logger): void
* @return void
*/
public function setLogger(LoggerInterface $logger)
{ {
$this->logger = $logger; $this->logger = $logger;
} }
@@ -1435,7 +1432,7 @@ class Readability implements LoggerAwareInterface
unset($tidy); unset($tidy);
} }
$this->html = self::ensureMetaCharset((string) $this->html); $this->html = self::entitizeNonAscii((string) $this->html);
if ('html5lib' === $this->parser || 'html5' === $this->parser) { if ('html5lib' === $this->parser || 'html5' === $this->parser) {
$this->dom = (new HTML5())->loadHTML($this->html); $this->dom = (new HTML5())->loadHTML($this->html);
@@ -1455,43 +1452,19 @@ class Readability implements LoggerAwareInterface
} }
/** /**
* Tries to insert `meta[charset]` tag into the proper place in the passed HTML document. * Converts non-ASCII UTF-8 characters to numeric HTML entities.
* *
* `DOMDocument::loadHTML` will parse HTML documents as ISO-8859-1 if there is no `meta[charset]` tag. * `DOMDocument::loadHTML` will parse HTML documents as ISO-8859-1 if there is no `meta[charset]` tag.
* This means that UTF-8-encoded HTML fragments such as those coming from JSON-LD `articleBody` field would be parsed with incorrect encoding. * This means that UTF-8-encoded HTML fragments such as those coming from JSON-LD `articleBody` field would be parsed with incorrect encoding.
* Unfortunately, we cannot just put the tag at the start of the HTML fragment, since that would cause parser to auto-insert a `html` element, losing the attributes of the original `html` tag.
* *
* @param string $html UTF-8 encoded document * @param string $html UTF-8 encoded document
*/ */
private static function ensureMetaCharset($html) private static function entitizeNonAscii($html)
{ {
$charsetTag = '<meta charset="utf-8">'; $convmap = [
0x80, 0x1FFFFF, 0, 0x10FFFF,
];
// Only look at first 1024 bytes since, according to HTML5 specification, return mb_encode_numericentity($html, $convmap, 'utf8', true);
// that’s where <meta> elements declaring a character encoding must be located.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#charset
$start = substr($html, 0, 1000);
if (1 === preg_match('/<meta[^>]+charset/i', $start)) {
// <meta> tag is already present, no need for modification.
return $html;
}
if (1 === preg_match('/<head[^>]*>/i', $start)) {
// <head> tag was located, <meta> tags go there.
$html = preg_replace('/<head[^>]*>/i', '$0' . $charsetTag, $html, 1);
return $html;
}
if (1 === preg_match('/<html[^>]*>/i', $start)) {
// <html> tag was located, let’s put it inside and have parser create <head>.
$html = preg_replace('/<html[^>]*>/i', '$0' . $charsetTag, $html, 1);
return $html;
}
// Fallback – just plop the <meta> at the start of the fragment.
return $charsetTag . $html;
} }
} }
+21 -4
View File
@@ -17,6 +17,7 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
public function testConstructDefault() public function testConstructDefault()
{ {
$readability = $this->getReadability(''); $readability = $this->getReadability('');
$this->assertSame('utf8', $readability->tidy_config['input-encoding']);
$this->assertNull($readability->url); $this->assertNull($readability->url);
$this->assertInstanceOf(\DOMDocument::class, $readability->dom); $this->assertInstanceOf(\DOMDocument::class, $readability->dom);
@@ -486,6 +487,20 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
$this->assertStringContainsString('<a href="alice-I.html">Down the Rabbit-Hole</a>', $readability->getContent()->getInnerHtml()); $this->assertStringContainsString('<a href="alice-I.html">Down the Rabbit-Hole</a>', $readability->getContent()->getInnerHtml());
} }
// https://github.com/wallabag/wallabag/issues/8158
public function testCharsetAfterTitle()
{
$readability = $this->getReadability('<!DOCTYPE html><html lang="et"><head><title>Tõde ja õigus I</title> <meta charset="utf-8"></head><body><p>See oli läinud aastasaja kolmanda veerandi lõpul. Päike lähenes silmapiirile, seistes sedavõrd madalas, et enam ei ulatunud valgustama ei mäkke ronivat hobust, kes puutelgedega vankrit vedas, ei vankril istuvat noort naist ega ka ligi kolmekümnelist meest, kes kõndis vankri kõrval.</p></body></html>', 'https://et.wikisource.org/wiki/T%C3%B5de_ja_%C3%B5igus_I/I');
$readability->convertLinksToFootnotes = true;
$res = $readability->init();
$this->assertTrue($res);
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getContent());
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getTitle());
$this->assertSame('Tõde ja õigus I', $readability->getTitle()->getInnerHtml());
$this->assertStringContainsString('Päike lähenes', $readability->getContent()->getInnerHtml());
}
/** /**
* @return array<string, array{0: string, 1: string, 2?: bool}> * @return array<string, array{0: string, 1: string, 2?: bool}>
*/ */
@@ -493,21 +508,21 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
{ {
return [ return [
'meta' => [ 'meta' => [
'<html lang="fr"><head><meta charset="utf-8"></head><body><article>' . str_repeat('<p>This is the awesome content :)</p>', 7) . '</article></body></html>', '<html lang="fr"><head><meta charset="utf-8"></head><body><article>' . str_repeat('<p>Tous les êtres humains naissent libres et égaux en dignité et en droits. Ils sont doués de raison et de conscience et doivent agir les uns envers les autres dans un esprit de fraternité.</p>', 7) . '</article></body></html>',
'fr', 'fr',
], ],
'head' => [ 'head' => [
'<html lang="fr"><head><title>Foo</title></head><body><article>' . str_repeat('<p>This is the awesome content :)</p>', 7) . '</article></body></html>', '<html lang="fr"><head><title>Foo</title></head><body><article>' . str_repeat('<p>Tous les êtres humains naissent libres et égaux en dignité et en droits. Ils sont doués de raison et de conscience et doivent agir les uns envers les autres dans un esprit de fraternité.</p>', 7) . '</article></body></html>',
'fr', 'fr',
], ],
'headless' => [ 'headless' => [
'<html lang="fr"><body><article>' . str_repeat('<p>This is the awesome content :)</p>', 7) . '</article></body></html>', '<html lang="fr"><body><article>' . str_repeat('<p>Tous les êtres humains naissent libres et égaux en dignité et en droits. Ils sont doués de raison et de conscience et doivent agir les uns envers les autres dans un esprit de fraternité.</p>', 7) . '</article></body></html>',
'fr', 'fr',
// tidy would add <head> tag. // tidy would add <head> tag.
false, false,
], ],
'fragment' => [ 'fragment' => [
'<article>' . str_repeat('<p>This is the awesome content :)</p>', 7) . '</article>', '<article>' . str_repeat('<p>Tous les êtres humains naissent libres et égaux en dignité et en droits. Ils sont doués de raison et de conscience et doivent agir les uns envers les autres dans un esprit de fraternité.</p>', 7) . '</article>',
'', '',
// tidy would add <html>. // tidy would add <html>.
false, false,
@@ -526,6 +541,8 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
$this->assertTrue($res); $this->assertTrue($res);
$this->assertInstanceOf(\DOMDocument::class, $readability->dom); $this->assertInstanceOf(\DOMDocument::class, $readability->dom);
$this->assertSame($lang, $readability->dom->documentElement->getAttribute('lang')); $this->assertSame($lang, $readability->dom->documentElement->getAttribute('lang'));
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getContent());
$this->assertStringContainsString('êtres', $readability->getContent()->getInnerHtml());
} }
private function getReadability($html, $url = null, $parser = 'libxml', $useTidy = true) private function getReadability($html, $url = null, $parser = 'libxml', $useTidy = true)