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
8 changes: 5 additions & 3 deletions inc/native/class-wp-markdown-native-post-catalogue.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ final class WP_Markdown_Native_Post_Catalogue {

public function __construct(
private readonly string $content_root,
private readonly string $state_root
private readonly string $state_root,
/** @var string[] */
private readonly array $excluded_roots = array()
) {}

/**
Expand All @@ -44,7 +46,7 @@ public function __construct(
public function remember( WP_Markdown_File_Witness $witness, array $file, object $post, array $row, bool $path_is_canonical = false ): void {
$this->load();
$path = (string) ( $file['absolute'] ?? '' );
if ( ! $path_is_canonical && null === $this->relative_path( $path ) ) {
if ( ( ! $path_is_canonical || array() !== $this->excluded_roots ) && null === $this->relative_path( $path ) ) {
return;
}
$this->entries[ $path ] = array( 'witness' => $witness, 'file' => $file, 'post' => $post, 'row' => $row );
Expand Down Expand Up @@ -222,7 +224,7 @@ private function hydrate( mixed $saved ): ?array {
return null;
}
$relative = str_replace( '\\', '/', $saved['path'] );
if ( '' === $relative || str_starts_with( $relative, '/' ) || in_array( '..', explode( '/', $relative ), true ) ) {
if ( '' === $relative || str_starts_with( $relative, '/' ) || in_array( '..', explode( '/', $relative ), true ) || in_array( strtok( $relative, '/' ), $this->excluded_roots, true ) ) {
return null;
}
$path = rtrim( $this->content_root, '/\\' ) . DIRECTORY_SEPARATOR . str_replace( '/', DIRECTORY_SEPARATOR, $relative );
Expand Down
20 changes: 14 additions & 6 deletions inc/native/class-wp-markdown-native-query-runtime.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@ public static function registry(
foreach ( array_keys( WP_Markdown_Native_Schema_Catalog::definitions( $multisite ) ) as $suffix ) {
self::register_core_table( $registry, $state_root, $content_root, $prefix, $base_prefix, $multisite, (string) $suffix, $global_state_root, $global_content_root );
}
} elseif ( $multisite && self::holds_canonical_site( $global_state_root, $global_content_root ) ) {
// A new site's local root is empty while wp_initialize_site() still
// needs established network tables such as sitemeta.
foreach ( array( 'blogs', 'blogmeta', 'registration_log', 'site', 'sitemeta', 'signups' ) as $suffix ) {
self::register_core_table( $registry, $state_root, $content_root, $prefix, $base_prefix, true, $suffix, $global_state_root, $global_content_root );
}
}
self::register_persisted_plugin_tables( $registry, $state_root, $prefix, $multisite );
return $registry;
Expand Down Expand Up @@ -207,7 +213,8 @@ public static function register_core_table(
}
if ( 'posts' === $suffix ) {
$posts = self::posts_schema();
$registry->register( $prefix . 'posts', $posts, new WP_Markdown_Native_Post_Provider( $provider_content_root, $posts, self::shared_storage( $provider_content_root ), $provider_state_root ) );
$network_root = $multisite && $prefix === $base_prefix;
$registry->register( $prefix . 'posts', $posts, new WP_Markdown_Native_Post_Provider( $provider_content_root, $posts, self::shared_storage( $provider_content_root, $network_root ), $provider_state_root, $network_root ) );
return true;
}
$bespoke = array(
Expand Down Expand Up @@ -269,7 +276,7 @@ public static function runtime(
new WP_Markdown_Native_Post_Mutation_Runtime(
$registry,
$parser,
self::shared_storage( $content_root ?? $state_root ),
self::shared_storage( $content_root ?? $state_root, $multisite && $prefix === $resolved_base ),
$transactions,
),
advisory_locks: $advisory_locks ?? new WP_Markdown_Native_Advisory_Locks( $state_root )
Expand Down Expand Up @@ -367,10 +374,11 @@ private static function holds_canonical_site( string $state_root, string $conten
* a read that remembers what it parsed must be told when a write changes
* a file underneath it.
*/
private static function shared_storage( string $content_root ): WP_Markdown_Storage {
$key = rtrim( $content_root, '/\\' );
private static function shared_storage( string $content_root, bool $network_root = false ): WP_Markdown_Storage {
$key = ( $network_root ? 'network:' : 'site:' ) . rtrim( $content_root, '/\\' );
if ( ! isset( self::$storages[ $key ] ) ) {
self::$storages[ $key ] = new WP_Markdown_Storage( $content_root );
// The network root owns sites/{blog_id}; it is not a post-type tree.
self::$storages[ $key ] = new WP_Markdown_Storage( $content_root, $network_root ? array( 'sites' ) : array() );
}
return self::$storages[ $key ];
}
Expand Down Expand Up @@ -656,7 +664,7 @@ public function __construct( string $state_root, private string $base_prefix, st
private string $content_root;

public function execute( WP_Markdown_Query_Request $request ): WP_Markdown_Query_Result {
$multisite = ( defined( 'MULTISITE' ) && MULTISITE ) || ( function_exists( 'is_multisite' ) && is_multisite() );
$multisite = ( defined( 'WP_INSTALLING_NETWORK' ) && WP_INSTALLING_NETWORK ) || ( defined( 'MULTISITE' ) && MULTISITE ) || ( function_exists( 'is_multisite' ) && is_multisite() );
if ( ! $multisite ) {
return $this->prefix_runtime->execute( $request );
}
Expand Down
7 changes: 4 additions & 3 deletions inc/native/class-wp-markdown-native-table-providers.php
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,8 @@ public function __construct(
string $content_root,
WP_Markdown_Native_Table_Schema $schema,
?WP_Markdown_Storage $storage = null,
?string $state_root = null
?string $state_root = null,
private bool $network_root = false
) {
parent::__construct( $content_root, $schema );
$this->storage = $storage ?? new WP_Markdown_Storage( $content_root );
Expand All @@ -256,7 +257,7 @@ public function __construct(
// recorded under and are kept. The scoped views are lists of which
// files answered a read, which a new or removed file changes, so they
// are rebuilt — from the surviving parses, not from the corpus.
$this->catalogue = new WP_Markdown_Native_Post_Catalogue( $content_root, $state_root ?? $content_root );
$this->catalogue = new WP_Markdown_Native_Post_Catalogue( $content_root, $state_root ?? $content_root, $network_root ? array( 'sites' ) : array() );
$this->storage->add_file_mutation_observer( function ( string $path ): void {
$this->catalogue->forget( $path );
$this->scoped_posts = array();
Expand Down Expand Up @@ -351,7 +352,7 @@ private function located_candidates( WP_Markdown_Native_Table_Access $access ):
foreach ( $predicate->values() as $value ) {
$id = (int) $value;
$file = $this->catalogue->file_for( $id );
if ( null === $file ) {
if ( null === $file && ! $this->network_root ) {
$file = $this->storage->indexed_post_file( $id );
}
if ( null === $file ) {
Expand Down
19 changes: 19 additions & 0 deletions tests/probe-native-multisite-wordpress.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,27 @@ function mdi_native_multisite_probe_tables(): array {
mdi_native_multisite_probe_assert( get_post( $post_id ) instanceof WP_Post, 'site_post', $checks );
mdi_native_multisite_probe_assert( in_array( $site_prefix . 'options', $site_tables, true ) && in_array( $site_prefix . 'posts', $site_tables, true ), 'site_show_tables', $checks );
mdi_native_multisite_probe_assert( false === $wpdb->query( "SELECT option_value FROM {$wpdb->base_prefix}options WHERE option_name = 'mdi_network_option'" ), 'cross_prefix_site_table_rejected', $checks );
$foreign_id = wp_insert_attachment(
array(
'import_id' => 987654321,
'post_title' => 'MDI foreign attachment',
'post_status' => 'inherit',
'post_mime_type' => 'image/png',
),
'foreign-logo.png',
0,
true
);
if ( is_wp_error( $foreign_id ) ) {
throw new RuntimeException( $foreign_id->get_error_message() );
}
$foreign_id = (int) $foreign_id;
mdi_native_multisite_probe_assert( get_post( $foreign_id ) instanceof WP_Post, 'foreign_attachment_exists_on_site', $checks );
restore_current_blog();

$foreign_post = get_post( $foreign_id );
mdi_native_multisite_probe_assert( null === $foreign_post, 'foreign_attachment_is_not_visible_after_restore', $checks );

$base_tables = mdi_native_multisite_probe_tables();
$network_transaction = array(
'begin' => $wpdb->query( 'START TRANSACTION' ),
Expand Down
7 changes: 7 additions & 0 deletions tests/smoke-native-generated-core-query.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@
}

$multisite = WP_Markdown_Native_Runtime_Factory::runtime( $root, 'wp_2_', 'wp_', true );
$new_site_root = $root . '/sites/2';
mkdir( $new_site_root, 0755, true );
$new_site_runtime = WP_Markdown_Native_Runtime_Factory::runtime( $new_site_root, 'wp_2_', 'wp_', true, $new_site_root, $root, $root );
$network_tables = array(
'blogs' => 'blog_id',
'blogmeta' => 'meta_id',
Expand All @@ -75,6 +78,7 @@
}
$site_termmeta = $multisite->execute( new WP_Markdown_Query_Request( 'SELECT meta_id FROM wp_2_termmeta LIMIT 0', 'wp_2_' ) );
$wrong_network_prefix = $multisite->execute( new WP_Markdown_Query_Request( 'SELECT blog_id FROM wp_2_blogs LIMIT 0', 'wp_2_' ) );
$new_site_network_option = $new_site_runtime->execute( new WP_Markdown_Query_Request( "SELECT meta_value FROM wp_sitemeta WHERE meta_key = 'ms_files_rewriting' AND site_id = 1", 'wp_2_' ) );

$checks = array(
'generated commentmeta schema executes the retained conjunctive blocker' => 'retained' === ( $commentmeta->wpdb_state()['last_result'][0]->meta_value ?? null ),
Expand All @@ -90,6 +94,7 @@
'eligible multisite globals use base_prefix while site tables use active prefix' => $network_registered
&& 0 === $site_termmeta->return_value()
&& false === $wrong_network_prefix->return_value(),
'an empty new-site root retains established network-global core tables' => 0 === $new_site_network_option->return_value(),
);

$failed = false;
Expand All @@ -102,6 +107,8 @@
@unlink( $root . '/_tables/terms.json' );
@unlink( $root . '/_tables/term_relationships.json' );
@rmdir( $root . '/_tables' );
@rmdir( $new_site_root );
@rmdir( $root . '/sites' );
@rmdir( $root . '/_options' );
@rmdir( $root );
exit( $failed ? 1 : 0 );
41 changes: 41 additions & 0 deletions tests/smoke-native-install-network-topology.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php
/** Network installation routes global schema before multisite bootstrap completes. */

declare( strict_types=1 );

define( 'ABSPATH', __DIR__ . '/' );
define( 'WP_INSTALLING_NETWORK', true );
require_once __DIR__ . '/../inc/native/class-wp-markdown-native-query-runtime.php';

$root = sys_get_temp_dir() . '/mdi-native-install-network-' . bin2hex( random_bytes( 6 ) );
mkdir( $root, 0755 );
$GLOBALS['wpdb'] = (object) array( 'base_prefix' => 'wptests_' );
$runtime = new WP_Markdown_Native_WordPress_Query_Runtime( $root, 'wptests_', $root );
$created = $runtime->execute(
new WP_Markdown_Query_Request(
'CREATE TABLE wptests_blogs ( blog_id bigint(20) NOT NULL auto_increment, site_id bigint(20) NOT NULL default 0, domain varchar(200) NOT NULL default \'\', path varchar(100) NOT NULL default \'\', registered datetime NOT NULL default \'1970-01-01 00:00:00\', last_updated datetime NOT NULL default \'1970-01-01 00:00:00\', public tinyint(2) NOT NULL default 1, archived tinyint(2) NOT NULL default 0, mature tinyint(2) NOT NULL default 0, spam tinyint(2) NOT NULL default 0, deleted tinyint(2) NOT NULL default 0, lang_id int(11) NOT NULL default 0, PRIMARY KEY (blog_id), KEY domain (domain(50),path(5)), KEY lang_id (lang_id) )',
'wptests_'
)
);
$inserted = $runtime->execute(
new WP_Markdown_Query_Request(
"INSERT INTO wptests_blogs (blog_id, site_id, domain, path, registered, last_updated, public, archived, mature, spam, deleted, lang_id) VALUES (1, 1, 'example.com', '/', '2026-01-01 00:00:00', '2026-01-01 00:00:00', 1, 0, 0, 0, 0, 0)",
'wptests_'
)
);
$blog = $runtime->execute( new WP_Markdown_Query_Request( 'SELECT blog_id FROM wptests_blogs WHERE blog_id = 1', 'wptests_' ) );
$checks = array(
'network installation creates the global blogs schema before multisite bootstrap' => true === $created->return_value(),
'network installation persists the base blog row' => 1 === $inserted->return_value() && '1' === ( $blog->wpdb_state()['last_result'][0]->blog_id ?? null ),
);
$failed = false;
foreach ( $checks as $label => $passed ) {
fwrite( $passed ? STDOUT : STDERR, ( $passed ? 'PASS' : 'FAIL' ) . ": {$label}\n" );
$failed = $failed || ! $passed;
}

foreach ( new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $root, FilesystemIterator::SKIP_DOTS ), RecursiveIteratorIterator::CHILD_FIRST ) as $entry ) {
$entry->isDir() ? rmdir( $entry->getPathname() ) : unlink( $entry->getPathname() );
}
rmdir( $root );
exit( $failed ? 1 : 0 );