From 56e16bda31b0d82b6701d1d656749f12ca0dcf11 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Mon, 15 Jan 2024 17:40:06 +0000 Subject: [PATCH 01/36] Comments: Use `post_password_required()` for comment capability checks. Follow-up to [56836], [57123]. Fixes #59929. git-svn-id: https://develop.svn.wordpress.org/trunk@57285 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/class-wp-comments-list-table.php | 2 +- src/wp-admin/includes/class-wp-list-table.php | 2 +- src/wp-admin/includes/dashboard.php | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/wp-admin/includes/class-wp-comments-list-table.php b/src/wp-admin/includes/class-wp-comments-list-table.php index 122a719450206..993fb7cafcd05 100644 --- a/src/wp-admin/includes/class-wp-comments-list-table.php +++ b/src/wp-admin/includes/class-wp-comments-list-table.php @@ -648,7 +648,7 @@ public function single_row( $item ) { $edit_post_cap = $post ? 'edit_post' : 'edit_posts'; if ( ! current_user_can( $edit_post_cap, $comment->comment_post_ID ) - && ( ! empty( $post->post_password ) + && ( post_password_required( $comment->comment_post_ID ) || ! current_user_can( 'read_post', $comment->comment_post_ID ) ) ) { // The user has no access to the post and thus cannot see the comments. diff --git a/src/wp-admin/includes/class-wp-list-table.php b/src/wp-admin/includes/class-wp-list-table.php index 31168803d9ba8..30d7c854cb63c 100644 --- a/src/wp-admin/includes/class-wp-list-table.php +++ b/src/wp-admin/includes/class-wp-list-table.php @@ -832,7 +832,7 @@ protected function comments_bubble( $post_id, $pending_comments ) { $edit_post_cap = $post_object ? 'edit_post' : 'edit_posts'; if ( ! current_user_can( $edit_post_cap, $post_id ) - && ( ! empty( $post_object->post_password ) + && ( post_password_required( $post_id ) || ! current_user_can( 'read_post', $post_id ) ) ) { // The user has no access to the post and thus cannot see the comments. diff --git a/src/wp-admin/includes/dashboard.php b/src/wp-admin/includes/dashboard.php index bbbeb0b8555f9..ea870a1f5b1ee 100644 --- a/src/wp-admin/includes/dashboard.php +++ b/src/wp-admin/includes/dashboard.php @@ -1088,10 +1088,8 @@ function wp_dashboard_recent_comments( $total_items = 5 ) { } foreach ( $possible as $comment ) { - $comment_post = get_post( $comment->comment_post_ID ); - if ( ! current_user_can( 'edit_post', $comment->comment_post_ID ) - && ( ! empty( $comment_post->post_password ) + && ( post_password_required( $comment->comment_post_ID ) || ! current_user_can( 'read_post', $comment->comment_post_ID ) ) ) { // The user has no access to the post and thus cannot see the comments. From 0d109bda84fa9f6cf5fa60ccf8fc36a0700e81f7 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 15 Jan 2024 18:55:59 +0000 Subject: [PATCH 02/36] Upgrade/Install: Fix JavaScript localization on install page. Blocks registration causes scripts to be initialized and localized very early, before the current locale has been properly set on the installation page. This changes `determine_locale()` so that the locale chosen during installation is recognized and loaded earlier, ensuring proper script localization. Props sabernhardt, NekoJonez, jornp, costdev. Fixes #58696 git-svn-id: https://develop.svn.wordpress.org/trunk@57286 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/install.php | 2 +- src/wp-includes/l10n.php | 9 +++++ tests/phpunit/tests/l10n/determineLocale.php | 37 +++++++++++++++++++- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/wp-admin/install.php b/src/wp-admin/install.php index bf102893ff9df..34d5d4c7f7ab7 100644 --- a/src/wp-admin/install.php +++ b/src/wp-admin/install.php @@ -329,7 +329,7 @@ function display_setup_form( $error = null ) { */ $language = ''; if ( ! empty( $_REQUEST['language'] ) ) { - $language = preg_replace( '/[^a-zA-Z0-9_]/', '', $_REQUEST['language'] ); + $language = sanitize_locale_name( $_REQUEST['language'] ); } elseif ( isset( $GLOBALS['wp_local_package'] ) ) { $language = $GLOBALS['wp_local_package']; } diff --git a/src/wp-includes/l10n.php b/src/wp-includes/l10n.php index 67bfb1a26e8cb..598a485468b9c 100644 --- a/src/wp-includes/l10n.php +++ b/src/wp-includes/l10n.php @@ -150,6 +150,15 @@ function determine_locale() { ( isset( $_GET['_locale'] ) && 'user' === $_GET['_locale'] && wp_is_json_request() ) ) { $determined_locale = get_user_locale(); + } elseif ( + ( ! empty( $_REQUEST['language'] ) || isset( $GLOBALS['wp_local_package'] ) ) + && wp_installing() + ) { + if ( ! empty( $_REQUEST['language'] ) ) { + $determined_locale = sanitize_locale_name( $_REQUEST['language'] ); + } else { + $determined_locale = $GLOBALS['wp_local_package']; + } } if ( ! $determined_locale ) { diff --git a/tests/phpunit/tests/l10n/determineLocale.php b/tests/phpunit/tests/l10n/determineLocale.php index 51ab1c28b95f6..3a675dc515098 100644 --- a/tests/phpunit/tests/l10n/determineLocale.php +++ b/tests/phpunit/tests/l10n/determineLocale.php @@ -20,7 +20,15 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { } public function tear_down() { - unset( $_SERVER['CONTENT_TYPE'], $_GET['_locale'], $_COOKIE['wp_lang'], $GLOBALS['pagenow'] ); + unset( + $_SERVER['CONTENT_TYPE'], + $_GET['_locale'], + $_COOKIE['wp_lang'], + $GLOBALS['pagenow'], + $GLOBALS['wp_local_package'], + $_REQUEST['language'] + ); + wp_installing( false ); parent::tear_down(); } @@ -273,4 +281,31 @@ static function () { $this->assertSame( 'siteLocale', determine_locale() ); } + + public function test_language_param_not_installing() { + $_REQUEST['language'] = 'de_DE'; + $this->assertSame( 'en_US', determine_locale() ); + } + + public function test_language_param_installing() { + $_REQUEST['language'] = 'de_DE'; + wp_installing( true ); + $this->assertSame( 'de_DE', determine_locale() ); + } + + public function test_language_param_installing_incorrect_string() { + $_REQUEST['language'] = '####'; // Something sanitize_locale_name() strips away. + wp_installing( true ); + $this->assertSame( 'en_US', determine_locale() ); + } + + public function test_wp_local_package_global_not_installing() { + $_REQUEST['language'] = 'de_DE'; + $this->assertSame( 'en_US', determine_locale() ); + } + public function test_wp_local_package_global_installing() { + $_REQUEST['language'] = 'de_DE'; + wp_installing( true ); + $this->assertSame( 'de_DE', determine_locale() ); + } } From 24531a9afc8ef75822da45b85c147b492674e715 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 15 Jan 2024 19:03:27 +0000 Subject: [PATCH 03/36] I18N: Cache list of language file paths in `WP_Textdomain_Registry`. Loading a list of language file paths using `glob()` can be expensive if involving thousands of files. Expands scope of `WP_Textdomain_Registry` to cache list of language file paths in object cache and provides a way to invalidate that cache upon translation updates. Plugins can clear the cache using calls such as `wp_cache_delete( 'cached_mo_files_' . md5( $path ), 'translations' );` Props mreishus, swissspidy Fixes #58919 git-svn-id: https://develop.svn.wordpress.org/trunk@57287 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-textdomain-registry.php | 140 +++++++++++++++--- src/wp-includes/l10n.php | 8 +- src/wp-settings.php | 1 + .../tests/l10n/wpTextdomainRegistry.php | 104 ++++++++++--- 4 files changed, 207 insertions(+), 46 deletions(-) diff --git a/src/wp-includes/class-wp-textdomain-registry.php b/src/wp-includes/class-wp-textdomain-registry.php index 99d72eb7a3084..f0c8b8ded7e0f 100644 --- a/src/wp-includes/class-wp-textdomain-registry.php +++ b/src/wp-includes/class-wp-textdomain-registry.php @@ -51,8 +51,11 @@ class WP_Textdomain_Registry { * Holds a cached list of available .mo files to improve performance. * * @since 6.1.0 + * @since 6.5.0 This property is no longer used. * * @var array + * + * @deprecated */ protected $cached_mo_files = array(); @@ -65,6 +68,18 @@ class WP_Textdomain_Registry { */ protected $domains_with_translations = array(); + /** + * Initializes the registry. + * + * Hooks into the {@see 'upgrader_process_complete'} filter + * to invalidate MO files caches. + * + * @since 6.5.0 + */ + public function init() { + add_action( 'upgrader_process_complete', array( $this, 'invalidate_mo_files_cache' ), 10, 2 ); + } + /** * Returns the languages directory path for a specific domain and locale. * @@ -134,6 +149,106 @@ public function set_custom_path( $domain, $path ) { $this->custom_paths[ $domain ] = rtrim( $path, '/' ); } + /** + * Retrieves .mo files from the specified path. + * + * Allows early retrieval through the {@see 'pre_get_mo_files_from_path'} filter to optimize + * performance, especially in directories with many files. + * + * @since 6.5.0 + * + * @param string $path The directory path to search for .mo files. + * @return array Array of .mo file paths. + */ + public function get_language_files_from_path( $path ) { + $path = trailingslashit( $path ); + + /** + * Filters the .mo files retrieved from a specified path before the actual lookup. + * + * Returning a non-null value from the filter will effectively short-circuit + * the MO files lookup, returning that value instead. + * + * This can be useful in situations where the directory contains a large number of files + * and the default glob() function becomes expensive in terms of performance. + * + * @since 6.5.0 + * + * @param null|array $mo_files List of .mo files. Default null. + * @param string $path The path from which .mo files are being fetched. + **/ + $mo_files = apply_filters( 'pre_get_language_files_from_path', null, $path ); + + if ( null !== $mo_files ) { + return $mo_files; + } + + $cache_key = 'cached_mo_files_' . md5( $path ); + $mo_files = wp_cache_get( $cache_key, 'translations' ); + + if ( false === $mo_files ) { + $mo_files = glob( $path . '*.mo' ); + if ( false === $mo_files ) { + $mo_files = array(); + } + wp_cache_set( $cache_key, $mo_files, 'translations' ); + } + + return $mo_files; + } + + /** + * Invalidate the cache for .mo files. + * + * This function deletes the cache entries related to .mo files when triggered + * by specific actions, such as the completion of an upgrade process. + * + * @since 6.5.0 + * + * @param WP_Upgrader $upgrader Unused. WP_Upgrader instance. In other contexts this might be a + * Theme_Upgrader, Plugin_Upgrader, Core_Upgrade, or Language_Pack_Upgrader instance. + * @param array $hook_extra { + * Array of bulk item update data. + * + * @type string $action Type of action. Default 'update'. + * @type string $type Type of update process. Accepts 'plugin', 'theme', 'translation', or 'core'. + * @type bool $bulk Whether the update process is a bulk update. Default true. + * @type array $plugins Array of the basename paths of the plugins' main files. + * @type array $themes The theme slugs. + * @type array $translations { + * Array of translations update data. + * + * @type string $language The locale the translation is for. + * @type string $type Type of translation. Accepts 'plugin', 'theme', or 'core'. + * @type string $slug Text domain the translation is for. The slug of a theme/plugin or + * 'default' for core translations. + * @type string $version The version of a theme, plugin, or core. + * } + * } + * @return void + */ + public function invalidate_mo_files_cache( $upgrader, $hook_extra ) { + if ( 'translation' !== $hook_extra['type'] || array() === $hook_extra['translations'] ) { + return; + } + + $translation_types = array_unique( wp_list_pluck( $hook_extra['translations'], 'type' ) ); + + foreach ( $translation_types as $type ) { + switch ( $type ) { + case 'plugin': + wp_cache_delete( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/plugins/' ), 'translations' ); + break; + case 'theme': + wp_cache_delete( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/themes/' ), 'translations' ); + break; + default: + wp_cache_delete( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) ), 'translations' ); + break; + } + } + } + /** * Returns possible language directory paths for a given text domain. * @@ -156,7 +271,7 @@ private function get_paths_for_domain( $domain ) { } /** - * Gets the path to the language directory for the current locale. + * Gets the path to the language directory for the current domain and locale. * * Checks the plugins and themes language directories as well as any * custom directory set via {@see load_plugin_textdomain()} or {@see load_theme_textdomain()}. @@ -175,13 +290,11 @@ private function get_path_from_lang_dir( $domain, $locale ) { $found_location = false; foreach ( $locations as $location ) { - if ( ! isset( $this->cached_mo_files[ $location ] ) ) { - $this->set_cached_mo_files( $location ); - } + $files = $this->get_language_files_from_path( $location ); $path = "$location/$domain-$locale.mo"; - foreach ( $this->cached_mo_files[ $location ] as $mo_path ) { + foreach ( $files as $mo_path ) { if ( ! in_array( $domain, $this->domains_with_translations, true ) && str_starts_with( str_replace( "$location/", '', $mo_path ), "$domain-" ) @@ -215,21 +328,4 @@ private function get_path_from_lang_dir( $domain, $locale ) { return false; } - - /** - * Reads and caches all available MO files from a given directory. - * - * @since 6.1.0 - * - * @param string $path Language directory path. - */ - private function set_cached_mo_files( $path ) { - $this->cached_mo_files[ $path ] = array(); - - $mo_files = glob( $path . '/*.mo' ); - - if ( $mo_files ) { - $this->cached_mo_files[ $path ] = $mo_files; - } - } } diff --git a/src/wp-includes/l10n.php b/src/wp-includes/l10n.php index 598a485468b9c..726e3da1a5a56 100644 --- a/src/wp-includes/l10n.php +++ b/src/wp-includes/l10n.php @@ -1398,15 +1398,21 @@ function translate_user_role( $name, $domain = 'default' ) { * @since 3.0.0 * @since 4.7.0 The results are now filterable with the {@see 'get_available_languages'} filter. * + * @global WP_Textdomain_Registry $wp_textdomain_registry WordPress Textdomain Registry. + * * @param string $dir A directory to search for language files. * Default WP_LANG_DIR. * @return string[] An array of language codes or an empty array if no languages are present. * Language codes are formed by stripping the .mo extension from the language file names. */ function get_available_languages( $dir = null ) { + global $wp_textdomain_registry; + $languages = array(); - $lang_files = glob( ( is_null( $dir ) ? WP_LANG_DIR : $dir ) . '/*.mo' ); + $path = is_null( $dir ) ? WP_LANG_DIR : $dir; + $lang_files = $wp_textdomain_registry->get_language_files_from_path( $path ); + if ( $lang_files ) { foreach ( $lang_files as $lang_file ) { $lang_file = basename( $lang_file, '.mo' ); diff --git a/src/wp-settings.php b/src/wp-settings.php index c354747f32148..c8835db31a005 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -380,6 +380,7 @@ * @global WP_Textdomain_Registry $wp_textdomain_registry WordPress Textdomain Registry. */ $GLOBALS['wp_textdomain_registry'] = new WP_Textdomain_Registry(); +$GLOBALS['wp_textdomain_registry']->init(); // Load multisite-specific files. if ( is_multisite() ) { diff --git a/tests/phpunit/tests/l10n/wpTextdomainRegistry.php b/tests/phpunit/tests/l10n/wpTextdomainRegistry.php index fc53dd9c56c13..c333eb927227d 100644 --- a/tests/phpunit/tests/l10n/wpTextdomainRegistry.php +++ b/tests/phpunit/tests/l10n/wpTextdomainRegistry.php @@ -18,21 +18,21 @@ public function set_up() { $this->instance = new WP_Textdomain_Registry(); } + public function tear_down() { + wp_cache_delete( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/foobar/' ), 'translations' ); + wp_cache_delete( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/plugins/' ), 'translations' ); + wp_cache_delete( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/themes/' ), 'translations' ); + wp_cache_delete( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) ), 'translations' ); + + parent::tear_down(); + } + /** * @covers ::has * @covers ::get * @covers ::set_custom_path */ public function test_set_custom_path() { - $reflection = new ReflectionClass( $this->instance ); - $reflection_property = $reflection->getProperty( 'cached_mo_files' ); - $reflection_property->setAccessible( true ); - - $this->assertEmpty( - $reflection_property->getValue( $this->instance ), - 'Cache not empty by default' - ); - $this->instance->set_custom_path( 'foo', WP_LANG_DIR . '/bar' ); $this->assertTrue( @@ -48,10 +48,9 @@ public function test_set_custom_path() { $this->instance->get( 'foo', 'de_DE' ), 'Custom path for textdomain not returned' ); - $this->assertArrayHasKey( - WP_LANG_DIR . '/bar', - $reflection_property->getValue( $this->instance ), - 'Custom path missing from cache' + $this->assertNotFalse( + wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . 'bar/' ), 'translations' ), + 'List of files in custom path not cached' ); } @@ -60,22 +59,12 @@ public function test_set_custom_path() { * @dataProvider data_domains_locales */ public function test_get( $domain, $locale, $expected ) { - $reflection = new ReflectionClass( $this->instance ); - $reflection_property = $reflection->getProperty( 'cached_mo_files' ); - $reflection_property->setAccessible( true ); - $actual = $this->instance->get( $domain, $locale ); $this->assertSame( $expected, $actual, 'Expected languages directory path not matching actual one' ); - - $this->assertArrayHasKey( - WP_LANG_DIR . '/plugins', - $reflection_property->getValue( $this->instance ), - 'Default plugins path missing from cache' - ); } /** @@ -91,6 +80,75 @@ public function test_set_populates_cache() { ); } + /** + * @covers ::get_language_files_from_path + */ + public function test_get_language_files_from_path_caches_results() { + $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) . '/foobar/' ); + $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) . '/plugins/' ); + $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) . '/themes/' ); + $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) ); + + $this->assertNotFalse( wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/plugins/' ), 'translations' ) ); + $this->assertNotFalse( wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/themes/' ), 'translations' ) ); + $this->assertNotFalse( wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/foobar/' ), 'translations' ) ); + $this->assertNotFalse( wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) ), 'translations' ) ); + } + + /** + * @covers ::get_language_files_from_path + */ + public function test_get_language_files_from_path_short_circuit() { + add_filter( 'pre_get_language_files_from_path', '__return_empty_array' ); + $result = $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) . '/plugins/' ); + remove_filter( 'pre_get_language_files_from_path', '__return_empty_array' ); + + $cache = wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/plugins/' ), 'translations' ); + + $this->assertEmpty( $result ); + $this->assertFalse( $cache ); + } + + /** + * @covers ::invalidate_mo_files_cache + */ + public function test_invalidate_mo_files_cache() { + $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) . '/plugins/' ); + $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) . '/themes/' ); + $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) ); + + $this->instance->invalidate_mo_files_cache( + null, + array( + 'type' => 'translation', + 'translations' => array( + (object) array( + 'type' => 'plugin', + 'slug' => 'internationalized-plugin', + 'language' => 'de_DE', + 'version' => '99.9.9', + ), + (object) array( + 'type' => 'theme', + 'slug' => 'internationalized-theme', + 'language' => 'de_DE', + 'version' => '99.9.9', + ), + (object) array( + 'type' => 'core', + 'slug' => 'default', + 'language' => 'es_ES', + 'version' => '99.9.9', + ), + ), + ) + ); + + $this->assertFalse( wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/plugins/' ), 'translations' ) ); + $this->assertFalse( wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/themes/' ), 'translations' ) ); + $this->assertFalse( wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) ), 'translations' ) ); + } + public function data_domains_locales() { return array( 'Non-existent plugin' => array( From 8dee7819693c3b0c5bc98eb2d15b2a37452bc62a Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Tue, 16 Jan 2024 04:04:27 +0000 Subject: [PATCH 04/36] Docs: Format `new_admin_email_content` placeholders as a list. Format the email placeholders for the `new_admin_email_content` hook as a list for clarity and to avoid parsing errors in docblock consumers. Props dd32, shooper, stevenlinx. Fixes #60262. git-svn-id: https://develop.svn.wordpress.org/trunk@57289 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/misc.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/wp-admin/includes/misc.php b/src/wp-admin/includes/misc.php index 12d724f62c152..3f48065cdfafd 100644 --- a/src/wp-admin/includes/misc.php +++ b/src/wp-admin/includes/misc.php @@ -1499,11 +1499,11 @@ function update_option_new_admin_email( $old_value, $value ) { * Filters the text of the email sent when a change of site admin email address is attempted. * * The following strings have a special meaning and will get replaced dynamically: - * ###USERNAME### The current user's username. - * ###ADMIN_URL### The link to click on to confirm the email change. - * ###EMAIL### The proposed new site admin email address. - * ###SITENAME### The name of the site. - * ###SITEURL### The URL to the site. + * - ###USERNAME### The current user's username. + * - ###ADMIN_URL### The link to click on to confirm the email change. + * - ###EMAIL### The proposed new site admin email address. + * - ###SITENAME### The name of the site. + * - ###SITEURL### The URL to the site. * * @since MU (3.0.0) * @since 4.9.0 This filter is no longer Multisite specific. From c3fd1145396ce059505fe1e225b860e425860d5c Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 16 Jan 2024 12:10:47 +0000 Subject: [PATCH 05/36] I18N: Do not use `trailingslashit` in `WP_Textdomain_Registry`. This usage of `trailingslashit()`, introduced in [57287], is not only redundant, but also discouraged in order to avoid `formatting.php` dependency (which might not always be loaded). Props SergeyBiryukov. See #58919. git-svn-id: https://develop.svn.wordpress.org/trunk@57290 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-textdomain-registry.php | 8 ++--- .../tests/l10n/wpTextdomainRegistry.php | 36 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/wp-includes/class-wp-textdomain-registry.php b/src/wp-includes/class-wp-textdomain-registry.php index f0c8b8ded7e0f..4121ecb0911c9 100644 --- a/src/wp-includes/class-wp-textdomain-registry.php +++ b/src/wp-includes/class-wp-textdomain-registry.php @@ -161,7 +161,7 @@ public function set_custom_path( $domain, $path ) { * @return array Array of .mo file paths. */ public function get_language_files_from_path( $path ) { - $path = trailingslashit( $path ); + $path = rtrim( $path, '/' ) . '/'; /** * Filters the .mo files retrieved from a specified path before the actual lookup. @@ -237,13 +237,13 @@ public function invalidate_mo_files_cache( $upgrader, $hook_extra ) { foreach ( $translation_types as $type ) { switch ( $type ) { case 'plugin': - wp_cache_delete( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/plugins/' ), 'translations' ); + wp_cache_delete( 'cached_mo_files_' . md5( WP_LANG_DIR . '/plugins' ), 'translations' ); break; case 'theme': - wp_cache_delete( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/themes/' ), 'translations' ); + wp_cache_delete( 'cached_mo_files_' . md5( WP_LANG_DIR . '/themes' ), 'translations' ); break; default: - wp_cache_delete( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) ), 'translations' ); + wp_cache_delete( 'cached_mo_files_' . md5( WP_LANG_DIR ), 'translations' ); break; } } diff --git a/tests/phpunit/tests/l10n/wpTextdomainRegistry.php b/tests/phpunit/tests/l10n/wpTextdomainRegistry.php index c333eb927227d..6b117b5f50cf0 100644 --- a/tests/phpunit/tests/l10n/wpTextdomainRegistry.php +++ b/tests/phpunit/tests/l10n/wpTextdomainRegistry.php @@ -19,10 +19,10 @@ public function set_up() { } public function tear_down() { - wp_cache_delete( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/foobar/' ), 'translations' ); - wp_cache_delete( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/plugins/' ), 'translations' ); - wp_cache_delete( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/themes/' ), 'translations' ); - wp_cache_delete( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) ), 'translations' ); + wp_cache_delete( 'cached_mo_files_' . md5( WP_LANG_DIR . '/foobar' ), 'translations' ); + wp_cache_delete( 'cached_mo_files_' . md5( WP_LANG_DIR . '/plugins' ), 'translations' ); + wp_cache_delete( 'cached_mo_files_' . md5( WP_LANG_DIR . '/themes' ), 'translations' ); + wp_cache_delete( 'cached_mo_files_' . md5( WP_LANG_DIR ), 'translations' ); parent::tear_down(); } @@ -84,14 +84,14 @@ public function test_set_populates_cache() { * @covers ::get_language_files_from_path */ public function test_get_language_files_from_path_caches_results() { - $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) . '/foobar/' ); - $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) . '/plugins/' ); - $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) . '/themes/' ); + $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) . 'foobar/' ); + $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) . 'plugins/' ); + $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) . 'themes/' ); $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) ); - $this->assertNotFalse( wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/plugins/' ), 'translations' ) ); - $this->assertNotFalse( wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/themes/' ), 'translations' ) ); - $this->assertNotFalse( wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/foobar/' ), 'translations' ) ); + $this->assertNotFalse( wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . 'plugins/' ), 'translations' ) ); + $this->assertNotFalse( wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . 'themes/' ), 'translations' ) ); + $this->assertNotFalse( wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . 'foobar/' ), 'translations' ) ); $this->assertNotFalse( wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) ), 'translations' ) ); } @@ -100,10 +100,10 @@ public function test_get_language_files_from_path_caches_results() { */ public function test_get_language_files_from_path_short_circuit() { add_filter( 'pre_get_language_files_from_path', '__return_empty_array' ); - $result = $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) . '/plugins/' ); + $result = $this->instance->get_language_files_from_path( WP_LANG_DIR . '/plugins' ); remove_filter( 'pre_get_language_files_from_path', '__return_empty_array' ); - $cache = wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/plugins/' ), 'translations' ); + $cache = wp_cache_get( 'cached_mo_files_' . md5( WP_LANG_DIR . 'plugins' ), 'translations' ); $this->assertEmpty( $result ); $this->assertFalse( $cache ); @@ -113,9 +113,9 @@ public function test_get_language_files_from_path_short_circuit() { * @covers ::invalidate_mo_files_cache */ public function test_invalidate_mo_files_cache() { - $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) . '/plugins/' ); - $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) . '/themes/' ); - $this->instance->get_language_files_from_path( trailingslashit( WP_LANG_DIR ) ); + $this->instance->get_language_files_from_path( WP_LANG_DIR . '/plugins' ); + $this->instance->get_language_files_from_path( WP_LANG_DIR . '/themes' ); + $this->instance->get_language_files_from_path( WP_LANG_DIR ); $this->instance->invalidate_mo_files_cache( null, @@ -144,9 +144,9 @@ public function test_invalidate_mo_files_cache() { ) ); - $this->assertFalse( wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/plugins/' ), 'translations' ) ); - $this->assertFalse( wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) . '/themes/' ), 'translations' ) ); - $this->assertFalse( wp_cache_get( 'cached_mo_files_' . md5( trailingslashit( WP_LANG_DIR ) ), 'translations' ) ); + $this->assertFalse( wp_cache_get( 'cached_mo_files_' . md5( WP_LANG_DIR . '/plugins' ), 'translations' ) ); + $this->assertFalse( wp_cache_get( 'cached_mo_files_' . md5( WP_LANG_DIR . '/themes' ), 'translations' ) ); + $this->assertFalse( wp_cache_get( 'cached_mo_files_' . md5( WP_LANG_DIR ), 'translations' ) ); } public function data_domains_locales() { From dd691394f2de7ca4270c2936fd0ea7741d97af5d Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Tue, 16 Jan 2024 17:01:30 +0000 Subject: [PATCH 06/36] Media: Consider inline image CSS width to backfill `width` and `height` attributes. Prior to this changeset, WordPress core would use the original image size, which in the particular case of inline images would be severely off, as they are usually very small. This could lead to incorrect application of `fetchpriority="high"` and other performance optimizations. Props westonruter, flixos90, joemcgill, mukesh27. Fixes #59352. git-svn-id: https://develop.svn.wordpress.org/trunk@57294 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/media.php | 7 ++ .../media/wpImgTagAddWidthAndHeightAttr.php | 85 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 tests/phpunit/tests/media/wpImgTagAddWidthAndHeightAttr.php diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 9cf301e95c11a..38ec2213b7506 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -2117,6 +2117,13 @@ function wp_img_tag_add_width_and_height_attr( $image, $context, $attachment_id $size_array = wp_image_src_get_dimensions( $image_src, $image_meta, $attachment_id ); if ( $size_array ) { + // If the width is enforced through style (e.g. in an inline image), calculate the dimension attributes. + $style_width = preg_match( '/style="width:\s*(\d+)px;"/', $image, $match_width ) ? (int) $match_width[1] : 0; + if ( $style_width ) { + $size_array[1] = (int) round( $size_array[1] * $style_width / $size_array[0] ); + $size_array[0] = $style_width; + } + $hw = trim( image_hwstring( $size_array[0], $size_array[1] ) ); return str_replace( 'attachment->create_upload_object( $file ); + self::$attachment_width = 680; + self::$attachment_height = 1024; + } + + public static function tear_down_after_class() { + wp_delete_attachment( self::$attachment_id, true ); + parent::tear_down_after_class(); + } + + /** + * Tests that `wp_img_tag_add_width_and_height_attr()` adds dimension attributes to an image when they are missing. + * + * @ticket 50367 + */ + public function test_add_width_and_height_when_missing() { + $image_tag = ''; + + $this->assertSame( + '', + wp_img_tag_add_width_and_height_attr( $image_tag, 'the_content', self::$attachment_id ) + ); + } + + /** + * Tests that `wp_img_tag_add_width_and_height_attr()` does not add dimension attributes when disabled via filter. + * + * @ticket 50367 + */ + public function test_do_not_add_width_and_height_when_disabled_via_filter() { + add_filter( 'wp_img_tag_add_width_and_height_attr', '__return_false' ); + $image_tag = ''; + + $this->assertSame( + $image_tag, + wp_img_tag_add_width_and_height_attr( $image_tag, 'the_content', self::$attachment_id ) + ); + } + + /** + * Tests that `wp_img_tag_add_width_and_height_attr()` does not add dimension attributes to an image without src. + * + * @ticket 50367 + */ + public function test_do_not_add_width_and_height_without_src() { + $image_tag = ''; + + $this->assertSame( + $image_tag, + wp_img_tag_add_width_and_height_attr( $image_tag, 'the_content', self::$attachment_id ) + ); + } + + /** + * Tests that `wp_img_tag_add_width_and_height_attr()` respects the style attribute from the inline image format to + * correctly set width and height based on that. + * + * @ticket 59352 + */ + public function test_consider_inline_image_style_attr_to_set_width_and_height() { + // '85px' is the original width (680px) divided by 8, so the expected height is equivalently 1024/8=128. + $image_tag = ''; + + $this->assertSame( + '', + wp_img_tag_add_width_and_height_attr( $image_tag, 'the_content', self::$attachment_id ) + ); + } +} From 35add7378e093c8808ef9c08ee4b07f71b14963d Mon Sep 17 00:00:00 2001 From: Joe Dolson Date: Tue, 16 Jan 2024 17:29:58 +0000 Subject: [PATCH 07/36] Administration: Remove empty form `action` attributes. Remove the `action` attribute in the login language selector, privacy forms, and classic widget forms. An empty `action` attribute is invalid HTML4 and unsupported HTML5. The `action` attribute is optional, but must have a valid URL when provided. Props Malae, audrasjb, bartkleinreesink, nicolefurlan, shubhamsedani, costdev, peterwilsoncc, rajinsharwar, joedolson. Fixes #58226. git-svn-id: https://develop.svn.wordpress.org/trunk@57295 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/options-privacy.php | 4 ++-- src/wp-admin/widgets-form.php | 2 +- src/wp-login.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wp-admin/options-privacy.php b/src/wp-admin/options-privacy.php index 6441a43491ac4..92de2ca9b9b68 100644 --- a/src/wp-admin/options-privacy.php +++ b/src/wp-admin/options-privacy.php @@ -270,7 +270,7 @@ static function ( $body_class ) { -
+ - +
- +

'inactive-widgets-control-remove' ); diff --git a/src/wp-login.php b/src/wp-login.php index 0641cd52e6a40..9eeac4a96ce46 100644 --- a/src/wp-login.php +++ b/src/wp-login.php @@ -364,7 +364,7 @@ function login_footer( $input_id = '' ) { if ( ! empty( $languages ) ) { ?>

- +