initial
This commit is contained in:
+127
@@ -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";
|
||||
}
|
||||
}
|
||||
+675
@@ -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"> </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';
|
||||
}
|
||||
+232
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user