From 4d5ed512fa893b205a9c88862210751bf56e2b6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Jakes=CC=8C?= Date: Tue, 6 Oct 2026 10:29:18 +0200 Subject: [PATCH 1/2] Fix SQLite driver connection options Use sqlite_pdo and sqlite_journal_mode with the current driver while retaining the legacy PDO option. This keeps the supplied connection instead of opening a separate in-memory database. Update driver stubs and add a regression probe against real integrations. --- ...ass-wp-markdown-sqlite-runtime-adapter.php | 5 +- tests/probe-sqlite-path-discovery.php | 93 +++++++++++++++++++ tests/smoke-drop-index-schema-persistence.php | 4 +- tests/smoke-ephemeral-option-policy.php | 6 +- tests/smoke-primary-cold-reconstruction.php | 2 +- tests/smoke-primary-storage-runtime.php | 2 +- tests/smoke-state-root.php | 4 +- tests/smoke-streaming-table-persistence.php | 3 +- 8 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 tests/probe-sqlite-path-discovery.php diff --git a/inc/sqlite/class-wp-markdown-sqlite-runtime-adapter.php b/inc/sqlite/class-wp-markdown-sqlite-runtime-adapter.php index 53134b2..a43189e 100644 --- a/inc/sqlite/class-wp-markdown-sqlite-runtime-adapter.php +++ b/inc/sqlite/class-wp-markdown-sqlite-runtime-adapter.php @@ -142,13 +142,14 @@ public function __construct( WP_Markdown_Storage $storage, ?WP_Markdown_Backend_Capabilities $backend_capabilities = null ) { + $pdo_option = defined( 'MARKDOWN_DB_SQLITE_LEGACY_RESULT_API' ) && MARKDOWN_DB_SQLITE_LEGACY_RESULT_API ? 'pdo' : 'sqlite_pdo'; parent::__construct( sprintf( 'mysql-on-sqlite:dbname=%s', $database ), null, null, array( - 'pdo' => $connection->get_pdo(), - 'journal_mode' => $connection->get_pdo()->query( 'PRAGMA journal_mode' )->fetchColumn(), + $pdo_option => $connection->get_pdo(), + 'sqlite_journal_mode' => $connection->get_pdo()->query( 'PRAGMA journal_mode' )->fetchColumn(), ) ); $connection->get_pdo()->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); diff --git a/tests/probe-sqlite-path-discovery.php b/tests/probe-sqlite-path-discovery.php new file mode 100644 index 0000000..bcc099a --- /dev/null +++ b/tests/probe-sqlite-path-discovery.php @@ -0,0 +1,93 @@ +isDir() && ! $file->isLink() ? rmdir( $file->getPathname() ) : unlink( $file->getPathname() ); + } + rmdir( $root ); +} ); +symlink( dirname( __DIR__ ), $content . '/plugins/markdown-database-integration' ); +symlink( $sqlite, $content . '/plugins/sqlite-database-integration' ); +copy( dirname( __DIR__ ) . '/db.php', $content . '/db.php' ); +chdir( $root ); + +define( 'ABSPATH', $wordpress . '/' ); +define( 'WPINC', 'wp-includes' ); +define( 'WP_CONTENT_DIR', $content ); +define( 'WP_DEBUG', false ); +define( 'DB_NAME', 'wordpress' ); +define( 'MARKDOWN_DB_VERSION', 'test' ); +define( 'MARKDOWN_DB_MODE', 'mirror' ); +$GLOBALS['table_prefix'] = 'wp_'; + +$expected_path = $root . '/configured/legacy.sqlite'; +define( 'FQDB', $expected_path ); +mkdir( dirname( $expected_path ), 0755, true ); +$seed = new PDO( 'sqlite:' . $expected_path ); +$seed->exec( 'CREATE TABLE path_probe (value TEXT)' ); +$seed->exec( "INSERT INTO path_probe VALUES ('configured database')" ); +$seed = null; + +require_once ABSPATH . WPINC . '/compat.php'; +require_once ABSPATH . WPINC . '/plugin.php'; +require_once ABSPATH . WPINC . '/load.php'; +require_once ABSPATH . WPINC . '/formatting.php'; +require_once ABSPATH . WPINC . '/class-wpdb.php'; + +set_error_handler( static function ( int $severity, string $message, string $file, int $line ): bool { + if ( ! ( error_reporting() & $severity ) ) { + return false; + } + throw new ErrorException( $message, 0, $severity, $file, $line ); +} ); +require $content . '/db.php'; +restore_error_handler(); + +if ( 'sqlite' !== MARKDOWN_DB_BACKEND || ! $GLOBALS['wpdb'] instanceof WP_Markdown_DB || ! $GLOBALS['wpdb']->ready ) { + throw new RuntimeException( 'The SQLite mirror did not connect.' ); +} +$pdo = $GLOBALS['@pdo']; +$active_path = $pdo->query( 'PRAGMA database_list' )->fetch( PDO::FETCH_ASSOC )['file']; +if ( realpath( $expected_path ) !== realpath( $active_path ) ) { + throw new RuntimeException( 'Mirror opened the wrong database: ' . json_encode( array( 'expected' => $expected_path, 'databases' => $pdo->query( 'PRAGMA database_list' )->fetchAll( PDO::FETCH_ASSOC ) ) ) ); +} +if ( 'configured database' !== $pdo->query( 'SELECT value FROM path_probe' )->fetchColumn() ) { + throw new RuntimeException( 'The existing database content was not preserved.' ); +} + +echo "PASS: the SQLite driver retains the supplied database connection.\n"; diff --git a/tests/smoke-drop-index-schema-persistence.php b/tests/smoke-drop-index-schema-persistence.php index a152df4..8ee8954 100644 --- a/tests/smoke-drop-index-schema-persistence.php +++ b/tests/smoke-drop-index-schema-persistence.php @@ -34,7 +34,7 @@ class WP_MySQL_On_SQLite { public function __construct( string $dsn, ?string $username = null, ?string $password = null, array $options = array() ) { unset( $dsn, $username, $password ); - $this->connection = new WP_SQLite_Connection( $options['pdo'] ); + $this->connection = new WP_SQLite_Connection( $options['sqlite_pdo'] ); } private WP_SQLite_Connection $connection; @@ -139,7 +139,7 @@ function mdi_drop_index_rm( string $path ): void { $cold_pdo = new PDO( 'sqlite::memory:' ); $cold_pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION ); -$cold_driver = new WP_MySQL_On_SQLite( 'mysql-on-sqlite:dbname=wordpress', null, null, array( 'pdo' => $cold_pdo ) ); +$cold_driver = new WP_MySQL_On_SQLite( 'mysql-on-sqlite:dbname=wordpress', null, null, array( 'sqlite_pdo' => $cold_pdo ) ); $loader = new WP_Markdown_Loader( $root, $cold_driver, new WP_Markdown_Storage( $root ), 'wp_' ); $load_plugin_tables = new ReflectionMethod( $loader, 'load_plugin_tables' ); $load_plugin_tables->invoke( $loader ); diff --git a/tests/smoke-ephemeral-option-policy.php b/tests/smoke-ephemeral-option-policy.php index 8ac9990..3be44bd 100644 --- a/tests/smoke-ephemeral-option-policy.php +++ b/tests/smoke-ephemeral-option-policy.php @@ -42,10 +42,10 @@ class WP_MySQL_On_SQLite { private WP_SQLite_Connection $connection; private PDO $pdo; - /** @param array{pdo?:PDO,rows?:object[]} $options */ + /** @param array{sqlite_pdo?:PDO,rows?:object[]} $options */ public function __construct( string $dsn, ?string $username = null, ?string $password = null, array $options = array() ) { unset( $dsn, $username, $password ); - $this->pdo = $options['pdo'] ?? new PDO( 'sqlite::memory:' ); + $this->pdo = $options['sqlite_pdo'] ?? new PDO( 'sqlite::memory:' ); $this->connection = new WP_SQLite_Connection( $this->pdo ); $this->pdo->exec( 'CREATE TABLE IF NOT EXISTS mdi_rows (option_id INTEGER, option_name TEXT, option_value TEXT, autoload TEXT)' ); foreach ( $options['rows'] ?? array() as $row ) { @@ -141,7 +141,7 @@ static function ( array $names ): array { $pdo = new PDO( 'sqlite::memory:' ); $pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION ); $pdo->exec( 'CREATE TABLE wp_options (option_id INTEGER PRIMARY KEY, option_name TEXT UNIQUE, option_value TEXT, autoload TEXT)' ); -$loader = new WP_Markdown_Loader( $root, new WP_MySQL_On_SQLite( 'mysql-on-sqlite:dbname=wordpress', null, null, array( 'pdo' => $pdo ) ), new WP_Markdown_Storage( $root ), 'wp_', $root ); +$loader = new WP_Markdown_Loader( $root, new WP_MySQL_On_SQLite( 'mysql-on-sqlite:dbname=wordpress', null, null, array( 'sqlite_pdo' => $pdo ) ), new WP_Markdown_Storage( $root ), 'wp_', $root ); $load_options = new ReflectionMethod( WP_Markdown_Loader::class, 'load_options' ); $load_options->invoke( $loader ); $loaded_value = $pdo->query( "SELECT option_value FROM wp_options WHERE option_name = 'cron'" )->fetchColumn(); diff --git a/tests/smoke-primary-cold-reconstruction.php b/tests/smoke-primary-cold-reconstruction.php index a7eebba..4c481da 100644 --- a/tests/smoke-primary-cold-reconstruction.php +++ b/tests/smoke-primary-cold-reconstruction.php @@ -22,7 +22,7 @@ class WP_MySQL_On_SQLite { private bool $in_transaction = false; public function __construct( string $dsn, ?string $username = null, ?string $password = null, array $options = array() ) { unset( $dsn, $username, $password ); - $this->connection = new WP_SQLite_Connection( $options['pdo'] ); + $this->connection = new WP_SQLite_Connection( $options['sqlite_pdo'] ); } public function get_connection(): WP_SQLite_Connection { return $this->connection; } public function get_insert_id(): int { return (int) $this->connection->get_pdo()->lastInsertId(); } diff --git a/tests/smoke-primary-storage-runtime.php b/tests/smoke-primary-storage-runtime.php index aa55acd..b016775 100644 --- a/tests/smoke-primary-storage-runtime.php +++ b/tests/smoke-primary-storage-runtime.php @@ -23,7 +23,7 @@ class WP_MySQL_On_SQLite { private bool $in_transaction = false; public function __construct( string $dsn, ?string $username = null, ?string $password = null, array $options = array() ) { unset( $dsn, $username, $password ); - $this->connection = new WP_SQLite_Connection( $options['pdo'] ); + $this->connection = new WP_SQLite_Connection( $options['sqlite_pdo'] ); } public function get_connection(): WP_SQLite_Connection { return $this->connection; } public function get_insert_id(): int { return (int) $this->connection->get_pdo()->lastInsertId(); } diff --git a/tests/smoke-state-root.php b/tests/smoke-state-root.php index 78a4218..8cb3f61 100644 --- a/tests/smoke-state-root.php +++ b/tests/smoke-state-root.php @@ -39,7 +39,7 @@ class WP_MySQL_On_SQLite { public function __construct( string $dsn, ?string $username = null, ?string $password = null, array $options = array() ) { unset( $dsn, $username, $password ); - $this->connection = new WP_SQLite_Connection( $options['pdo'] ); + $this->connection = new WP_SQLite_Connection( $options['sqlite_pdo'] ); } public function get_connection(): WP_SQLite_Connection { @@ -139,7 +139,7 @@ function mdi_state_schema( PDO $pdo ): void { $pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION ); mdi_state_schema( $pdo ); $connection = new WP_SQLite_Connection( $pdo ); -$driver = new WP_MySQL_On_SQLite( 'mysql-on-sqlite:dbname=wordpress', null, null, array( 'pdo' => $pdo ) ); +$driver = new WP_MySQL_On_SQLite( 'mysql-on-sqlite:dbname=wordpress', null, null, array( 'sqlite_pdo' => $pdo ) ); $storage = new WP_Markdown_Storage( $content_dir ); $loader = new WP_Markdown_Loader( $content_dir, $driver, $storage, 'wp_', $state_dir ); $pdo->exec( "INSERT INTO wp_terms (term_id, slug) VALUES (9, 'guides')" ); diff --git a/tests/smoke-streaming-table-persistence.php b/tests/smoke-streaming-table-persistence.php index 5abbd40..4ffa8ef 100644 --- a/tests/smoke-streaming-table-persistence.php +++ b/tests/smoke-streaming-table-persistence.php @@ -43,7 +43,8 @@ class MDI_Streaming_SQLite_Driver extends PDO { public function __construct( string $dsn, ?string $username = null, ?string $password = null, array $options = array() ) { unset( $dsn, $username, $password ); - $this->connection = new WP_SQLite_Connection( $options['pdo'] ); + $pdo_option = class_exists( 'WP_PDO_MySQL_On_SQLite', false ) ? 'pdo' : 'sqlite_pdo'; + $this->connection = new WP_SQLite_Connection( $options[ $pdo_option ] ); } #[ReturnTypeWillChange] From 7a19d60634f344bf66f8215885b158ea6a636e45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Jakes=CC=8C?= Date: Tue, 6 Oct 2026 13:49:45 +0200 Subject: [PATCH 2/2] Support SQLite storage discovery and DB_PATH Detect configured paths, managed storage metadata, and legacy layouts without creating or migrating a database during backend selection. Resolve configured paths before integration defaults are loaded, and initialize managed storage once in the drop-in. Mirror mode uses the resolved path for both the connection and directory protection. Keep primary index reconstruction and stock fallback configuration intact. Cover path precedence, neighboring legacy files, randomized storage, backend overrides, primary indexes, and installation fallbacks with current and older integrations. Related: https://github.com/WordPress/sqlite-database-integration/pull/502 Related: https://github.com/WordPress/sqlite-database-integration/pull/512 --- README.md | 8 +- db.php | 153 +++++++++++------ inc/class-wp-markdown-db.php | 11 +- tests/probe-sqlite-fallback.php | 201 +++++++++++++++++++++++ tests/probe-sqlite-path-discovery.php | 136 ++++++++++++--- tests/smoke-dropin-backend-selection.php | 111 +++++++++++-- 6 files changed, 529 insertions(+), 91 deletions(-) create mode 100644 tests/probe-sqlite-fallback.php diff --git a/README.md b/README.md index 70e1e65..829b6ed 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,11 @@ and the [CLI-first benchmark requirements](tests/bench/README.md#priorities-and- MDI selects its backend in `wp-content/db.php` before plugins load: - An explicit `MARKDOWN_DB_BACKEND` configuration always wins. -- Without one, an existing SQLite database (`FQDB`, when it names a file, or - `wp-content/database/.ht.sqlite`) keeps the SQLite backend so an upgrade does - not change an operating site's query engine. +- Without one, an existing SQLite database or managed `db-path.php` file keeps + the SQLite backend so an upgrade does not change an operating site's query + engine. Discovery honors `DB_PATH` before legacy settings and recognizes + legacy `.ht.sqlite` and `.ht.sqlite.php` files. It does not create or migrate + a database. - Otherwise, MDI selects **`mdi-native`**, where canonical Markdown and JSON files are the database. diff --git a/db.php b/db.php index a4441ed..287645f 100644 --- a/db.php +++ b/db.php @@ -80,10 +80,36 @@ function markdown_database_integration_configured_backend(): string { if ( defined( 'MARKDOWN_DB_BACKEND' ) ) { return (string) MARKDOWN_DB_BACKEND; } - if ( defined( 'FQDB' ) && is_file( FQDB ) ) { + if ( defined( 'DB_PATH' ) || defined( 'FQDB' ) ) { + $path = defined( 'DB_PATH' ) ? DB_PATH : FQDB; + return is_string( $path ) && is_file( $path ) ? 'sqlite' : 'mdi-native'; + } + + $directory = markdown_database_integration_sqlite_directory(); + if ( defined( 'DB_FILE' ) ) { + return is_file( $directory . DB_FILE ) ? 'sqlite' : 'mdi-native'; + } + + // Handle the randomized secret database path. + if ( is_file( $directory . 'db-path.php' ) ) { return 'sqlite'; } - return is_file( __DIR__ . '/database/.ht.sqlite' ) ? 'sqlite' : 'mdi-native'; + return is_file( $directory . '.ht.sqlite' ) || is_file( $directory . '.ht.sqlite.php' ) ? 'sqlite' : 'mdi-native'; + } +} + +if ( ! function_exists( 'markdown_database_integration_sqlite_directory' ) ) { + /** Resolve the SQLite storage directory before integration constants load. */ + function markdown_database_integration_sqlite_directory(): string { + if ( defined( 'FQDBDIR' ) ) { + $directory = FQDBDIR; + } elseif ( defined( 'DB_DIR' ) ) { + $directory = DB_DIR; + } else { + $directory = defined( 'WP_CONTENT_DIR' ) ? WP_CONTENT_DIR : __DIR__; + $directory .= '/database'; + } + return rtrim( $directory, '/\\' ) . '/'; } } @@ -326,12 +352,13 @@ function markdown_database_integration_primary_index_path( string $content_dir, define( 'WP_SQLITE_AST_DRIVER', true ); } -// Primary mode uses markdown-index.sqlite as the active query engine. The -// SQLite Integration install shim opens FQDB directly during wp_install(). Only -// point FQDB at the markdown index when the markdown store already represents an +// Primary mode uses markdown-index.sqlite as the active query engine. +// Only point the SQLite installation shim at the index when the store represents an // installed site. Partial seed stores fall back to the existing SQLite database // so already-installed Playground sites do not reset to the install screen. -if ( defined( 'MARKDOWN_DB_MODE' ) && 'primary' === MARKDOWN_DB_MODE ) { +$markdown_db_primary = defined( 'MARKDOWN_DB_MODE' ) && 'primary' === MARKDOWN_DB_MODE; +$markdown_db_install_fallback = false; +if ( $markdown_db_primary ) { $markdown_db_content_dir = defined( 'MARKDOWN_DB_CONTENT_DIR' ) ? MARKDOWN_DB_CONTENT_DIR : markdown_db_default_content_dir(); @@ -353,6 +380,37 @@ function markdown_database_integration_primary_index_path( string $content_dir, if ( ! defined( 'FQDB' ) ) { define( 'FQDB', MARKDOWN_DB_INDEX_PATH ); } + } else { + $markdown_db_install_fallback = true; + if ( ! defined( 'MARKDOWN_DB_INSTALL_FALLBACK' ) ) { + define( 'MARKDOWN_DB_INSTALL_FALLBACK', true ); + } + } +} + +// Select configured paths before older integrations define FQDB by default. +$markdown_db_sqlite_storage_file = $sqlite_plugin_implementation_folder_path . '/wp-includes/sqlite/class-wp-sqlite-storage.php'; +$markdown_db_sqlite_path = null; +if ( defined( 'DB_PATH' ) ) { + $markdown_db_sqlite_path = DB_PATH; +} elseif ( defined( 'FQDB' ) ) { + $markdown_db_sqlite_path = FQDB; +} elseif ( defined( 'DB_FILE' ) ) { + $markdown_db_sqlite_path = markdown_database_integration_sqlite_directory() . DB_FILE; +} elseif ( ! is_file( $markdown_db_sqlite_storage_file ) ) { + $markdown_db_sqlite_path = markdown_database_integration_sqlite_directory() . '.ht.sqlite'; + if ( ! is_file( $markdown_db_sqlite_path ) && is_file( $markdown_db_sqlite_path . '.php' ) ) { + $markdown_db_sqlite_path .= '.php'; + } +} + +// Older integrations read the legacy constants instead of DB_PATH. +if ( null !== $markdown_db_sqlite_path ) { + if ( ! defined( 'FQDB' ) ) { + define( 'FQDB', $markdown_db_sqlite_path ); + } + if ( ! defined( 'FQDBDIR' ) && ':memory:' !== $markdown_db_sqlite_path ) { + define( 'FQDBDIR', rtrim( dirname( $markdown_db_sqlite_path ), '/\\' ) . '/' ); } } @@ -368,6 +426,11 @@ function markdown_database_integration_primary_index_path( string $content_dir, // Load the SQLite v2 driver stack (parser, lexer, connection, driver). require_once $sqlite_plugin_implementation_folder_path . '/wp-includes/database/load.php'; +// Load SQLite storage support when available. +if ( is_file( $markdown_db_sqlite_storage_file ) ) { + require_once $markdown_db_sqlite_storage_file; +} + // SQLite Integration renamed the canonical PDO driver after its latest release. if ( ! class_exists( 'WP_MySQL_On_SQLite', false ) ) { if ( class_exists( 'WP_PDO_MySQL_On_SQLite', false ) ) { @@ -386,24 +449,6 @@ class_alias( 'WP_PDO_MySQL_On_SQLite', 'WP_MySQL_On_SQLite' ); require_once $sqlite_plugin_implementation_folder_path . '/wp-includes/sqlite/class-wp-sqlite-db.php'; require_once $sqlite_plugin_implementation_folder_path . '/wp-includes/sqlite/install-functions.php'; -if ( defined( 'MARKDOWN_DB_MODE' ) && 'primary' === MARKDOWN_DB_MODE ) { - $markdown_db_content_dir = defined( 'MARKDOWN_DB_CONTENT_DIR' ) - ? MARKDOWN_DB_CONTENT_DIR - : markdown_db_default_content_dir(); - $markdown_db_state_dir = defined( 'MARKDOWN_DB_STATE_DIR' ) - ? MARKDOWN_DB_STATE_DIR - : $markdown_db_content_dir; - - if ( ! markdown_database_integration_store_has_siteurl( $markdown_db_state_dir ) ) { - if ( ! defined( 'MARKDOWN_DB_INSTALL_FALLBACK' ) ) { - define( 'MARKDOWN_DB_INSTALL_FALLBACK', true ); - } - $db_name = defined( 'DB_NAME' ) && '' !== DB_NAME ? DB_NAME : 'database_name_here'; - $GLOBALS['wpdb'] = new WP_SQLite_DB( $db_name ); - return; - } -} - // Load our markdown classes. $markdown_plugin_dir = null; @@ -420,32 +465,46 @@ class_alias( 'WP_PDO_MySQL_On_SQLite', 'WP_MySQL_On_SQLite' ); } } -// If the markdown plugin isn't installed yet, fall back to standard SQLite. -if ( ! $markdown_plugin_dir ) { - // Fallback: standard SQLite behavior. - if ( defined( 'DB_NAME' ) && '' !== DB_NAME ) { - $db_name = DB_NAME; - } else { - $db_name = 'database_name_here'; +$markdown_db_sqlite_fallback = $markdown_db_install_fallback || ! $markdown_plugin_dir; + +if ( ! $markdown_db_sqlite_fallback ) { + // MDI is storage-only; content-format dependencies belong to the application. + $composer_autoload = $markdown_plugin_dir . '/vendor/autoload.php'; + if ( file_exists( $composer_autoload ) ) { + require_once $composer_autoload; } - $GLOBALS['wpdb'] = new WP_SQLite_DB( $db_name ); - return; + + require_once $markdown_plugin_dir . '/inc/class-wp-markdown-backend-capabilities.php'; + $markdown_db_backend = WP_Markdown_Backend_Resolver::configure_from_globals(); + WP_Markdown_Backend_Resolver::require_runtime_capabilities( + $markdown_db_backend, + defined( 'MARKDOWN_DB_MODE' ) ? (string) MARKDOWN_DB_MODE : 'mirror' + ); +} + +// Primary mode creates its index during reconstruction, after storage is loaded. +if ( class_exists( 'WP_SQLite_Storage' ) && ( ! $markdown_db_primary || $markdown_db_sqlite_fallback ) ) { + $storage = null !== $markdown_db_sqlite_path + ? WP_SQLite_Storage::with_explicit_path( $markdown_db_sqlite_path ) + : WP_SQLite_Storage::with_secret_path( FQDBDIR ); + $markdown_db_sqlite_path = $storage->initialize(); + if ( ! defined( 'FQDB' ) ) { + define( 'FQDB', $markdown_db_sqlite_path ); + } +} +if ( ! defined( 'DB_PATH' ) ) { + define( 'DB_PATH', $markdown_db_sqlite_path ); } -// Load composer autoloader if present. MDI is storage-only; content-format -// dependencies belong to the application layer above this drop-in. -$composer_autoload = $markdown_plugin_dir . '/vendor/autoload.php'; -if ( file_exists( $composer_autoload ) ) { - require_once $composer_autoload; +$db_name = defined( 'DB_NAME' ) && '' !== DB_NAME ? DB_NAME : 'database_name_here'; + +// Use standard SQLite for installation or when the markdown plugin is missing. +if ( $markdown_db_sqlite_fallback ) { + $GLOBALS['wpdb'] = new WP_SQLite_DB( $db_name ); + return; } // Load markdown integration classes. -require_once $markdown_plugin_dir . '/inc/class-wp-markdown-backend-capabilities.php'; -$markdown_db_backend = WP_Markdown_Backend_Resolver::configure_from_globals(); -WP_Markdown_Backend_Resolver::require_runtime_capabilities( - $markdown_db_backend, - defined( 'MARKDOWN_DB_MODE' ) ? (string) MARKDOWN_DB_MODE : 'mirror' -); require_once $markdown_plugin_dir . '/inc/class-wp-markdown-frontmatter-profiles.php'; require_once $markdown_plugin_dir . '/inc/class-wp-markdown-content-layout-profiles.php'; if ( defined( 'MARKDOWN_DB_CONTENT_LAYOUT_PROFILE_BOOTSTRAP' ) && is_file( MARKDOWN_DB_CONTENT_LAYOUT_PROFILE_BOOTSTRAP ) ) { @@ -466,12 +525,6 @@ class_alias( 'WP_PDO_MySQL_On_SQLite', 'WP_MySQL_On_SQLite' ); } // Create the database connection — our WP_Markdown_DB extends WP_SQLite_DB. -if ( defined( 'DB_NAME' ) && '' !== DB_NAME ) { - $db_name = DB_NAME; -} else { - $db_name = 'database_name_here'; -} - $GLOBALS['wpdb'] = new WP_Markdown_DB( $db_name ); markdown_database_integration_enable_native_shadow( $GLOBALS['wpdb'], $markdown_plugin_dir ); diff --git a/inc/class-wp-markdown-db.php b/inc/class-wp-markdown-db.php index 039a6ff..b94a252 100644 --- a/inc/class-wp-markdown-db.php +++ b/inc/class-wp-markdown-db.php @@ -128,8 +128,8 @@ public function db_connect( $allow_bail = true ) { $pdo = null; } else { // Phase 1 / mirror: use on-disk SQLite file. - $db_path = FQDB; - $this->ensure_database_directory_exists(); + $db_path = defined( 'DB_PATH' ) ? DB_PATH : FQDB; + $this->ensure_database_directory_exists( $db_path ); } // Create the markdown storage engine. @@ -674,8 +674,11 @@ private function ensure_directory_exists( string $dir ): void { /** * Ensure the SQLite database directory exists with protections. */ - private function ensure_database_directory_exists(): void { - $dir = dirname( FQDB ); + private function ensure_database_directory_exists( string $db_path ): void { + if ( ':memory:' === $db_path ) { + return; + } + $dir = dirname( $db_path ); $this->ensure_directory_exists( $dir ); // .htaccess protection. diff --git a/tests/probe-sqlite-fallback.php b/tests/probe-sqlite-fallback.php new file mode 100644 index 0000000..8e01da4 --- /dev/null +++ b/tests/probe-sqlite-fallback.php @@ -0,0 +1,201 @@ + array( 'FQDB', 'custom.sqlite', false ), + 'default-fqdb' => array( 'FQDB', '.ht.sqlite', false ), + 'existing-fqdb' => array( 'FQDB', 'custom.sqlite', true ), + 'memory-fqdb' => array( 'FQDB', ':memory:', false ), + 'new-db-path' => array( 'DB_PATH', 'custom.sqlite', false ), + 'default-db-path' => array( 'DB_PATH', '.ht.sqlite', false ), + 'existing-db-path' => array( 'DB_PATH', 'custom.sqlite', true ), + 'memory-db-path' => array( 'DB_PATH', ':memory:', false ), + 'conflicting-paths' => array( 'DB_PATH', 'custom.sqlite', false ), + 'new-db-file' => array( 'DB_FILE', 'custom.sqlite', false ), + 'default-db-file' => array( 'DB_FILE', '.ht.sqlite', false ), + 'existing-db-file' => array( 'DB_FILE', 'custom.sqlite', true ), +); + +if ( 3 === $argc ) { + $failed = 0; + foreach ( array( 'primary', 'missing-plugin', 'mirror' ) as $fallback ) { + $scenarios = 'mirror' === $fallback ? array_merge( array_keys( $configured_paths ), array( 'unknown-backend' ) ) : array( 'missing-name', 'empty-name', 'engine-override' ); + $layouts = array( 'mirror' === $fallback ? 'legacy-php' : 'legacy' ); + if ( $managed ) { + $layouts[] = 'randomized'; + } + foreach ( $scenarios as $scenario ) { + foreach ( $layouts as $layout ) { + $command = array( PHP_BINARY, __FILE__, $wordpress, $sqlite, $fallback, $scenario, $layout ); + passthru( implode( ' ', array_map( 'escapeshellarg', $command ) ), $status ); + $failed += 0 === $status ? 0 : 1; + } + } + } + exit( $failed ? 1 : 0 ); +} + +[ , , , $fallback, $scenario, $layout ] = $argv; +$mirror = 'mirror' === $fallback; +$root = sys_get_temp_dir() . '/mdi-sqlite-fallback-' . getmypid(); +$content = $root . '/wp-content'; +mkdir( $content . '/plugins', 0755, true ); +register_shutdown_function( static function () use ( $root ): void { + $files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $root, FilesystemIterator::SKIP_DOTS ), RecursiveIteratorIterator::CHILD_FIRST ); + foreach ( $files as $file ) { + $file->isDir() && ! $file->isLink() ? rmdir( $file->getPathname() ) : unlink( $file->getPathname() ); + } + rmdir( $root ); +} ); +if ( 'missing-plugin' !== $fallback ) { + symlink( dirname( __DIR__ ), $content . '/plugins/markdown-database-integration' ); +} +symlink( $sqlite, $content . '/plugins/sqlite-database-integration' ); +copy( dirname( __DIR__ ) . '/db.php', $content . '/db.php' ); +chdir( $root ); + +define( 'ABSPATH', $wordpress . '/' ); +define( 'WPINC', 'wp-includes' ); +define( 'WP_CONTENT_DIR', $content ); +define( 'WP_DEBUG', false ); +define( 'MARKDOWN_DB_VERSION', 'test' ); +define( 'MARKDOWN_DB_BACKEND', 'unknown-backend' === $scenario ? 'unknown' : 'sqlite' ); +define( 'MARKDOWN_DB_MODE', 'primary' === $fallback ? 'primary' : 'mirror' ); +if ( 'missing-name' !== $scenario ) { + define( 'DB_NAME', 'empty-name' === $scenario ? '' : 'wordpress' ); +} +if ( 'engine-override' === $scenario ) { + define( 'DB_ENGINE', 'mysql' ); +} +$GLOBALS['table_prefix'] = 'wp_'; + +$directory = $content . '/database'; +if ( 'randomized' === $layout ) { + require_once $sqlite . '/wp-includes/sqlite/class-wp-sqlite-storage.php'; + $expected_path = WP_SQLite_Storage::with_secret_path( $directory )->initialize(); +} else { + mkdir( $directory, 0755 ); + $expected_path = $directory . ( 'legacy-php' === $layout ? '/.ht.sqlite.php' : '/.ht.sqlite' ); +} +$seed = new PDO( 'sqlite:' . $expected_path ); +$seed->exec( 'CREATE TABLE fallback_probe (value TEXT)' ); +$seed->exec( "INSERT INTO fallback_probe VALUES ('existing database')" ); +$seed = null; + +$expected_value = 'existing database'; +if ( $mirror ) { + $neighbor_path = $expected_path; + $neighbor_hash = hash_file( 'sha256', $neighbor_path ); + $directory_before = scandir( $directory ); + $metadata_hash = $managed && 'randomized' === $layout ? hash_file( 'sha256', $directory . '/db-path.php' ) : null; +} +if ( isset( $configured_paths[ $scenario ] ) ) { + [ $constant, $filename, $existing ] = $configured_paths[ $scenario ]; + $expected_path = ':memory:' === $filename ? $filename : $directory . '/' . $filename; + define( $constant, 'DB_FILE' === $constant ? $filename : $expected_path ); + if ( 'conflicting-paths' === $scenario ) { + define( 'FQDB', $neighbor_path ); + } + $expected_value = null; + if ( $existing ) { + $seed = new PDO( 'sqlite:' . $expected_path ); + $seed->exec( 'CREATE TABLE fallback_probe (value TEXT)' ); + $seed->exec( "INSERT INTO fallback_probe VALUES ('configured database')" ); + $seed = null; + $expected_value = 'configured database'; + } +} + +foreach ( array( 'compat.php', 'plugin.php', 'load.php', 'formatting.php', 'class-wpdb.php' ) as $file ) { + require_once ABSPATH . WPINC . '/' . $file; +} +set_error_handler( static function ( int $severity, string $message, string $file, int $line ) use ( $scenario ): bool { + if ( ! ( error_reporting() & $severity ) ) { + return false; + } + if ( 'conflicting-paths' === $scenario && E_USER_WARNING === $severity && 'FQDB conflicts with DB_PATH.' === $message ) { + return true; + } + throw new ErrorException( $message, 0, $severity, $file, $line ); +} ); +try { + require $content . '/db.php'; +} catch ( WP_Markdown_Unknown_Backend $error ) { + if ( 'unknown-backend' !== $scenario ) { + throw $error; + } + if ( $directory_before !== scandir( $directory ) || $neighbor_hash !== hash_file( 'sha256', $neighbor_path ) ) { + throw new RuntimeException( 'An unsupported backend changed SQLite storage.' ); + } + echo "PASS: {$layout} storage is unchanged when backend selection fails.\n"; + exit( 0 ); +} +restore_error_handler(); +if ( 'unknown-backend' === $scenario ) { + throw new RuntimeException( 'An unsupported backend did not fail.' ); +} + +$wpdb = $GLOBALS['wpdb'] ?? null; +$expected_class = $mirror ? 'WP_Markdown_DB' : 'WP_SQLite_DB'; +if ( ! $wpdb instanceof WP_SQLite_DB || $expected_class !== get_class( $wpdb ) || ! $wpdb->ready ) { + throw new RuntimeException( 'The selected SQLite database did not connect.' ); +} +if ( 'primary' === $fallback && ! MARKDOWN_DB_INSTALL_FALLBACK ) { + throw new RuntimeException( 'Primary mode did not select the installation fallback.' ); +} +$expected_name = $mirror || 'engine-override' === $scenario ? 'wordpress' : 'database_name_here'; +if ( $expected_name !== $wpdb->get_var( 'SELECT DATABASE()' ) ) { + throw new RuntimeException( 'The fallback used the wrong database name.' ); +} +if ( $managed && ! $mirror ) { + $expected_path = include $directory . '/db-path.php'; +} +$pdo = $GLOBALS['@pdo']; +$active_path = $pdo->query( 'PRAGMA database_list' )->fetch( PDO::FETCH_ASSOC )['file']; +if ( ':memory:' === $expected_path ) { + if ( '' !== $active_path || is_file( $root . '/.htaccess' ) || is_file( $root . '/index.php' ) ) { + throw new RuntimeException( 'In-memory storage created filesystem artifacts.' ); + } +} else { + if ( realpath( $expected_path ) !== realpath( $active_path ) ) { + throw new RuntimeException( 'Opened the wrong database: ' . $active_path ); + } + if ( ! is_file( dirname( $active_path ) . '/.htaccess' ) || ! is_file( dirname( $active_path ) . '/index.php' ) ) { + throw new RuntimeException( 'The active database directory lacks protections.' ); + } +} +if ( null === $expected_value ) { + if ( 0 !== (int) $pdo->query( "SELECT COUNT(*) FROM sqlite_master WHERE name = 'fallback_probe'" )->fetchColumn() ) { + throw new RuntimeException( 'A new configured database inherited neighboring data.' ); + } +} elseif ( $expected_value !== $pdo->query( 'SELECT value FROM fallback_probe' )->fetchColumn() ) { + throw new RuntimeException( 'The selected database content was not preserved.' ); +} +if ( $mirror ) { + if ( DB_PATH !== $expected_path || $neighbor_hash !== hash_file( 'sha256', $neighbor_path ) ) { + throw new RuntimeException( 'The configured path changed or neighboring storage was modified.' ); + } + if ( null !== $metadata_hash && $metadata_hash !== hash_file( 'sha256', $directory . '/db-path.php' ) ) { + throw new RuntimeException( 'An explicit path changed managed storage metadata.' ); + } +} + +echo "PASS: {$fallback}/{$scenario}/{$layout} uses the selected SQLite storage.\n"; diff --git a/tests/probe-sqlite-path-discovery.php b/tests/probe-sqlite-path-discovery.php index bcc099a..ab300ed 100644 --- a/tests/probe-sqlite-path-discovery.php +++ b/tests/probe-sqlite-path-discovery.php @@ -18,19 +18,29 @@ if ( ! $wordpress || ! is_file( $wordpress . '/wp-includes/class-wpdb.php' ) || ! $sqlite || ! is_file( $sqlite . '/constants.php' ) ) { throw new RuntimeException( 'WordPress or SQLite integration source is missing.' ); } +$managed = is_file( $sqlite . '/wp-includes/sqlite/class-wp-sqlite-storage.php' ); if ( 3 === $argc ) { - $scenarios = array( 'fqdb' ); $failed = 0; - foreach ( $scenarios as $scenario ) { - $command = array( PHP_BINARY, __FILE__, $wordpress, $sqlite, $scenario ); - passthru( implode( ' ', array_map( 'escapeshellarg', $command ) ), $status ); - $failed += 0 === $status ? 0 : 1; + foreach ( array( 'mirror', 'primary' ) as $mode ) { + $scenarios = array( 'legacy', 'legacy-php', 'fqdb', 'db-file', 'db-path', 'new-db-path' ); + if ( 'mirror' === $mode ) { + $scenarios = array_merge( $scenarios, array( 'conflicting-paths', 'both-paths', 'memory' ) ); + } + if ( $managed ) { + $scenarios = array_merge( $scenarios, array( 'randomized', 'custom-managed', 'fresh-managed', 'missing-recorded' ) ); + } + foreach ( $scenarios as $scenario ) { + $command = array( PHP_BINARY, __FILE__, $wordpress, $sqlite, $scenario, $mode ); + passthru( implode( ' ', array_map( 'escapeshellarg', $command ) ), $status ); + $failed += 0 === $status ? 0 : 1; + } } exit( $failed ? 1 : 0 ); } $scenario = $argv[3]; +$mode = $argv[4] ?? 'mirror'; $root = sys_get_temp_dir() . '/mdi-sqlite-path-' . $scenario . '-' . getmypid(); $content = $root . '/wp-content'; mkdir( $content . '/plugins', 0755, true ); @@ -52,16 +62,73 @@ define( 'WP_DEBUG', false ); define( 'DB_NAME', 'wordpress' ); define( 'MARKDOWN_DB_VERSION', 'test' ); -define( 'MARKDOWN_DB_MODE', 'mirror' ); +define( 'MARKDOWN_DB_MODE', $mode ); $GLOBALS['table_prefix'] = 'wp_'; -$expected_path = $root . '/configured/legacy.sqlite'; -define( 'FQDB', $expected_path ); -mkdir( dirname( $expected_path ), 0755, true ); -$seed = new PDO( 'sqlite:' . $expected_path ); -$seed->exec( 'CREATE TABLE path_probe (value TEXT)' ); -$seed->exec( "INSERT INTO path_probe VALUES ('configured database')" ); -$seed = null; +$database_root = $content . '/database'; +$expected_path = $database_root . '/.ht.sqlite'; +switch ( $scenario ) { + case 'fqdb': + $expected_path = $root . '/configured/legacy.sqlite'; + define( 'FQDB', $expected_path ); + break; + case 'db-file': + $database_root = $root . '/configured'; + $expected_path = $database_root . '/custom.sqlite'; + define( 'DB_DIR', $database_root ); + define( 'DB_FILE', 'custom.sqlite' ); + break; + case 'db-path': + case 'conflicting-paths': + case 'both-paths': + case 'new-db-path': + $expected_path = $root . '/configured/active.sqlite'; + define( 'DB_PATH', $expected_path ); + if ( in_array( $scenario, array( 'conflicting-paths', 'both-paths' ), true ) ) { + define( 'FQDB', $root . '/unused/wrong.sqlite' ); + define( 'FQDBDIR', $root . '/unused/' ); + if ( 'both-paths' === $scenario ) { + mkdir( dirname( FQDB ), 0755 ); + $wrong = new PDO( 'sqlite:' . FQDB ); + $wrong->exec( 'CREATE TABLE wrong_database (value TEXT)' ); + $wrong = null; + $wrong_hash = hash_file( 'sha256', FQDB ); + } + } + break; + case 'memory': + $expected_path = ':memory:'; + define( 'DB_PATH', $expected_path ); + break; + case 'legacy-php': + $expected_path .= '.php'; + break; + case 'custom-managed': + $database_root = $root . '/configured'; + define( 'DB_DIR', $database_root ); + // Fall through to create real managed storage. + case 'randomized': + case 'missing-recorded': + require_once $sqlite . '/wp-includes/sqlite/class-wp-sqlite-storage.php'; + $expected_path = WP_SQLite_Storage::with_secret_path( $database_root )->initialize(); + if ( 'missing-recorded' === $scenario ) { + unlink( $expected_path ); + } + break; +} + +$existing = ! in_array( $scenario, array( 'new-db-path', 'memory', 'fresh-managed', 'missing-recorded' ), true ); +if ( $existing ) { + if ( ! is_dir( dirname( $expected_path ) ) ) { + mkdir( dirname( $expected_path ), 0755, true ); + } + $seed = new PDO( 'sqlite:' . $expected_path ); + $seed->exec( 'CREATE TABLE path_probe (value TEXT)' ); + $seed->exec( "INSERT INTO path_probe VALUES ('configured database')" ); + $seed = null; +} elseif ( 'missing-recorded' !== $scenario ) { + define( 'MARKDOWN_DB_BACKEND', 'sqlite' ); +} require_once ABSPATH . WPINC . '/compat.php'; require_once ABSPATH . WPINC . '/plugin.php'; @@ -73,21 +140,52 @@ if ( ! ( error_reporting() & $severity ) ) { return false; } + if ( E_USER_WARNING === $severity && in_array( $message, array( 'FQDB conflicts with DB_PATH.', 'FQDBDIR conflicts with DB_PATH.' ), true ) ) { + return true; + } throw new ErrorException( $message, 0, $severity, $file, $line ); } ); require $content . '/db.php'; restore_error_handler(); -if ( 'sqlite' !== MARKDOWN_DB_BACKEND || ! $GLOBALS['wpdb'] instanceof WP_Markdown_DB || ! $GLOBALS['wpdb']->ready ) { - throw new RuntimeException( 'The SQLite mirror did not connect.' ); +if ( 'sqlite' !== MARKDOWN_DB_BACKEND || ! $GLOBALS['wpdb'] instanceof WP_SQLite_DB || ! $GLOBALS['wpdb']->ready ) { + throw new RuntimeException( 'The SQLite database did not connect: ' . $GLOBALS['wpdb']->last_error ); +} +if ( 'primary' === $mode ) { + if ( ! defined( 'MARKDOWN_DB_INSTALL_FALLBACK' ) || $GLOBALS['wpdb'] instanceof WP_Markdown_DB ) { + throw new RuntimeException( 'Primary mode did not use the SQLite installation fallback.' ); + } +} elseif ( ! $GLOBALS['wpdb'] instanceof WP_Markdown_DB ) { + throw new RuntimeException( 'Mirror mode did not use the Markdown database.' ); } $pdo = $GLOBALS['@pdo']; $active_path = $pdo->query( 'PRAGMA database_list' )->fetch( PDO::FETCH_ASSOC )['file']; -if ( realpath( $expected_path ) !== realpath( $active_path ) ) { - throw new RuntimeException( 'Mirror opened the wrong database: ' . json_encode( array( 'expected' => $expected_path, 'databases' => $pdo->query( 'PRAGMA database_list' )->fetchAll( PDO::FETCH_ASSOC ) ) ) ); +if ( 'memory' === $scenario ) { + if ( '' !== $active_path || is_dir( $database_root ) || is_file( getcwd() . '/.htaccess' ) ) { + throw new RuntimeException( 'In-memory storage created filesystem artifacts.' ); + } +} else { + if ( $managed && in_array( $scenario, array( 'legacy', 'legacy-php', 'fresh-managed' ), true ) ) { + $expected_path = include $database_root . '/db-path.php'; + } elseif ( 'primary' === $mode && 'legacy-php' === $scenario ) { + $expected_path = FQDB; + } + if ( realpath( $expected_path ) !== realpath( $active_path ) ) { + throw new RuntimeException( 'Opened the wrong database: ' . json_encode( array( 'expected' => $expected_path, 'databases' => $pdo->query( 'PRAGMA database_list' )->fetchAll( PDO::FETCH_ASSOC ) ) ) ); + } + if ( ! is_file( dirname( $active_path ) . '/.htaccess' ) || ! is_file( dirname( $active_path ) . '/index.php' ) ) { + throw new RuntimeException( 'The active database directory lacks protections.' ); + } } -if ( 'configured database' !== $pdo->query( 'SELECT value FROM path_probe' )->fetchColumn() ) { +if ( $existing && 'configured database' !== $pdo->query( 'SELECT value FROM path_probe' )->fetchColumn() ) { throw new RuntimeException( 'The existing database content was not preserved.' ); } +if ( 'both-paths' === $scenario ) { + if ( $wrong_hash !== hash_file( 'sha256', FQDB ) || is_file( dirname( FQDB ) . '/.htaccess' ) || is_file( dirname( FQDB ) . '/index.php' ) ) { + throw new RuntimeException( 'Mirror changed the conflicting FQDB storage.' ); + } +} elseif ( is_dir( $root . '/unused' ) ) { + throw new RuntimeException( 'Mirror created the conflicting FQDB directory.' ); +} -echo "PASS: the SQLite driver retains the supplied database connection.\n"; +echo "PASS: {$mode}/{$scenario} connects to the selected database and protects its directory.\n"; diff --git a/tests/smoke-dropin-backend-selection.php b/tests/smoke-dropin-backend-selection.php index 599f96b..8182922 100644 --- a/tests/smoke-dropin-backend-selection.php +++ b/tests/smoke-dropin-backend-selection.php @@ -105,6 +105,80 @@ define( 'DB_PASSWORD', 'password' ); define( 'DB_HOST', 'host' ); define( 'MARKDOWN_DB_VERSION', 'test' ); + if ( in_array( $scenario, array( 'primary-cold-index', 'primary-warm-index' ), true ) ) { + define( 'MARKDOWN_DB_BACKEND', 'sqlite' ); + define( 'MARKDOWN_DB_MODE', 'primary' ); + mkdir( $content . '/markdown/_options', 0755 ); + file_put_contents( $content . '/markdown/_options/siteurl.json', '"https://example.test"' ); + if ( 'primary-warm-index' === $scenario ) { + file_put_contents( $content . '/markdown-index.sqlite', 'existing primary index' ); + } + file_put_contents( $sqlite . '/wp-includes/sqlite/class-wp-sqlite-storage.php', ' 'FQDB', + 'default-db-file' => 'DB_FILE', + default => 'DB_PATH', + }; + define( $constant, 'DB_FILE' === $constant ? 'custom.sqlite' : $database_dir . '/custom.sqlite' ); + file_put_contents( $database_dir . '/custom.sqlite', 'configured database' ); + $expected_backend = 'sqlite'; + if ( 'default-conflicting-paths' === $scenario ) { + define( 'FQDB', $database_dir . '/missing.sqlite' ); + } + } + if ( in_array( $scenario, array( 'default-missing-db-path', 'default-missing-fqdb', 'default-missing-db-file' ), true ) ) { + $constant = match ( $scenario ) { + 'default-missing-fqdb' => 'FQDB', + 'default-missing-db-file' => 'DB_FILE', + default => 'DB_PATH', + }; + define( $constant, 'DB_FILE' === $constant ? 'missing.sqlite' : $database_dir . '/missing.sqlite' ); + if ( 'DB_PATH' === $constant ) { + define( 'FQDB', $database_dir . '/.ht.sqlite' ); + } + $expected_backend = 'mdi-native'; + } + $snapshot = static function () use ( $database_dir ): array { + $files = array( $database_dir => is_dir( $database_dir ) ); + if ( is_dir( $database_dir ) ) { + foreach ( new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $database_dir, FilesystemIterator::SKIP_DOTS ), RecursiveIteratorIterator::SELF_FIRST ) as $file ) { + $files[ $file->getPathname() ] = $file->isDir() ? 'directory' : hash_file( 'sha256', $file->getPathname() ); + } + } + return $files; + }; + $storage_before = $snapshot(); + if ( 'mysql-content' === $scenario ) { + define( 'MARKDOWN_DB_BACKEND', 'mysql-content' ); + } if ( 'unknown' === $scenario ) { define( 'MARKDOWN_DB_BACKEND', 'unknown' ); } elseif ( 'incomplete' === $scenario ) { @@ -112,18 +186,15 @@ $GLOBALS['markdown_db_backend_declarations'] = array( 'incomplete' => array() ); } elseif ( 'sqlite' === $scenario ) { define( 'MARKDOWN_DB_BACKEND', 'sqlite' ); - } elseif ( in_array( $scenario, array( 'mdi-native', 'default-native', 'default-legacy-sqlite' ), true ) ) { + } elseif ( in_array( $scenario, array( 'mdi-native', 'mysql-content' ), true ) || str_starts_with( $scenario, 'default-' ) ) { // The default scenarios leave MARKDOWN_DB_BACKEND undefined on purpose. if ( 'mdi-native' === $scenario ) { define( 'MARKDOWN_DB_BACKEND', 'mdi-native' ); } - if ( 'default-legacy-sqlite' === $scenario ) { - mkdir( $content . '/database', 0755, true ); - file_put_contents( $content . '/database/.ht.sqlite', 'SQLite format 3' ); - } define( 'MARKDOWN_DB_STATE_DIR', $content . '/markdown' ); define( 'MARKDOWN_DB_CONTENT_DIR', $content . '/markdown' ); class wpdb { + public bool $is_mysql = false; public string $prefix = ''; public string $base_prefix = ''; public array $last_result = array(); @@ -188,6 +259,16 @@ class wpdb { public string $prefix = 'wp_'; public string $base_prefix = 'wp_'; } }; $bootstrap(); + if ( in_array( $scenario, array( 'primary-cold-index', 'primary-warm-index' ), true ) ) { + if ( ! $GLOBALS['wpdb'] instanceof WP_Markdown_DB || DB_PATH !== $content . '/markdown-index.sqlite' || defined( 'MARKDOWN_DB_INSTALL_FALLBACK' ) ) { + throw new RuntimeException( 'An installed primary site did not use its index.' ); + } + if ( 'primary-cold-index' === $scenario ? file_exists( DB_PATH ) : 'existing primary index' !== file_get_contents( DB_PATH ) ) { + throw new RuntimeException( 'The drop-in created or changed the primary index.' ); + } + echo "PASS: {$scenario} leaves index initialization to primary mode.\n"; + exit( 0 ); + } if ( in_array( $scenario, array( 'mysql-full', 'mysql-full-shadow', 'mysql-full-no-sink', 'mysql-full-prior-outbox' ), true ) && $GLOBALS['wpdb'] instanceof WP_Markdown_MySQL_WPDB && ( $GLOBALS['markdown_db_mysql_outbox'] ?? null ) instanceof WP_Markdown_MySQL_Outbox ) { if ( 'mysql-full-prior-outbox' === $scenario && ! $GLOBALS['wpdb']->markdown_db_mysql_connection()->upgraded ) { throw new RuntimeException( 'Prior outbox schema was not upgraded.' ); } if ( 'mysql-full-shadow' === $scenario && ! ( $GLOBALS['markdown_db_native_shadow_verifier'] ?? null ) instanceof WP_Markdown_Native_Shadow_Verifier ) { throw new RuntimeException( 'Native shadow verifier was not attached.' ); } @@ -198,18 +279,18 @@ class wpdb { public string $prefix = 'wp_'; public string $base_prefix = 'wp_'; echo "PASS: mdi-native installs the filesystem-backed wpdb boundary.\n"; exit( 0 ); } - if ( 'default-native' === $scenario ) { - if ( ! $GLOBALS['wpdb'] instanceof WP_Markdown_Native_WPDB || 'mdi-native' !== MARKDOWN_DB_BACKEND ) { - throw new RuntimeException( 'An unconfigured install did not serve canonical files natively.' ); - } - echo "PASS: an unconfigured install serves canonical files without SQLite.\n"; + if ( $storage_before !== $snapshot() ) { + throw new RuntimeException( 'Backend detection changed SQLite storage.' ); + } + if ( 'mysql-content' === $scenario && $GLOBALS['wpdb'] instanceof wpdb && MARKDOWN_DB_RETAINED_DROPIN ) { + echo "PASS: explicit mysql-content overrides existing SQLite storage.\n"; exit( 0 ); } - if ( 'default-legacy-sqlite' === $scenario ) { - if ( $GLOBALS['wpdb'] instanceof WP_Markdown_Native_WPDB || 'sqlite' !== MARKDOWN_DB_BACKEND ) { - throw new RuntimeException( 'An existing SQLite database did not keep its query engine.' ); + if ( str_starts_with( $scenario, 'default-' ) ) { + if ( $expected_backend !== MARKDOWN_DB_BACKEND || ( 'mdi-native' === $expected_backend ) !== ( $GLOBALS['wpdb'] instanceof WP_Markdown_Native_WPDB ) ) { + throw new RuntimeException( 'Unexpected backend for ' . $scenario . ': ' . MARKDOWN_DB_BACKEND ); } - echo "PASS: an existing SQLite database keeps its query engine.\n"; + echo "PASS: {$scenario} selects {$expected_backend} without changing storage.\n"; exit( 0 ); } if ( in_array( $scenario, array( 'mysql-full-incompatible', 'mysql-full-outbox-incompatible' ), true ) && $GLOBALS['wpdb'] instanceof wpdb && str_starts_with( (string) ( $GLOBALS['markdown_db_mysql_full_diagnostic']['code'] ?? '' ), 'markdown_db_mysql_full_' ) ) { @@ -236,7 +317,7 @@ class wpdb { public string $prefix = 'wp_'; public string $base_prefix = 'wp_'; } $failed = 0; -foreach ( array( 'sqlite', 'default-native', 'default-legacy-sqlite', 'mdi-native', 'mysql-full', 'mysql-full-shadow', 'mysql-full-no-sink', 'mysql-full-prior-outbox', 'mysql-full-incompatible', 'mysql-full-outbox-incompatible', 'unknown', 'incomplete' ) as $scenario ) { +foreach ( array( 'sqlite', 'primary-cold-index', 'primary-warm-index', 'default-native', 'default-legacy-sqlite', 'default-legacy-php', 'default-db-dir', 'default-fqdbdir', 'default-randomized', 'default-missing-recorded', 'default-interrupted-migration', 'default-unread-metadata', 'default-db-path', 'default-fqdb', 'default-db-file', 'default-conflicting-paths', 'default-missing-db-path', 'default-missing-fqdb', 'default-missing-db-file', 'mdi-native', 'mysql-content', 'mysql-full', 'mysql-full-shadow', 'mysql-full-no-sink', 'mysql-full-prior-outbox', 'mysql-full-incompatible', 'mysql-full-outbox-incompatible', 'unknown', 'incomplete' ) as $scenario ) { passthru( escapeshellarg( PHP_BINARY ) . ' ' . escapeshellarg( __FILE__ ) . ' ' . escapeshellarg( $scenario ), $status ); if ( 0 !== $status ) { ++$failed;