Make WordPress Core

Changeset 62293


Ignore:
Timestamp:
05/02/2026 05:54:39 AM (6 months ago)
Author:
westonruter
Message:

I18N: Harden against undefined index in _load_script_textdomain_from_src().

This guards against an undefined index warning being raised when a script or script module is registered with a URL that lacks a path component.

This also adds full PHPStan type definitions for wp_parse_url(), ensuring that the _load_script_textdomain_from_src() function has no PHPStan errors at rule level 10.

Developed in ​https://github.com/WordPress/wordpress-develop/pull/11690

Follow-up to r62278.

Props westonruter, manzoorwanijk, mukesh27.
See #65015, #64238.

Location:
trunk
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/http.php

    r61470 r62293  
    717717 *               doesn't exist in the given URL; a string or - in the case of
    718718 *               PHP_URL_PORT - integer when it does. See parse_url()'s return values.
     719 *
     720 * @phpstan-param int<-1, 7> $component
     721 * @phpstan-return (
     722 *     $component is -1
     723 *         ? false|array{
     724 *               scheme?: string,
     725 *               host?: string,
     726 *               port?: int<0, 65535>,
     727 *               user?: string,
     728 *               pass?: string,
     729 *               path?: string,
     730 *               query?: string,
     731 *               fragment?: string,
     732 *           }
     733 *         : (
     734 *             $component is 2
     735 *                 ? int<0, 65535>|null
     736 *                 : string|null
     737 *         )
     738 * )
    719739 */
    720740function wp_parse_url( $url, $component = -1 ) {
    … …  
    764784 *               doesn't exist in the given URL; a string or - in the case of
    765785 *               PHP_URL_PORT - integer when it does. See parse_url()'s return values.
     786 *
     787 * @phpstan-param false|array{
     788 *     scheme?: string,
     789 *     host?: string,
     790 *     port?: int<0, 65535>,
     791 *     user?: string,
     792 *     pass?: string,
     793 *     path?: string,
     794 *     query?: string,
     795 *     fragment?: string,
     796 * } $url_parts
     797 * @phpstan-param int<-1, 7> $component
     798 * @phpstan-return (
     799 *     $component is -1
     800 *         ? false|array{
     801 *               scheme?: string,
     802 *               host?: string,
     803 *               port?: int<0, 65535>,
     804 *               user?: string,
     805 *               pass?: string,
     806 *               path?: string,
     807 *               query?: string,
     808 *               fragment?: string,
     809 *           }
     810 *         : (
     811 *             $component is 2
     812 *                 ? int<0, 65535>|null
     813 *                 : string|null
     814 *         )
     815 * )
    766816 */
    767817function _get_component_from_parsed_url_array( $url_parts, $component = -1 ) {
    … …  
    790840 * @param int $constant PHP_URL_* constant.
    791841 * @return string|false The named key or false.
     842 *
     843 * @phpstan-param int<-1, 7> $constant
     844 * @phpstan-return 'scheme'|'host'|'port'|'user'|'pass'|'path'|'query'|'fragment'|false
    792845 */
    793846function _wp_translate_php_url_constant_to_key( $constant ) {
  • trunk/src/wp-includes/l10n.php

    r62278 r62293  
    12071207 */
    12081208function _load_script_textdomain_from_src( string $handle, string $src, string $domain, string $path, bool $is_module ) {
     1209        /** @var WP_Textdomain_Registry $wp_textdomain_registry */
    12091210        global $wp_textdomain_registry;
    12101211
    … …  
    12151216        }
    12161217
    1217         $path = untrailingslashit( $path );
     1218        if ( $path ) {
     1219                $path = untrailingslashit( $path );
     1220        }
    12181221
    12191222        // If a path was given and the handle file exists simply return it.
    … …  
    12321235        $languages_path = WP_LANG_DIR;
    12331236
    1234         $src_url     = wp_parse_url( $src );
     1237        $src_url = wp_parse_url( $src );
     1238        if ( ! $src_url ) {
     1239                return load_script_translations( false, $handle, $domain );
     1240        }
     1241        $src_url['path'] ??= '';
     1242
    12351243        $content_url = wp_parse_url( content_url() );
     1244        if ( ! $content_url ) {
     1245                return load_script_translations( false, $handle, $domain );
     1246        }
     1247
    12361248        $plugins_url = wp_parse_url( plugins_url() );
    12371249        $site_url    = wp_parse_url( site_url() );
    … …  
    13051317
    13061318        // If the source is not from WP.
    1307         if ( false === $relative ) {
     1319        if ( ! is_string( $relative ) ) {
    13081320                return load_script_translations( false, $handle, $domain );
    13091321        }
  • trunk/tests/phpunit/tests/l10n/loadScriptTextdomain.php

    r61424 r62293  
    173173                $this->assertSame( $expected, load_script_textdomain( $handle ) );
    174174        }
     175
     176        /**
     177         * Tests that an unparseable script source URL short-circuits to
     178         * `load_script_translations( false, ... )` instead of falling through
     179         * to the relative-path computation.
     180         *
     181         * @ticket 65015
     182         */
     183        public function test_unparseable_src_returns_false(): void {
     184                $handle = 'test-unparseable-src';
     185                $src    = 'http:///example';
     186
     187                $this->assertFalse( wp_parse_url( $src ), 'Test prerequisite failed: the test src should be unparseable.' );
     188
     189                wp_enqueue_script( $handle, $src, array(), null );
     190
     191                $this->assertFalse( load_script_textdomain( $handle, 'default', DIR_TESTDATA . '/languages' ) );
     192        }
     193
     194        /**
     195         * Tests that an unparseable `content_url()` return value short-circuits
     196         * to `load_script_translations( false, ... )` instead of computing
     197         * `$relative` from a corrupted parsed-URL array.
     198         *
     199         * The `MockAction` spy on `pre_load_script_translations` is necessary
     200         * here because the function's tail end also calls `load_script_translations( false, ... )`,
     201         * so a regression that bypasses the early return would still return false
     202         * via the fallback path. Asserting on the recorded `$file` arguments pins
     203         * the test to the intended branch.
     204         *
     205         * @ticket 65015
     206         */
     207        public function test_unparseable_content_url_returns_false(): void {
     208                $handle = 'test-unparseable-content-url';
     209                $src    = '/wp-includes/js/script.js';
     210
     211                add_filter(
     212                        'content_url',
     213                        static function () {
     214                                return 'http:///example';
     215                        }
     216                );
     217
     218                $mock = new MockAction();
     219                add_filter( 'pre_load_script_translations', array( $mock, 'filter' ), 10, 4 );
     220
     221                wp_enqueue_script( $handle, $src, array(), null );
     222
     223                $this->assertFalse( load_script_textdomain( $handle, 'default', DIR_TESTDATA . '/languages' ) );
     224                $this->assertSame(
     225                        array(
     226                                DIR_TESTDATA . '/languages/en_US-' . $handle . '.json',
     227                                false,
     228                        ),
     229                        array_column( $mock->get_args(), 1 ),
     230                        'Expected the unparseable content_url branch to short-circuit before any relative-path lookup.'
     231                );
     232        }
     233
     234        /**
     235         * Tests that the `load_script_textdomain_relative_path` filter returning
     236         * a non-string, non-false value (e.g., a callback that forgets to return)
     237         * short-circuits via the `! is_string( $relative )` guard rather than
     238         * falling through to string functions like `str_ends_with()` and `md5()`.
     239         *
     240         * @ticket 65015
     241         */
     242        public function test_non_string_relative_path_filter_returns_false(): void {
     243                $handle = 'test-non-string-relative-path';
     244                $src    = '/wp-includes/js/script.js';
     245
     246                add_filter( 'load_script_textdomain_relative_path', '__return_null' );
     247
     248                wp_enqueue_script( $handle, $src, array(), null );
     249
     250                $this->assertFalse( load_script_textdomain( $handle, 'default', DIR_TESTDATA . '/languages' ) );
     251        }
     252
     253        /**
     254         * Tests that a script source URL with no path component does not trigger
     255         * an undefined index warning when the path is read further down in the
     256         * function. The result is reached via the regular fallback path
     257         * (no host/path match) rather than an early return.
     258         *
     259         * @ticket 65015
     260         */
     261        public function test_src_without_path_component_does_not_warn(): void {
     262                $handle = 'test-src-without-path';
     263                $src    = 'https://example.com';
     264
     265                $parsed = wp_parse_url( $src );
     266                $this->assertIsArray( $parsed, 'Test prerequisite failed: the test src should parse.' );
     267                $this->assertArrayNotHasKey( 'path', $parsed, 'Test prerequisite failed: the test src should have no path component.' );
     268
     269                wp_enqueue_script( $handle, $src, array(), null );
     270
     271                $this->assertFalse( load_script_textdomain( $handle, 'default', DIR_TESTDATA . '/languages' ) );
     272        }
    175273}
Note: See TracChangeset for help on using the changeset viewer.