Skip to content

Commit 8ca33f5

Browse files
committed
Encoding: Introduce WHATWG-compliant encoding name canonicalization. (WordPress#14055)
Trac ticket: Core-66259 See WordPress#10640. Introduces a character encoding name canonicalization, coupled with a decoder and encoder interface that code can call to reliably interact with web-standards byte streams and a provided encoding name.
1 parent 79ff428 commit 8ca33f5

5 files changed

Lines changed: 823 additions & 0 deletions

File tree

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
<?php
2+
3+
/**
4+
* WP_Encoding class for interacting with character encoding names and byte streams,
5+
* according to the WHATWG Encoding specification.
6+
*
7+
* @link https://encoding.spec.whatwg.org/
8+
*
9+
* @group encoding
10+
*/
11+
final class WP_Encoding {
12+
private static ?bool $mb_present = null;
13+
14+
/**
15+
* Returns a UTF-8 string loaded from an encoded byte stream.
16+
*
17+
* @see self::export_from_utf8() for exporting from UTF-8.
18+
*
19+
* @since 7.2.0
20+
*
21+
* @param string $source_encoding
22+
* @param string $encoded_bytes
23+
* @return string|null
24+
*/
25+
public static function load_into_utf8( string $source_encoding, string $encoded_bytes ): ?string {
26+
$encoding = self::encoding_from_label( $source_encoding );
27+
if ( null === $encoding ) {
28+
return null;
29+
}
30+
31+
if ( 'replacement' === $encoding ) {
32+
return '';
33+
}
34+
35+
self::$mb_present ??= \function_exists( '\mb_convert_encoding' );
36+
37+
/*
38+
* In a future expansion, when the decoders from the WHATWG spec are
39+
* introduced, this will return according to those fallbacks. For now,
40+
* without the `mbstring` extension, this will fail.
41+
*/
42+
if ( ! self::$mb_present ) {
43+
return null;
44+
}
45+
46+
try {
47+
return \mb_convert_encoding( $encoded_bytes, $encoding, 'UTF-8' );
48+
} catch ( \Throwable $e ) {
49+
return null;
50+
}
51+
}
52+
53+
/**
54+
* Returns an encoded byte stream from a source UTF-8 string.
55+
*
56+
* @see self::load_into_utf8() for importing into UTF-8.
57+
*
58+
* @since 7.2.0
59+
*
60+
* @param string $destination_encoding
61+
* @param string $utf8_bytes
62+
* @return string|null
63+
*/
64+
public static function export_from_utf8( string $destination_encoding, string $utf8_bytes ): ?string {
65+
$encoding = self::encoding_from_label( $destination_encoding );
66+
if ( null === $encoding || 'replacement' === $encoding ) {
67+
return null;
68+
}
69+
70+
self::$mb_present ??= \function_exists( '\mb_convert_encoding' );
71+
72+
/*
73+
* In a future expansion, when the encoders from the WHATWG spec are
74+
* introduced, this will return according to those fallbacks. For now,
75+
* without the `mbstring` extension, this will fail.
76+
*/
77+
if ( ! self::$mb_present ) {
78+
return null;
79+
}
80+
81+
try {
82+
return \mb_convert_encoding( $utf8_bytes, 'UTF-8', $encoding );
83+
} catch ( \Throwable $e ) {
84+
return null;
85+
}
86+
}
87+
88+
/**
89+
* Canonicalizes a character-encoding label into a supported encoding name.
90+
*
91+
* Note! “Supported” does not mean that there’s a way to definitive way to
92+
* convert the encoding; it means that the encoding is not rejected
93+
* by WordPress as unsupported. Actual support currently depends on
94+
* the presence of the `mbstring` extension with support built-in
95+
* for the returned encoding.
96+
*
97+
* The `replacement` encoding implies that a decoder always returns the empty
98+
* string. This catches encodings which pose security risks and should never
99+
* be attempted to be decoded. The `x-user-defined` is a special form of
100+
* decoding byte streams into UTF-8 while preserving the source bytes.
101+
*
102+
* Example:
103+
*
104+
* // Common labels map to well-known encoding names.
105+
* 'UTF-8' === WP_Encoding::encoding_from_label( 'utf8' );
106+
* 'windows-1252' === WP_Encoding::encoding_from_label( 'ISO-8859-1' );
107+
* 'windows-1252' === WP_Encoding::encoding_from_label( 'cp1252' );
108+
*
109+
* // Leading and trailing whitespace is trimmed; matches are ASCII-case-insensitive.
110+
* 'UTF-8' === WP_Encoding::encoding_from_label( 'utf8' );
111+
* 'UTF-8' === WP_Encoding::encoding_from_label( 'UTF8' );
112+
* 'UTF-8' === WP_Encoding::encoding_from_label( " utf-8\t" );
113+
*
114+
* // For security purposes, some labels map to a rejected pseudo-encoding.
115+
* 'replacement' === WP_Encoding::encoding_from_label( 'iso-2022-cn' );
116+
*
117+
* // Unknown or unrecognized labels are rejected entirely.
118+
* null === WP_Encoding::encoding_from_label( 'utf-7' );
119+
* null === WP_Encoding::encoding_from_label( 'UTF-8; latin1' );
120+
* null === WP_Encoding::encoding_from_label( '<meta charset="utf-8">' );
121+
*
122+
* @see self::load_into_utf8() for importing into UTF-8 from these encodings.
123+
* @see self::export_from_utf8() for exporting from UTF-8 into these encodings.
124+
*
125+
* @link https://encoding.spec.whatwg.org/#names-and-labels
126+
*
127+
* @since 7.2.0
128+
*
129+
* @param string $label Candidate encoding name, e.g. "latin1" or "UTF-8".
130+
* @return string|'replacment'|null Canonical name of encoding, if supported, else `null`.
131+
* `'replacement'` means that the decode should always be
132+
* the empty string, as a security precaution.
133+
*/
134+
public static function encoding_from_label( string $label ): ?string {
135+
/*
136+
* > To get an encoding from a string label, run these steps:
137+
* > 1. Remove any leading and trailing ASCII whitespace from label.
138+
* > 2. If label is an ASCII case-insensitive match for any of the labels listed in the table below,
139+
* > then return the corresponding encoding; otherwise return failure.
140+
*/
141+
$label = \trim( $label, " \t\f\r\n" );
142+
$label = \strtolower( $label );
143+
144+
/*
145+
* Pre-filter on the most-common reported names to avoid additional processing.
146+
* A survey of 231,566 root-domain pages from the top ten-million visited sites
147+
* revealed that 13,402 (5.8%) reported a charset. Among those, these case-sensitive
148+
* labels accounted for the overwhelming majority of reports:
149+
*
150+
* | Label | Count | % |
151+
* | UTF-8 | 9,295 | 69 % |
152+
* | iso-8859-1 | 2,150 | 16 % |
153+
* | windows-1252 | 663 | 4.9% |
154+
* | us-ascii | 470 | 3.5% |
155+
* | windows-1251 | 162 | 1.2% |
156+
*
157+
* These account for 95% of reported charset labels, comprising only three distinct
158+
* supported charsets. This is worth the quick-pass even though it induces a tiny
159+
* amount of re-computation in the case that none of these match.
160+
*/
161+
if ( 'utf-8' === $label ) {
162+
return 'UTF-8';
163+
}
164+
165+
if ( 'iso-8859-1' === $label || 'windows-1252' === $label || 'us-ascii' === $label ) {
166+
return 'windows-1252';
167+
}
168+
169+
if ( 'windows-1251' === $label ) {
170+
return 'windows-1251';
171+
}
172+
173+
/*
174+
* If whitespace exists within the label, it’s probably the accidental concatenation
175+
* of multiple labels and no disambiguation is possible. If a semicolon is trapped
176+
* in the name then this probably comes from mis-parsing of a `charset` property on
177+
* a MIME content-type.
178+
*/
179+
if ( \strlen( $label ) !== \strcspn( $label, " \t\f\r\n;" ) ) {
180+
return null;
181+
}
182+
183+
$label = " {$label} ";
184+
185+
/**
186+
* Mapping from encoding name to space-separated list of its labels.
187+
*
188+
* > The table below lists all encodings and their labels user agents must
189+
* > support. User agents must not support any other encodings or labels.
190+
*
191+
* Every label will be surrounded on each side by spaces, as labels containing
192+
* spaces are rejected. This allows for efficient string lookup.
193+
*
194+
* @see \generate_charset_labels_table()
195+
*
196+
* @since 7.2.0
197+
*
198+
* @var $table non-empty-array<non-empty-string, non-empty-string>
199+
*/
200+
$table = require __DIR__ . '/wp-supported-encodings.php';
201+
202+
foreach ( $table as $name => $labels ) {
203+
if ( \str_contains( $labels, $label ) ) {
204+
return $name;
205+
}
206+
}
207+
208+
return null;
209+
}
210+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
/**
4+
* Auto-generated class for mapping character set labels to supported names.
5+
*
6+
* ⚠️ !!! THIS ENTIRE FILE IS AUTOMATICALLY GENERATED !!! ⚠️
7+
* Do not modify this file directly.
8+
*
9+
* To regenerate, run the generation script directly.
10+
*
11+
* Example:
12+
*
13+
* php tests/phpunit/data/encoding/generate-labels-table.php
14+
*
15+
* @package WordPress
16+
* @since 7.2.0
17+
*/
18+
19+
// phpcs:disable
20+
21+
/**
22+
* Mapping from encoding name to space-separated list of its labels.
23+
*
24+
* > The table below lists all encodings and their labels user agents must
25+
* > support. User agents must not support any other encodings or labels.
26+
*
27+
* Every label will be surrounded on each side by spaces, as labels containing
28+
* spaces are rejected. This allows for efficient string lookup.
29+
*
30+
* @see \generate_charset_labels_table()
31+
*
32+
* @since 7.2.0
33+
*
34+
* @type array<non-empty-string, non-empty-string>
35+
*/
36+
return array(
37+
'UTF-8' => ' unicode-1-1-utf-8 unicode11utf8 unicode20utf8 utf-8 utf8 x-unicode20utf8 ',
38+
'IBM866' => ' 866 cp866 csibm866 ibm866 ',
39+
'ISO-8859-2' => ' csisolatin2 iso-8859-2 iso-ir-101 iso8859-2 iso88592 iso_8859-2 iso_8859-2:1987 l2 latin2 ',
40+
'ISO-8859-3' => ' csisolatin3 iso-8859-3 iso-ir-109 iso8859-3 iso88593 iso_8859-3 iso_8859-3:1988 l3 latin3 ',
41+
'ISO-8859-4' => ' csisolatin4 iso-8859-4 iso-ir-110 iso8859-4 iso88594 iso_8859-4 iso_8859-4:1988 l4 latin4 ',
42+
'ISO-8859-5' => ' csisolatincyrillic cyrillic iso-8859-5 iso-ir-144 iso8859-5 iso88595 iso_8859-5 iso_8859-5:1988 ',
43+
'ISO-8859-6' => ' arabic asmo-708 csiso88596e csiso88596i csisolatinarabic ecma-114 iso-8859-6 iso-8859-6-e iso-8859-6-i iso-ir-127 iso8859-6 iso88596 iso_8859-6 iso_8859-6:1987 ',
44+
'ISO-8859-7' => ' csisolatingreek ecma-118 elot_928 greek greek8 iso-8859-7 iso-ir-126 iso8859-7 iso88597 iso_8859-7 iso_8859-7:1987 sun_eu_greek ',
45+
'ISO-8859-8' => ' csiso88598e csisolatinhebrew hebrew iso-8859-8 iso-8859-8-e iso-ir-138 iso8859-8 iso88598 iso_8859-8 iso_8859-8:1988 visual ',
46+
'ISO-8859-8-I' => ' csiso88598i iso-8859-8-i logical ',
47+
'ISO-8859-10' => ' csisolatin6 iso-8859-10 iso-ir-157 iso8859-10 iso885910 l6 latin6 ',
48+
'ISO-8859-13' => ' iso-8859-13 iso8859-13 iso885913 ',
49+
'ISO-8859-14' => ' iso-8859-14 iso8859-14 iso885914 ',
50+
'ISO-8859-15' => ' csisolatin9 iso-8859-15 iso8859-15 iso885915 iso_8859-15 l9 ',
51+
'ISO-8859-16' => ' iso-8859-16 ',
52+
'KOI8-R' => ' cskoi8r koi koi8 koi8-r koi8_r ',
53+
'KOI8-U' => ' koi8-ru koi8-u ',
54+
'macintosh' => ' csmacintosh mac macintosh x-mac-roman ',
55+
'windows-874' => ' dos-874 iso-8859-11 iso8859-11 iso885911 tis-620 windows-874 ',
56+
'windows-1250' => ' cp1250 windows-1250 x-cp1250 ',
57+
'windows-1251' => ' cp1251 windows-1251 x-cp1251 ',
58+
'windows-1252' => ' ansi_x3.4-1968 ascii cp1252 cp819 csisolatin1 ibm819 iso-8859-1 iso-ir-100 iso8859-1 iso88591 iso_8859-1 iso_8859-1:1987 l1 latin1 us-ascii windows-1252 x-cp1252 ',
59+
'windows-1253' => ' cp1253 windows-1253 x-cp1253 ',
60+
'windows-1254' => ' cp1254 csisolatin5 iso-8859-9 iso-ir-148 iso8859-9 iso88599 iso_8859-9 iso_8859-9:1989 l5 latin5 windows-1254 x-cp1254 ',
61+
'windows-1255' => ' cp1255 windows-1255 x-cp1255 ',
62+
'windows-1256' => ' cp1256 windows-1256 x-cp1256 ',
63+
'windows-1257' => ' cp1257 windows-1257 x-cp1257 ',
64+
'windows-1258' => ' cp1258 windows-1258 x-cp1258 ',
65+
'x-mac-cyrillic' => ' x-mac-cyrillic x-mac-ukrainian ',
66+
'GBK' => ' chinese csgb2312 csiso58gb231280 gb2312 gb_2312 gb_2312-80 gbk iso-ir-58 x-gbk ',
67+
'gb18030' => ' gb18030 ',
68+
'Big5' => ' big5 big5-hkscs cn-big5 csbig5 x-x-big5 ',
69+
'EUC-JP' => ' cseucpkdfmtjapanese euc-jp x-euc-jp ',
70+
'ISO-2022-JP' => ' csiso2022jp iso-2022-jp ',
71+
'Shift_JIS' => ' csshiftjis ms932 ms_kanji shift-jis shift_jis sjis windows-31j x-sjis ',
72+
'EUC-KR' => ' cseuckr csksc56011987 euc-kr iso-ir-149 korean ks_c_5601-1987 ks_c_5601-1989 ksc5601 ksc_5601 windows-949 ',
73+
'replacement' => ' csiso2022kr hz-gb-2312 iso-2022-cn iso-2022-cn-ext iso-2022-kr replacement ',
74+
'UTF-16BE' => ' unicodefffe utf-16be ',
75+
'UTF-16LE' => ' csunicode iso-10646-ucs-2 ucs-2 unicode unicodefeff utf-16 utf-16le ',
76+
'x-user-defined' => ' x-user-defined ',
77+
);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# WHATWG Encodings and Labels
2+
3+
This directory contains the listing of encoding labels and their associated encoding name.
4+
5+
- https://encoding.spec.whatwg.org/#names-and-labels
6+
7+
The non-normative [`encodings.json`](https://encoding.spec.whatwg.org/encodings.json) file comes from the WHATWG server, and
8+
is cached here in the test directory so that it doesn't need to be constantly re-downloaded.
9+
10+
## Updating the optimized lookup class.
11+
12+
The [`class-wp-encodings.php`][1] file contains an optimized lookup map for the names and lables in `encodings.json`.
13+
Run the [`generate-labels-table.php`][2] file to update the auto-generated Core module.
14+
15+
```bash
16+
~$ php tests/phpunit/data/html5-entities/generate-html5-named-character-references.php
17+
OK: Successfully generated optimized lookup class.
18+
```
19+
20+
[1]: ../../../../src/wp-includes/class-wp-encodings.php
21+
[2]: generate-labels-table.php

0 commit comments

Comments
 (0)