Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions inc/class-domain-mapping.php
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,14 @@ public function verify_dns_mapping($current_site, $domain, $path) { // phpcs:ign

$mapping = Domain::get_by_domain($domains);
if ($mapping) {
wp_send_json($mapping->to_array());
// Not using wp_send_json(): it calls get_option('blog_charset') before $wpdb->set_prefix() has run during early multisite bootstrap, producing a MariaDB syntax error against an empty options table name.
if ( ! headers_sent()) {
header('Content-Type: application/json; charset=UTF-8');
}

echo wp_json_encode($mapping->to_array());

exit;
}
}
}
Expand Down Expand Up @@ -646,8 +653,8 @@ public function replace_url($url, $current_mapping = null) {
$domain = rtrim($domain . '/' . preg_quote(ltrim($path, '/'), '#'), '/');
}

$regex = '#^(\w+://)' . $domain . '#i';
$mangled = preg_replace($regex, '${1}' . $current_mapping->get_domain(), $url);
$regex = '#^(\w+://)' . $domain . '#i';
$mangled = preg_replace($regex, '${1}' . $current_mapping->get_domain(), $url);

/*
* Another try if we don't need to deal with subdirectory.
Expand Down
104 changes: 89 additions & 15 deletions tests/WP_Ultimo/Domain_Mapping_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@
/**
* Test replace_url with a host-less (relative) URL returns it unchanged.
*
* parse_url('/foo', PHP_URL_HOST) returns null. Without a host guard,

Check warning on line 530 in tests/WP_Ultimo/Domain_Mapping_Test.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Doc comment long description must start with a capital letter
* preg_quote(null, '#') triggers a PHP 8.1 deprecation notice.
*/
public function test_replace_url_relative_url_returns_original(): void {
Expand Down Expand Up @@ -639,8 +639,14 @@
$this->domain_mapping->current_mapping = null;

$sources = [
100 => ['url' => 'http://example.com/image-100.jpg', 'value' => 100],
200 => ['url' => 'http://example.com/image-200.jpg', 'value' => 200],
100 => [
'url' => 'http://example.com/image-100.jpg',
'value' => 100,
],
200 => [
'url' => 'http://example.com/image-200.jpg',
'value' => 200,
],
];

$result = $this->domain_mapping->fix_srcset($sources);
Expand All @@ -654,7 +660,10 @@
public function test_fix_srcset_returns_array(): void {

$sources = [
100 => ['url' => 'http://example.com/image.jpg', 'value' => 100],
100 => [
'url' => 'http://example.com/image.jpg',
'value' => 100,
],
];

$result = $this->domain_mapping->fix_srcset($sources);
Expand Down Expand Up @@ -866,7 +875,7 @@
/**
* Test startup registers allowed_redirect_hosts filter when called explicitly.
*
* startup() does not register allowed_redirect_hosts directly; init() does.

Check warning on line 878 in tests/WP_Ultimo/Domain_Mapping_Test.php

View workflow job for this annotation

GitHub Actions / Code Quality Checks

Doc comment long description must start with a capital letter
* This test verifies that after calling add_filter manually, has_filter works.
*/
public function test_allowed_redirect_hosts_filter_can_be_registered(): void {
Expand Down Expand Up @@ -960,7 +969,7 @@
* @param string $domain Domain string.
* @return Domain
*/
private function create_db_domain( string $domain ): Domain {
private function create_db_domain(string $domain): Domain {

$result = wu_create_domain(
[
Expand All @@ -974,7 +983,7 @@
);

if ( is_wp_error( $result ) || ! $result instanceof Domain ) {
$this->markTestSkipped( 'Could not create domain record: ' . ( is_wp_error( $result ) ? $result->get_error_message() : 'unknown' ) );
$this->markTestSkipped( 'Could not create domain record: ' . (is_wp_error( $result ) ? $result->get_error_message() : 'unknown') );
}

return $result;
Expand All @@ -985,7 +994,7 @@
*
* @param string $domain Domain string.
*/
private function flush_domain_cache( string $domain ): void {
private function flush_domain_cache(string $domain): void {

wp_cache_delete( 'domain:' . $domain, 'domain_mappings' );
wp_cache_delete( 'domain:www.' . $domain, 'domain_mappings' );
Expand Down Expand Up @@ -1066,6 +1075,63 @@
$this->assertTrue(is_callable([$this->domain_mapping, 'verify_dns_mapping']));
}

/**
* Regression: when verify_dns_mapping fires very early in the multisite
* bootstrap (`pre_get_site_by_path` / `ms_site_not_found`), `$wpdb->options`
* is empty because `$wpdb->set_prefix()` runs later in `ms-settings.php`.
*
* Calling `wp_send_json()` at that stage internally calls
* `get_option( 'blog_charset' )`, which produced
*
* "WordPress database error You have an error in your SQL syntax;
* check the manual that corresponds to your MariaDB server version
* for the right syntax to use near 'WHERE option_name = 'blog_charset'
* LIMIT 1' at line 1 for query SELECT option_value FROM WHERE
* option_name = 'blog_charset' LIMIT 1"
*
* The fix emits the JSON response inline with a hard-coded UTF-8 charset,
* bypassing `get_option()` entirely.
*
* Because the matching branch calls `exit`, this regression test exercises
* the early-bootstrap path with a matching nonce but no persisted mapping —
* that path still runs `wp_hash()` and the `require_once` includes against
* the empty `$wpdb->options`, and must not produce a `wpdb->last_error`.
*/
public function test_verify_dns_mapping_does_not_query_options_table_in_early_bootstrap(): void {

global $wpdb;

// Capture any wpdb error produced during the call.
$wpdb->last_error = '';

// Simulate the empty $wpdb->options state seen in the real bug:
// at `pre_get_site_by_path` time, `set_prefix` has not run yet.
$saved_options = $wpdb->options;
$wpdb->options = '';
$_REQUEST['async_check_dns_nonce'] = wp_hash('regression-blog-charset.test');

try {
// No mapping is persisted for this domain, so Domain::get_by_domain()
// returns false and the method returns without reaching the `exit`
// branch. That still exercises the wp_hash() and require_once paths
// against the empty options table.
$this->domain_mapping->verify_dns_mapping(null, 'regression-blog-charset.test', '/');
} finally {
// Restore options table reference and clean up no matter what.
$wpdb->options = $saved_options;
unset($_REQUEST['async_check_dns_nonce']);
}

// Critically, no SQL syntax error was produced — the old code path
// would have populated wpdb->last_error with the malformed
// "SELECT option_value FROM WHERE option_name = 'blog_charset'" query.
$this->assertSame(
'',
$wpdb->last_error,
'verify_dns_mapping must not query the options table during early multisite bootstrap.'
);
}

// ----------------------------------------------------------------
// register_mapped_filters (with current_blog set)
// ----------------------------------------------------------------
Expand Down Expand Up @@ -1117,7 +1183,10 @@
$this->domain_mapping->current_mapping = $mapping;

$sources = [
100 => ['url' => 'http://example.com/image.jpg', 'value' => 100],
100 => [
'url' => 'http://example.com/image.jpg',
'value' => 100,
],
];

$result = $this->domain_mapping->fix_srcset($sources);
Expand Down Expand Up @@ -1325,7 +1394,7 @@
// Verify the domain is gone from the DB.
$this->flush_domain_cache( $domain_str );

$fetched = Domain::get_by_domain( [ $domain_str ] );
$fetched = Domain::get_by_domain( [$domain_str] );
$this->assertNull( $fetched );
}

Expand All @@ -1349,16 +1418,22 @@
$this->domain_mapping->current_mapping = $mapping;

$sources = [
100 => [ 'url' => 'http://example.org/image-100.jpg', 'value' => 100 ],
200 => [ 'url' => 'http://example.org/image-200.jpg', 'value' => 200 ],
100 => [
'url' => 'http://example.org/image-100.jpg',
'value' => 100,
],
200 => [
'url' => 'http://example.org/image-200.jpg',
'value' => 200,
],
];

$result = $this->domain_mapping->fix_srcset( $sources );

$this->assertIsArray( $result );
// At least one URL should be mangled to the mapped domain.
$all_urls = array_column( $result, 'url' );
$mangled = array_filter( $all_urls, fn( $u ) => str_contains( $u, 'srcset-mapped.example.com' ) );
$mangled = array_filter( $all_urls, fn($u) => str_contains( $u, 'srcset-mapped.example.com' ) );
$this->assertNotEmpty( $mangled );

$this->domain_mapping->current_mapping = null;
Expand Down Expand Up @@ -1396,11 +1471,11 @@
$this->domain_mapping->register_mapped_filters();

// site_url filter should now be registered.
$this->assertTrue( has_filter( 'site_url', [ $this->domain_mapping, 'mangle_url' ] ) !== false );
$this->assertTrue( has_filter( 'site_url', [$this->domain_mapping, 'mangle_url'] ) !== false );

// Clean up.
remove_filter( 'site_url', [ $this->domain_mapping, 'mangle_url' ], -10 );
remove_filter( 'home_url', [ $this->domain_mapping, 'mangle_url' ], -10 );
remove_filter( 'site_url', [$this->domain_mapping, 'mangle_url'], -10 );
remove_filter( 'home_url', [$this->domain_mapping, 'mangle_url'], -10 );
unset( $_SERVER['HTTP_HOST'] );
$domain->delete();
$this->flush_domain_cache( $domain_str );
Expand Down Expand Up @@ -1512,5 +1587,4 @@

$this->domain_mapping->current_mapping = null;
}

}
Loading