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( '

%s [%s]
%s

', __( '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, '', 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 = '
 
' . PHP_EOL; // Queries. if ( $e instanceof WP_SQLite_Driver_Exception ) { $driver = $e->getDriver(); $output .= '
' . PHP_EOL; $output .= '

MySQL query:

' . PHP_EOL; $output .= '

' . $driver->get_last_mysql_query() . '

' . PHP_EOL; $output .= '

Queries made or created this session were:

' . PHP_EOL; $output .= '
    ' . PHP_EOL; foreach ( $driver->get_last_sqlite_queries() as $q ) { $message = "Executing: {$q['sql']} | " . ( $q['params'] ? 'parameters: ' . implode( ', ', $q['params'] ) : '(no parameters)' ); $output .= '
  1. ' . htmlspecialchars( $message ) . '
  2. ' . PHP_EOL; } $output .= '
' . PHP_EOL; $output .= '
' . PHP_EOL; } // Message. $output .= '
' . PHP_EOL; $output .= $e->getMessage() . PHP_EOL; $output .= '
' . PHP_EOL; // Backtrace. $output .= '

Backtrace:

' . PHP_EOL; $output .= '
' . $e->getTraceAsString() . '
' . PHP_EOL; return $output; } }