From a0b542a6768201b0269f669dda6a63c2d969cf8d Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Mon, 20 Oct 2025 23:35:29 +0000 Subject: [PATCH 1/3] Coding Standards: Use more meaningful variable names in Plugin Upgrader. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the [https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/#naming-conventions Naming Conventions]: > Don’t abbreviate variable names unnecessarily; let the code be unambiguous and self-documenting. This commit includes renaming of the following variables: * `$r` to `$upgrade_data`. * `$res` to `$connected`. * `$pluginfiles` to `$plugin_files` — Per naming conventions, separate words via underscores. * `$info` to `$new_plugin_data`. Follow-up to [6779], [8550], [9141], [11005], [12157], [18618], [56525]. Props costdev, mukesh27. See #63168. git-svn-id: https://develop.svn.wordpress.org/trunk@60997 602fd350-edb4-49c9-b593-d223f7449a82 --- .../includes/class-plugin-upgrader.php | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/wp-admin/includes/class-plugin-upgrader.php b/src/wp-admin/includes/class-plugin-upgrader.php index 7871546a092eb..2f059b7e05887 100644 --- a/src/wp-admin/includes/class-plugin-upgrader.php +++ b/src/wp-admin/includes/class-plugin-upgrader.php @@ -206,7 +206,7 @@ public function upgrade( $plugin, $args = array() ) { } // Get the URL to the zip file. - $r = $current->response[ $plugin ]; + $upgrade_data = $current->response[ $plugin ]; add_filter( 'upgrader_pre_install', array( $this, 'deactivate_plugin_before_upgrade' ), 10, 2 ); add_filter( 'upgrader_pre_install', array( $this, 'active_before' ), 10, 2 ); @@ -223,7 +223,7 @@ public function upgrade( $plugin, $args = array() ) { $this->run( array( - 'package' => $r->package, + 'package' => $upgrade_data->package, 'destination' => WP_PLUGIN_DIR, 'clear_destination' => true, 'clear_working' => true, @@ -301,8 +301,8 @@ public function bulk_upgrade( $plugins, $args = array() ) { $this->skin->header(); // Connect to the filesystem first. - $res = $this->fs_connect( array( WP_CONTENT_DIR, WP_PLUGIN_DIR ) ); - if ( ! $res ) { + $connected = $this->fs_connect( array( WP_CONTENT_DIR, WP_PLUGIN_DIR ) ); + if ( ! $connected ) { $this->skin->footer(); return false; } @@ -341,32 +341,32 @@ public function bulk_upgrade( $plugins, $args = array() ) { } // Get the URL to the zip file. - $r = $current->response[ $plugin ]; + $upgrade_data = $current->response[ $plugin ]; $this->skin->plugin_active = is_plugin_active( $plugin ); - if ( isset( $r->requires ) && ! is_wp_version_compatible( $r->requires ) ) { + if ( isset( $upgrade_data->requires ) && ! is_wp_version_compatible( $upgrade_data->requires ) ) { $result = new WP_Error( 'incompatible_wp_required_version', sprintf( /* translators: 1: Current WordPress version, 2: WordPress version required by the new plugin version. */ __( 'Your WordPress version is %1$s, however the new plugin version requires %2$s.' ), $wp_version, - $r->requires + $upgrade_data->requires ) ); $this->skin->before( $result ); $this->skin->error( $result ); $this->skin->after(); - } elseif ( isset( $r->requires_php ) && ! is_php_version_compatible( $r->requires_php ) ) { + } elseif ( isset( $upgrade_data->requires_php ) && ! is_php_version_compatible( $upgrade_data->requires_php ) ) { $result = new WP_Error( 'incompatible_php_required_version', sprintf( /* translators: 1: Current PHP version, 2: PHP version required by the new plugin version. */ __( 'The PHP version on your server is %1$s, however the new plugin version requires %2$s.' ), PHP_VERSION, - $r->requires_php + $upgrade_data->requires_php ) ); @@ -377,7 +377,7 @@ public function bulk_upgrade( $plugins, $args = array() ) { add_filter( 'upgrader_source_selection', array( $this, 'check_package' ) ); $result = $this->run( array( - 'package' => $r->package, + 'package' => $upgrade_data->package, 'destination' => WP_PLUGIN_DIR, 'clear_destination' => true, 'clear_working' => true, @@ -478,9 +478,9 @@ public function check_package( $source ) { $files = glob( $working_directory . '*.php' ); if ( $files ) { foreach ( $files as $file ) { - $info = get_plugin_data( $file, false, false ); - if ( ! empty( $info['Name'] ) ) { - $this->new_plugin_data = $info; + $new_plugin_data = get_plugin_data( $file, false, false ); + if ( ! empty( $new_plugin_data['Name'] ) ) { + $this->new_plugin_data = $new_plugin_data; break; } } @@ -490,8 +490,8 @@ public function check_package( $source ) { return new WP_Error( 'incompatible_archive_no_plugins', $this->strings['incompatible_archive'], __( 'No valid plugins were found.' ) ); } - $requires_php = isset( $info['RequiresPHP'] ) ? $info['RequiresPHP'] : null; - $requires_wp = isset( $info['RequiresWP'] ) ? $info['RequiresWP'] : null; + $requires_php = isset( $new_plugin_data['RequiresPHP'] ) ? $new_plugin_data['RequiresPHP'] : null; + $requires_wp = isset( $new_plugin_data['RequiresWP'] ) ? $new_plugin_data['RequiresWP'] : null; if ( ! is_php_version_compatible( $requires_php ) ) { $error = sprintf( @@ -542,9 +542,9 @@ public function plugin_info() { } // Assume the requested plugin is the first in the list. - $pluginfiles = array_keys( $plugin ); + $plugin_files = array_keys( $plugin ); - return $this->result['destination_name'] . '/' . $pluginfiles[0]; + return $this->result['destination_name'] . '/' . $plugin_files[0]; } /** From f4c8b13ea043d51b5a29556b32f22829c983b97b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 20 Oct 2025 23:48:10 +0000 Subject: [PATCH 2/3] Themes: Update `Tests_Template::test_taxonomy_template_hierarchy` to account for new taxonomy template. Follow-up to [60994]. See #35326. git-svn-id: https://develop.svn.wordpress.org/trunk@60998 602fd350-edb4-49c9-b593-d223f7449a82 --- tests/phpunit/tests/template.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index 89136c00e3361..5ac5cb52943f7 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -184,6 +184,7 @@ public function test_taxonomy_template_hierarchy() { array( 'taxonomy-taxo-foo-😀.php', 'taxonomy-taxo-foo-%f0%9f%98%80.php', + "taxonomy-taxo-{$term->term_id}.php", 'taxonomy-taxo.php', 'taxonomy.php', 'archive.php', From 8e0c7eb7e80573a591213ea09f3d9cefcd66f2b3 Mon Sep 17 00:00:00 2001 From: Dennis Snell Date: Mon, 22 Sep 2025 23:04:12 -0500 Subject: [PATCH 3/3] Charset: Track detection of non-characters when scanning UTF-8. Noncharacters are code points that are permantently reserved in the Unicode Standard for internal use. They are not recommended for use in open interchange of Unicode text data. However, they are valid code points and will not cause a string to return as invalid. Still, HTML and XML both impose semantic rules on their use and it may be important for code to know whether they are present in a string. This patch introduces a new function, `wp_has_noncharacters()`, which answers this question. This is accomplished through an inline check with the fallback UTF-8 scanner. There are 66 noncharacters, making it difficult to find them properly with common string search functionality. While the inline check adds overhead to the scanning process, the rare occurrance of noncharacters should lead to minimal actual overhead due to strong branch prediction. See https://www.unicode.org/versions/Unicode17.0.0/core-spec/chapter-23/#G12612 --- src/wp-includes/compat-utf8.php | 66 +++++- src/wp-includes/utf8.php | 42 ++++ .../tests/unicode/wpHasNoncharacters.php | 188 ++++++++++++++++++ 3 files changed, 285 insertions(+), 11 deletions(-) create mode 100644 tests/phpunit/tests/unicode/wpHasNoncharacters.php diff --git a/src/wp-includes/compat-utf8.php b/src/wp-includes/compat-utf8.php index dfff646d60bf2..629cc1864f92f 100644 --- a/src/wp-includes/compat-utf8.php +++ b/src/wp-includes/compat-utf8.php @@ -35,19 +35,21 @@ * @since 6.9.0 * @access private * - * @param string $bytes UTF-8 encoded string which might include invalid spans of bytes. - * @param int $at Where to start scanning. - * @param int $invalid_length Will be set to how many bytes are to be ignored after `$at`. - * @param int|null $max_bytes Stop scanning after this many bytes have been seen. - * @param int|null $max_code_points Stop scanning after this many code points have been seen. + * @param string $bytes UTF-8 encoded string which might include invalid spans of bytes. + * @param int $at Where to start scanning. + * @param int $invalid_length Will be set to how many bytes are to be ignored after `$at`. + * @param int|null $max_bytes Stop scanning after this many bytes have been seen. + * @param int|null $max_code_points Stop scanning after this many code points have been seen. + * @param bool $has_noncharacters Set to indicate if scanned string contained noncharacters. * @return int How many code points were successfully scanned. */ -function _wp_scan_utf8( string $bytes, int &$at, int &$invalid_length, ?int $max_bytes = null, ?int $max_code_points = null ): int { - $byte_length = strlen( $bytes ); - $end = min( $byte_length, $at + ( $max_bytes ?? PHP_INT_MAX ) ); - $invalid_length = 0; - $count = 0; - $max_count = $max_code_points ?? PHP_INT_MAX; +function _wp_scan_utf8( string $bytes, int &$at, int &$invalid_length, ?int $max_bytes = null, ?int $max_code_points = null, ?bool &$has_noncharacters = null ): int { + $byte_length = strlen( $bytes ); + $end = min( $byte_length, $at + ( $max_bytes ?? PHP_INT_MAX ) ); + $invalid_length = 0; + $count = 0; + $max_count = $max_code_points ?? PHP_INT_MAX; + $has_noncharacters = false; for ( $i = $at; $i < $end && $count <= $max_count; $i++ ) { /* @@ -145,6 +147,15 @@ function _wp_scan_utf8( string $bytes, int &$at, int &$invalid_length, ?int $max ) { ++$count; $i += 2; + + // Covers the range U+FDD0–U+FDEF, U+FFFE, U+FFFF. + if ( 0xEF === $b1 ) { + $has_noncharacters |= ( + ( 0xB7 === $b2 && $b3 >= 0x90 && $b3 <= 0xAF ) || + ( 0xBF === $b2 && ( 0xBE === $b3 || 0xBF === $b3 ) ) + ); + } + continue; } @@ -162,6 +173,14 @@ function _wp_scan_utf8( string $bytes, int &$at, int &$invalid_length, ?int $max ) { ++$count; $i += 3; + + // Covers U+1FFFE, U+1FFFF, U+2FFFE, U+2FFFF, …, U+10FFFE, U+10FFFF. + $has_noncharacters |= ( + ( 0x0F === ( $b2 & 0x0F ) ) && + 0xBF === $b3 && + ( 0xBE === $b4 || 0xBF === $b4 ) + ); + continue; } @@ -380,6 +399,31 @@ function _wp_utf8_codepoint_span( string $text, int $byte_offset, int $max_code_ return $byte_offset - $was_at; } +/** + * Fallback support for determining if a string contains Unicode noncharacters. + * + * @since 6.9.0 + * @access private + * + * @see \wp_has_noncharacters() + * + * @param string $text Are there noncharacters in this string? + * @return bool Whether noncharacters were found in the string. + */ +function _wp_has_noncharacters_fallback( string $text ): bool { + $at = 0; + $invalid_length = 0; + $has_noncharacters = false; + $end = strlen( $text ); + + while ( $at < $end && ! $has_noncharacters ) { + _wp_scan_utf8( $text, $at, $invalid_length, null, null, $has_noncharacters ); + $at += $invalid_length; + } + + return $has_noncharacters; +} + /** * Converts a string from ISO-8859-1 to UTF-8, maintaining backwards compatibility * with the deprecated function from the PHP standard library. diff --git a/src/wp-includes/utf8.php b/src/wp-includes/utf8.php index f071b47fa82aa..da10838f233d1 100644 --- a/src/wp-includes/utf8.php +++ b/src/wp-includes/utf8.php @@ -133,3 +133,45 @@ function wp_scrub_utf8( $text ) { return _wp_scrub_utf8_fallback( $text ); } endif; + +if ( _wp_can_use_pcre_u() ) : + /** + * Returns whether the given string contains Unicode noncharacters. + * + * XML recommends against using noncharacters and HTML forbids their + * use in attribute names. Unicode recommends that they not be used + * in open exchange of data. + * + * Noncharacters are code points within the following ranges: + * - U+FDD0–U+FDEF + * - U+FFFE–U+FFFF + * - U+1FFFE, U+1FFFF, U+2FFFE, U+2FFFF, …, U+10FFFE, U+10FFFF + * + * @see https://www.unicode.org/versions/Unicode17.0.0/core-spec/chapter-23/#G12612 + * @see https://www.w3.org/TR/xml/#charsets + * @see https://html.spec.whatwg.org/#attributes-2 + * + * @since 6.9.0 + * + * @param string $text Are there noncharacters in this string? + * @return bool Whether noncharacters were found in the string. + */ + function wp_has_noncharacters( string $text ): bool { + return 1 === preg_match( + '/[\x{FDD0}-\x{FDEF}\x{FFFE}\x{FFFF}\x{1FFFE}\x{1FFFF}\x{2FFFE}\x{2FFFF}\x{3FFFE}\x{3FFFF}\x{4FFFE}\x{4FFFF}\x{5FFFE}\x{5FFFF}\x{6FFFE}\x{6FFFF}\x{7FFFE}\x{7FFFF}\x{8FFFE}\x{8FFFF}\x{9FFFE}\x{9FFFF}\x{AFFFE}\x{AFFFF}\x{BFFFE}\x{BFFFF}\x{CFFFE}\x{CFFFF}\x{DFFFE}\x{DFFFF}\x{EFFFE}\x{EFFFF}\x{FFFFE}\x{FFFFF}\x{10FFFE}\x{10FFFF}]/u', + $text + ); + } +else : + /** + * Fallback function for detecting noncharacters in a text. + * + * @ignore + * @private + * + * @since 6.9.0 + */ + function wp_has_noncharacters( string $text ): bool { + return _wp_has_noncharacters_fallback( $text ); + } +endif; diff --git a/tests/phpunit/tests/unicode/wpHasNoncharacters.php b/tests/phpunit/tests/unicode/wpHasNoncharacters.php new file mode 100644 index 0000000000000..d3022dd922df2 --- /dev/null +++ b/tests/phpunit/tests/unicode/wpHasNoncharacters.php @@ -0,0 +1,188 @@ +assertTrue( + wp_has_noncharacters( $noncharacter ), + 'Failed to detect entire string as noncharacter.' + ); + + $this->assertTrue( + wp_has_noncharacters( "{$noncharacter} and more." ), + 'Failed to detect noncharacter prefix.' + ); + + $this->assertTrue( + wp_has_noncharacters( "Some text and then a {$noncharacter} and more." ), + 'Failed to detect medial noncharacter.' + ); + + $this->assertTrue( + wp_has_noncharacters( "Some text and a {$noncharacter}." ), + 'Failed to detect noncharacter suffix.' + ); + } + + /** + * Ensures that a noncharacter inside a string will be properly detected + * using the fallback function when Unicode PCRE support is missing. + * + * @ticket 63863 + * + * @dataProvider data_noncharacters + * + * @param string $noncharacter Noncharacter as a UTF-8 string. + */ + public function test_fallback_detects_non_characters( string $noncharacter ) { + $this->assertTrue( + _wp_has_noncharacters_fallback( $noncharacter ), + 'Failed to detect entire string as noncharacter.' + ); + + $this->assertTrue( + _wp_has_noncharacters_fallback( "{$noncharacter} and more." ), + 'Failed to detect noncharacter prefix.' + ); + + $this->assertTrue( + _wp_has_noncharacters_fallback( "Some text and then a {$noncharacter} and more." ), + 'Failed to detect medial noncharacter.' + ); + + $this->assertTrue( + _wp_has_noncharacters_fallback( "Some text and a {$noncharacter}." ), + 'Failed to detect noncharacter suffix.' + ); + } + + /** + * Ensures that Unicode characters are not falsely detect as noncharacters. + * + * @ticket 63863 + */ + public function test_avoids_false_positives() { + // Get all the noncharacters in one long string, each surrounded on both sides by null bytes. + $noncharacters = implode( + "\x00", + array_map( + static function ( $c ) { + return "\x00{$c}"; + }, + array_column( array_values( iterator_to_array( self::data_noncharacters() ) ), 0 ) + ) + ) . "\x00"; + + $this->assertFalse( + wp_has_noncharacters( "\x00" ), + 'Falsely detected noncharacter in U+0000' + ); + + for ( $code_point = 1; $code_point <= 0x10FFFF; $code_point++ ) { + // Surrogate halves are invalid UTF-8. + if ( $code_point >= 0xD800 && $code_point <= 0xDFFF ) { + continue; + } + + $char = mb_chr( $code_point ); + $hex_char = strtoupper( str_pad( dechex( $code_point ), 4, '0', STR_PAD_LEFT ) ); + + if ( str_contains( $noncharacters, $char ) ) { + $this->assertTrue( + wp_has_noncharacters( $char ), + "Failed to detect noncharacter as test verification for U+{$hex_char}" + ); + } else { + $this->assertFalse( + wp_has_noncharacters( $char ), + "Falsely detected noncharacter in U+{$hex_char}." + ); + } + } + } + + /** + * Ensures that Unicode characters are not falsely detect as noncharacters + * using the fallback function when Unicode PCRE support is missing. + * + * @ticket 63863 + */ + public function test_fallback_avoids_false_positives() { + // Get all the noncharacters in one long string, each surrounded on both sides by null bytes. + $noncharacters = implode( + "\x00", + array_map( + static function ( $c ) { + return "\x00{$c}"; + }, + array_column( array_values( iterator_to_array( self::data_noncharacters() ) ), 0 ) + ) + ) . "\x00"; + + $this->assertFalse( + _wp_has_noncharacters_fallback( "\x00" ), + 'Falsely detected noncharacter in U+0000' + ); + + for ( $code_point = 1; $code_point <= 0x10FFFF; $code_point++ ) { + // Surrogate halves are invalid UTF-8. + if ( $code_point >= 0xD800 && $code_point <= 0xDFFF ) { + continue; + } + + $char = mb_chr( $code_point ); + $hex_char = strtoupper( str_pad( dechex( $code_point ), 4, '0', STR_PAD_LEFT ) ); + + if ( str_contains( $noncharacters, $char ) ) { + $this->assertTrue( + _wp_has_noncharacters_fallback( $char ), + "Failed to detect noncharacter as test verification for U+{$hex_char}" + ); + } else { + $this->assertFalse( + _wp_has_noncharacters_fallback( $char ), + "Falsely detected noncharacter in U+{$hex_char}." + ); + } + } + } + + /** + * Data provider + * + * @return array[] + */ + public static function data_noncharacters() { + for ( $code_point = 0xFDD0; $code_point <= 0xFDEF; $code_point++ ) { + $hex_char = strtoupper( str_pad( dechex( $code_point ), 4, '0', STR_PAD_LEFT ) ); + yield "U+{$hex_char}" => array( mb_chr( $code_point ) ); + } + + yield 'U+FFFE' => array( "\u{FFFE}" ); + yield 'U+FFFF' => array( "\u{FFFF}" ); + + for ( $plane = 0x10000; $plane <= 0x10FFFF; $plane += 0x10000 ) { + $code_point = $plane + 0xFFFE; + $hex_char = strtoupper( str_pad( dechex( $code_point ), 4, '0', STR_PAD_LEFT ) ); + yield "U+{$hex_char}" => array( mb_chr( $code_point ) ); + + $code_point = $plane + 0xFFFF; + $hex_char = strtoupper( str_pad( dechex( $code_point ), 4, '0', STR_PAD_LEFT ) ); + yield "U+{$hex_char}" => array( mb_chr( $code_point ) ); + } + } +}