From c3f8c30d4769d539d7cd76a91ced27a224ef7475 Mon Sep 17 00:00:00 2001 From: Michal Iwanow <4765119+mcliwanow@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:09:58 +0200 Subject: [PATCH 01/10] Openverse: Strip only the leading subpath when building the redirect target. `get_target_url()` passed `$count = 1` as the fourth argument to `str_replace()`, with the comment "Only replace the leading Openverse subpath". `str_replace()` has no limit parameter. Its fourth argument is a by-reference output that receives the number of replacements performed, so the `1` was overwritten and every occurrence of `/openverse` in the request URI was removed. Any request whose path contained the string a second time was silently corrupted, including paths where it appears inside a longer segment: /openverse/image/openverse-logo/ -> {origin}/image-logo/ /openverse/tag/openverse/ -> {origin}/tag/ /openverse/search/?q=/openverse -> {origin}/search/?q= The remainder is appended to a bare origin, which the setting's `sanitize_callback` guarantees has no trailing slash, so the remainder has to supply the separator. It was appended unchanged, producing a malformed URL whenever it did not already start with a slash. What changed: * Remove the subpath with `str_starts_with()` and `substr()`, so only a leading occurrence is stripped. * Prefix the remainder with a single slash so it always contributes a path. * Read `REQUEST_URI` through `wp_unslash()` and `esc_url_raw()`. `sanitize_text_field()` is not usable here: it strips percent-encoded octets and would turn `?q=cat%20dog` into `?q=catdog`. * Register the configured origin through `allowed_redirect_hosts` and send the redirect with `wp_safe_redirect()`. * Escape the target URL echoed by the disabled-redirect branch of `index.php`. Testing: both examples from the `get_target_url()` docblock, locale-prefixed requests, and search URLs containing spaces and non-ASCII characters produce byte-identical output before and after. `/openverse` with no trailing slash now resolves to `{origin}/` rather than `{origin}`, which is what the docblock already documented. Co-Authored-By: Claude Fable 5 --- .../themes/pub/wporg-openverse/functions.php | 48 +++++++++++++++++-- .../themes/pub/wporg-openverse/index.php | 4 +- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php index a861d77e38..05ba3b1a1f 100644 --- a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php +++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php @@ -255,6 +255,11 @@ function wporg_ov_customizer( $wp_customize ) { * Examples: * - https://ru.wordpress.org/openverse → {ov_redirect_url}/ru/ * - https://wordpress.org/openverse/search/?q=dog → {ov_redirect_url}/search/?q=dog + * + * The returned URL always carries at least a trailing path separator, so a bare + * `/openverse` resolves to `{ov_redirect_url}/` rather than to the origin alone. + * + * @return string */ function get_target_url() { $target_url = get_theme_mod( 'ov_redirect_url', OPENVERSE_STANDALONE_URL ); @@ -265,15 +270,50 @@ function get_target_url() { $target_url .= '/' . $locale; } - $path = $_SERVER['REQUEST_URI']; - if ( $path ) { - $count = 1; // Only replace the leading Openverse subpath. - $target_url .= str_replace( OPENVERSE_SUBPATH, '', $path, $count ); + // Sanitising is required by WordPress.Security.ValidatedSanitizedInput. + // Not `sanitize_text_field()`, which strips percent-encoded octets and + // would turn `?q=cat%20dog` into `?q=catdog`. + $path = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; + + // Only the leading subpath is removed. `str_replace()` cannot express that: + // it replaced every occurrence, including one in a later segment such as + // `/image/openverse-logo/` or one in the query string. + if ( str_starts_with( $path, OPENVERSE_SUBPATH ) ) { + $path = substr( $path, strlen( OPENVERSE_SUBPATH ) ); } + // The origin has no trailing slash, so the path must supply the separator. + // Prepending it is also what keeps the remainder in the path rather than + // the authority; `ltrim()` only collapses a doubled slash. + $target_url .= '/' . ltrim( $path, '/' ); + return $target_url; } +/** + * Allow the redirect to reach the standalone Openverse site. + * + * The host comes from the configured origin, not from the generated target + * URL, so that a malformed target cannot authorise its own destination. + * + * @param string[] $hosts Allowed host names. + * @return string[] Allowed host names, plus the standalone Openverse host when + * the redirect is enabled. + */ +function allow_standalone_redirect_host( $hosts ) { + if ( ! get_theme_mod( 'ov_is_redirect_enabled' ) ) { + return $hosts; + } + + $host = wp_parse_url( get_theme_mod( 'ov_redirect_url', OPENVERSE_STANDALONE_URL ), PHP_URL_HOST ); + if ( $host ) { + $hosts[] = $host; + } + + return $hosts; +} +add_filter( 'allowed_redirect_hosts', __NAMESPACE__ . '\allow_standalone_redirect_host' ); + /** * Provide configuration for the theme to redirect to the given standalone * Openverse site. The destination URL can be configured and the behaviour can diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/index.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/index.php index 0a7e323f96..e829237bbf 100644 --- a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/index.php +++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/index.php @@ -24,10 +24,10 @@ $target_url = get_target_url(); if ( $is_redirect_enabled ) { - wp_redirect( $target_url, 301 ); + wp_safe_redirect( $target_url, 301 ); exit; } else { - echo ""; + echo ''; } get_header(); From 6a4c65b4ae4242c8dd956b5d076821c87be97575 Mon Sep 17 00:00:00 2001 From: Michal Iwanow <4765119+mcliwanow@users.noreply.github.com> Date: Tue, 1 Sep 2026 16:47:35 +0200 Subject: [PATCH 02/10] Openverse: Match the subpath as a whole segment and validate before redirecting. Two follow-ups from review. `str_starts_with( $path, OPENVERSE_SUBPATH )` also matched a longer first segment, so `/openverse-search` had the prefix removed and was forwarded to `{origin}/-search`. Match an exact path, or one followed by `/` or `?`. `wp_safe_redirect()` does not skip a target it cannot validate, it sends the visitor to `admin_url()` instead. Paired with the 301 here, an `ov_redirect_url` that fails validation (a non-http scheme, which the setting's `sanitize_callback` does not reject) turned every `/openverse/*` request into a permanently cached redirect into wp-admin. Validate first, and fall through to rendering the page when the target is unusable. Co-Authored-By: Claude Fable 5 --- .../themes/pub/wporg-openverse/functions.php | 13 +++++++++---- .../wp-content/themes/pub/wporg-openverse/index.php | 7 ++++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php index 05ba3b1a1f..41956d07e7 100644 --- a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php +++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php @@ -275,10 +275,15 @@ function get_target_url() { // would turn `?q=cat%20dog` into `?q=catdog`. $path = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; - // Only the leading subpath is removed. `str_replace()` cannot express that: - // it replaced every occurrence, including one in a later segment such as - // `/image/openverse-logo/` or one in the query string. - if ( str_starts_with( $path, OPENVERSE_SUBPATH ) ) { + // Only a leading, whole-segment subpath is removed. `str_replace()` could + // express neither constraint: it replaced every occurrence, including one + // in a later segment such as `/image/openverse-logo/`, one in the query + // string, and the `/openverse` inside a longer segment like + // `/openverse-search`. + if ( OPENVERSE_SUBPATH === $path + || str_starts_with( $path, OPENVERSE_SUBPATH . '/' ) + || str_starts_with( $path, OPENVERSE_SUBPATH . '?' ) + ) { $path = substr( $path, strlen( OPENVERSE_SUBPATH ) ); } diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/index.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/index.php index e829237bbf..1e190035cd 100644 --- a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/index.php +++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/index.php @@ -18,12 +18,17 @@ If the theme mod `ov_is_redirect_enabled` is set to `true`, redirect to the standalone site and exit immediately. If not, print what would have been the redirect URL to the HTML as a comment. + + The target is validated before redirecting. Left to itself + `wp_safe_redirect()` sends an unusable target to `admin_url()` instead, and + a 301 into wp-admin would sit in visitors' caches long after the setting + that caused it was corrected. Rendering the page is the safer failure. */ $is_redirect_enabled = get_theme_mod( 'ov_is_redirect_enabled' ); $target_url = get_target_url(); -if ( $is_redirect_enabled ) { +if ( $is_redirect_enabled && wp_validate_redirect( $target_url, false ) ) { wp_safe_redirect( $target_url, 301 ); exit; } else { From b20e54ecb90e33c8d820ac348838999187984a81 Mon Sep 17 00:00:00 2001 From: Michal Iwanow <4765119+mcliwanow@users.noreply.github.com> Date: Wed, 2 Sep 2026 10:20:54 +0200 Subject: [PATCH 03/10] Openverse: Require an absolute target before redirecting. `wp_validate_redirect()` repairs rather than rejects. Given a target with no host it prepends the current directory and returns that, and the allowed-host check is skipped entirely because `parse_url()` found no host to check. So a scheme-less `ov_redirect_url`, which the setting's `sanitize_callback` permits, passed the guard added in 6a4c65b4a and produced a permanent redirect to a path under `/openverse/`. Check the host first. This target is always absolute, so no host means no redirect. Also pass `''` rather than `false` as the fallback, matching the documented type. Co-Authored-By: Claude Opus 5 --- .../wp-content/themes/pub/wporg-openverse/index.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/index.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/index.php index 1e190035cd..804f065a43 100644 --- a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/index.php +++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/index.php @@ -19,16 +19,21 @@ standalone site and exit immediately. If not, print what would have been the redirect URL to the HTML as a comment. - The target is validated before redirecting. Left to itself + The target is checked before redirecting. Left to itself `wp_safe_redirect()` sends an unusable target to `admin_url()` instead, and a 301 into wp-admin would sit in visitors' caches long after the setting that caused it was corrected. Rendering the page is the safer failure. + + The host has to be checked separately because `wp_validate_redirect()` + repairs rather than rejects: given a target with no host it prepends the + current directory and returns that, skipping the allowed-host check + entirely. This target is always absolute, so no host means no redirect. */ $is_redirect_enabled = get_theme_mod( 'ov_is_redirect_enabled' ); $target_url = get_target_url(); -if ( $is_redirect_enabled && wp_validate_redirect( $target_url, false ) ) { +if ( $is_redirect_enabled && wp_parse_url( $target_url, PHP_URL_HOST ) && wp_validate_redirect( $target_url, '' ) ) { wp_safe_redirect( $target_url, 301 ); exit; } else { From 61c67edc40f844822ac27345ba350a7de88032be Mon Sep 17 00:00:00 2001 From: Michal Iwanow <4765119+mcliwanow@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:33:51 +0200 Subject: [PATCH 04/10] Openverse: Require an http or https scheme on the redirect target. `wp_validate_redirect()` rewrites a scheme-relative target to `http`, so `//openverse.org` passed the host check added in b20e54ecb and produced `Location: http://openverse.org/...`. Before this branch the same setting emitted the target unchanged and the browser resolved it against the current scheme, so this was a downgrade introduced here rather than an existing one. Move the checks into `is_valid_target_url()` now that there are three of them, and require the scheme to be `http` or `https`. Co-Authored-By: Claude Opus 5 --- .../themes/pub/wporg-openverse/functions.php | 25 +++++++++++++++++++ .../themes/pub/wporg-openverse/index.php | 7 +----- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php index 41956d07e7..d9b91d8a96 100644 --- a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php +++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php @@ -295,6 +295,31 @@ function get_target_url() { return $target_url; } +/** + * Whether a redirect target is usable. + * + * `wp_validate_redirect()` repairs rather than rejects. Given a target with no + * host it prepends the current directory, and given a scheme-relative one it + * assumes `http`, so both come back truthy. The scheme and host are checked + * first because this target is always absolute. + * + * @param string $target_url URL the theme intends to redirect to. + * @return bool + */ +function is_valid_target_url( $target_url ) { + $parts = wp_parse_url( $target_url ); + + if ( empty( $parts['host'] ) || empty( $parts['scheme'] ) ) { + return false; + } + + if ( ! in_array( $parts['scheme'], array( 'http', 'https' ), true ) ) { + return false; + } + + return (bool) wp_validate_redirect( $target_url, '' ); +} + /** * Allow the redirect to reach the standalone Openverse site. * diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/index.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/index.php index 804f065a43..5f676adc32 100644 --- a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/index.php +++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/index.php @@ -23,17 +23,12 @@ `wp_safe_redirect()` sends an unusable target to `admin_url()` instead, and a 301 into wp-admin would sit in visitors' caches long after the setting that caused it was corrected. Rendering the page is the safer failure. - - The host has to be checked separately because `wp_validate_redirect()` - repairs rather than rejects: given a target with no host it prepends the - current directory and returns that, skipping the allowed-host check - entirely. This target is always absolute, so no host means no redirect. */ $is_redirect_enabled = get_theme_mod( 'ov_is_redirect_enabled' ); $target_url = get_target_url(); -if ( $is_redirect_enabled && wp_parse_url( $target_url, PHP_URL_HOST ) && wp_validate_redirect( $target_url, '' ) ) { +if ( $is_redirect_enabled && is_valid_target_url( $target_url ) ) { wp_safe_redirect( $target_url, 301 ); exit; } else { From cdc87b43e3bd2c8045cdf1d080466c72d6225d08 Mon Sep 17 00:00:00 2001 From: Michal Iwanow <4765119+mcliwanow@users.noreply.github.com> Date: Wed, 2 Sep 2026 12:01:51 +0200 Subject: [PATCH 05/10] Openverse: Add a test suite for the redirect target. The strip and the redirect guard have both been wrong once, in ways that were invisible until someone tried a specific request, so pin them down. `Target_Url_Test` covers what `get_target_url()` maps a request to, that the forwarded URL stays on the configured host whatever the remainder looks like, locale insertion, and which targets `is_valid_target_url()` will redirect to. 31 cases, mostly data providers. Registers the theme's first suite, so this adds `environments/openverse/` and an `openverse:test` script alongside the existing environments. The suite entry lists the theme path explicitly: the workflow derives extra paths from a test environment's `plugins`, and this one installs a theme. The tests extend the plain PHPUnit `TestCase` rather than `WP_UnitTestCase`, which does not work with the PHPUnit 11 runner. Nothing here touches the database. `locales-stub.php` stands in for the wporg locales mu-plugin, which this environment does not install. Run with `npm run openverse:test` from `environments/`. Co-Authored-By: Claude Opus 5 --- .github/unit-tests-suites.yml | 8 + environments/openverse/.wp-env.test.json | 12 + .../openverse/bin/after-start-test.sh | 14 ++ environments/package.json | 2 + .../themes/pub/wporg-openverse/phpunit.xml | 13 ++ .../wporg-openverse/tests/Target_Url_Test.php | 206 ++++++++++++++++++ .../pub/wporg-openverse/tests/bootstrap.php | 60 +++++ .../wporg-openverse/tests/locales-stub.php | 24 ++ 8 files changed, 339 insertions(+) create mode 100644 environments/openverse/.wp-env.test.json create mode 100755 environments/openverse/bin/after-start-test.sh create mode 100644 wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/phpunit.xml create mode 100644 wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/Target_Url_Test.php create mode 100644 wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/bootstrap.php create mode 100644 wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/locales-stub.php diff --git a/.github/unit-tests-suites.yml b/.github/unit-tests-suites.yml index 5c52b60061..59b1901bd6 100644 --- a/.github/unit-tests-suites.yml +++ b/.github/unit-tests-suites.yml @@ -99,3 +99,11 @@ suites: script: make:test paths: - 'environments/make/**' + + openverse: + type: wordpress + name: Openverse Theme + script: openverse:test + paths: + - 'environments/openverse/**' + - 'wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/**' diff --git a/environments/openverse/.wp-env.test.json b/environments/openverse/.wp-env.test.json new file mode 100644 index 0000000000..4ed3d81ea3 --- /dev/null +++ b/environments/openverse/.wp-env.test.json @@ -0,0 +1,12 @@ +{ + "core": "WordPress/WordPress#master", + "phpVersion": "8.4", + "testsEnvironment": false, + "plugins": [], + "themes": [ + "../wordpress.org/public_html/wp-content/themes/pub/wporg-openverse" + ], + "lifecycleScripts": { + "afterStart": "bash openverse/bin/after-start-test.sh" + } +} diff --git a/environments/openverse/bin/after-start-test.sh b/environments/openverse/bin/after-start-test.sh new file mode 100755 index 0000000000..2815193f22 --- /dev/null +++ b/environments/openverse/bin/after-start-test.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# +# Runs after wp-env start for the test environment. +# Installs PHPUnit 11 and Yoast polyfills in the test container. +# + +set -euo pipefail + +CONFIG="--config openverse/.wp-env.test.json" +RUN="npx wp-env $CONFIG run cli" + +echo "Installing PHPUnit 11 and polyfills..." +$RUN composer global require -W phpunit/phpunit:^11.0 2>&1 +$RUN composer require --dev yoast/phpunit-polyfills:^4.0 --working-dir=/wordpress-phpunit 2>&1 diff --git a/environments/package.json b/environments/package.json index 9dd9208342..3bab252c3f 100644 --- a/environments/package.json +++ b/environments/package.json @@ -25,6 +25,8 @@ "make:test": "npm run make:test:env -- start && npm run make:test:posting-access && npm run make:test:trac-watcher && npm run make:test:cli", "jobs:env": "wp-env --config jobs/.wp-env.json", "browsehappy:env": "wp-env --config browsehappy/.wp-env.json", + "openverse:test:env": "wp-env --config openverse/.wp-env.test.json", + "openverse:test": "npm run openverse:test:env -- start && npm run openverse:test:env -- run cli --env-cwd=wp-content/themes/wporg-openverse phpunit", "translate:env": "wp-env --config translate/.wp-env.json", "translate:import": "npm run translate:env -- run cli -- wp eval-file wp-content/env-bin/import-from-wporg.php", "translate:refresh": "npm run translate:env -- run cli -- wp option delete wporg_translate_env_seeded" diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/phpunit.xml b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/phpunit.xml new file mode 100644 index 0000000000..cb45860339 --- /dev/null +++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/phpunit.xml @@ -0,0 +1,13 @@ + + + + tests/ + tests/bootstrap.php + tests/locales-stub.php + + + diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/Target_Url_Test.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/Target_Url_Test.php new file mode 100644 index 0000000000..c6bc289249 --- /dev/null +++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/Target_Url_Test.php @@ -0,0 +1,206 @@ +request_uri = $_SERVER['REQUEST_URI'] ?? null; + + add_filter( 'theme_mod_ov_is_redirect_enabled', '__return_true' ); + add_filter( 'theme_mod_ov_redirect_url', array( $this, 'origin' ) ); + } + + /** + * Restores the globals and filters the test changed. + */ + protected function tearDown(): void { + remove_filter( 'theme_mod_ov_is_redirect_enabled', '__return_true' ); + remove_filter( 'theme_mod_ov_redirect_url', array( $this, 'origin' ) ); + remove_filter( 'locale', array( $this, 'russian' ) ); + + if ( null === $this->request_uri ) { + unset( $_SERVER['REQUEST_URI'] ); + } else { + $_SERVER['REQUEST_URI'] = $this->request_uri; + } + + parent::tearDown(); + } + + /** + * Filter callback supplying the configured origin. + */ + public function origin(): string { + return self::ORIGIN; + } + + /** + * Filter callback switching the site to Russian. + */ + public function russian(): string { + return 'ru_RU'; + } + + /** + * Requests and the URL each one should be forwarded to. + * + * @return array + */ + public static function requests(): array { + return array( + 'site root' => array( '/openverse/', self::ORIGIN . '/' ), + 'no trailing slash' => array( '/openverse', self::ORIGIN . '/' ), + 'query straight after' => array( '/openverse?q=dog', self::ORIGIN . '/?q=dog' ), + 'search' => array( '/openverse/search/?q=dog', self::ORIGIN . '/search/?q=dog' ), + 'encoded space' => array( '/openverse/search/?q=cat%20dog', self::ORIGIN . '/search/?q=cat%20dog' ), + 'non-ascii' => array( '/openverse/search/?q=caf%C3%A9', self::ORIGIN . '/search/?q=caf%C3%A9' ), + 'several parameters' => array( '/openverse/search/?q=dog&license=cc0', self::ORIGIN . '/search/?q=dog&license=cc0' ), + 'page' => array( '/openverse/about/', self::ORIGIN . '/about/' ), + 'subpath inside a later segment' => array( '/openverse/image/openverse-logo/', self::ORIGIN . '/image/openverse-logo/' ), + 'subpath as a later segment' => array( '/openverse/tag/openverse/', self::ORIGIN . '/tag/openverse/' ), + 'subpath in the query' => array( '/openverse/search/?q=/openverse', self::ORIGIN . '/search/?q=/openverse' ), + 'longer first segment' => array( '/openverse-search', self::ORIGIN . '/openverse-search' ), + ); + } + + /** + * Only a leading, whole-segment subpath is removed, and the rest is + * forwarded untouched. + * + * @param string $request_uri The incoming request. + * @param string $expected The URL it should map to. + */ + #[DataProvider( 'requests' )] + public function test_forwards_the_request_path( string $request_uri, string $expected ): void { + $_SERVER['REQUEST_URI'] = $request_uri; + + $this->assertSame( $expected, get_target_url() ); + } + + /** + * Requests whose remainder could be read as part of a host name. + * + * @return array + */ + public static function awkward_requests(): array { + return array( + 'at sign after the subpath' => array( '/openverse/openverse@example.com/' ), + 'dot after the subpath' => array( '/openverse/openverse.example.com/' ), + 'doubled slash' => array( '/openverse//example.com' ), + 'at sign only' => array( '/openverse/@example.com' ), + 'backslash' => array( '/openverse/\\\\example.com' ), + 'encoded slashes' => array( '/openverse/%2F%2Fexample.com' ), + 'traversal' => array( '/openverse/..//example.com' ), + 'subpath twice over' => array( '/openverse/openverse/openverse@example.com/' ), + ); + } + + /** + * The forwarded URL always stays on the configured host. + * + * The remainder is appended to an origin that carries no trailing slash, so + * anything that does not start the path cleanly would land in the authority + * instead. + * + * @param string $request_uri The incoming request. + */ + #[DataProvider( 'awkward_requests' )] + public function test_keeps_the_configured_host( string $request_uri ): void { + $_SERVER['REQUEST_URI'] = $request_uri; + + $this->assertSame( + 'openverse.org', + wp_parse_url( get_target_url(), PHP_URL_HOST ), + 'Forwarded to a different host: ' . get_target_url() + ); + } + + /** + * A locale slug is inserted between the origin and the path. + */ + public function test_inserts_the_locale_before_the_path(): void { + add_filter( 'locale', array( $this, 'russian' ) ); + $_SERVER['REQUEST_URI'] = '/openverse/search/?q=dog'; + + $this->assertSame( self::ORIGIN . '/ru/search/?q=dog', get_target_url() ); + } + + /** + * Configured origins, and whether the theme will redirect to them. + * + * @return array + */ + public static function targets(): array { + return array( + 'https' => array( 'https://openverse.org/search/', true ), + 'http' => array( 'http://openverse.org/search/', true ), + 'explicit port' => array( 'https://openverse.org:8443/search/', true ), + 'no scheme' => array( 'openverse.org/search/', false ), + 'scheme relative' => array( '//openverse.org/search/', false ), + 'other scheme' => array( 'ftp://openverse.org/search/', false ), + 'javascript' => array( 'javascript:alert(1)', false ), + 'path only' => array( '/search/', false ), + 'empty' => array( '', false ), + 'unlisted host' => array( 'https://example.com/search/', false ), + ); + } + + /** + * Only an absolute http or https URL on an allowed host is redirected to. + * + * `wp_validate_redirect()` repairs rather than rejects, so a target with no + * host or no scheme comes back truthy and cannot be relied on alone. + * + * @param string $target The URL the theme would redirect to. + * @param bool $expected Whether it should be redirected to. + */ + #[DataProvider( 'targets' )] + public function test_redirects_only_to_an_absolute_http_url( string $target, bool $expected ): void { + $this->assertSame( $expected, is_valid_target_url( $target ) ); + } +} diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/bootstrap.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/bootstrap.php new file mode 100644 index 0000000000..274f265c69 --- /dev/null +++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/bootstrap.php @@ -0,0 +1,60 @@ + (object) array( 'slug' => 'ru' ), + ); + } +} From 5f43f8cf2c8a281d147e089231ba3c579280094a Mon Sep 17 00:00:00 2001 From: Michal Iwanow <4765119+mcliwanow@users.noreply.github.com> Date: Wed, 2 Sep 2026 12:36:03 +0200 Subject: [PATCH 06/10] Openverse: Read the redirect switch and the origin as they were written. Three things an audit of the redirect guard turned up. None of them let a visitor change where the redirect goes; all three are ways an operator can end up serving a permanent redirect they did not ask for. `wp theme mod set` stores the string it is handed, so `wp theme mod set ov_is_redirect_enabled false` stored `'false'`, which PHP reads as true, and switched the redirect on. Both call sites now read the flag through `is_redirect_enabled()`, which passes it to `wp_validate_boolean()`. That covers `'false'` and `'FALSE'`; `'no'` and `'off'` stay truthy, as they do everywhere else in WordPress. The trailing slash on `ov_redirect_url` was only stripped by the Customizer's `sanitize_callback`, which `wp theme mod set` bypasses, so a value set that way produced a doubled slash in every forwarded URL. `get_target_url()` now applies `untrailingslashit()` itself. `allow_standalone_redirect_host()` claimed reading the host from the setting stopped a malformed target authorising its own destination. The target's authority is always the origin's, so parsing either gives the same host and the choice changes nothing today. It is still the right way round, and the docblock now says why without overstating it. Tests: the switch's string forms, the configured origin's variants, and a walk of every forwarded request through `wp_validate_redirect()` to pin that a target the guard accepts is one core sends unchanged. Without that last one a guard looser than core passes while sending visitors to a cached wp-admin 301. 67 tests, up from 31. Co-Authored-By: Claude Opus 5 --- .../themes/pub/wporg-openverse/functions.php | 27 +++- .../themes/pub/wporg-openverse/index.php | 3 +- .../wporg-openverse/tests/Target_Url_Test.php | 130 +++++++++++++++++- 3 files changed, 150 insertions(+), 10 deletions(-) diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php index d9b91d8a96..829c1f73e0 100644 --- a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php +++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php @@ -262,7 +262,7 @@ function wporg_ov_customizer( $wp_customize ) { * @return string */ function get_target_url() { - $target_url = get_theme_mod( 'ov_redirect_url', OPENVERSE_STANDALONE_URL ); + $target_url = untrailingslashit( get_theme_mod( 'ov_redirect_url', OPENVERSE_STANDALONE_URL ) ); $curr_locale = get_locale(); $locale = get_locale_slug( $curr_locale ); @@ -295,6 +295,19 @@ function get_target_url() { return $target_url; } +/** + * Whether the redirect to the standalone site is switched on. + * + * `wp theme mod set` stores the string it is handed, so the setting can hold + * `'false'`, which PHP reads as true. `wp_validate_boolean()` reads it the way + * whoever typed it meant it. + * + * @return bool + */ +function is_redirect_enabled() { + return wp_validate_boolean( get_theme_mod( 'ov_is_redirect_enabled', false ) ); +} + /** * Whether a redirect target is usable. * @@ -303,6 +316,11 @@ function get_target_url() { * assumes `http`, so both come back truthy. The scheme and host are checked * first because this target is always absolute. * + * The `http`/`https` comparison is deliberately case-sensitive, and states a + * contract rather than carrying the weight: `wp_validate_redirect()` compares + * the same way, so an upper-case scheme is refused either way. Lower-casing it + * here would not make one usable. + * * @param string $target_url URL the theme intends to redirect to. * @return bool */ @@ -323,15 +341,16 @@ function is_valid_target_url( $target_url ) { /** * Allow the redirect to reach the standalone Openverse site. * - * The host comes from the configured origin, not from the generated target - * URL, so that a malformed target cannot authorise its own destination. + * The host is read from the configured origin rather than the generated target + * URL. The two always share an authority today, so it makes no difference now; + * it keeps the filter right if the target is ever assembled differently. * * @param string[] $hosts Allowed host names. * @return string[] Allowed host names, plus the standalone Openverse host when * the redirect is enabled. */ function allow_standalone_redirect_host( $hosts ) { - if ( ! get_theme_mod( 'ov_is_redirect_enabled' ) ) { + if ( ! is_redirect_enabled() ) { return $hosts; } diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/index.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/index.php index 5f676adc32..45a68a4fe7 100644 --- a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/index.php +++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/index.php @@ -25,10 +25,9 @@ that caused it was corrected. Rendering the page is the safer failure. */ -$is_redirect_enabled = get_theme_mod( 'ov_is_redirect_enabled' ); $target_url = get_target_url(); -if ( $is_redirect_enabled && is_valid_target_url( $target_url ) ) { +if ( is_redirect_enabled() && is_valid_target_url( $target_url ) ) { wp_safe_redirect( $target_url, 301 ); exit; } else { diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/Target_Url_Test.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/Target_Url_Test.php index c6bc289249..c9129d4b33 100644 --- a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/Target_Url_Test.php +++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/Target_Url_Test.php @@ -12,6 +12,7 @@ use PHPUnit\Framework\TestCase; use function WordPressdotorg\Openverse\Theme\get_target_url; +use function WordPressdotorg\Openverse\Theme\is_redirect_enabled; use function WordPressdotorg\Openverse\Theme\is_valid_target_url; /** @@ -153,10 +154,12 @@ public static function awkward_requests(): array { public function test_keeps_the_configured_host( string $request_uri ): void { $_SERVER['REQUEST_URI'] = $request_uri; - $this->assertSame( - 'openverse.org', - wp_parse_url( get_target_url(), PHP_URL_HOST ), - 'Forwarded to a different host: ' . get_target_url() + $target = get_target_url(); + + $this->assertStringStartsWith( + self::ORIGIN . '/', + $target, + 'Forwarded somewhere other than a path under the origin: ' . $target ); } @@ -203,4 +206,123 @@ public static function targets(): array { public function test_redirects_only_to_an_absolute_http_url( string $target, bool $expected ): void { $this->assertSame( $expected, is_valid_target_url( $target ) ); } + + /** + * Every request the theme forwards survives the redirect it is handed to. + * + * `wp_safe_redirect()` sends a target it cannot validate to `admin_url()` + * rather than refusing, so a guard that is looser than core leaves a 301 + * into wp-admin in visitors' caches. Asserting the guard on its own cannot + * catch that: this walks the whole path, from request to sent header. + * + * @param string $request_uri The incoming request. + */ + #[DataProvider( 'every_request' )] + public function test_a_forwarded_request_is_sent_unchanged( string $request_uri ): void { + $_SERVER['REQUEST_URI'] = $request_uri; + $target = get_target_url(); + + $this->assertTrue( is_valid_target_url( $target ), "Refused to redirect to {$target}" ); + $this->assertSame( + $target, + wp_validate_redirect( $target, 'FELL-BACK' ), + "wp_safe_redirect() would not have sent {$target}" + ); + } + + /** + * Every request used anywhere in this file. + * + * @return array + */ + public static function every_request(): array { + $requests = array(); + + foreach ( self::requests() as $name => $case ) { + $requests[ $name ] = array( $case[0] ); + } + + return array_merge( $requests, self::awkward_requests() ); + } + + /** + * Values the switch can hold, and whether each one means "on". + * + * @return array + */ + public static function switch_values(): array { + return array( + 'boolean true' => array( true, true ), + 'boolean false' => array( false, false ), + 'one' => array( '1', true ), + 'zero' => array( '0', false ), + 'empty string' => array( '', false ), + 'the word true' => array( 'true', true ), + 'the word false' => array( 'false', false ), + 'upper case' => array( 'FALSE', false ), + ); + } + + /** + * The switch reads the string forms `wp theme mod set` stores. + * + * The command stores its argument verbatim, so the setting can hold + * `'false'`, and a plain truthiness test would turn the redirect on for it. + * + * @param mixed $stored What the theme mod holds. + * @param bool $expected Whether the redirect should run. + */ + #[DataProvider( 'switch_values' )] + public function test_reads_the_switch_as_it_was_written( $stored, bool $expected ): void { + remove_filter( 'theme_mod_ov_is_redirect_enabled', '__return_true' ); + add_filter( 'theme_mod_ov_is_redirect_enabled', static fn() => $stored ); + + $this->assertSame( $expected, is_redirect_enabled() ); + + remove_all_filters( 'theme_mod_ov_is_redirect_enabled' ); + add_filter( 'theme_mod_ov_is_redirect_enabled', '__return_true' ); + } + + /** + * Origins the setting can hold, and the URL each one forwards a search to. + * + * @return array + */ + public static function origins(): array { + return array( + 'plain' => array( 'https://openverse.org', 'https://openverse.org/search/' ), + 'trailing slash' => array( 'https://openverse.org/', 'https://openverse.org/search/' ), + 'http' => array( 'http://openverse.org', 'http://openverse.org/search/' ), + 'explicit port' => array( 'https://openverse.org:8443', 'https://openverse.org:8443/search/' ), + 'upper case scheme' => array( 'HTTPS://openverse.org', null ), + 'no scheme' => array( 'openverse.org', null ), + 'scheme relative' => array( '//openverse.org', null ), + 'other scheme' => array( 'ftp://openverse.org', null ), + ); + } + + /** + * A configured origin either forwards to itself or is refused outright. + * + * The trailing-slash row matters because the Customizer strips one and + * `wp theme mod set` does not. + * + * @param string $origin The configured origin. + * @param string|null $expected The URL a search forwards to, or null when + * the theme should render the page instead. + */ + #[DataProvider( 'origins' )] + public function test_forwards_only_to_a_usable_origin( string $origin, ?string $expected ): void { + remove_filter( 'theme_mod_ov_redirect_url', array( $this, 'origin' ) ); + add_filter( 'theme_mod_ov_redirect_url', static fn() => $origin ); + $_SERVER['REQUEST_URI'] = '/openverse/search/'; + + $target = get_target_url(); + $sent = is_valid_target_url( $target ) ? $target : null; + + $this->assertSame( $expected, $sent ); + + remove_all_filters( 'theme_mod_ov_redirect_url' ); + add_filter( 'theme_mod_ov_redirect_url', array( $this, 'origin' ) ); + } } From ec519ee766c72b991ede0a557ac3f1398ae5f32c Mon Sep 17 00:00:00 2001 From: Michal Iwanow <4765119+mcliwanow@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:17:13 +0200 Subject: [PATCH 07/10] Openverse: Compare the target's host with the configured origin. `get_target_url()` appends the path straight after the origin, so a target's authority is always the one an administrator configured and no request can change it. The guard leaned on `wp_safe_redirect()` to enforce that, which meant registering an `allowed_redirect_hosts` filter, and that filter widens the safe-redirect allow-list for every other caller on the subsite, `redirect_to` on login included. It also brought `wp_validate_redirect()`'s `admin_url()` fallback with it, which is what a permanent redirect into wp-admin needed to be possible at all. Compare the host against the origin instead and go back to `wp_redirect()`, which runs `wp_sanitize_redirect()` just the same. The result is narrower than what it replaces: the allow-list also accepted the home host, so a target on wordpress.org would have passed. `get_standalone_origin()` gives the two callers one reading of the setting. Co-Authored-By: Claude Opus 5 --- .../themes/pub/wporg-openverse/functions.php | 55 +++++++------------ .../themes/pub/wporg-openverse/index.php | 9 ++- .../{phpunit.xml => phpunit.xml.dist} | 0 .../wporg-openverse/tests/Target_Url_Test.php | 19 +++---- 4 files changed, 32 insertions(+), 51 deletions(-) rename wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/{phpunit.xml => phpunit.xml.dist} (100%) diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php index 829c1f73e0..c3954c0654 100644 --- a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php +++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php @@ -262,7 +262,7 @@ function wporg_ov_customizer( $wp_customize ) { * @return string */ function get_target_url() { - $target_url = untrailingslashit( get_theme_mod( 'ov_redirect_url', OPENVERSE_STANDALONE_URL ) ); + $target_url = get_standalone_origin(); $curr_locale = get_locale(); $locale = get_locale_slug( $curr_locale ); @@ -295,6 +295,18 @@ function get_target_url() { return $target_url; } +/** + * The origin the standalone Openverse site is served from. + * + * The Customizer's `sanitize_callback` drops a trailing slash and + * `wp theme mod set` does not, so it is dropped here too. + * + * @return string + */ +function get_standalone_origin() { + return untrailingslashit( get_theme_mod( 'ov_redirect_url', OPENVERSE_STANDALONE_URL ) ); +} + /** * Whether the redirect to the standalone site is switched on. * @@ -311,15 +323,11 @@ function is_redirect_enabled() { /** * Whether a redirect target is usable. * - * `wp_validate_redirect()` repairs rather than rejects. Given a target with no - * host it prepends the current directory, and given a scheme-relative one it - * assumes `http`, so both come back truthy. The scheme and host are checked - * first because this target is always absolute. - * - * The `http`/`https` comparison is deliberately case-sensitive, and states a - * contract rather than carrying the weight: `wp_validate_redirect()` compares - * the same way, so an upper-case scheme is refused either way. Lower-casing it - * here would not make one usable. + * The path is appended straight after the origin, so a target's authority is + * always the one an administrator configured. Comparing the two makes that an + * enforced property rather than an assumption, and it is why this does not need + * `wp_safe_redirect()`: no request can reach a host the origin did not supply, + * and the allow-list that would require widens redirects for the whole site. * * @param string $target_url URL the theme intends to redirect to. * @return bool @@ -335,33 +343,8 @@ function is_valid_target_url( $target_url ) { return false; } - return (bool) wp_validate_redirect( $target_url, '' ); -} - -/** - * Allow the redirect to reach the standalone Openverse site. - * - * The host is read from the configured origin rather than the generated target - * URL. The two always share an authority today, so it makes no difference now; - * it keeps the filter right if the target is ever assembled differently. - * - * @param string[] $hosts Allowed host names. - * @return string[] Allowed host names, plus the standalone Openverse host when - * the redirect is enabled. - */ -function allow_standalone_redirect_host( $hosts ) { - if ( ! is_redirect_enabled() ) { - return $hosts; - } - - $host = wp_parse_url( get_theme_mod( 'ov_redirect_url', OPENVERSE_STANDALONE_URL ), PHP_URL_HOST ); - if ( $host ) { - $hosts[] = $host; - } - - return $hosts; + return $parts['host'] === wp_parse_url( get_standalone_origin(), PHP_URL_HOST ); } -add_filter( 'allowed_redirect_hosts', __NAMESPACE__ . '\allow_standalone_redirect_host' ); /** * Provide configuration for the theme to redirect to the given standalone diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/index.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/index.php index 45a68a4fe7..a178465cfb 100644 --- a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/index.php +++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/index.php @@ -19,16 +19,15 @@ standalone site and exit immediately. If not, print what would have been the redirect URL to the HTML as a comment. - The target is checked before redirecting. Left to itself - `wp_safe_redirect()` sends an unusable target to `admin_url()` instead, and - a 301 into wp-admin would sit in visitors' caches long after the setting - that caused it was corrected. Rendering the page is the safer failure. + The target is checked before redirecting. A misconfigured `ov_redirect_url` + would otherwise send a permanent redirect that sits in visitors' caches long + after the setting was corrected. Rendering the page is the safer failure. */ $target_url = get_target_url(); if ( is_redirect_enabled() && is_valid_target_url( $target_url ) ) { - wp_safe_redirect( $target_url, 301 ); + wp_redirect( $target_url, 301 ); exit; } else { echo ''; diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/phpunit.xml b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/phpunit.xml.dist similarity index 100% rename from wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/phpunit.xml rename to wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/phpunit.xml.dist diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/Target_Url_Test.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/Target_Url_Test.php index c9129d4b33..ee85b23a72 100644 --- a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/Target_Url_Test.php +++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/Target_Url_Test.php @@ -189,15 +189,15 @@ public static function targets(): array { 'javascript' => array( 'javascript:alert(1)', false ), 'path only' => array( '/search/', false ), 'empty' => array( '', false ), - 'unlisted host' => array( 'https://example.com/search/', false ), + 'another host' => array( 'https://example.com/search/', false ), ); } /** * Only an absolute http or https URL on an allowed host is redirected to. * - * `wp_validate_redirect()` repairs rather than rejects, so a target with no - * host or no scheme comes back truthy and cannot be relied on alone. + * The authority comes from the configured origin, never from the request, + * so the guard checks the target's host against that origin directly. * * @param string $target The URL the theme would redirect to. * @param bool $expected Whether it should be redirected to. @@ -208,12 +208,11 @@ public function test_redirects_only_to_an_absolute_http_url( string $target, boo } /** - * Every request the theme forwards survives the redirect it is handed to. + * Every request the theme forwards is accepted and sent as it was built. * - * `wp_safe_redirect()` sends a target it cannot validate to `admin_url()` - * rather than refusing, so a guard that is looser than core leaves a 301 - * into wp-admin in visitors' caches. Asserting the guard on its own cannot - * catch that: this walks the whole path, from request to sent header. + * Asserting the guard on its own only restates it. This walks the whole + * path, from request through the guard to the header `wp_redirect()` would + * send, so a target the guard accepts but core would mangle shows up here. * * @param string $request_uri The incoming request. */ @@ -225,8 +224,8 @@ public function test_a_forwarded_request_is_sent_unchanged( string $request_uri $this->assertTrue( is_valid_target_url( $target ), "Refused to redirect to {$target}" ); $this->assertSame( $target, - wp_validate_redirect( $target, 'FELL-BACK' ), - "wp_safe_redirect() would not have sent {$target}" + wp_sanitize_redirect( $target ), + "wp_redirect() would not have sent {$target} unchanged" ); } From c3520daaea96a93597367f30783911f891fd90e2 Mon Sep 17 00:00:00 2001 From: Michal Iwanow <4765119+mcliwanow@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:23:10 +0200 Subject: [PATCH 08/10] Openverse: Use pl_PL in the locale test. Co-Authored-By: Claude Opus 5 --- .../pub/wporg-openverse/tests/Target_Url_Test.php | 12 ++++++------ .../pub/wporg-openverse/tests/locales-stub.php | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/Target_Url_Test.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/Target_Url_Test.php index ee85b23a72..ae0b18924d 100644 --- a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/Target_Url_Test.php +++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/Target_Url_Test.php @@ -62,7 +62,7 @@ protected function setUp(): void { protected function tearDown(): void { remove_filter( 'theme_mod_ov_is_redirect_enabled', '__return_true' ); remove_filter( 'theme_mod_ov_redirect_url', array( $this, 'origin' ) ); - remove_filter( 'locale', array( $this, 'russian' ) ); + remove_filter( 'locale', array( $this, 'polish' ) ); if ( null === $this->request_uri ) { unset( $_SERVER['REQUEST_URI'] ); @@ -81,10 +81,10 @@ public function origin(): string { } /** - * Filter callback switching the site to Russian. + * Filter callback switching the site to Polish. */ - public function russian(): string { - return 'ru_RU'; + public function polish(): string { + return 'pl_PL'; } /** @@ -167,10 +167,10 @@ public function test_keeps_the_configured_host( string $request_uri ): void { * A locale slug is inserted between the origin and the path. */ public function test_inserts_the_locale_before_the_path(): void { - add_filter( 'locale', array( $this, 'russian' ) ); + add_filter( 'locale', array( $this, 'polish' ) ); $_SERVER['REQUEST_URI'] = '/openverse/search/?q=dog'; - $this->assertSame( self::ORIGIN . '/ru/search/?q=dog', get_target_url() ); + $this->assertSame( self::ORIGIN . '/pl/search/?q=dog', get_target_url() ); } /** diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/locales-stub.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/locales-stub.php index 251f0ded5a..d0d24ff9e7 100644 --- a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/locales-stub.php +++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/locales-stub.php @@ -18,7 +18,7 @@ */ function get_locales(): array { return array( - 'ru_RU' => (object) array( 'slug' => 'ru' ), + 'pl_PL' => (object) array( 'slug' => 'pl' ), ); } } From 217b5cefddda478c9c8d908a9d045a95138be94f Mon Sep 17 00:00:00 2001 From: Michal Iwanow <4765119+mcliwanow@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:26:04 +0200 Subject: [PATCH 09/10] Openverse: Yoda condition in the target host comparison. Co-Authored-By: Claude Opus 5 --- .../wp-content/themes/pub/wporg-openverse/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php index c3954c0654..cc9f1727bb 100644 --- a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php +++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php @@ -343,7 +343,7 @@ function is_valid_target_url( $target_url ) { return false; } - return $parts['host'] === wp_parse_url( get_standalone_origin(), PHP_URL_HOST ); + return wp_parse_url( get_standalone_origin(), PHP_URL_HOST ) === $parts['host']; } /** From 715c71ede1a2268c59811f4c52518bb75aaf08ae Mon Sep 17 00:00:00 2001 From: Michal Iwanow <4765119+mcliwanow@users.noreply.github.com> Date: Thu, 3 Sep 2026 10:39:34 +0200 Subject: [PATCH 10/10] Openverse: Accept an upper-case scheme on the configured origin. Schemes are case-insensitive, and `parse_url()` reports them as written, so an `ov_redirect_url` saved as `HTTPS://openverse.org` failed the guard and switched the redirect off on a value that is legal. Lower-case before comparing. Only the check is normalised; the URL keeps the case it was configured with. Co-Authored-By: Claude Opus 5 --- .../wp-content/themes/pub/wporg-openverse/functions.php | 2 +- .../themes/pub/wporg-openverse/tests/Target_Url_Test.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php index cc9f1727bb..06b562afe9 100644 --- a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php +++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/functions.php @@ -339,7 +339,7 @@ function is_valid_target_url( $target_url ) { return false; } - if ( ! in_array( $parts['scheme'], array( 'http', 'https' ), true ) ) { + if ( ! in_array( strtolower( $parts['scheme'] ), array( 'http', 'https' ), true ) ) { return false; } diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/Target_Url_Test.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/Target_Url_Test.php index ae0b18924d..bfabeefbbd 100644 --- a/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/Target_Url_Test.php +++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-openverse/tests/Target_Url_Test.php @@ -293,7 +293,7 @@ public static function origins(): array { 'trailing slash' => array( 'https://openverse.org/', 'https://openverse.org/search/' ), 'http' => array( 'http://openverse.org', 'http://openverse.org/search/' ), 'explicit port' => array( 'https://openverse.org:8443', 'https://openverse.org:8443/search/' ), - 'upper case scheme' => array( 'HTTPS://openverse.org', null ), + 'upper case scheme' => array( 'HTTPS://openverse.org', 'HTTPS://openverse.org/search/' ), 'no scheme' => array( 'openverse.org', null ), 'scheme relative' => array( '//openverse.org', null ), 'other scheme' => array( 'ftp://openverse.org', null ),