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
69 changes: 55 additions & 14 deletions packages/plugin-sqlite-database-integration/constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,58 @@
}

/**
* Notice:
* Your scripts have the permission to create directories or files on your server.
* If you write in your wp-config.php like below, we take these definitions.
* define('DB_DIR', '/full_path_to_the_database_directory/');
* define('DB_FILE', 'database_file_name');
* DB_PATH is the absolute database file path, or ":memory:" for an in-memory database.
*
* Conflicting deprecated constants trigger a warning. DB_PATH takes precedence.
* The database directory is also used for storage locks and must be writable by PHP.
* When not configured, the drop-in defines DB_PATH after initializing the storage.
*
* Example: define( 'DB_PATH', '/private/wordpress/database.sqlite' );
*/
if ( 'sqlite' === DB_ENGINE && defined( 'DB_PATH' ) ) {
if ( ! is_string( DB_PATH ) ) {
throw new RuntimeException( 'DB_PATH must be a string.' );
}

$database_directory = ':memory:' === DB_PATH ? null : rtrim( dirname( DB_PATH ), '/' . DIRECTORY_SEPARATOR );

if ( defined( 'DB_DIR' ) && rtrim( (string) DB_DIR, '/' . DIRECTORY_SEPARATOR ) !== $database_directory ) {
trigger_error( 'DB_DIR conflicts with DB_PATH.', E_USER_WARNING );
}
if ( defined( 'DB_FILE' ) && ( ':memory:' === DB_PATH || DB_FILE !== basename( DB_PATH ) ) ) {
trigger_error( 'DB_FILE conflicts with DB_PATH.', E_USER_WARNING );
}
if ( defined( 'FQDBDIR' ) && rtrim( (string) FQDBDIR, '/' . DIRECTORY_SEPARATOR ) !== $database_directory ) {
trigger_error( 'FQDBDIR conflicts with DB_PATH.', E_USER_WARNING );
}
if ( defined( 'FQDB' ) && FQDB !== DB_PATH ) {
trigger_error( 'FQDB conflicts with DB_PATH.', E_USER_WARNING );
}
unset( $database_directory );
}

/**
* DB_DIR selects the database directory when DB_PATH is not configured.
*
* @deprecated 3.1.0 Define DB_PATH instead.
*/

/**
* DB_FILE selects a filename inside DB_DIR or FQDBDIR.
*
* @deprecated 3.1.0 Define DB_PATH instead.
*/

/**
* FQDBDIR is a directory where the sqlite database file is placed.
* If DB_DIR is defined, it is used as FQDBDIR.
* Defaults to the directory containing DB_PATH, or the legacy directory setting.
*
* @deprecated 3.0.0 Define DB_DIR instead of overriding FQDBDIR.
* @deprecated 3.0.0 Define DB_PATH instead of overriding FQDBDIR.
*/
if ( ! defined( 'FQDBDIR' ) ) {
if ( defined( 'DB_DIR' ) ) {
if ( defined( 'DB_PATH' ) && is_string( DB_PATH ) && ':memory:' !== DB_PATH ) {
define( 'FQDBDIR', rtrim( dirname( DB_PATH ), '/\\' ) . '/' );
} elseif ( ! defined( 'DB_PATH' ) && defined( 'DB_DIR' ) ) {
define( 'FQDBDIR', rtrim( DB_DIR, '/\\' ) . '/' );
} elseif ( defined( 'WP_CONTENT_DIR' ) ) {
define( 'FQDBDIR', WP_CONTENT_DIR . '/database/' );
Expand All @@ -39,12 +76,16 @@
/**
* FQDB is the absolute path to the SQLite database file.
*
* If DB_FILE is defined, FQDB is defined here using FQDBDIR. When SQLite is
* used without DB_FILE, managed storage defines FQDB after resolving the
* randomized database path.
* Defaults to DB_PATH, or FQDBDIR combined with DB_FILE. Otherwise, the drop-in
* defines FQDB after resolving the secret database path.
*
* @deprecated 3.0.0 Define DB_DIR and DB_FILE instead of overriding FQDB.
* @deprecated 3.0.0 Define DB_PATH instead of overriding FQDB.
*/
if ( ! defined( 'FQDB' ) && defined( 'DB_FILE' ) ) {
define( 'FQDB', FQDBDIR . DB_FILE );
if ( ! defined( 'FQDB' ) ) {
if ( defined( 'DB_PATH' ) ) {
define( 'FQDB', DB_PATH );
} elseif ( defined( 'DB_FILE' ) ) {
// Avoid compile-time warnings for invalid constants on PHP 7.2.
define( 'FQDB', constant( 'FQDBDIR' ) . constant( 'DB_FILE' ) );
}
}
6 changes: 4 additions & 2 deletions packages/plugin-sqlite-database-integration/health-check.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@ function sqlite_plugin_filter_debug_data( $info ) {

$info['wp-database']['fields']['database_file'] = array(
'label' => __( 'Database file', 'sqlite-database-integration' ),
'value' => FQDB,
'value' => DB_PATH,
'private' => true,
);

$info['wp-database']['fields']['database_size'] = array(
'label' => __( 'Database size', 'sqlite-database-integration' ),
'value' => size_format( filesize( FQDB ) ),
'value' => ':memory:' === DB_PATH
? __( 'Not available', 'sqlite-database-integration' )
: size_format( filesize( DB_PATH ) ),
);

unset( $info['wp-database']['fields']['extension'] );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ public function db_connect( $allow_bail = true ) {
$dbh = new WP_MySQL_On_SQLite(
sprintf(
'mysql-on-sqlite:path=%s;dbname=%s',
str_replace( ';', ';;', FQDB ),
str_replace( ';', ';;', DB_PATH ),
str_replace( ';', ';;', $this->dbname )
),
null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
*
* The storage handles the path of the SQLite database:
* - An explicit database path is used as is.
* - A managed database is stored under a randomized protected path.
* - A database in a legacy path is migrated to the managed storage.
* - A database with a secret path is stored in a randomized protected directory.
* - A database in a legacy path is migrated to a secret path.
*
* The storage implements a locking mechanism for initialization and maintenance.
* Locking uses a dedicated empty SQLite database and a maintenance file:
Expand Down Expand Up @@ -49,9 +49,10 @@ class WP_SQLite_Storage {
private const TOKEN_BYTE_LENGTH = 16;

/**
* Managed database root directory.
* Directory with the storage lock files and, for a secret path, the database.
* Null for an in-memory database.
*
* @var string
* @var string|null
*/
private $database_root;

Expand All @@ -65,21 +66,21 @@ class WP_SQLite_Storage {
/**
* Absolute path of the database path file.
*
* @var string
* @var string|null
*/
private $database_path_file;

/**
* Absolute path of the SQLite locking database.
*
* @var string
* @var string|null
*/
private $lock_path;

/**
* Absolute path of the storage maintenance marker.
*
* @var string
* @var string|null
*/
private $maintenance_path;

Expand All @@ -105,28 +106,55 @@ class WP_SQLite_Storage {
private $database_lock_timeout = 10000;

/**
* Create a SQLite storage manager.
* Create a storage manager for a database with a secret path.
*
* The database is stored in a randomized directory inside the root directory.
* Legacy databases in the root directory are migrated to it.
*
* @param string $root Root directory of the database storage.
* @return self The storage manager.
* @throws RuntimeException When the root directory is not an absolute path.
*/
public static function with_secret_path( string $root ): self {
if ( ! self::is_absolute_path( $root ) ) {
throw new RuntimeException( 'The SQLite database directory must be an absolute filesystem path.' );
}

$storage = new self();
$storage->set_database_root( $root );
return $storage;
}

/**
* Create a storage manager for an explicit database path.
*
* @param string|null $database_root Managed database root. Defaults to FQDBDIR.
* @param string|null $database_path Explicit database path or ":memory:". Managed storage is used when null.
* @param string $path Absolute database path or ":memory:".
* @return self The storage manager.
* @throws RuntimeException When the database path is invalid.
*/
public function __construct( ?string $database_root = null, ?string $database_path = null ) {
if ( '' === $database_path ) {
throw new RuntimeException( 'The SQLite database path is invalid.' );
public static function with_explicit_path( string $path ): self {
if ( ':memory:' !== $path ) {
if ( ! self::is_absolute_path( $path ) ) {
throw new RuntimeException( 'The SQLite database path must be an absolute filesystem path or ":memory:".' );
}
if ( is_dir( $path ) || in_array( substr( $path, -1 ), array( '/', DIRECTORY_SEPARATOR ), true ) ) {
throw new RuntimeException( 'The SQLite database path must point to a file, not a directory.' );
}
}
$this->database_root = rtrim( $database_root ?? FQDBDIR, '/\\' ) . '/';
$this->database_path = $database_path;
$this->database_path_file = $this->database_root . self::DATABASE_PATH_FILENAME;
$this->lock_path = $this->database_root . self::LOCK_FILENAME;
$this->maintenance_path = $this->database_root . self::MAINTENANCE_FILENAME;

$storage = new self();
$storage->database_path = $path;
if ( ':memory:' !== $path ) {
$storage->set_database_root( dirname( $path ) );
}
return $storage;
}

/**
* Initialize the SQLite database storage.
*
* Uses an explicit file path or ":memory:" as configured. Otherwise, initializes
* managed storage with a randomized path and migrates legacy databases as needed.
* a secret randomized path and migrates legacy databases as needed.
*
* @return string Absolute path to the SQLite database file, or ":memory:".
* @throws RuntimeException When the storage cannot be initialized.
Expand All @@ -153,13 +181,13 @@ public function initialize(): string {
return $this->database_path;
}

// Initialized managed database path.
// Initialized secret database path.
$database_path = $this->read_recorded_database_path();
if ( null !== $database_path && @is_file( $database_path ) ) {
return $database_path;
}

// Initialize or repair the managed storage under the storage lock.
// Initialize or repair the secret path under the storage lock.
// Preserve a lock already held by this instance for a larger operation.
$this->lock();
try {
Expand Down Expand Up @@ -210,6 +238,11 @@ public function initialize(): string {
* @throws RuntimeException When the lock cannot be acquired.
*/
public function lock(): void {
// An in-memory database has no files to lock.
if ( ':memory:' === $this->database_path ) {
return;
}

$was_locked = null !== $this->storage_lock_connection;

// Serialize maintenance through the dedicated locking database.
Expand Down Expand Up @@ -245,7 +278,7 @@ public function lock(): void {
if ( null === $database_path ) {
$database_path = $this->read_recorded_database_path();

// The managed database may still be at a legacy path.
// The database may still be at a legacy path.
if ( null === $database_path || ! @is_file( $database_path ) ) {
$database_path = $this->database_root . self::DATABASE_FILENAME;
}
Expand Down Expand Up @@ -282,6 +315,46 @@ public function unlock(): void {
}
}

/**
* Create a storage manager. Use with_secret_path() or with_explicit_path().
*/
private function __construct() {
}

/**
* Set the storage root directory and the paths derived from it.
*
* @param string $database_root Storage root directory.
*/
private function set_database_root( string $database_root ): void {
$this->database_root = rtrim( $database_root, '/\\' ) . '/';
$this->database_path_file = $this->database_root . self::DATABASE_PATH_FILENAME;
$this->lock_path = $this->database_root . self::LOCK_FILENAME;
$this->maintenance_path = $this->database_root . self::MAINTENANCE_FILENAME;
}

/**
* Check whether a path is an absolute filesystem path.
*
* @param string $path Filesystem path.
* @return bool Whether the path is absolute.
*/
private static function is_absolute_path( string $path ): bool {
if ( '' === $path || false !== strpos( $path, "\0" ) ) {
return false;
}

if ( '/' === DIRECTORY_SEPARATOR ) {
return '/' === $path[0];
} else {
// Match Windows absolute path with a drive letter or UNC share.
return 1 === preg_match(
'~^(?:[a-zA-Z]:/|//[^/]+/[^/]+/)~',
str_replace( '\\', '/', $path )
);
}
}

/**
* Wait until no maintenance process holds an active storage lock.
*/
Expand Down Expand Up @@ -373,7 +446,7 @@ function_exists( 'opcache_invalidate' )
}

/**
* Move a legacy database into a managed path.
* Move a legacy database into a secret path.
*
* @param string $legacy_path Absolute path of the legacy database file.
* @param string $database_path Absolute destination path.
Expand Down
Loading
Loading