This commit is contained in:
2026-07-02 15:54:39 -06:00
commit 9883323161
17470 changed files with 4470592 additions and 0 deletions
@@ -0,0 +1,127 @@
<?php
class WP_SQLite_Crosscheck_DB_ extends WP_SQLite_DB {
public function __construct( string $dbname ) {
parent::__construct( $dbname );
$GLOBALS['sqlite'] = $this;
$GLOBALS['mysql'] = new wpdb(
DB_USER,
DB_PASSWORD,
DB_NAME,
DB_HOST
);
// $this->resetDatabases();
}
private function resetDatabases() {
if ( file_exists( FQDB ) ) {
unlink( FQDB );
}
$GLOBALS['mysql']->query( 'DROP DATABASE IF EXISTS ' . DB_NAME );
$GLOBALS['mysql']->query( 'CREATE DATABASE ' . DB_NAME );
$GLOBALS['mysql']->query( 'USE ' . DB_NAME );
}
public function query( $query ) {
/**
* In MySQL, AUTO_INCREMENT columns don't reuse IDs assigned in rollback transactions
* In SQLite, AUTOINCREMENT columns do reuse IDs assigned in rollback transactions
*
* Let's store the current AUTOINCREMENT value for each table, and restore it afterwards.
*/
if ( preg_match( '/^\s*rollback/i', $query ) ) {
$autoincrements = array();
$tables = $GLOBALS['@pdo']->query( "SELECT name as `table` FROM sqlite_master WHERE type='table' ORDER BY name" )->fetchAll();
foreach ( $tables as $table ) {
$table = $table['table'];
$autoincrement = $GLOBALS['@pdo']->query( "SELECT seq FROM sqlite_sequence WHERE name = '$table'" )->fetchColumn();
$autoincrements[ $table ] = $autoincrement ?: 1;
}
}
$sqlite_retval = parent::query( $query );
if ( preg_match( '/^\s*rollback/i', $query ) ) {
foreach ( $autoincrements as $table => $autoincrement ) {
$GLOBALS['@pdo']->query( "UPDATE sqlite_sequence SET seq = $autoincrement WHERE name = '$table'" );
}
}
$this->crosscheck( $query, $sqlite_retval );
return $sqlite_retval;
}
private function crosscheck( $query, $sqlite_retval ) {
// echo $query."\n\n";
// Be lenient on cross-checking some query types
if ( preg_match( '/^\s*SET storage_engine/i', $query ) ) {
return;
}
$this->show_errors = false;
$this->suppress_errors = true;
$GLOBALS['mysql']->show_errors = false;
$GLOBALS['mysql']->suppress_errors = true;
ob_start();
$mysql_retval = $GLOBALS['mysql']->query( $query );
ob_end_clean();
$tests = array(
array( 'retval', $mysql_retval, $sqlite_retval ),
array( 'num_rows', $GLOBALS['mysql']->num_rows, $GLOBALS['sqlite']->num_rows ),
array( 'insert_id', $GLOBALS['mysql']->insert_id, $GLOBALS['sqlite']->insert_id ),
array( 'rows_affected', $GLOBALS['mysql']->rows_affected, $GLOBALS['sqlite']->rows_affected ),
);
foreach ( $tests as $test ) {
list($factor, $mysql, $sqlite) = $test;
if ( $mysql !== $sqlite ) {
if ( 'insert_id' === $factor ) {
// On multi-inserts MySQL returns the first inserted ID
// while SQLite returns the last one. The cached insert_id
// value stays the same for a number of subsequent queries.
// Let's forgive this for now.
continue;
}
if ( 'rows_affected' === $factor && $mysql_retval === $mysql ) {
// SQLite doesn't provide the rowcount() functionality
continue;
}
if ( 'retval' === $factor && $GLOBALS['mysql']->rows_affected === $mysql ) {
// SQLite doesn't provide the rowcount() functionality
continue;
}
echo "======================================================\n";
echo "======== *** $factor *** differed for query ========= \n";
echo "======================================================\n";
echo "MySQL query: \n";
echo "$query\n\n";
echo "SQLite queries: \n";
foreach ( $this->dbh->last_translation->queries as $query ) {
echo $query->sql . "\n";
}
echo "\n";
$this->report_factor(
'error',
$GLOBALS['mysql']->last_error,
$GLOBALS['sqlite']->last_error
);
foreach ( $tests as $test ) {
$this->report_factor(
$test[0],
$test[1],
$test[2]
);
}
// throw new Exception();
break;
}
}
}
private function report_factor( $factor, $mysql, $sqlite ) {
echo "$factor: \n";
echo ' MySQL: ' . var_export( $mysql, true ) . "\n";
echo ' SQLite: ' . var_export( $sqlite, true ) . "\n\n";
}
}
@@ -0,0 +1,675 @@
<?php
/**
* Extend and replace the wpdb class.
*
* @package wp-sqlite-integration
* @since 1.0.0
*/
/**
* This class extends wpdb and replaces it.
*
* It also rewrites some methods that use mysql specific functions.
*/
class WP_SQLite_DB extends wpdb {
/**
* Database Handle
*
* @var WP_SQLite_Driver
*/
protected $dbh;
/**
* Backward compatibility, see wpdb::$allow_unsafe_unquoted_parameters.
*
* This property is mirroring "wpdb::$allow_unsafe_unquoted_parameters",
* because some tests are accessing it externally using PHP reflection.
*
* @var
*/
private $allow_unsafe_unquoted_parameters = true;
/**
* Connects to the SQLite database.
*
* Unlike for MySQL, no credentials and host are needed.
*
* @param string $dbname Database name.
*/
public function __construct( $dbname ) {
/**
* We need to initialize the "$wpdb" global early, so that the SQLite
* driver can configure the database. The call stack goes like this:
*
* 1. The "parent::__construct()" call executes "$this->db_connect()".
* 2. The database connection call initializes the SQLite driver.
* 3. The SQLite driver initializes and runs "WP_SQLite_Configurator".
* 4. The configurator uses "WP_SQLite_Information_Schema_Reconstructor",
* which requires "wp-admin/includes/schema.php" when in WordPress.
* 5. The "wp-admin/includes/schema.php" requires the "$wpdb" global,
* which creates a circular dependency.
*/
$GLOBALS['wpdb'] = $this;
parent::__construct( '', '', $dbname, '' );
$this->charset = 'utf8mb4';
}
/**
* Method to set character set for the database.
*
* This overrides wpdb::set_charset(), only to dummy out the MySQL function.
*
* @see wpdb::set_charset()
*
* @param resource $dbh The resource given by mysql_connect.
* @param string $charset Optional. The character set. Default null.
* @param string $collate Optional. The collation. Default null.
*/
public function set_charset( $dbh, $charset = null, $collate = null ) {
}
/**
* Method to get the character set for the database.
* Hardcoded to utf8mb4 for now.
*
* @param string $table The table name.
* @param string $column The column name.
*
* @return string The character set.
*/
public function get_col_charset( $table, $column ) {
// Hardcoded for now.
return 'utf8mb4';
}
/**
* Changes the current SQL mode, and ensures its WordPress compatibility.
*
* If no modes are passed, it will ensure the current MySQL server modes are compatible.
*
* This overrides wpdb::set_sql_mode() while closely mirroring its implementation.
*
* @param array $modes Optional. A list of SQL modes to set. Default empty array.
*/
public function set_sql_mode( $modes = array() ) {
if ( empty( $modes ) ) {
$result = $this->dbh->query( 'SELECT @@SESSION.sql_mode' );
if ( ! isset( $result[0] ) ) {
return;
}
$modes_str = $result[0]->{'@@SESSION.sql_mode'};
if ( empty( $modes_str ) ) {
return;
}
$modes = explode( ',', $modes_str );
}
$modes = array_change_key_case( $modes, CASE_UPPER );
/**
* Filters the list of incompatible SQL modes to exclude.
*
* @since 3.9.0
*
* @param array $incompatible_modes An array of incompatible modes.
*/
$incompatible_modes = (array) apply_filters( 'incompatible_sql_modes', $this->incompatible_modes );
foreach ( $modes as $i => $mode ) {
if ( in_array( $mode, $incompatible_modes, true ) ) {
unset( $modes[ $i ] );
}
}
$modes_str = implode( ',', $modes );
$this->dbh->query( "SET SESSION sql_mode='$modes_str'" );
}
/**
* Closes the current database connection.
* Noop in SQLite.
*
* @return bool True to indicate the connection was successfully closed.
*/
public function close() {
return true;
}
/**
* Method to select the database connection.
*
* This overrides wpdb::select(), only to dummy out the MySQL function.
*
* @see wpdb::select()
*
* @param string $db MySQL database name. Not used.
* @param resource|null $dbh Optional link identifier.
*/
public function select( $db, $dbh = null ) {
$this->ready = true;
}
/**
* Method to escape characters.
*
* This overrides wpdb::_real_escape() to avoid using mysql_real_escape_string().
*
* @see wpdb::_real_escape()
*
* @param string $data The string to escape.
*
* @return string escaped
*/
public function _real_escape( $data ) {
if ( ! is_scalar( $data ) ) {
return '';
}
$escaped = addslashes( $data );
return $this->add_placeholder_escape( $escaped );
}
/**
* Prints SQL/DB error.
*
* This overrides wpdb::print_error() while closely mirroring its implementation.
*
* @global array $EZSQL_ERROR Stores error information of query and error string.
*
* @param string $str The error to display.
* @return void|false Void if the showing of errors is enabled, false if disabled.
*/
public function print_error( $str = '' ) {
global $EZSQL_ERROR;
if ( ! $str ) {
$str = $this->last_error;
}
$EZSQL_ERROR[] = array(
'query' => $this->last_query,
'error_str' => $str,
);
if ( $this->suppress_errors ) {
return false;
}
$caller = $this->get_caller();
if ( $caller ) {
// Not translated, as this will only appear in the error log.
$error_str = sprintf( 'WordPress database error %1$s for query %2$s made by %3$s', $str, $this->last_query, $caller );
} else {
$error_str = sprintf( 'WordPress database error %1$s for query %2$s', $str, $this->last_query );
}
error_log( $error_str );
// Are we showing errors?
if ( ! $this->show_errors ) {
return false;
}
wp_load_translations_early();
// If there is an error then take note of it.
if ( is_multisite() ) {
$msg = sprintf(
"%s [%s]\n%s\n",
__( 'WordPress database error:' ),
$str,
$this->last_query
);
if ( defined( 'ERRORLOGFILE' ) ) {
error_log( $msg, 3, ERRORLOGFILE );
}
if ( defined( 'DIEONDBERROR' ) ) {
wp_die( $msg );
}
} else {
$str = htmlspecialchars( $str, ENT_QUOTES );
$query = htmlspecialchars( $this->last_query, ENT_QUOTES );
printf(
'<div id="error"><p class="wpdberror"><strong>%s</strong> [%s]<br /><code>%s</code></p></div>',
__( 'WordPress database error:' ),
$str,
$query
);
}
}
/**
* Method to flush cached data.
*
* This overrides wpdb::flush(). This is not necessarily overridden, because
* $result will never be resource.
*
* @see wpdb::flush
*/
public function flush() {
$this->last_result = array();
$this->col_info = null;
$this->last_query = null;
$this->rows_affected = 0;
$this->num_rows = 0;
$this->last_error = '';
$this->result = null;
}
/**
* Method to do the database connection.
*
* This overrides wpdb::db_connect() to avoid using MySQL function.
*
* @see wpdb::db_connect()
*
* @param bool $allow_bail Not used.
* @return void
*/
public function db_connect( $allow_bail = true ) {
if ( $this->dbh ) {
return;
}
$this->init_charset();
$pdo = null;
if ( isset( $GLOBALS['@pdo'] ) ) {
$pdo = $GLOBALS['@pdo'];
}
// Migrate the database file from a legacy path, if it exists.
if ( ! defined( 'DB_FILE' ) && ! file_exists( FQDB ) ) {
$old_db_path = FQDBDIR . '.ht.sqlite.php';
if ( file_exists( $old_db_path ) ) {
if ( ! rename( $old_db_path, FQDB ) ) {
wp_die( 'Failed to rename database file.', 'Error!' );
}
foreach ( array( '-wal', '-shm', '-journal' ) as $suffix ) {
if ( file_exists( $old_db_path . $suffix ) ) {
if ( ! rename( $old_db_path . $suffix, FQDB . $suffix ) ) {
wp_die( 'Failed to rename database file.', 'Error!' );
}
}
}
}
}
if ( null === $this->dbname || '' === $this->dbname ) {
$this->bail(
'The database name was not set. The SQLite driver requires a database name to be set to emulate MySQL information schema tables.',
'db_connect_fail'
);
return false;
}
$this->ensure_database_directory( FQDB );
try {
$connection = new WP_SQLite_Connection(
array(
'pdo' => $pdo,
'path' => FQDB,
'journal_mode' => defined( 'SQLITE_JOURNAL_MODE' ) ? SQLITE_JOURNAL_MODE : null,
)
);
$this->dbh = new WP_SQLite_Driver( $connection, $this->dbname );
$GLOBALS['@pdo'] = $this->dbh->get_connection()->get_pdo();
} catch ( Throwable $e ) {
$this->last_error = $this->format_error_message( $e );
}
if ( $this->last_error ) {
return false;
}
$this->ready = true;
$this->set_sql_mode();
}
/**
* Method to dummy out wpdb::check_connection()
*
* @param bool $allow_bail Not used.
*
* @return bool
*/
public function check_connection( $allow_bail = true ) {
return true;
}
/**
* Prepares a SQL query for safe execution.
*
* See "wpdb::prepare()". This override only fixes a WPDB test issue.
*
* @param string $query Query statement with `sprintf()`-like placeholders.
* @param array|mixed $args The array of variables or the first variable to substitute.
* @param mixed ...$args Further variables to substitute when using individual arguments.
* @return string|void Sanitized query string, if there is a query to prepare.
*/
public function prepare( $query, ...$args ) {
/*
* Sync "$allow_unsafe_unquoted_parameters" with the WPDB parent property.
* This is only needed because some WPDB tests are accessing the private
* property externally via PHP reflection. This should be fixed WP tests.
*/
$wpdb_allow_unsafe_unquoted_parameters = $this->__get( 'allow_unsafe_unquoted_parameters' );
if ( $wpdb_allow_unsafe_unquoted_parameters !== $this->allow_unsafe_unquoted_parameters ) {
$property = new ReflectionProperty( 'wpdb', 'allow_unsafe_unquoted_parameters' );
$property->setAccessible( true );
$property->setValue( $this, $this->allow_unsafe_unquoted_parameters );
$property->setAccessible( false );
}
return parent::prepare( $query, ...$args );
}
/**
* Performs a database query.
*
* This overrides wpdb::query() while closely mirroring its implementation.
*
* @see wpdb::query()
*
* @param string $query Database query.
*
* @param string $query Database query.
* @return int|bool Boolean true for CREATE, ALTER, TRUNCATE and DROP queries. Number of rows
* affected/selected for all other queries. Boolean false on error.
*/
public function query( $query ) {
// Query Monitor integration:
$query_monitor_active = defined( 'SQLITE_QUERY_MONITOR_LOADED' ) && SQLITE_QUERY_MONITOR_LOADED;
if ( $query_monitor_active && $this->show_errors ) {
$this->hide_errors();
}
if ( ! $this->ready ) {
return false;
}
$query = apply_filters( 'query', $query );
if ( ! $query ) {
$this->insert_id = 0;
return false;
}
$this->flush();
// Log how the function was called.
$this->func_call = "\$db->query(\"$query\")";
// Keep track of the last query for debug.
$this->last_query = $query;
// Save the query count before running another query.
$last_query_count = count( $this->queries ?? array() );
/*
* @TODO: WPDB uses "$this->check_current_query" to check table/column
* charset and strip all invalid characters from the query.
* This is an involved process that we can bypass for SQLite,
* if we simply strip all invalid UTF-8 characters from the query.
*
* To do so, mb_convert_encoding can be used with an optional
* fallback to a htmlspecialchars method. E.g.:
* https://github.com/nette/utils/blob/be534713c227aeef57ce1883fc17bc9f9e29eca2/src/Utils/Strings.php#L42
*/
$this->_do_query( $query );
if ( $this->last_error ) {
// Clear insert_id on a subsequent failed insert.
if ( $this->insert_id && preg_match( '/^\s*(insert|replace)\s/i', $query ) ) {
$this->insert_id = 0;
}
$this->print_error();
return false;
}
if ( preg_match( '/^\s*(create|alter|truncate|drop)\s/i', $query ) ) {
$return_val = true;
} elseif ( preg_match( '/^\s*(insert|delete|update|replace)\s/i', $query ) ) {
$this->rows_affected = $this->dbh->get_last_return_value();
// Take note of the insert_id.
if ( preg_match( '/^\s*(insert|replace)\s/i', $query ) ) {
$this->insert_id = $this->dbh->get_insert_id();
}
// Return number of rows affected.
$return_val = $this->rows_affected;
} else {
$num_rows = 0;
if ( is_array( $this->result ) ) {
$this->last_result = $this->result;
$num_rows = count( $this->result );
}
// Log and return the number of rows selected.
$this->num_rows = $num_rows;
$return_val = $num_rows;
}
// Query monitor integration:
if ( $query_monitor_active && class_exists( 'QM_Backtrace' ) ) {
if ( did_action( 'qm/cease' ) ) {
$this->queries = array();
}
$i = $last_query_count;
if ( ! isset( $this->queries[ $i ] ) ) {
return $return_val;
}
$this->queries[ $i ]['trace'] = new QM_Backtrace();
if ( ! isset( $this->queries[ $i ][3] ) ) {
$this->queries[ $i ][3] = $this->time_start;
}
if ( $this->last_error && ! $this->suppress_errors ) {
$this->queries[ $i ]['result'] = new WP_Error( 'qmdb', $this->last_error );
} else {
$this->queries[ $i ]['result'] = (int) $return_val;
}
// Add SQLite query data.
$this->queries[ $i ]['sqlite_queries'] = $this->dbh->get_last_sqlite_queries();
}
return $return_val;
}
/**
* Internal function to perform the SQLite query call.
*
* This closely mirrors wpdb::_do_query().
*
* @see wpdb::_do_query()
*
* @param string $query The query to run.
*/
private function _do_query( $query ) {
if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) {
$this->timer_start();
}
try {
$this->result = $this->dbh->query( $query );
} catch ( Throwable $e ) {
$this->last_error = $this->format_error_message( $e );
}
++$this->num_queries;
if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) {
$this->log_query(
$query,
$this->timer_stop(),
$this->get_caller(),
$this->time_start,
array()
);
}
}
/**
* Method to set the class variable $col_info.
*
* This overrides wpdb::load_col_info(), which uses a mysql function.
*
* @see wpdb::load_col_info()
*/
protected function load_col_info() {
if ( $this->col_info ) {
return;
}
$this->col_info = array();
foreach ( $this->dbh->get_last_column_meta() as $column ) {
$this->col_info[] = (object) array(
'name' => $column['name'],
'orgname' => $column['mysqli:orgname'],
'table' => $column['table'],
'orgtable' => $column['mysqli:orgtable'],
'def' => '', // Unused, always ''.
'db' => $column['mysqli:db'],
'catalog' => 'def', // Unused, always 'def'.
'max_length' => 0, // As of PHP 8.1, this is always 0.
'length' => $column['len'],
'charsetnr' => $column['mysqli:charsetnr'],
'flags' => $column['mysqli:flags'],
'type' => $column['mysqli:type'],
'decimals' => $column['precision'],
);
}
}
/**
* Method to return what the database can do.
*
* This overrides wpdb::has_cap() to avoid using MySQL functions.
* SQLite supports subqueries, but not support collation, group_concat and set_charset.
*
* @see wpdb::has_cap()
*
* @param string $db_cap The feature to check for. Accepts 'collation',
* 'group_concat', 'subqueries', 'set_charset',
* 'utf8mb4', or 'utf8mb4_520'.
*
* @return bool Whether the database feature is supported, false otherwise.
*/
public function has_cap( $db_cap ) {
return 'subqueries' === strtolower( $db_cap );
}
/**
* Method to return database version number.
*
* This overrides wpdb::db_version() to avoid using MySQL function.
* It returns mysql version number, but it means nothing for SQLite.
* So it return the newest mysql version.
*
* @see wpdb::db_version()
*/
public function db_version() {
return '8.0';
}
/**
* Returns the version of the SQLite engine.
*
* @return string SQLite engine version as a string.
*/
public function db_server_info() {
return $this->dbh->get_sqlite_version();
}
/**
* Make sure the SQLite database directory exists and is writable.
* Create .htaccess and index.php files to prevent direct access.
*
* @param string $database_path The path to the SQLite database file.
*/
private function ensure_database_directory( string $database_path ) {
$dir = dirname( $database_path );
// Set the umask to 0000 to apply permissions exactly as specified.
// A non-zero umask affects new file and directory permissions.
$umask = umask( 0 );
// Ensure database directory.
if ( ! is_dir( $dir ) ) {
if ( ! @mkdir( $dir, 0700, true ) ) {
wp_die( sprintf( 'Failed to create database directory: %s', $dir ), 'Error!' );
}
}
if ( ! is_writable( $dir ) ) {
wp_die( sprintf( 'Database directory is not writable: %s', $dir ), 'Error!' );
}
// Ensure .htaccess file to prevent direct access.
$path = $dir . DIRECTORY_SEPARATOR . '.htaccess';
if ( ! is_file( $path ) ) {
$result = file_put_contents( $path, 'DENY FROM ALL', LOCK_EX );
if ( false === $result ) {
wp_die( sprintf( 'Failed to create file: %s', $path ), 'Error!' );
}
chmod( $path, 0600 );
}
// Ensure index.php file to prevent direct access.
$path = $dir . DIRECTORY_SEPARATOR . 'index.php';
if ( ! is_file( $path ) ) {
$result = file_put_contents( $path, '<?php // Silence is gold. ?>', LOCK_EX );
if ( false === $result ) {
wp_die( sprintf( 'Failed to create file: %s', $path ), 'Error!' );
}
chmod( $path, 0600 );
}
// Restore the original umask value.
umask( $umask );
}
/**
* Format SQLite driver error message.
*
* @return string
*/
private function format_error_message( Throwable $e ) {
$output = '<div style="clear:both">&nbsp;</div>' . PHP_EOL;
// Queries.
if ( $e instanceof WP_SQLite_Driver_Exception ) {
$driver = $e->getDriver();
$output .= '<div class="queries" style="clear:both;margin-bottom:2px;border:red dotted thin;">' . PHP_EOL;
$output .= '<p>MySQL query:</p>' . PHP_EOL;
$output .= '<p>' . $driver->get_last_mysql_query() . '</p>' . PHP_EOL;
$output .= '<p>Queries made or created this session were:</p>' . PHP_EOL;
$output .= '<ol>' . PHP_EOL;
foreach ( $driver->get_last_sqlite_queries() as $q ) {
$message = "Executing: {$q['sql']} | " . ( $q['params'] ? 'parameters: ' . implode( ', ', $q['params'] ) : '(no parameters)' );
$output .= '<li>' . htmlspecialchars( $message ) . '</li>' . PHP_EOL;
}
$output .= '</ol>' . PHP_EOL;
$output .= '</div>' . PHP_EOL;
}
// Message.
$output .= '<div style="clear:both;margin-bottom:2px;border:red dotted thin;" class="error_message" style="border-bottom:dotted blue thin;">' . PHP_EOL;
$output .= $e->getMessage() . PHP_EOL;
$output .= '</div>' . PHP_EOL;
// Backtrace.
$output .= '<p>Backtrace:</p>' . PHP_EOL;
$output .= '<pre>' . $e->getTraceAsString() . '</pre>' . PHP_EOL;
return $output;
}
}
@@ -0,0 +1,71 @@
<?php
/**
* Main integration file.
*
* @package wp-sqlite-integration
* @since 1.0.0
*/
/**
* Load the "SQLITE_DRIVER_VERSION" constant.
*/
require_once __DIR__ . '/../database/version.php';
// Require the constants file.
require_once __DIR__ . '/../../constants.php';
// Bail early if DB_ENGINE is not defined as sqlite.
if ( ! defined( 'DB_ENGINE' ) || 'sqlite' !== DB_ENGINE ) {
return;
}
if ( ! extension_loaded( 'pdo' ) ) {
wp_die(
new WP_Error(
'pdo_not_loaded',
sprintf(
'<h1>%1$s</h1><p>%2$s</p>',
'PHP PDO Extension is not loaded',
'Your PHP installation appears to be missing the PDO extension which is required for this version of WordPress and the type of database you have specified.'
)
),
'PHP PDO Extension is not loaded.'
);
}
if ( ! extension_loaded( 'pdo_sqlite' ) ) {
wp_die(
new WP_Error(
'pdo_driver_not_loaded',
sprintf(
'<h1>%1$s</h1><p>%2$s</p>',
'PDO Driver for SQLite is missing',
'Your PHP installation appears not to have the right PDO drivers loaded. These are required for this version of WordPress and the type of database you have specified.'
)
),
'PDO Driver for SQLite is missing.'
);
}
require_once __DIR__ . '/../database/load.php';
require_once __DIR__ . '/class-wp-sqlite-db.php';
require_once __DIR__ . '/install-functions.php';
$db_name = defined( 'DB_NAME' ) ? DB_NAME : '';
/*
* Debug: Cross-check with MySQL.
* This is for debugging purpose only and requires files
* that are present in the GitHub repository
* but not the plugin published on WordPress.org.
*/
$crosscheck_tests_file_path = __DIR__ . '/class-wp-sqlite-crosscheck-db.php';
if ( defined( 'SQLITE_DEBUG_CROSSCHECK' ) && SQLITE_DEBUG_CROSSCHECK && file_exists( $crosscheck_tests_file_path ) ) {
require_once $crosscheck_tests_file_path;
$GLOBALS['wpdb'] = new WP_SQLite_Crosscheck_DB( $db_name );
} else {
$GLOBALS['wpdb'] = new WP_SQLite_DB( $db_name );
// Boot the Query Monitor plugin if it is active.
require_once __DIR__ . '/../../integrations/query-monitor/boot.php';
}
@@ -0,0 +1,232 @@
<?php
/**
* Main integration file.
*
* @package wp-sqlite-integration
* @since 1.0.0
*/
/**
* Function to create tables according to the schemas of WordPress.
*
* This is executed only once while installation.
*
* @since 1.0.0
*
* @return boolean
*
* @throws PDOException If the database connection fails.
*/
function sqlite_make_db_sqlite() {
global $wpdb;
include_once ABSPATH . 'wp-admin/includes/schema.php';
$table_schemas = wp_get_db_schema();
$queries = explode( ';', $table_schemas );
try {
$pdo_class = PHP_VERSION_ID >= 80400 ? PDO\SQLite::class : PDO::class; // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO
$pdo = new $pdo_class( 'sqlite:' . FQDB, null, null, array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ) ); // phpcs:ignore WordPress.DB.RestrictedClasses
} catch ( PDOException $err ) {
$err_data = $err->errorInfo; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$message = 'Database connection error!<br />';
$message .= sprintf( 'Error message is: %s', $err_data[2] );
wp_die( $message, 'Database Error!' );
}
$translator = new WP_SQLite_Driver(
new WP_SQLite_Connection( array( 'pdo' => $pdo ) ),
$wpdb->dbname
);
$query = null;
try {
$translator->begin_transaction();
foreach ( $queries as $query ) {
$query = trim( $query );
if ( empty( $query ) ) {
continue;
}
$translator->query( $query );
}
$translator->commit();
} catch ( PDOException $err ) {
$translator->rollback();
$message = sprintf(
'Error occurred while creating tables or indexes...<br />Query was: %s<br />',
var_export( $query, true )
);
$message .= sprintf( 'Error message is: %s', $err->getMessage() );
wp_die( $message, 'Database Error!' );
}
/*
* Debug: Cross-check with MySQL.
* This is for debugging purpose only and requires files
* that are present in the GitHub repository
* but not the plugin published on WordPress.org.
*/
if ( defined( 'SQLITE_DEBUG_CROSSCHECK' ) && SQLITE_DEBUG_CROSSCHECK ) {
$host = DB_HOST;
$port = 3306;
if ( str_contains( $host, ':' ) ) {
$host_parts = explode( ':', $host );
$host = $host_parts[0];
$port = $host_parts[1];
}
$dsn = 'mysql:host=' . $host . '; port=' . $port . '; dbname=' . DB_NAME;
$pdo_class = PHP_VERSION_ID >= 80400 ? PDO\MySQL::class : PDO::class; // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO
$pdo_mysql = new $pdo_class( $dsn, DB_USER, DB_PASSWORD, array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ) ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO
$pdo_mysql->query( 'SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";' );
$pdo_mysql->query( 'SET time_zone = "+00:00";' );
foreach ( $queries as $query ) {
$query = trim( $query );
if ( empty( $query ) ) {
continue;
}
try {
$pdo_mysql->beginTransaction();
$pdo_mysql->query( $query );
} catch ( PDOException $err ) {
$err_data = $err->errorInfo; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$err_code = $err_data[1];
// phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
if ( 5 == $err_code || 6 == $err_code ) {
// If the database is locked, commit again.
$pdo_mysql->commit();
} else {
$pdo_mysql->rollBack();
$message = sprintf(
'Error occurred while creating tables or indexes...<br />Query was: %s<br />',
var_export( $query, true )
);
$message .= sprintf( 'Error message is: %s', $err_data[2] );
wp_die( $message, 'Database Error!' );
}
}
}
}
$pdo = null;
return true;
}
if ( ! function_exists( 'wp_install' ) ) {
/**
* Installs the site.
*
* Runs the required functions to set up and populate the database,
* including primary admin user and initial options.
*
* @since 1.0.0
*
* @param string $blog_title Site title.
* @param string $user_name User's username.
* @param string $user_email User's email.
* @param bool $is_public Whether the site is public.
* @param string $deprecated Optional. Not used.
* @param string $user_password Optional. User's chosen password. Default empty (random password).
* @param string $language Optional. Language chosen. Default empty.
* @return array {
* Data for the newly installed site.
*
* @type string $url The URL of the site.
* @type int $user_id The ID of the site owner.
* @type string $password The password of the site owner, if their user account didn't already exist.
* @type string $password_message The explanatory message regarding the password.
* }
*/
function wp_install( $blog_title, $user_name, $user_email, $is_public, $deprecated = '', $user_password = '', $language = '' ) {
if ( ! empty( $deprecated ) ) {
_deprecated_argument( __FUNCTION__, '2.6.0' );
}
wp_check_mysql_version();
wp_cache_flush();
/* SQLite changes: Replace the call to make_db_current_silent() with sqlite_make_db_sqlite(). */
sqlite_make_db_sqlite(); // phpcs:ignore PHPCompatibility.Extensions.RemovedExtensions.sqliteRemoved
populate_options();
populate_roles();
update_option( 'blogname', $blog_title );
update_option( 'admin_email', $user_email );
update_option( 'blog_public', $is_public );
// Freshness of site - in the future, this could get more specific about actions taken, perhaps.
update_option( 'fresh_site', 1 );
if ( $language ) {
update_option( 'WPLANG', $language );
}
$guessurl = wp_guess_url();
update_option( 'siteurl', $guessurl );
// If not a public site, don't ping.
if ( ! $is_public ) {
update_option( 'default_pingback_flag', 0 );
}
/*
* Create default user. If the user already exists, the user tables are
* being shared among sites. Just set the role in that case.
*/
$user_id = username_exists( $user_name );
$user_password = trim( $user_password );
$email_password = false;
$user_created = false;
if ( ! $user_id && empty( $user_password ) ) {
$user_password = wp_generate_password( 12, false );
$message = __( '<strong><em>Note that password</em></strong> carefully! It is a <em>random</em> password that was generated just for you.', 'sqlite-database-integration' );
$user_id = wp_create_user( $user_name, $user_password, $user_email );
update_user_meta( $user_id, 'default_password_nag', true );
$email_password = true;
$user_created = true;
} elseif ( ! $user_id ) {
// Password has been provided.
$message = '<em>' . __( 'Your chosen password.', 'sqlite-database-integration' ) . '</em>';
$user_id = wp_create_user( $user_name, $user_password, $user_email );
$user_created = true;
} else {
$message = __( 'User already exists. Password inherited.', 'sqlite-database-integration' );
}
$user = new WP_User( $user_id );
$user->set_role( 'administrator' );
if ( $user_created ) {
$user->user_url = $guessurl;
wp_update_user( $user );
}
wp_install_defaults( $user_id );
wp_install_maybe_enable_pretty_permalinks();
flush_rewrite_rules();
wp_new_blog_notification( $blog_title, $guessurl, $user_id, ( $email_password ? $user_password : __( 'The password you chose during installation.', 'sqlite-database-integration' ) ) );
wp_cache_flush();
/**
* Fires after a site is fully installed.
*
* @since 3.9.0
*
* @param WP_User $user The site owner.
*/
do_action( 'wp_install', $user );
return array(
'url' => $guessurl,
'user_id' => $user_id,
'password' => $user_password,
'password_message' => $message,
);
}
}