diff --git a/wp/wp-content/plugins/sqlite-database-integration/activate.php b/wp/wp-content/plugins/sqlite-database-integration/activate.php
index 1001914a2..5dc21d2b6 100644
--- a/wp/wp-content/plugins/sqlite-database-integration/activate.php
+++ b/wp/wp-content/plugins/sqlite-database-integration/activate.php
@@ -14,10 +14,15 @@
* @param string $plugin The plugin basename.
*/
function sqlite_plugin_activation_redirect( $plugin ) {
- if ( plugin_basename( SQLITE_MAIN_FILE ) === $plugin ) {
- if ( wp_safe_redirect( admin_url( 'options-general.php?page=sqlite-integration' ) ) ) {
- exit;
- }
+ if (
+ plugin_basename( SQLITE_MAIN_FILE ) !== $plugin
+ || ! current_user_can( sqlite_plugin_get_manage_capability() )
+ ) {
+ return;
+ }
+
+ if ( wp_safe_redirect( sqlite_plugin_get_admin_page_url() ) ) {
+ exit;
}
}
add_action( 'activated_plugin', 'sqlite_plugin_activation_redirect' );
@@ -25,36 +30,47 @@ function sqlite_plugin_activation_redirect( $plugin ) {
/**
* Check the URL to ensure we're on the plugin page,
* the user has clicked the button to install SQLite,
- * and the nonce is valid.
+ * the user has permission to manage the database drop-in, and the nonce is valid.
* If the above conditions are met, run the sqlite_plugin_copy_db_file() function,
* and redirect to the install screen.
*
* @since 1.0.0
*/
function sqlite_activation() {
- global $current_screen;
- if ( isset( $current_screen->base ) && 'settings_page_sqlite-integration' === $current_screen->base ) {
+ if (
+ ! isset( $_GET['page'], $_GET['confirm-install'] )
+ || 'sqlite-integration' !== $_GET['page']
+ ) {
return;
}
- if ( isset( $_GET['confirm-install'] ) && wp_verify_nonce( $_GET['_wpnonce'], 'sqlite-install' ) ) {
- // Handle upgrading from the performance-lab plugin.
- if ( isset( $_GET['upgrade-from-pl'] ) ) {
- global $wp_filesystem;
- require_once ABSPATH . '/wp-admin/includes/file.php';
- // Delete the previous db.php file.
- $wp_filesystem->delete( WP_CONTENT_DIR . '/db.php' );
- // Deactivate the performance-lab SQLite module.
- $pl_option_name = defined( 'PERFLAB_MODULES_SETTING' ) ? PERFLAB_MODULES_SETTING : 'perflab_modules_settings';
- $pl_option = get_option( $pl_option_name, array() );
- unset( $pl_option['database/sqlite'] );
- update_option( $pl_option_name, $pl_option );
- }
- sqlite_plugin_copy_db_file();
- // WordPress will automatically redirect to the install screen here.
- wp_redirect( admin_url() );
- exit;
+ if ( ! current_user_can( sqlite_plugin_get_manage_capability() ) ) {
+ wp_die(
+ esc_html__( 'Sorry, you are not allowed to install the SQLite database drop-in.', 'sqlite-database-integration' ),
+ 403
+ );
+ }
+
+ check_admin_referer( 'sqlite-install' );
+
+ // Handle upgrading from the performance-lab plugin.
+ if ( isset( $_GET['upgrade-from-pl'] ) ) {
+ global $wp_filesystem;
+ require_once ABSPATH . '/wp-admin/includes/file.php';
+ // Delete the previous db.php file.
+ $wp_filesystem->delete( WP_CONTENT_DIR . '/db.php' );
+ // Deactivate the performance-lab SQLite module.
+ $pl_option_name = defined( 'PERFLAB_MODULES_SETTING' ) ? PERFLAB_MODULES_SETTING : 'perflab_modules_settings';
+ $pl_option = get_option( $pl_option_name, array() );
+ unset( $pl_option['database/sqlite'] );
+ update_option( $pl_option_name, $pl_option );
}
+
+ sqlite_plugin_copy_db_file();
+
+ // WordPress will automatically redirect to the install screen here.
+ wp_redirect( admin_url() );
+ exit;
}
add_action( 'admin_init', 'sqlite_activation' );
diff --git a/wp/wp-content/plugins/sqlite-database-integration/admin-notices.php b/wp/wp-content/plugins/sqlite-database-integration/admin-notices.php
index a455cc86c..06e2b6fac 100644
--- a/wp/wp-content/plugins/sqlite-database-integration/admin-notices.php
+++ b/wp/wp-content/plugins/sqlite-database-integration/admin-notices.php
@@ -12,6 +12,9 @@
* When the plugin gets merged in wp-core, this is not to be ported.
*/
function sqlite_plugin_admin_notice() {
+ if ( ! current_user_can( sqlite_plugin_get_manage_capability() ) ) {
+ return;
+ }
// Don't print notices in the plugin's admin screen.
global $current_screen;
@@ -66,11 +69,11 @@ function sqlite_plugin_admin_notice() {
/* translators: 1: db.php drop-in path, 2: Admin URL to deactivate the module */
__( 'The SQLite Integration plugin is active, but the %1$s file is missing. Please deactivate the plugin and re-activate it to try again.', 'sqlite-database-integration' ),
'' . esc_html( basename( WP_CONTENT_DIR ) ) . '/db.php',
- esc_url( admin_url( 'plugins.php' ) )
+ esc_url( self_admin_url( 'plugins.php' ) )
)
);
}
-add_action( 'admin_notices', 'sqlite_plugin_admin_notice' ); // Add the admin notices.
+add_action( is_multisite() ? 'network_admin_notices' : 'admin_notices', 'sqlite_plugin_admin_notice' ); // Add the admin notices.
// Remove the PL-plugin admin notices for SQLite.
remove_action( 'admin_notices', 'perflab_sqlite_plugin_admin_notice' );
diff --git a/wp/wp-content/plugins/sqlite-database-integration/admin-page.php b/wp/wp-content/plugins/sqlite-database-integration/admin-page.php
index 828156267..0d8c81f13 100644
--- a/wp/wp-content/plugins/sqlite-database-integration/admin-page.php
+++ b/wp/wp-content/plugins/sqlite-database-integration/admin-page.php
@@ -12,20 +12,30 @@
* @since 1.0.0
*/
function sqlite_add_admin_menu() {
- add_options_page(
+ $parent_slug = is_multisite() ? 'settings.php' : 'options-general.php';
+
+ add_submenu_page(
+ $parent_slug,
__( 'SQLite integration', 'sqlite-database-integration' ),
__( 'SQLite integration', 'sqlite-database-integration' ),
- 'manage_options',
+ sqlite_plugin_get_manage_capability(),
'sqlite-integration',
'sqlite_integration_admin_screen'
);
}
-add_action( 'admin_menu', 'sqlite_add_admin_menu' );
+add_action( is_multisite() ? 'network_admin_menu' : 'admin_menu', 'sqlite_add_admin_menu' );
/**
* The admin page contents.
*/
function sqlite_integration_admin_screen() {
+ if ( ! current_user_can( sqlite_plugin_get_manage_capability() ) ) {
+ wp_die(
+ esc_html__( 'Sorry, you are not allowed to access the SQLite integration settings.', 'sqlite-database-integration' ),
+ 403
+ );
+ }
+
$db_dropin_path = WP_CONTENT_DIR . '/db.php';
/*
@@ -56,7 +66,7 @@ function sqlite_integration_admin_screen() {
printf(
/* translators: 1: Admin URL to deactivate the module, 2: db.php drop-in path, */
__( 'The SQLite drop-in is enabled. To disable it and get back to your previous, MySQL database, you can deactivate the plugin. Alternatively, you can manually delete the %2$s file from your server.', 'sqlite-database-integration' ),
- esc_url( admin_url( 'plugins.php' ) ),
+ esc_url( self_admin_url( 'plugins.php' ) ),
'' . esc_html( basename( WP_CONTENT_DIR ) ) . '/db.php'
);
?>
@@ -78,7 +88,16 @@ function sqlite_integration_admin_screen() {
?>
-
+ '1',
+ 'upgrade-from-pl' => '1',
+ ),
+ sqlite_plugin_get_admin_page_url()
+ );
+ ?>
+
-
+
'sqlite-db-integration',
'parent' => 'top-secondary',
'title' => $title,
- 'href' => esc_url( admin_url( 'options-general.php?page=sqlite-integration' ) ),
+ 'href' => esc_url( sqlite_plugin_get_admin_page_url() ),
'meta' => false,
);
$admin_bar->add_node( $args );
}
add_action( 'admin_bar_menu', 'sqlite_plugin_adminbar_item', 999 );
+
+/**
+ * Get the SQLite integration admin page URL.
+ *
+ * @access private
+ *
+ * @return string Admin page URL.
+ */
+function sqlite_plugin_get_admin_page_url() {
+ if ( is_multisite() ) {
+ return network_admin_url( 'settings.php?page=sqlite-integration' );
+ }
+ return admin_url( 'options-general.php?page=sqlite-integration' );
+}
diff --git a/wp/wp-content/plugins/sqlite-database-integration/capabilities.php b/wp/wp-content/plugins/sqlite-database-integration/capabilities.php
new file mode 100644
index 000000000..deab5108c
--- /dev/null
+++ b/wp/wp-content/plugins/sqlite-database-integration/capabilities.php
@@ -0,0 +1,80 @@
+set_prefix( $table_prefix );
- // Get the perflab options, remove the database/sqlite module and update the option.
- $row = $wpdb_mysql->get_row( $wpdb_mysql->prepare( "SELECT option_value FROM $wpdb_mysql->options WHERE option_name = %s LIMIT 1", 'active_plugins' ) );
- if ( is_object( $row ) ) {
- $value = maybe_unserialize( $row->option_value );
- if ( is_array( $value ) ) {
- $value_flipped = array_flip( $value );
- $items = array_reverse( explode( DIRECTORY_SEPARATOR, SQLITE_MAIN_FILE ) );
- $item = $items[1] . DIRECTORY_SEPARATOR . $items[0];
- unset( $value_flipped[ $item ] );
- $value = array_flip( $value_flipped );
- $wpdb_mysql->update( $wpdb_mysql->options, array( 'option_value' => maybe_serialize( $value ) ), array( 'option_name' => 'active_plugins' ) );
- }
- }
+ sqlite_plugin_deactivate_in_mysql( $wpdb_mysql, $network_deactivating );
},
PHP_INT_MAX
);
@@ -66,3 +62,52 @@ function () {
wp_cache_flush();
}
register_deactivation_hook( SQLITE_MAIN_FILE, 'sqlite_plugin_remove_db_file' ); // Remove db.php file on plugin deactivation.
+
+/**
+ * Deactivate the plugin in the original MySQL database.
+ *
+ * @access private
+ *
+ * @param wpdb $wpdb_mysql MySQL database connection.
+ * @param bool $network_deactivating Whether the plugin is being deactivated network-wide.
+ */
+function sqlite_plugin_deactivate_in_mysql( $wpdb_mysql, $network_deactivating ) {
+ if ( $network_deactivating ) {
+ $network_id = get_current_network_id();
+ $row = $wpdb_mysql->get_row( $wpdb_mysql->prepare( "SELECT meta_value AS active_plugins FROM $wpdb_mysql->sitemeta WHERE site_id = %d AND meta_key = %s LIMIT 1", $network_id, 'active_sitewide_plugins' ) );
+ $table = $wpdb_mysql->sitemeta;
+ $value_column = 'meta_value';
+ $where = array(
+ 'site_id' => $network_id,
+ 'meta_key' => 'active_sitewide_plugins',
+ );
+ } else {
+ $row = $wpdb_mysql->get_row( $wpdb_mysql->prepare( "SELECT option_value AS active_plugins FROM $wpdb_mysql->options WHERE option_name = %s LIMIT 1", 'active_plugins' ) );
+ $table = $wpdb_mysql->options;
+ $value_column = 'option_value';
+ $where = array( 'option_name' => 'active_plugins' );
+ }
+
+ if ( ! is_object( $row ) ) {
+ return;
+ }
+
+ $active_plugins = maybe_unserialize( $row->active_plugins );
+ if ( ! is_array( $active_plugins ) ) {
+ return;
+ }
+
+ $item = plugin_basename( SQLITE_MAIN_FILE );
+ if ( $network_deactivating ) {
+ $key = $item;
+ } else {
+ $key = array_search( $item, $active_plugins, true );
+ }
+
+ if ( false === $key || ! array_key_exists( $key, $active_plugins ) ) {
+ return;
+ }
+
+ unset( $active_plugins[ $key ] );
+ $wpdb_mysql->update( $table, array( $value_column => maybe_serialize( $active_plugins ) ), $where );
+}
diff --git a/wp/wp-content/plugins/sqlite-database-integration/load.php b/wp/wp-content/plugins/sqlite-database-integration/load.php
index fe8fbd5d5..ba8e6f5d8 100644
--- a/wp/wp-content/plugins/sqlite-database-integration/load.php
+++ b/wp/wp-content/plugins/sqlite-database-integration/load.php
@@ -3,8 +3,9 @@
* Plugin Name: SQLite Database Integration
* Description: SQLite database driver drop-in.
* Author: The WordPress Team
- * Version: 3.0.0-rc.7
+ * Version: 3.0.0-rc.8
* Requires PHP: 7.2
+ * Network: true
* Textdomain: sqlite-database-integration
*
* This feature plugin allows WordPress to use SQLite instead of MySQL as its database.
@@ -20,6 +21,7 @@
define( 'SQLITE_MAIN_FILE', __FILE__ );
+require_once __DIR__ . '/capabilities.php';
require_once __DIR__ . '/admin-page.php';
require_once __DIR__ . '/activate.php';
require_once __DIR__ . '/deactivate.php';
diff --git a/wp/wp-content/plugins/sqlite-database-integration/readme.txt b/wp/wp-content/plugins/sqlite-database-integration/readme.txt
index d19a6521c..ae7a6fe95 100644
--- a/wp/wp-content/plugins/sqlite-database-integration/readme.txt
+++ b/wp/wp-content/plugins/sqlite-database-integration/readme.txt
@@ -4,7 +4,7 @@ Contributors: wordpressdotorg, aristath, janjakes, zieladam, berislav.grgic
Requires at least: 6.4
Tested up to: 7.0
Requires PHP: 7.2
-Stable tag: 3.0.0-rc.7
+Stable tag: 3.0.0-rc.8
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Tags: performance, database
@@ -44,6 +44,17 @@ with SQLite syntax and behavior.
== Changelog ==
+= 3.0.0-rc.8 =
+
+* Preserve aliases in `UPDATE JOIN` translation ([#462](https://github.com/WordPress/sqlite-database-integration/pull/462))
+* Restrict SQLite installation permissions ([#464](https://github.com/WordPress/sqlite-database-integration/pull/464))
+* Improve string escaping ([#466](https://github.com/WordPress/sqlite-database-integration/pull/466))
+* Stabilize WordPress E2E tests ([#459](https://github.com/WordPress/sqlite-database-integration/pull/459))
+* Address WordPress PHPUnit test fails: charset detection, length validation etc ([#331](https://github.com/WordPress/sqlite-database-integration/pull/331))
+* Add Unicode support to the user-defined `REVERSE()` function ([#453](https://github.com/WordPress/sqlite-database-integration/pull/453))
+* Simplify the MySQL-on-SQLite driver API ([#449](https://github.com/WordPress/sqlite-database-integration/pull/449))
+* Add WordPress 7.0 compatibility ([#450](https://github.com/WordPress/sqlite-database-integration/pull/450))
+
= 3.0.0-rc.7 =
* Preserve configured SQLite journal mode in driver wrapper ([#447](https://github.com/WordPress/sqlite-database-integration/pull/447))
diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-mysql-on-sqlite.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-mysql-on-sqlite.php
index c8f20e27a..7e06150b2 100644
--- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-mysql-on-sqlite.php
+++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-mysql-on-sqlite.php
@@ -1009,6 +1009,75 @@ public function exec( $query ) {
return $stmt->rowCount();
}
+ /**
+ * PDO API: Quote a string for use in a MySQL query.
+ *
+ * @param string $string The string to quote.
+ * @param int $type The PDO parameter type.
+ * @return string The quoted string.
+ */
+ #[ReturnTypeWillChange]
+ // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.stringFound
+ public function quote( $string, $type = PDO::PARAM_STR ) {
+ // Mirror PDO\MySQL::quote() value validation.
+ if (
+ is_array( $string )
+ || is_resource( $string )
+ || ( is_object( $string ) && ! method_exists( $string, '__toString' ) )
+ ) {
+ $given_type = is_object( $string ) ? get_class( $string ) : gettype( $string );
+ throw new TypeError(
+ sprintf(
+ 'WP_MySQL_On_SQLite::quote(): Argument #1 ($string) must be of type string, %s given',
+ $given_type
+ )
+ );
+ }
+ $string = (string) $string;
+
+ // Handle binary and national character prefixes.
+ $prefix = '';
+ if ( PDO::PARAM_LOB === ( $type & PDO::PARAM_LOB ) ) {
+ $prefix = '_binary';
+ } elseif (
+ PDO::PARAM_STR_NATL === ( $type & PDO::PARAM_STR_NATL )
+ && PDO::PARAM_STR_CHAR !== ( $type & PDO::PARAM_STR_CHAR )
+ ) {
+ $prefix = 'N';
+ }
+
+ /*
+ * PDO uses mysqlnd by default and can alternatively use libmysqlclient.
+ * This escaped character mapping matches the escaping of both drivers.
+ * Their malformed multibyte sequence handling is not needed for UTF-8.
+ *
+ * @see https://github.com/php/php-src/blob/dd6e76cce27aaa0ed9f7520648ed1081dfb6af36/ext/mysqlnd/mysqlnd_charset.c#L905
+ * @see https://github.com/mysql/mysql-server/blob/dc86e412f18b36ce271f791026714e8caa0ec919/mysys/charset.cc#L413
+ *
+ * We can't use "addcslashes()" here, because it has an unusual handling
+ * of the ASCII NULL and Control+Z characters, escaping them to "\000"
+ * and "\032" instead of "\0" and "\Z", respectively.
+ *
+ * It is important to use "strtr()" and not "str_replace()", because
+ * "str_replace()" applies replacements one after another, modifying
+ * intermediate changes rather than just the original string:
+ *
+ * - str_replace( [ 'a', 'b' ], [ 'b', 'c' ], 'ab' ); // 'cc' (bad)
+ * - strtr( 'ab', [ 'a' => 'b', 'b' => 'c' ] ); // 'bc' (good)
+ */
+ $backslash = chr( 92 );
+ $replacements = array(
+ chr( 0 ) => $backslash . '0', // An ASCII NULL character (\0).
+ chr( 10 ) => $backslash . 'n', // A newline (linefeed) character (\n).
+ chr( 13 ) => $backslash . 'r', // A carriage return character (\r).
+ $backslash => $backslash . $backslash, // A backslash character (\).
+ "'" => $backslash . "'", // A single quote character (').
+ '"' => $backslash . '"', // A double quote character (").
+ chr( 26 ) => $backslash . 'Z', // An ASCII 26 (Control+Z) character.
+ );
+ return $prefix . "'" . strtr( $string, $replacements ) . "'";
+ }
+
/**
* PDO API: Begin a transaction.
*
@@ -2147,10 +2216,24 @@ private function execute_update_statement( WP_Parser_Node $node ): void {
)->fetchAll( PDO::FETCH_COLUMN );
}
- $matched_tables = array_merge( $matched_temporary_tables, $matched_persistent_tables );
- $updates_multiple_tables = count( $matched_tables ) > 1;
- if ( 1 === count( $matched_tables ) ) {
- $table_or_alias = $matched_tables[0];
+ $matched_tables = array_merge( $matched_temporary_tables, $matched_persistent_tables );
+ $matched_aliases = array();
+ foreach ( $table_alias_map as $alias => $data ) {
+ // Derived tables do not have a table name.
+ if ( null === $data['table_name'] ) {
+ continue;
+ }
+
+ foreach ( $matched_tables as $matched_table ) {
+ if ( 0 === strcasecmp( $data['table_name'], $matched_table ) ) {
+ $matched_aliases[] = $alias;
+ break;
+ }
+ }
+ }
+ $updates_multiple_tables = count( $matched_aliases ) > 1;
+ if ( 1 === count( $matched_aliases ) ) {
+ $table_or_alias = $matched_aliases[0];
} else {
break;
}
@@ -3524,7 +3607,14 @@ private function execute_set_system_variable_statement(
if ( WP_MySQL_Lexer::SESSION_SYMBOL === $type ) {
if ( 'sql_mode' === $name ) {
- $modes = explode( ',', strtoupper( $value ) );
+ // MySQL ignores trailing ASCII spaces in SQL mode names.
+ $modes = explode( ',', strtoupper( $value ) );
+ foreach ( $modes as $i => $mode ) {
+ $modes[ $i ] = rtrim( $mode, ' ' );
+ }
+ if ( in_array( 'NO_BACKSLASH_ESCAPES', $modes, true ) ) {
+ throw $this->new_not_supported_exception( "SQL mode 'NO_BACKSLASH_ESCAPES'" );
+ }
$this->active_sql_modes = $modes;
} else {
$this->session_system_variables[ $name ] = $value;
diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-connection.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-connection.php
index 170ba01c8..2b85f078d 100644
--- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-connection.php
+++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-connection.php
@@ -57,9 +57,9 @@ class WP_SQLite_Connection {
/**
* A query logger callback.
*
- * @var callable(string, array): void
+ * @var (callable(string, array): void)|null
*/
- private $query_logger;
+ private $query_logger = null;
/**
* Constructor.
@@ -274,11 +274,11 @@ public function get_pdo(): PDO {
}
/**
- * Set a logger for the queries.
+ * Set or clear a logger for SQLite queries.
*
- * @param callable(string, array): void $logger A query logger callback.
+ * @param (callable(string, array): void)|null $logger A query logger callback, or null to clear it.
*/
- public function set_query_logger( callable $logger ): void {
+ public function set_query_logger( ?callable $logger ): void {
$this->query_logger = $logger;
}
}
diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/version.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/version.php
index bb54d64dc..e436dd0e7 100644
--- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/version.php
+++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/version.php
@@ -5,4 +5,4 @@
*
* This constant needs to be updated on plugin release!
*/
-define( 'SQLITE_DRIVER_VERSION', '3.0.0-rc.7' );
+define( 'SQLITE_DRIVER_VERSION', '3.0.0-rc.8' );
diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-db.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-db.php
index b94c66d20..d7100aa8e 100644
--- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-db.php
+++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-db.php
@@ -20,6 +20,13 @@ class WP_SQLite_DB extends wpdb {
*/
protected $dbh;
+ /**
+ * Whether the PDO instance was provided externally through $GLOBALS['@pdo'].
+ *
+ * @var bool
+ */
+ private $is_pdo_external;
+
/**
* Backward compatibility, see wpdb::$allow_unsafe_unquoted_parameters.
*
@@ -71,17 +78,59 @@ public function set_charset( $dbh, $charset = null, $collate = null ) {
}
/**
- * Method to get the character set for the database.
- * Hardcoded to utf8mb4 for now.
+ * Retrieves the character set for the given column.
+ *
+ * This overrides wpdb::get_col_charset() to enable the parent's implementation
+ * for SQLite by temporarily setting the is_mysql flag.
*
- * @param string $table The table name.
- * @param string $column The column name.
+ * @see wpdb::get_col_charset()
*
- * @return string The character set.
+ * @param string $table Table name.
+ * @param string $column Column name.
+ * @return string|false|WP_Error Column character set as a string. False if the column has
+ * no character set. WP_Error object on failure.
*/
public function get_col_charset( $table, $column ) {
- // Hardcoded for now.
- return 'utf8mb4';
+ $original_is_mysql = $this->is_mysql ?? null;
+
+ /*
+ * The parent method returns early when `$this->is_mysql` is falsy.
+ * Since SQLite doesn't set this flag, we enable it temporarily so
+ * the parent can run its full logic — querying column metadata via
+ * SHOW FULL COLUMNS (which the SQLite driver translates) and
+ * populating the `$this->col_meta` cache.
+ */
+ try {
+ $this->is_mysql = true;
+ return parent::get_col_charset( $table, $column );
+ } finally {
+ $this->is_mysql = $original_is_mysql;
+ }
+ }
+
+ /**
+ * Retrieves the maximum string length allowed in a given column.
+ *
+ * This overrides wpdb::get_col_length() to enable the parent's implementation
+ * for SQLite by temporarily setting the is_mysql flag.
+ *
+ * @see wpdb::get_col_length()
+ *
+ * @param string $table Table name.
+ * @param string $column Column name.
+ * @return array|false|WP_Error Column length information, false if the column has
+ * no length. WP_Error object on failure.
+ */
+ public function get_col_length( $table, $column ) {
+ $original_is_mysql = $this->is_mysql ?? null;
+
+ // See get_col_charset() for an explanation of the is_mysql flag.
+ try {
+ $this->is_mysql = true;
+ return parent::get_col_length( $table, $column );
+ } finally {
+ $this->is_mysql = $original_is_mysql;
+ }
}
/**
@@ -130,14 +179,104 @@ public function set_sql_mode( $modes = array() ) {
/**
* Closes the current database connection.
- * Noop in SQLite.
*
- * @return bool True to indicate the connection was successfully closed.
+ * This overrides wpdb::close() while closely mirroring its implementation.
+ *
+ * @see wpdb::close()
+ *
+ * @return bool True if the connection was successfully closed,
+ * false if it wasn't, or if the connection doesn't exist.
*/
public function close() {
+ if ( ! $this->dbh ) {
+ return false;
+ }
+
+ $connection = $this->dbh->get_connection();
+ $pdo = $connection->get_pdo();
+
+ try {
+ if ( $this->dbh->inTransaction() ) {
+ $this->dbh->rollBack();
+ } elseif ( $pdo->inTransaction() ) {
+ $pdo->rollBack();
+ } else {
+ /*
+ * On PHP < 8.4, PDO cannot detect transactions started via SQL.
+ * A savepoint ensures ROLLBACK succeeds with or without one.
+ */
+ $pdo->exec( 'SAVEPOINT wp_sqlite_db_close' );
+ $pdo->exec( 'ROLLBACK' );
+ }
+ } catch ( Throwable $e ) {
+ return false;
+ }
+
+ /*
+ * @TODO: Replace and deprecate the $GLOBALS['@pdo'] injection mechanism.
+ * PDO has no close method and is released only when all references are unset.
+ * Until then, retain external PDOs so reconnects reuse the same database.
+ */
+ if (
+ ! $this->is_pdo_external
+ && isset( $GLOBALS['@pdo'] )
+ && $GLOBALS['@pdo'] === $pdo
+ ) {
+ unset( $GLOBALS['@pdo'] );
+ }
+
+ $connection->set_query_logger( null );
+ $this->result = null;
+ $this->dbh = null;
+ $this->ready = false;
+ $this->has_connected = false;
+
return true;
}
+ /**
+ * Determines the best charset and collation to use given a charset and collation.
+ *
+ * For example, when able, utf8mb4 should be used instead of utf8.
+ *
+ * This overrides wpdb::determine_charset() while closely mirroring its implementation.
+ * The override is needed because the parent checks for a mysqli connection object.
+ *
+ * @param string $charset The character set to check.
+ * @param string $collate The collation to check.
+ * @return array {
+ * The most appropriate character set and collation to use.
+ *
+ * @type string $charset Character set.
+ * @type string $collate Collation.
+ * }
+ */
+ public function determine_charset( $charset, $collate ) {
+ if ( ! $this->dbh ) {
+ return compact( 'charset', 'collate' );
+ }
+
+ if ( 'utf8' === $charset ) {
+ $charset = 'utf8mb4';
+ }
+
+ if ( 'utf8mb4' === $charset ) {
+ // _general_ is outdated, so we can upgrade it to _unicode_, instead.
+ if ( ! $collate || 'utf8_general_ci' === $collate ) {
+ $collate = 'utf8mb4_unicode_ci';
+ } else {
+ $collate = str_replace( 'utf8_', 'utf8mb4_', $collate );
+ }
+ }
+
+ // _unicode_520_ is a better collation, we should use that when it's available.
+ if ( $this->has_cap( 'utf8mb4_520' ) && 'utf8mb4_unicode_ci' === $collate ) {
+ $collate = 'utf8mb4_unicode_520_ci';
+ }
+
+ return compact( 'charset', 'collate' );
+ }
+
/**
* Method to select the database connection.
*
@@ -162,12 +301,20 @@ public function select( $db, $dbh = null ) {
* @param string $data The string to escape.
*
* @return string escaped
+ * @throws RuntimeException When the database connection is not initialized.
*/
public function _real_escape( $data ) {
if ( ! is_scalar( $data ) ) {
return '';
}
- $escaped = addslashes( $data );
+
+ if ( ! $this->dbh ) {
+ throw new RuntimeException( 'Cannot escape data without an active database connection.' );
+ }
+
+ // Escape the string without bounding quotes to mirror mysqli_real_escape_string().
+ $quoted = $this->dbh->quote( (string) $data );
+ $escaped = substr( $quoted, 1, -1 );
return $this->add_placeholder_escape( $escaped );
}
@@ -268,19 +415,21 @@ public function flush() {
* @see wpdb::db_connect()
*
* @param bool $allow_bail Not used.
- * @return void
+ * @return bool True on a successful connection, false on failure.
*/
public function db_connect( $allow_bail = true ) {
if ( $this->dbh ) {
- return;
+ return $this->ready;
}
- $this->init_charset();
- $pdo = null;
- if ( isset( $GLOBALS['@pdo'] ) ) {
- $pdo = $GLOBALS['@pdo'];
+ $this->last_error = '';
+ if ( ! isset( $this->charset ) ) {
+ $this->init_charset();
}
+ $this->is_pdo_external = isset( $GLOBALS['@pdo'] );
+ $pdo = $this->is_pdo_external ? $GLOBALS['@pdo'] : null;
+
// 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';
@@ -317,7 +466,7 @@ public function db_connect( $allow_bail = true ) {
if ( null !== $pdo ) {
$options['pdo'] = $pdo;
}
- $this->dbh = new WP_MySQL_On_SQLite(
+ $dbh = new WP_MySQL_On_SQLite(
sprintf(
'mysql-on-sqlite:path=%s;dbname=%s',
str_replace( ';', ';;', FQDB ),
@@ -327,27 +476,38 @@ public function db_connect( $allow_bail = true ) {
null,
$options
);
- $this->dbh->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO
- $GLOBALS['@pdo'] = $this->dbh->get_connection()->get_pdo();
+ $dbh->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO
+ $pdo = $dbh->get_connection()->get_pdo();
+ $this->dbh = $dbh;
+ $GLOBALS['@pdo'] = $pdo;
} catch ( Throwable $e ) {
$this->last_error = $this->format_error_message( $e );
}
if ( $this->last_error ) {
return false;
}
+
+ $this->has_connected = true;
+ $this->set_charset( $this->dbh );
+
$this->ready = true;
$this->set_sql_mode();
+ return true;
}
/**
- * Method to dummy out wpdb::check_connection()
+ * Checks that the database connection is available.
*
* @param bool $allow_bail Not used.
*
- * @return bool
+ * @return bool True when the connection is available, false otherwise.
*/
public function check_connection( $allow_bail = true ) {
- return true;
+ if ( $this->dbh ) {
+ return true;
+ }
+
+ return $this->db_connect( $allow_bail );
}
/**
@@ -420,14 +580,13 @@ public function query( $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.
+ * @TODO: wpdb uses "$this->check_current_query" and table metadata to
+ * reject queries containing invalid text. Implement equivalent handling
+ * for SQLite without relying on the MySQL-specific conversion pipeline.
*
- * 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
+ * PCRE's "u" modifier can validate UTF-8 without constructing a converted
+ * query copy: 1 === preg_match( '//u', $query ). The implementation must
+ * preserve wpdb's exemptions for prevalidated and binary data.
*/
$this->_do_query( $query );
@@ -559,21 +718,22 @@ protected function load_col_info() {
}
/**
- * Method to return what the database can do.
+ * Determines whether the database supports a given feature.
*
- * This overrides wpdb::has_cap() to avoid using MySQL functions.
- * SQLite supports subqueries, but not support collation, group_concat and set_charset.
+ * The utf8mb4 check is handled here because older WordPress versions inspect
+ * the MySQL client library. All other capabilities use the parent logic.
*
* @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.
+ * @param string $db_cap The feature to check for.
+ * @return bool True when the database feature is supported, false otherwise.
*/
public function has_cap( $db_cap ) {
- return 'subqueries' === strtolower( $db_cap );
+ if ( 'utf8mb4' === strtolower( $db_cap ) ) {
+ return true;
+ }
+
+ return parent::has_cap( $db_cap );
}
/**
@@ -592,9 +752,13 @@ public function db_version() {
/**
* Returns the version of the SQLite engine.
*
- * @return string SQLite engine version as a string.
+ * @return string SQLite engine version, or an empty string while disconnected.
*/
public function db_server_info() {
+ if ( ! $this->dbh ) {
+ return '';
+ }
+
return $this->dbh->get_sqlite_version();
}