diff --git a/admin/load.php b/admin/load.php index 8b80f899be..bdda5d2487 100644 --- a/admin/load.php +++ b/admin/load.php @@ -163,7 +163,7 @@ function perflab_render_modules_page_field( $module_slug, $module_data, $module_ - +
diff --git a/modules/database/sqlite/can-load.php b/modules/database/sqlite/can-load.php index d375c1f181..ca52eee515 100644 --- a/modules/database/sqlite/can-load.php +++ b/modules/database/sqlite/can-load.php @@ -13,8 +13,9 @@ */ return function() { - // If the PERFLAB_SQLITE_DB_DROPIN_VERSION constant is defined, then the module is already active. - if ( defined( 'PERFLAB_SQLITE_DB_DROPIN_VERSION' ) ) { + // If the PERFLAB_SQLITE_DB_DROPIN_VERSION or SQLITE_DB_DROPIN_VERSION constants + // are defined, then the module is already active. + if ( defined( 'PERFLAB_SQLITE_DB_DROPIN_VERSION' ) || defined( 'SQLITE_DB_DROPIN_VERSION' ) ) { return true; } diff --git a/modules/database/sqlite/constants.php b/modules/database/sqlite/constants.php index 4c443c3831..5a6118544c 100644 --- a/modules/database/sqlite/constants.php +++ b/modules/database/sqlite/constants.php @@ -6,12 +6,23 @@ * @package performance-lab */ +// Backwards-compatibility. +if ( ! defined( 'PERFLAB_SQLITE_DB_DROPIN_VERSION' ) && defined( 'SQLITE_DB_DROPIN_VERSION' ) ) { + define( 'PERFLAB_SQLITE_DB_DROPIN_VERSION', SQLITE_DB_DROPIN_VERSION ); +} + // Temporary - This will be in wp-config.php once SQLite is merged in Core. -if ( ! defined( 'DATABASE_TYPE' ) ) { - if ( defined( 'PERFLAB_SQLITE_DB_DROPIN_VERSION' ) ) { - define( 'DATABASE_TYPE', 'sqlite' ); +if ( ! defined( 'DB_ENGINE' ) ) { + if ( defined( 'SQLITE_DB_DROPIN_VERSION' ) ) { + define( 'DB_ENGINE', 'sqlite' ); + } elseif ( defined( 'DATABASE_ENGINE' ) ) { + // Backwards compatibility with previous versions of the standalone plugin. + define( 'DB_ENGINE', DATABASE_ENGINE ); + } elseif ( defined( 'DATABASE_TYPE' ) ) { + // Backwards compatibility with previous versions of the performance-lab plugin. + define( 'DB_ENGINE', DATABASE_TYPE ); } else { - define( 'DATABASE_TYPE', 'mysql' ); + define( 'DB_ENGINE', 'mysql' ); } } diff --git a/modules/database/sqlite/site-health.php b/modules/database/sqlite/site-health.php index 0cbe0d9c9c..c0b9977374 100644 --- a/modules/database/sqlite/site-health.php +++ b/modules/database/sqlite/site-health.php @@ -1,40 +1,34 @@ 'DATABASE_TYPE', - 'value' => ( defined( 'DATABASE_TYPE' ) ? DATABASE_TYPE : __( 'Undefined', 'performance-lab' ) ), - 'debug' => ( defined( 'DATABASE_TYPE' ) ? DATABASE_TYPE : 'undefined' ), + $info['wp-constants']['fields']['DB_ENGINE'] = array( + 'label' => 'DB_ENGINE', + 'value' => ( defined( 'DB_ENGINE' ) ? DB_ENGINE : __( 'Undefined', 'performance-lab' ) ), + 'debug' => ( defined( 'DB_ENGINE' ) ? DB_ENGINE : 'undefined' ), ); - $info['wp-database']['fields']['database_type'] = array( + $info['wp-database']['fields']['db_engine'] = array( 'label' => __( 'Database type', 'performance-lab' ), - 'value' => 'sqlite' === $database_type ? 'SQLite' : 'MySQL/MariaDB', + 'value' => 'sqlite' === $db_engine ? 'SQLite' : 'MySQL/MariaDB', ); - if ( 'sqlite' === $database_type ) { + if ( 'sqlite' === $db_engine ) { $info['wp-database']['fields']['database_version'] = array( 'label' => __( 'SQLite version', 'performance-lab' ), 'value' => class_exists( 'SQLite3' ) ? SQLite3::version()['versionString'] : null, @@ -65,4 +59,24 @@ function perflab_sqlite_plugin_filter_debug_data( $info ) { return $info; } -add_filter( 'debug_information', 'perflab_sqlite_plugin_filter_debug_data' ); // Filter debug data in site-health screen. +add_filter( 'debug_information', 'sqlite_plugin_filter_debug_data' ); // Filter debug data in site-health screen. + +/** + * Filter site_status tests in site-health screen. + * + * When the plugin gets merged in wp-core, these should be merged in src/wp-admin/includes/class-wp-site-health.php + * + * @param array $tests The tests. + * @return array + */ +function sqlite_plugin_filter_site_status_tests( $tests ) { + $db_engine = defined( 'DB_ENGINE' ) && 'sqlite' === DB_ENGINE ? 'sqlite' : 'mysql'; + + if ( 'sqlite' === $db_engine ) { + unset( $tests['direct']['utf8mb4_support'] ); + unset( $tests['direct']['sql_server'] ); + } + + return $tests; +} +add_filter( 'site_status_tests', 'sqlite_plugin_filter_site_status_tests' ); diff --git a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-alter-query.php b/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-alter-query.php deleted file mode 100644 index d9909732fa..0000000000 --- a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-alter-query.php +++ /dev/null @@ -1,611 +0,0 @@ -_query = 'SELECT 1=1'; - return $this->_query; - } - - $tmp_query = array(); - $re_command = ''; - $command = str_ireplace( $match[0], '', $query ); - $tmp_tokens['query_type'] = trim( $match[1] ); - $tmp_tokens['table_name'] = trim( $match[2] ); - $command_array = explode( ',', $command ); - - $single_command = array_shift( $command_array ); - if ( ! empty( $command_array ) ) { - $re_command = "ALTER TABLE {$tmp_tokens['table_name']} "; - $re_command .= implode( ',', $command_array ); - } - $command_tokens = $this->command_tokenizer( $single_command ); - if ( empty( $command_tokens ) ) { - $this->_query = 'SELECT 1=1'; - return $this->_query; - } - $tokens = array_merge( $tmp_tokens, $command_tokens ); - $command_name = strtolower( $tokens['command'] ); - switch ( $command_name ) { - case 'add column': - case 'rename to': - case 'add index': - case 'drop index': - $tmp_query = $this->handle_single_command( $tokens ); - break; - - case 'add primary key': - $tmp_query = $this->handle_add_primary_key( $tokens ); - break; - - case 'drop primary key': - $tmp_query = $this->handle_drop_primary_key( $tokens ); - break; - - case 'modify column': - $tmp_query = $this->handle_modify_command( $tokens ); - break; - - case 'change column': - $tmp_query = $this->handle_change_command( $tokens ); - break; - - case 'alter column': - $tmp_query = $this->handle_alter_command( $tokens ); - break; - - default: - break; - } - if ( ! is_array( $tmp_query ) ) { - $this->_query[] = $tmp_query; - } else { - $this->_query = $tmp_query; - } - if ( '' !== $re_command ) { - $this->_query = array_merge( $this->_query, array( 'recursion' => $re_command ) ); - } - - return $this->_query; - } - - /** - * Function to analyze ALTER TABLE command and sets the data to an array. - * - * @param string $command The command string. - * - * @return array - * @access private - */ - private function command_tokenizer( $command ) { - $tokens = array(); - $pattern = '/^(ADD|DROP|RENAME|MODIFY|CHANGE|ALTER)\\s*(\\w+)?\\s*(\\w+(\(.+\)|))?\\s*/ims'; - if ( preg_match( $pattern, $command, $match ) ) { - $the_rest = str_ireplace( $match[0], '', $command ); - $match_1 = trim( $match[1] ); - $match_2 = trim( $match[2] ); - $match_3 = isset( $match[3] ) ? trim( $match[3] ) : ''; - switch ( strtolower( $match_1 ) ) { - case 'add': - if ( in_array( strtolower( $match_2 ), array( 'fulltext', 'constraint', 'foreign' ), true ) ) { - break; - } - - if ( stripos( 'column', $match_2 ) !== false ) { - $tokens['command'] = $match_1 . ' ' . $match_2; - $tokens['column_name'] = $match_3; - $tokens['column_def'] = trim( $the_rest ); - } elseif ( stripos( 'primary', $match_2 ) !== false ) { - $tokens['command'] = $match_1 . ' ' . $match_2 . ' ' . $match_3; - $tokens['column_name'] = $the_rest; - } elseif ( stripos( 'unique', $match_2 ) !== false ) { - list($index_name, $col_name) = preg_split( '/[\(\)]/s', trim( $the_rest ), -1, PREG_SPLIT_DELIM_CAPTURE ); - $tokens['unique'] = true; - $tokens['command'] = $match_1 . ' ' . $match_3; - $tokens['index_name'] = trim( $index_name ); - $tokens['column_name'] = '(' . trim( $col_name ) . ')'; - } elseif ( in_array( strtolower( $match_2 ), array( 'index', 'key' ), true ) ) { - $tokens['command'] = $match_1 . ' ' . $match_2; - $tokens['index_name'] = $match_3; - if ( '' === $match_3 ) { - $tokens['index_name'] = str_replace( array( '(', ')' ), '', $the_rest ); - } - $tokens['column_name'] = trim( $the_rest ); - } else { - $tokens['command'] = $match_1 . ' COLUMN'; - $tokens['column_name'] = $match_2; - $tokens['column_def'] = $match_3 . ' ' . $the_rest; - } - break; - - case 'drop': - if ( stripos( 'column', $match_2 ) !== false ) { - $tokens['command'] = $match_1 . ' ' . $match_2; - $tokens['column_name'] = trim( $match_3 ); - } elseif ( stripos( 'primary', $match_2 ) !== false ) { - $tokens['command'] = $match_1 . ' ' . $match_2 . ' ' . $match_3; - } elseif ( in_array( strtolower( $match_2 ), array( 'index', 'key' ), true ) ) { - $tokens['command'] = $match_1 . ' ' . $match_2; - $tokens['index_name'] = $match_3; - } elseif ( stripos( 'primary', $match_2 ) !== false ) { - $tokens['command'] = $match_1 . ' ' . $match_2 . ' ' . $match_3; - } else { - $tokens['command'] = $match_1 . ' COLUMN'; - $tokens['column_name'] = $match_2; - } - break; - - case 'rename': - if ( stripos( 'to', $match_2 ) !== false ) { - $tokens['command'] = $match_1 . ' ' . $match_2; - $tokens['column_name'] = $match_3; - } else { - $tokens['command'] = $match_1 . ' TO'; - $tokens['column_name'] = $match_2; - } - break; - - case 'modify': - if ( stripos( 'column', $match_2 ) !== false ) { - $tokens['command'] = $match_1 . ' ' . $match_2; - $tokens['column_name'] = $match_3; - $tokens['column_def'] = trim( $the_rest ); - } else { - $tokens['command'] = $match_1 . ' COLUMN'; - $tokens['column_name'] = $match_2; - $tokens['column_def'] = $match_3 . ' ' . trim( $the_rest ); - } - break; - - case 'change': - $the_rest = trim( $the_rest ); - if ( stripos( 'column', $match_2 ) !== false ) { - $tokens['command'] = $match_1 . ' ' . $match_2; - $tokens['old_column'] = $match_3; - list($new_col) = explode( ' ', $the_rest ); - $tmp_col = preg_replace( '/\(.+?\)/im', '', $new_col ); - if ( array_key_exists( strtolower( $tmp_col ), $this->array_types ) ) { - $tokens['column_def'] = $the_rest; - } else { - $tokens['new_column'] = $new_col; - $col_def = str_replace( $new_col, '', $the_rest ); - $tokens['column_def'] = trim( $col_def ); - } - } else { - $tokens['command'] = $match_1 . ' column'; - $tokens['old_column'] = $match_2; - $tmp_col = preg_replace( '/\(.+?\)/im', '', $match_3 ); - if ( array_key_exists( strtolower( $tmp_col ), $this->array_types ) ) { - $tokens['column_def'] = $match_3 . ' ' . $the_rest; - } else { - $tokens['new_column'] = $match_3; - $tokens['column_def'] = $the_rest; - } - } - break; - - case 'alter': - $tokens['default_command'] = 'DROP DEFAULT'; - if ( stripos( 'column', $match_2 ) !== false ) { - $tokens['command'] = $match_1 . ' ' . $match_2; - $tokens['column_name'] = $match_3; - list($set_or_drop) = explode( ' ', $the_rest ); - if ( stripos( 'set', $set_or_drop ) !== false ) { - $tokens['default_command'] = 'SET DEFAULT'; - $default_value = str_ireplace( 'set default', '', $the_rest ); - $tokens['default_value'] = trim( $default_value ); - } - } else { - $tokens['command'] = $match_1 . ' COLUMN'; - $tokens['column_name'] = $match_2; - if ( stripos( 'set', $match_3 ) !== false ) { - $tokens['default_command'] = 'SET DEFAULT'; - $default_value = str_ireplace( 'default', '', $the_rest ); - $tokens['default_value'] = trim( $default_value ); - } - } - break; - - default: - break; - } - } - return $tokens; - } - - /** - * Function to handle single command. - * - * @access private - * - * @param array $queries An array of string queries. - * - * @return string - */ - private function handle_single_command( $queries ) { - $tokenized_query = $queries; - - // Command is "add column". - if ( stripos( $tokenized_query['command'], 'add column' ) !== false ) { - $column_def = $this->convert_field_types( $tokenized_query['column_name'], $tokenized_query['column_def'] ); - return "ALTER TABLE {$tokenized_query['table_name']} ADD COLUMN {$tokenized_query['column_name']} $column_def"; - } - - // Command is "rename". - if ( stripos( $tokenized_query['command'], 'rename' ) !== false ) { - return "ALTER TABLE {$tokenized_query['table_name']} RENAME TO {$tokenized_query['column_name']}"; - } - - // Command is "add index". - if ( stripos( $tokenized_query['command'], 'add index' ) !== false ) { - $unique = isset( $tokenized_query['unique'] ) ? 'UNIQUE' : ''; - return "CREATE $unique INDEX IF NOT EXISTS {$tokenized_query['index_name']} ON {$tokenized_query['table_name']} {$tokenized_query['column_name']}"; - } - - // Command is "drop index". - if ( stripos( $tokenized_query['command'], 'drop index' ) !== false ) { - return "DROP INDEX IF EXISTS {$tokenized_query['index_name']}"; - } - - // Fallback. - return 'SELECT 1=1'; - } - - /** - * Function to handle ADD PRIMARY KEY. - * - * @access private - * - * @param array $queries An array of string queries. - * - * @return array - */ - private function handle_add_primary_key( $queries ) { - $tokenized_query = $queries; - $tbl_name = $tokenized_query['table_name']; - $temp_table = 'temp_' . $tokenized_query['table_name']; - $query_obj = ( new Perflab_SQLite_DB() )->get_results( "SELECT sql FROM sqlite_master WHERE tbl_name='$tbl_name'" ); - for ( $i = 0; $i < count( $query_obj ); $i++ ) { - $index_queries[ $i ] = $query_obj[ $i ]->sql; - } - $table_query = ", PRIMARY KEY {$tokenized_query['column_name']}"; - $query = array( - $table_query, - "INSERT INTO $temp_table SELECT * FROM {$tokenized_query['table_name']}", - "DROP TABLE IF EXISTS {$tokenized_query['table_name']}", - "ALTER TABLE $temp_table RENAME TO {$tokenized_query['table_name']}", - ); - - foreach ( $index_queries as $index ) { - $query[] = $index; - } - - return $query; - } - - /** - * Function to handle DROP PRIMARY KEY. - * - * @access private - * - * @param array $queries An of string $queries. - * - * @return array - */ - private function handle_drop_primary_key( $queries ) { - $tokenized_query = $queries; - $temp_table = 'temp_' . $tokenized_query['table_name']; - $query_obj = ( new Perflab_SQLite_DB() )->get_results( "SELECT sql FROM sqlite_master WHERE tbl_name='{$tokenized_query['table_name']}'" ); - for ( $i = 0; $i < count( $query_obj ); $i++ ) { - $index_queries[ $i ] = $query_obj[ $i ]->sql; - } - $table_query = array_shift( $index_queries ); - $pattern1 = '/^\\s*PRIMARY\\s*KEY\\s*\(.*\)/im'; - $pattern2 = '/^\\s*.*(PRIMARY\\s*KEY\\s*(:?AUTOINCREMENT|))\\s*(?!\()/im'; - if ( preg_match( $pattern1, $table_query, $match ) ) { - $table_query = str_replace( $match[0], '', $table_query ); - } elseif ( preg_match( $pattern2, $table_query, $match ) ) { - $table_query = str_replace( $match[1], '', $table_query ); - } - $table_query = str_replace( $tokenized_query['table_name'], $temp_table, $table_query ); - $query = array( - $table_query, - "INSERT INTO $temp_table SELECT * FROM {$tokenized_query['table_name']}", - "DROP TABLE IF EXISTS {$tokenized_query['table_name']}", - "ALTER TABLE $temp_table RENAME TO {$tokenized_query['table_name']}", - ); - foreach ( $index_queries as $index ) { - $query[] = $index; - } - - return $query; - } - - /** - * Function to handle MODIFY COLUMN. - * - * @access private - * - * @param array $queries An array of string queries. - * - * @return string|array - */ - private function handle_modify_command( $queries ) { - $tokenized_query = $queries; - $temp_table = 'temp_' . $tokenized_query['table_name']; - $column_def = $this->convert_field_types( $tokenized_query['column_name'], $tokenized_query['column_def'] ); - $query_obj = ( new Perflab_SQLite_DB() )->get_results( "SELECT sql FROM sqlite_master WHERE tbl_name='{$tokenized_query['table_name']}'" ); - for ( $i = 0; $i < count( $query_obj ); $i++ ) { - $index_queries[ $i ] = $query_obj[ $i ]->sql; - } - $create_query = array_shift( $index_queries ); - if ( stripos( $create_query, $tokenized_query['column_name'] ) === false ) { - return 'SELECT 1=1'; - } - $pattern = "/{$tokenized_query['column_name']}\\s*{$column_def}\\s*[,)]/i"; - if ( preg_match( $pattern, $create_query ) ) { - return 'SELECT 1=1'; - } - $pattern_1 = "/{$tokenized_query['table_name']}/i"; - $pattern_2 = "/\\b{$tokenized_query['column_name']}\\s*.*(?=,)/ims"; - $pattern_3 = "/\\b{$tokenized_query['column_name']}\\s*.*(?=\))/ims"; - - $create_query = preg_replace( $pattern_1, $temp_table, $create_query ); - if ( preg_match( $pattern_2, $create_query ) ) { - $create_query = preg_replace( $pattern_2, "{$tokenized_query['column_name']} {$column_def}", $create_query ); - } elseif ( preg_match( $pattern_3, $create_query ) ) { - $create_query = preg_replace( $pattern_3, "{$tokenized_query['column_name']} {$column_def}", $create_query ); - } - $query = array( - $create_query, - "INSERT INTO $temp_table SELECT * FROM {$tokenized_query['table_name']}", - "DROP TABLE IF EXISTS {$tokenized_query['table_name']}", - "ALTER TABLE $temp_table RENAME TO {$tokenized_query['table_name']}", - ); - foreach ( $index_queries as $index ) { - $query[] = $index; - } - - return $query; - } - - /** - * Function to handle CHANGE COLUMN. - * - * @access private - * - * @param array $queries An array of string queries. - * - * @return string|array - */ - private function handle_change_command( $queries ) { - $col_check = false; - $old_fields = ''; - $tokenized_query = $queries; - $temp_table = 'temp_' . $tokenized_query['table_name']; - $column_name = $tokenized_query['old_column']; - if ( isset( $tokenized_query['new_column'] ) ) { - $column_name = $tokenized_query['new_column']; - } - $column_def = $this->convert_field_types( $column_name, $tokenized_query['column_def'] ); - $col_obj = ( new Perflab_SQLite_DB() )->get_results( "SHOW COLUMNS FROM {$tokenized_query['table_name']}" ); - foreach ( $col_obj as $col ) { - if ( stripos( $col->Field, $tokenized_query['old_column'] ) !== false ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $col_check = true; - } - $old_fields .= $col->Field . ','; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - } - if ( false === $col_check ) { - return 'SELECT 1=1'; - } - $old_fields = rtrim( $old_fields, ',' ); - $new_fields = str_ireplace( $tokenized_query['old_column'], $column_name, $old_fields ); - $query_obj = ( new Perflab_SQLite_DB() )->get_results( "SELECT sql FROM sqlite_master WHERE tbl_name='{$tokenized_query['table_name']}'" ); - for ( $i = 0; $i < count( $query_obj ); $i++ ) { - $index_queries[ $i ] = $query_obj[ $i ]->sql; - } - $create_query = array_shift( $index_queries ); - $create_query = preg_replace( "/{$tokenized_query['table_name']}/i", $temp_table, $create_query ); - - $pattern_1 = "/\\b{$tokenized_query['old_column']}\\s*(.+?)(?=,)/ims"; - $pattern_2 = "/\\b{$tokenized_query['old_column']}\\s*(.+?)(?=\))/ims"; - if ( preg_match( $pattern_1, $create_query, $match ) ) { - if ( stripos( trim( $match[1] ), $column_def ) !== false ) { - return 'SELECT 1=1'; - } - $pattern = "/\\b{$tokenized_query['old_column']}\\s*.+?(?=,)/ims"; - $create_query = preg_replace( $pattern, "{$column_name} {$column_def}", $create_query, 1 ); - } elseif ( preg_match( $pattern_2, $create_query, $match ) ) { - if ( stripos( trim( $match[1] ), $column_def ) !== false ) { - return 'SELECT 1=1'; - } - $pattern = "/\\b{$tokenized_query['old_column']}\\s*.*(?=\))/ims"; - $create_query = preg_replace( $pattern, "{$column_name} {$column_def}", $create_query, 1 ); - } - $query = array( - $create_query, - "INSERT INTO $temp_table ($new_fields) SELECT $old_fields FROM {$tokenized_query['table_name']}", - "DROP TABLE IF EXISTS {$tokenized_query['table_name']}", - "ALTER TABLE $temp_table RENAME TO {$tokenized_query['table_name']}", - ); - foreach ( $index_queries as $index ) { - $query[] = $index; - } - - return $query; - } - - /** - * Function to handle ALTER COLUMN. - * - * @access private - * - * @param array $queries An array of string queries. - * - * @return string|array - */ - private function handle_alter_command( $queries ) { - $tokenized_query = $queries; - $temp_table = 'temp_' . $tokenized_query['table_name']; - $def_value = null; - if ( isset( $tokenized_query['default_value'] ) ) { - $def_value = $this->convert_field_types( $tokenized_query['column_name'], $tokenized_query['default_value'] ); - $def_value = 'DEFAULT ' . $def_value; - } - $query_obj = ( new Perflab_SQLite_DB() )->get_results( "SELECT sql FROM sqlite_master WHERE tbl_name='{$tokenized_query['table_name']}'" ); - for ( $i = 0; $i < count( $query_obj ); $i++ ) { - $index_queries[ $i ] = $query_obj[ $i ]->sql; - } - $create_query = array_shift( $index_queries ); - if ( stripos( $create_query, $tokenized_query['column_name'] ) === false ) { - return 'SELECT 1=1'; - } - $pattern_1 = "/\\s*({$tokenized_query['column_name']})\\s*(.*)?(DEFAULT\\s*.*)[,)]/im"; - $pattern_2 = "/\\s*({$tokenized_query['column_name']})\\s*(.*)?[,)]/im"; - if ( preg_match( $pattern_1, $create_query, $match ) ) { - $col_name = trim( $match[1] ); - $col_def = trim( $match[2] ); - $col_def_esc = str_replace( array( '(', ')' ), array( '\(', '\)' ), $col_def ); - $checked_col_def = $this->convert_field_types( $col_name, $col_def ); - $old_default = trim( $match[3] ); - $replacement = $col_name . ' ' . $checked_col_def; - if ( ! is_null( $def_value ) ) { - $replacement .= ' ' . $def_value; - } - $pattern = "/$col_name\\s*$col_def_esc\\s*$old_default/im"; - $create_query = preg_replace( $pattern, $replacement, $create_query ); - $create_query = str_ireplace( $tokenized_query['table_name'], $temp_table, $create_query ); - } elseif ( preg_match( $pattern_2, $create_query, $match ) ) { - $col_name = trim( $match[1] ); - $col_def = trim( $match[2] ); - $col_def_esc = str_replace( array( '(', ')' ), array( '\(', '\)' ), $col_def ); - $checked_col_def = $this->convert_field_types( $col_name, $col_def ); - $replacement = $col_name . ' ' . $checked_col_def; - if ( ! is_null( $def_value ) ) { - $replacement .= ' ' . $def_value; - } - $pattern = "/$col_name\\s*$col_def_esc/im"; - $create_query = preg_replace( $pattern, $replacement, $create_query ); - $create_query = str_ireplace( $tokenized_query['table_name'], $temp_table, $create_query ); - } else { - return 'SELECT 1=1'; - } - $query = array( - $create_query, - "INSERT INTO $temp_table SELECT * FROM {$tokenized_query['table_name']}", - "DROP TABLE IF EXISTS {$tokenized_query['table_name']}", - "ALTER TABLE $temp_table RENAME TO {$tokenized_query['table_name']}", - ); - foreach ( $index_queries as $index ) { - $query[] = $index; - } - - return $query; - } - - /** - * Function to change the field definition to SQLite compatible data type. - * - * @access private - * - * @param string $col_name Column name. - * @param string $col_def Column definition. - * - * @return string - */ - private function convert_field_types( $col_name, $col_def ) { - $array_curtime = array( 'current_timestamp', 'current_time', 'current_date' ); - $array_reptime = array( "'0000-00-00 00:00:00'", "'0000-00-00 00:00:00'", "'0000-00-00'" ); - $def_string = str_replace( '`', '', $col_def ); - foreach ( $this->array_types as $o => $r ) { - $pattern = "/\\b$o\\s*(\([^\)]*\)*)?\\s*/ims"; - if ( preg_match( $pattern, $def_string ) ) { - $def_string = preg_replace( $pattern, "$r ", $def_string ); - break; - } - } - $def_string = preg_replace( '/unsigned/im', '', $def_string ); - $def_string = preg_replace( '/auto_increment/im', 'PRIMARY KEY AUTOINCREMENT', $def_string ); - // When you use ALTER TABLE ADD, you can't use current_*. so we replace. - $def_string = str_ireplace( $array_curtime, $array_reptime, $def_string ); - // This is enum. - $pattern_enum = '/enum\((.*?)\)([^,\)]*)/ims'; - if ( preg_match( $pattern_enum, $col_def, $matches ) ) { - $def_string = 'TEXT' . $matches[2] . ' CHECK (' . $col_name . ' IN (' . $matches[1] . '))'; - } - - return $def_string; - } - - /** - * Variable to store the data definition table. - * - * @access private - * @var associative array - */ - private $array_types = array( - 'bit' => 'INTEGER', - 'bool' => 'INTEGER', - 'boolean' => 'INTEGER', - 'tinyint' => 'INTEGER', - 'smallint' => 'INTEGER', - 'mediumint' => 'INTEGER', - 'bigint' => 'INTEGER', - 'integer' => 'INTEGER', - 'int' => 'INTEGER', - 'float' => 'REAL', - 'double' => 'REAL', - 'decimal' => 'REAL', - 'dec' => 'REAL', - 'numeric' => 'REAL', - 'fixed' => 'REAL', - 'datetime' => 'TEXT', - 'date' => 'TEXT', - 'timestamp' => 'TEXT', - 'time' => 'TEXT', - 'year' => 'TEXT', - 'varchar' => 'TEXT', - 'char' => 'TEXT', - 'varbinary' => 'BLOB', - 'binary' => 'BLOB', - 'tinyblob' => 'BLOB', - 'mediumblob' => 'BLOB', - 'longblob' => 'BLOB', - 'blob' => 'BLOB', - 'tinytext' => 'TEXT', - 'mediumtext' => 'TEXT', - 'longtext' => 'TEXT', - 'text' => 'TEXT', - ); -} diff --git a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-create-query.php b/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-create-query.php deleted file mode 100644 index c9823d655f..0000000000 --- a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-create-query.php +++ /dev/null @@ -1,497 +0,0 @@ -_query = $query; - $this->_errors [] = ''; - if ( preg_match( '/^CREATE\\s*(UNIQUE|FULLTEXT|)\\s*INDEX/ims', $this->_query, $match ) ) { - // We manipulate CREATE INDEX query in the Perflab_SQLite_PDO_Engine class. - // FULLTEXT index creation is simply ignored. - if ( isset( $match[1] ) && stripos( $match[1], 'fulltext' ) !== false ) { - return 'SELECT 1=1'; - } - return $this->_query; - } - if ( preg_match( '/^CREATE\\s*(TEMP|TEMPORARY|)\\s*TRIGGER\\s*/im', $this->_query ) ) { - // If WordPress comes to use foreign key constraint, trigger will be needed. - // We don't use it for now. - return $this->_query; - } - $this->strip_backticks(); - $this->quote_illegal_field(); - $this->get_table_name(); - $this->rewrite_comments(); - $this->rewrite_field_types(); - $this->rewrite_character_set(); - $this->rewrite_engine_info(); - $this->rewrite_unsigned(); - $this->rewrite_autoincrement(); - $this->rewrite_primary_key(); - $this->rewrite_foreign_key(); - $this->rewrite_unique_key(); - $this->rewrite_enum(); - $this->rewrite_set(); - $this->rewrite_key(); - $this->add_if_not_exists(); - - return $this->post_process(); - } - - /** - * Method to get table name from the query string. - * - * 'IF NOT EXISTS' clause is removed for the easy regular expression usage. - * It will be added at the end of the process. - * - * @access private - */ - private function get_table_name() { - // $pattern = '/^\\s*CREATE\\s*(TEMP|TEMPORARY)?\\s*TABLE\\s*(IF NOT EXISTS)?\\s*([^\(]*)/imsx'; - $pattern = '/^\\s*CREATE\\s*(?:TEMP|TEMPORARY)?\\s*TABLE\\s*(?:IF\\s*NOT\\s*EXISTS)?\\s*([^\(]*)/imsx'; - if ( preg_match( $pattern, $this->_query, $matches ) ) { - $this->table_name = trim( $matches[1] ); - } - } - - /** - * Method to change the MySQL field types to SQLite compatible types. - * - * If column name is the same as the key value, e.g. "date" or "timestamp", - * and the column is on the top of the line, we add a single quote and avoid - * to be replaced. But this doesn't work if that column name is in the middle - * of the line. - * Order of the key value is important. Don't change it. - * - * @access private - */ - private function rewrite_field_types() { - $array_types = array( - 'bit' => 'integer', - 'bool' => 'integer', - 'boolean' => 'integer', - 'tinyint' => 'integer', - 'smallint' => 'integer', - 'mediumint' => 'integer', - 'int' => 'integer', - 'integer' => 'integer', - 'bigint' => 'integer', - 'float' => 'real', - 'double' => 'real', - 'decimal' => 'real', - 'dec' => 'real', - 'numeric' => 'real', - 'fixed' => 'real', - 'date' => 'text', - 'datetime' => 'text', - 'timestamp' => 'text', - 'time' => 'text', - 'year' => 'text', - 'char' => 'text', - 'varchar' => 'text', - 'binary' => 'integer', - 'varbinary' => 'blob', - 'tinyblob' => 'blob', - 'tinytext' => 'text', - 'blob' => 'blob', - 'text' => 'text', - 'mediumblob' => 'blob', - 'mediumtext' => 'text', - 'longblob' => 'blob', - 'longtext' => 'text', - ); - foreach ( $array_types as $o => $r ) { - if ( preg_match( "/^\\s*(?_query, $match ) ) { - $ptrn = "/$match[1]/im"; - $replaced = str_ireplace( $ptrn, '#placeholder#', $this->_query ); - $replaced = str_ireplace( $o, "'{$o}'", $replaced ); - $this->_query = str_replace( '#placeholder#', $ptrn, $replaced ); - } - $pattern = "/\\b(?_query ) ) { - // ; - } else { - $this->_query = preg_replace( $pattern, " $r ", $this->_query ); - } - } - } - - /** - * Method for stripping the comments from the SQL statement. - * - * @access private - */ - private function rewrite_comments() { - $this->_query = preg_replace( - '/# --------------------------------------------------------/', - '-- ******************************************************', - $this->_query - ); - $this->_query = preg_replace( '/#/', '--', $this->_query ); - } - - /** - * Method for stripping the engine and other stuffs. - * - * TYPE, ENGINE and AUTO_INCREMENT are removed here. - * - * @access private - */ - private function rewrite_engine_info() { - $this->_query = preg_replace( '/\\s*(TYPE|ENGINE)\\s*=\\s*.*(?_query ); - $this->_query = preg_replace( '/ AUTO_INCREMENT\\s*=\\s*[0-9]*/ims', '', $this->_query ); - } - - /** - * Method for stripping unsigned. - * - * SQLite doesn't have unsigned int data type. So UNSIGNED INT(EGER) is converted - * to INTEGER here. - * - * @access private - */ - private function rewrite_unsigned() { - $this->_query = preg_replace( '/\\bunsigned\\b/ims', ' ', $this->_query ); - } - - /** - * Method for rewriting primary key auto_increment. - * - * If the field type is 'INTEGER PRIMARY KEY', it is automatically autoincremented - * by SQLite. There's a little difference between PRIMARY KEY and AUTOINCREMENT, so - * we may well convert to PRIMARY KEY only. - * - * @access private - */ - private function rewrite_autoincrement() { - $this->_query = preg_replace( - '/\\bauto_increment\\s*primary\\s*key\\s*(,)?/ims', - ' PRIMARY KEY AUTOINCREMENT \\1', - $this->_query, - -1, - $count - ); - $this->_query = preg_replace( - '/\\bauto_increment\\b\\s*(,)?/ims', - ' PRIMARY KEY AUTOINCREMENT $1', - $this->_query, - -1, - $count - ); - if ( $count > 0 ) { - $this->has_primary_key = true; - } - } - - /** - * Method for rewriting primary key. - * - * @access private - */ - private function rewrite_primary_key() { - if ( $this->has_primary_key ) { - $this->_query = preg_replace( '/\\s*primary key\\s*.*?\([^\)]*\)\\s*(,|)/i', ' ', $this->_query ); - } else { - // If primary key has an index name, we remove that name. - $this->_query = preg_replace( '/\\bprimary\\s*key\\s*.*?\\s*(\(.*?\))/im', 'PRIMARY KEY \\1', $this->_query ); - } - } - - /** - * Method for rewriting foreign key. - * - * @access private - */ - private function rewrite_foreign_key() { - $pattern = '/\\s*foreign\\s*key\\s*(|.*?)\([^\)]+?\)\\s*references\\s*.*/i'; - if ( preg_match_all( $pattern, $this->_query, $match ) ) { - if ( isset( $match[1] ) ) { - $this->_query = str_ireplace( $match[1], '', $this->_query ); - } - } - } - - /** - * Method for rewriting unique key. - * - * @access private - */ - private function rewrite_unique_key() { - $this->_query = preg_replace_callback( - '/\\bunique key\\b([^\(]*)(\(.*\))/im', - array( $this, '_rewrite_unique_key' ), - $this->_query - ); - } - - /** - * Callback method for rewrite_unique_key. - * - * @access private - * - * @param array $matches An array of matches from the Regex. - * @return string - */ - private function _rewrite_unique_key( $matches ) { - $index_name = trim( $matches[1] ); - $col_name = trim( $matches[2] ); - $tbl_name = $this->table_name; - if ( preg_match( '/\(\\d+?\)/', $col_name ) ) { - $col_name = preg_replace( '/\(\\d+?\)/', '', $col_name ); - } - $_wpdb = new Perflab_SQLite_DB(); - $results = $_wpdb->get_results( "SELECT name FROM sqlite_master WHERE type='index'" ); - $_wpdb = null; - if ( $results ) { - foreach ( $results as $result ) { - if ( $result->name === $index_name ) { - $r = rand( 0, 50 ); - $index_name = $index_name . "_$r"; - break; - } - } - } - $index_name = str_replace( ' ', '', $index_name ); - $this->index_queries[] = "CREATE UNIQUE INDEX $index_name ON " . $tbl_name . $col_name; - - return ''; - } - - /** - * Method for handling ENUM fields. - * - * SQLite doesn't support enum, so we change it to check constraint. - * - * @access private - */ - private function rewrite_enum() { - $pattern = '/(,|\))([^,]*)enum\((.*?)\)([^,\)]*)/ims'; - $this->_query = preg_replace_callback( $pattern, array( $this, '_rewrite_enum' ), $this->_query ); - } - - /** - * Call back method for rewrite_enum() and rewrite_set(). - * - * @access private - * - * @param array $matches An array of matches from the Regex. - * - * @return string - */ - private function _rewrite_enum( $matches ) { - $output = $matches[1] . ' ' . $matches[2] . ' TEXT ' . $matches[4] . ' CHECK (' . $matches[2] . ' IN (' . $matches[3] . ')) '; - - return $output; - } - - /** - * Method for rewriting usage of set. - * - * It is similar but not identical to enum. SQLite does not support either. - * - * @access private - */ - private function rewrite_set() { - $pattern = '/\b(\w)*\bset\\s*\((.*?)\)\\s*(.*?)(,)*/ims'; - $this->_query = preg_replace_callback( $pattern, array( $this, '_rewrite_enum' ), $this->_query ); - } - - /** - * Method for rewriting usage of key to create an index. - * - * SQLite cannot create non-unique indices as part of the create query, - * so we need to create an index by hand and append it to the create query. - * - * @access private - */ - private function rewrite_key() { - $this->_query = preg_replace_callback( - '/,\\s*(KEY|INDEX)\\s*(\\w+)?\\s*(\(.+\))/im', - array( $this, '_rewrite_key' ), - $this->_query - ); - } - - /** - * Callback method for rewrite_key. - * - * @param array $matches an array of matches from the Regex. - * - * @access private - * @return string - */ - private function _rewrite_key( $matches ) { - $index_name = trim( $matches[2] ); - $col_name = trim( $matches[3] ); - if ( preg_match( '/\([0-9]+?\)/', $col_name, $match ) ) { - $col_name = preg_replace_callback( '/\([0-9]+?\)/', array( $this, '_remove_length' ), $col_name ); - } - $tbl_name = $this->table_name; - $_wpdb = new Perflab_SQLite_DB(); - $results = $_wpdb->get_results( "SELECT name FROM sqlite_master WHERE type='index'" ); - $_wpdb = null; - if ( $results ) { - foreach ( $results as $result ) { - if ( $result->name === $index_name ) { - $r = rand( 0, 50 ); - $index_name = $index_name . "_$r"; - break; - } - } - } - $this->index_queries[] = 'CREATE INDEX ' . $index_name . ' ON ' . $tbl_name . $col_name; - - return ''; - } - - /** - * Call back method to remove unnecessary string. - * - * This method is deprecated. - * - * @param string $match The string to be removed. Not used. - * - * @return string whose length is zero - * @access private - */ - private function _remove_length( $match ) { - return ''; - } - - /** - * Method to assemble the main query and index queries into an array. - * - * It return the array of the queries to be executed separately. - * - * @return array - * @access private - */ - private function post_process() { - $mainquery = $this->_query; - do { - $count = 0; - $mainquery = preg_replace( '/,\\s*\)/imsx', ')', $mainquery, -1, $count ); - } while ( $count > 0 ); - do { - $count = 0; - $mainquery = preg_replace( '/\(\\s*?,/imsx', '(', $mainquery, -1, $count ); - } while ( $count > 0 ); - $return_val[] = $mainquery; - $return_val = array_merge( $return_val, $this->index_queries ); - - return $return_val; - } - - /** - * Method to add IF NOT EXISTS to query string. - * - * This adds IF NOT EXISTS to every query string, which prevent the exception - * from being thrown. - * - * @access private - */ - private function add_if_not_exists() { - $pattern_table = '/^\\s*CREATE\\s*(TEMP|TEMPORARY)?\\s*TABLE\\s*(IF NOT EXISTS)?\\s*/ims'; - $this->_query = preg_replace( $pattern_table, 'CREATE $1 TABLE IF NOT EXISTS ', $this->_query ); - $pattern_index = '/^\\s*CREATE\\s*(UNIQUE)?\\s*INDEX\\s*(IF NOT EXISTS)?\\s*/ims'; - for ( $i = 0; $i < count( $this->index_queries ); $i++ ) { - $this->index_queries[ $i ] = preg_replace( - $pattern_index, - 'CREATE $1 INDEX IF NOT EXISTS ', - $this->index_queries[ $i ] - ); - } - } - - /** - * Method to strip back quotes. - * - * @access private - */ - private function strip_backticks() { - $this->_query = str_replace( '`', '', $this->_query ); - foreach ( $this->index_queries as &$query ) { - $query = str_replace( '`', '', $query ); - } - } - - /** - * Method to remove the character set information from within mysql queries. - * - * This removes DEFAULT CHAR(ACTER) SET and COLLATE, which is meaningless for - * SQLite. - * - * @access private - */ - private function rewrite_character_set() { - $pattern_charset = '/\\b(default\\s*character\\s*set|default\\s*charset|character\\s*set)\\s*(?_query = preg_replace( $patterns, '', $this->_query ); - } - - /** - * Method to quote illegal field name for SQLite - * - * @access private - */ - private function quote_illegal_field() { - $this->_query = preg_replace( "/^\\s*(?_query ); - } -} diff --git a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-object-array.php b/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-object-array.php deleted file mode 100644 index d09c81bdd1..0000000000 --- a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-object-array.php +++ /dev/null @@ -1,35 +0,0 @@ - $value ) { - if ( ! $node ) { - $node =& $this; - } - if ( is_array( $value ) ) { - $node->$key = new stdClass(); - self::__construct( $value, $node->$key ); - } else { - $node->$key = $value; - } - } - } -} diff --git a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-pdo-driver.php b/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-pdo-driver.php deleted file mode 100644 index f05d229bda..0000000000 --- a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-pdo-driver.php +++ /dev/null @@ -1,793 +0,0 @@ -query_type = $query_type; - $this->_query = $query; - $this->parse_query(); - switch ( $this->query_type ) { - case 'truncate': - $this->handle_truncate_query(); - break; - - case 'alter': - $this->handle_alter_query(); - break; - - case 'create': - $this->handle_create_query(); - break; - - case 'describe': - case 'desc': - $this->handle_describe_query(); - break; - - case 'show': - $this->handle_show_query(); - break; - - case 'showcolumns': - $this->handle_show_columns_query(); - break; - - case 'showindex': - $this->handle_show_index(); - break; - - case 'select': - // $this->strip_backticks(); - $this->handle_sql_count(); - $this->rewrite_date_sub(); - $this->delete_index_hints(); - $this->rewrite_regexp(); - // $this->rewrite_boolean(); - $this->fix_date_quoting(); - $this->rewrite_between(); - $this->handle_orderby_field(); - break; - - case 'insert': - // $this->safe_strip_backticks(); - $this->execute_duplicate_key_update(); - $this->rewrite_insert_ignore(); - $this->rewrite_regexp(); - $this->fix_date_quoting(); - break; - - case 'update': - // $this->safe_strip_backticks(); - $this->rewrite_update_ignore(); - // $this->_rewrite_date_sub(); - $this->rewrite_limit_usage(); - $this->rewrite_order_by_usage(); - $this->rewrite_regexp(); - $this->rewrite_between(); - break; - - case 'delete': - // $this->strip_backticks(); - $this->rewrite_limit_usage(); - $this->rewrite_order_by_usage(); - $this->rewrite_date_sub(); - $this->rewrite_regexp(); - $this->delete_workaround(); - break; - - case 'replace': - // $this->safe_strip_backticks(); - $this->rewrite_date_sub(); - $this->rewrite_regexp(); - break; - - case 'optimize': - $this->rewrite_optimize(); - break; - - case 'pragma': - break; - - default: - if ( defined( WP_DEBUG ) && WP_DEBUG ) { - break; - } - $this->return_true(); - break; - } - - return $this->_query; - } - - /** - * Method to parse query string and determine which operation is needed. - * - * Remove backticks and change true/false values into 1/0. And determines - * if rewriting CALC_FOUND_ROWS or ON DUPLICATE KEY UPDATE etc is needed. - * - * @access private - */ - private function parse_query() { - $tokens = preg_split( "/(\\\'|''|')/s", $this->_query, -1, PREG_SPLIT_DELIM_CAPTURE ); - $literal = false; - $query_string = ''; - foreach ( $tokens as $token ) { - if ( "'" === $token ) { - $literal = ! $literal; - } else { - if ( false === $literal ) { - if ( strpos( $token, '`' ) !== false ) { - $token = str_replace( '`', '', $token ); - } - if ( preg_match( '/\\bTRUE\\b/i', $token ) ) { - $token = str_ireplace( 'TRUE', '1', $token ); - } - if ( preg_match( '/\\bFALSE\\b/i', $token ) ) { - $token = str_ireplace( 'FALSE', '0', $token ); - } - if ( stripos( $token, 'SQL_CALC_FOUND_ROWS' ) !== false ) { - $this->rewrite_calc_found = true; - } - if ( stripos( $token, 'ON DUPLICATE KEY UPDATE' ) !== false ) { - $this->rewrite_duplicate_key = true; - } - if ( stripos( $token, 'USE INDEX' ) !== false ) { - $this->rewrite_index_hint = true; - } - if ( stripos( $token, 'IGNORE INDEX' ) !== false ) { - $this->rewrite_index_hint = true; - } - if ( stripos( $token, 'FORCE INDEX' ) !== false ) { - $this->rewrite_index_hint = true; - } - if ( stripos( $token, 'BETWEEN' ) !== false ) { - $this->rewrite_between = true; - $this->num_of_rewrite_between++; - } - if ( stripos( $token, 'ORDER BY FIELD' ) !== false ) { - $this->orderby_field = true; - } - } - } - $query_string .= $token; - } - $this->_query = $query_string; - } - - /** - * Method to handle SHOW TABLES query. - * - * @access private - */ - private function handle_show_query() { - $this->_query = str_ireplace( ' FULL', '', $this->_query ); - $table_name = ''; - $pattern = '/^\\s*SHOW\\s*TABLES\\s*.*?(LIKE\\s*(.*))$/im'; - if ( preg_match( $pattern, $this->_query, $matches ) ) { - $table_name = str_replace( array( "'", ';' ), '', $matches[2] ); - } - $suffix = empty( $table_name ) ? '' : ' AND name LIKE ' . "'" . $table_name . "'"; - $this->_query = "SELECT name FROM sqlite_master WHERE type='table'" . $suffix . ' ORDER BY name DESC'; - } - - /** - * Method to emulate the SQL_CALC_FOUND_ROWS placeholder for MySQL. - * - * This is a kind of tricky play. - * 1. remove SQL_CALC_FOUND_ROWS option, and give it to the pdo engine - * 2. make another $wpdb instance, and execute the rewritten query - * 3. give the returned value (integer: number of the rows) to the original instance variable without LIMIT - * - * We no longer use SELECT COUNT query, because it returns the inexact values when used with WP_Meta_Query(). - * - * This kind of statement is required for WordPress to calculate the paging information. - * see also WP_Query class in wp-includes/query.php - */ - private function handle_sql_count() { - if ( ! $this->rewrite_calc_found ) { - return; - } - global $wpdb; - // First strip the code. this is the end of rewriting process. - $this->_query = str_ireplace( 'SQL_CALC_FOUND_ROWS', '', $this->_query ); - // We make the data for next SELECE FOUND_ROWS() statement. - $unlimited_query = preg_replace( '/\\bLIMIT\\s*.*/imsx', '', $this->_query ); - $_wpdb = new Perflab_SQLite_DB(); - $result = $_wpdb->query( $unlimited_query ); - $wpdb->dbh->found_rows_result = $result; - $_wpdb = null; - } - - /** - * Method to rewrite INSERT IGNORE to INSERT OR IGNORE. - * - * @access private - */ - private function rewrite_insert_ignore() { - $this->_query = str_ireplace( 'INSERT IGNORE', 'INSERT OR IGNORE ', $this->_query ); - } - - /** - * Method to rewrite UPDATE IGNORE to UPDATE OR IGNORE. - * - * @access private - */ - private function rewrite_update_ignore() { - $this->_query = str_ireplace( 'UPDATE IGNORE', 'UPDATE OR IGNORE ', $this->_query ); - } - - /** - * Method to rewrite DATE_ADD() function. - * - * DATE_ADD has a parameter PHP function can't parse, so we quote the list and - * pass it to the user defined function. - * - * @access private - */ - private function rewrite_date_add() { - // (date,interval expression unit) - $pattern = '/\\s*date_add\\s*\(([^,]*),([^\)]*)\)/imsx'; - if ( preg_match( $pattern, $this->_query, $matches ) ) { - $expression = "'" . trim( $matches[2] ) . "'"; - $this->_query = preg_replace( $pattern, " date_add($matches[1], $expression) ", $this->_query ); - } - } - - /** - * Method to rewrite DATE_SUB() function. - * - * DATE_SUB has a parameter PHP function can't parse, so we quote the list and - * pass it to the user defined function. - * - * @access private - */ - private function rewrite_date_sub() { - // (date,interval expression unit) - $pattern = '/\\s*date_sub\\s*\(([^,]*),([^\)]*)\)/imsx'; - if ( preg_match( $pattern, $this->_query, $matches ) ) { - $expression = "'" . trim( $matches[2] ) . "'"; - $this->_query = preg_replace( $pattern, " date_sub($matches[1], $expression) ", $this->_query ); - } - } - - /** - * Method to handle CREATE query. - * - * If the query is CREATE query, it will be passed to the query_create.class.php. - * So this method can't be used. It's here for safety. - * - * @access private - */ - private function handle_create_query() { - $engine = new Perflab_SQLite_Create_Query(); - $this->_query = $engine->rewrite_query( $this->_query ); - $engine = null; - } - - /** - * Method to handle ALTER query. - * - * If the query is ALTER query, it will be passed ot the query_alter.class.php. - * So this method can't be used. It is here for safety. - * - * @access private - */ - private function handle_alter_query() { - $engine = new Perflab_SQLite_Alter_Query(); - $this->_query = $engine->rewrite_query( $this->_query, 'alter' ); - $engine = null; - } - - /** - * Method to handle DESCRIBE or DESC query. - * - * DESCRIBE is required for WordPress installation process. DESC is - * an alias for DESCRIBE, but it is not used in core WordPress. - * - * @access private - */ - private function handle_describe_query() { - $pattern = '/^\\s*(DESCRIBE|DESC)\\s*(.*)/i'; - if ( preg_match( $pattern, $this->_query, $match ) ) { - $tablename = preg_replace( '/[\';]/', '', $match[2] ); - $this->_query = "PRAGMA table_info($tablename)"; - } - } - - /** - * Method to remove LIMIT clause from DELETE or UPDATE query. - * - * The author of the original 'PDO for WordPress' says update method of wpdb - * insists on adding LIMIT. But the newest version of WordPress doesn't do that. - * Nevertheless some plugins use DELETE with LIMIT, UPDATE with LIMIT. - * We need to exclude sub query's LIMIT. And if SQLite is compiled with - * ENABLE_UPDATE_DELETE_LIMIT option, we don't remove it. - * - * @access private - */ - private function rewrite_limit_usage() { - $_wpdb = new Perflab_SQLite_DB(); - $options = $_wpdb->get_results( 'PRAGMA compile_options' ); - foreach ( $options as $opt ) { - if ( isset( $opt->compile_option ) && stripos( $opt->compile_option, 'ENABLE_UPDATE_DELETE_LIMIT' ) !== false ) { - return; - } - } - if ( stripos( $this->_query, '(select' ) === false ) { - $this->_query = preg_replace( '/\\s*LIMIT\\s*[0-9]$/i', '', $this->_query ); - } - } - - /** - * Method to remove ORDER BY clause from DELETE or UPDATE query. - * - * SQLite compiled without SQLITE_ENABLE_UPDATE_DELETE_LIMIT option can't - * execute UPDATE with ORDER BY, DELETE with GROUP BY. - * We need to exclude sub query's GROUP BY. - * - * @access private - */ - private function rewrite_order_by_usage() { - $_wpdb = new Perflab_SQLite_DB(); - $options = $_wpdb->get_results( 'PRAGMA compile_options' ); - foreach ( $options as $opt ) { - if ( isset( $opt->compile_option ) && stripos( $opt->compile_option, 'ENABLE_UPDATE_DELETE_LIMIT' ) !== false ) { - return; - } - } - if ( stripos( $this->_query, '(select' ) === false ) { - $this->_query = preg_replace( '/\\s+ORDER\\s+BY\\s*.*$/i', '', $this->_query ); - } - } - - /** - * Method to handle TRUNCATE query. - * - * @access private - */ - private function handle_truncate_query() { - $pattern = '/TRUNCATE TABLE (.*)/im'; - $this->_query = preg_replace( $pattern, 'DELETE FROM $1', $this->_query ); - } - - /** - * Method to handle OPTIMIZE query. - * - * Original query has the table names, but they are simply ignored. - * Table names are meaningless in SQLite. - * - * @access private - */ - private function rewrite_optimize() { - $this->_query = 'VACUUM'; - } - - /** - * Method to rewrite day. - * - * Jusitn Adie says: some wp UI interfaces (notably the post interface) - * badly composes the day part of the date leading to problems in sqlite - * sort ordering etc. - * - * I don't understand that... - * - * @access private - */ - private function rewrite_badly_formed_dates() { - $pattern = '/([12]\d{3,}-\d{2}-)(\d )/ims'; - $this->_query = preg_replace( $pattern, '${1}0$2', $this->_query ); - } - - /** - * Method to remove INDEX HINT. - * - * @access private - */ - private function delete_index_hints() { - $pattern = '/\\s*(use|ignore|force)\\s+index\\s*\(.*?\)/i'; - $this->_query = preg_replace( $pattern, '', $this->_query ); - } - - /** - * Method to fix the date string and quoting. - * - * This is required for the calendar widget. - * - * WHERE month(fieldname)=08 is converted to month(fieldname)='8' - * WHERE month(fieldname)='08' is converted to month(fieldname)='8' - * - * I use preg_replace_callback instead of 'e' option because of security reason. - * cf. PHP manual (regular expression) - * - * @access private - */ - private function fix_date_quoting() { - $pattern = '/(month|year|second|day|minute|hour|dayofmonth)\\s*\((.*?)\)\\s*=\\s*["\']?(\d{1,4})[\'"]?\\s*/im'; - $this->_query = preg_replace_callback( $pattern, array( $this, '_fix_date_quoting' ), $this->_query ); - } - - /** - * Call back method to rewrite date string. - * - * @param string $match The match from preg_replace_callback. - * - * @return string - * @access private - */ - private function _fix_date_quoting( $match ) { - return "{$match[1]}({$match[2]})='" . intval( $match[3] ) . "' "; - } - - /** - * Method to rewrite REGEXP() function. - * - * This method changes function name to regexpp() and pass it to the user defined - * function. - * - * @access private - */ - private function rewrite_regexp() { - $pattern = '/\s([^\s]*)\s*regexp\s*(\'.*?\')/im'; - $this->_query = preg_replace( $pattern, ' regexpp(\1, \2)', $this->_query ); - } - - /** - * Method to handle SHOW COLUMN query. - * - * @access private - */ - private function handle_show_columns_query() { - $this->_query = str_ireplace( ' FULL', '', $this->_query ); - $pattern_like = '/^\\s*SHOW\\s*(COLUMNS|FIELDS)\\s*FROM\\s*(.*)?\\s*LIKE\\s*(.*)?/i'; - $pattern = '/^\\s*SHOW\\s*(COLUMNS|FIELDS)\\s*FROM\\s*(.*)?/i'; - if ( preg_match( $pattern_like, $this->_query, $matches ) ) { - $table_name = str_replace( "'", '', trim( $matches[2] ) ); - $column_name = str_replace( "'", '', trim( $matches[3] ) ); - $query_string = "SELECT sql FROM sqlite_master WHERE tbl_name='$table_name' AND sql LIKE '%$column_name%'"; - $this->_query = $query_string; - } elseif ( preg_match( $pattern, $this->_query, $matches ) ) { - $table_name = $matches[2]; - $query_string = preg_replace( $pattern, "PRAGMA table_info($table_name)", $this->_query ); - $this->_query = $query_string; - } - } - - /** - * Method to handle SHOW INDEX query. - * - * Moved the WHERE clause manipulation to pdoengin.class.php (ver 1.3.1) - * - * @access private - */ - private function handle_show_index() { - $pattern = '/^\\s*SHOW\\s*(?:INDEX|INDEXES|KEYS)\\s*FROM\\s*(\\w+)?/im'; - if ( preg_match( $pattern, $this->_query, $match ) ) { - $table_name = preg_replace( "/[\';]/", '', $match[1] ); - $table_name = trim( $table_name ); - $this->_query = "SELECT * FROM sqlite_master WHERE tbl_name='$table_name'"; - } - } - - /** - * Method to handle ON DUPLICATE KEY UPDATE statement. - * - * First we use SELECT query and check if INSERT is allowed or not. - * Rewriting procedure looks like a detour, but I've got no other ways. - * - * Added the literal check since the version 1.5.1. - * - * @access private - */ - private function execute_duplicate_key_update() { - if ( ! $this->rewrite_duplicate_key ) { - return; - } - $unique_keys_for_cond = array(); - $unique_keys_for_check = array(); - $pattern = '/^\\s*INSERT\\s*INTO\\s*(\\w+)?\\s*(.*)\\s*ON\\s*DUPLICATE\\s*KEY\\s*UPDATE\\s*(.*)$/ims'; - if ( preg_match( $pattern, $this->_query, $match_0 ) ) { - $table_name = trim( $match_0[1] ); - $insert_data = trim( $match_0[2] ); - $update_data = trim( $match_0[3] ); - // Prepare two unique key data for the table. - // 1. array('col1', 'col2, col3', etc) 2. array('col1', 'col2', 'col3', etc). - $_wpdb = new Perflab_SQLite_DB(); - $indexes = $_wpdb->get_results( "SHOW INDEX FROM {$table_name}" ); - if ( ! empty( $indexes ) ) { - foreach ( $indexes as $index ) { - if ( 0 === $index->Non_unique ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $unique_keys_for_cond[] = $index->Column_name; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - if ( strpos( $index->Column_name, ',' ) !== false ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $unique_keys_for_check = array_merge( - $unique_keys_for_check, - explode( ',', $index->Column_name ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - ); - } else { - $unique_keys_for_check[] = $index->Column_name; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - } - } - } - $unique_keys_for_check = array_map( 'trim', $unique_keys_for_check ); - } else { - // Without unique key or primary key, UPDATE statement will affect all the rows! - $query = "INSERT INTO $table_name $insert_data"; - $this->_query = $query; - $_wpdb = null; - - return; - } - // Data check. - if ( preg_match( '/^\((.*)\)\\s*VALUES\\s*\((.*)\)$/ims', $insert_data, $match_1 ) ) { - $col_array = explode( ',', $match_1[1] ); - $ins_data_array = explode( ',', $match_1[2] ); - foreach ( $col_array as $col ) { - $val = trim( array_shift( $ins_data_array ) ); - $ins_data_assoc[ trim( $col ) ] = $val; - } - $condition = ''; - foreach ( $unique_keys_for_cond as $unique_key ) { - if ( strpos( $unique_key, ',' ) !== false ) { - $unique_key_array = explode( ',', $unique_key ); - $counter = count( $unique_key_array ); - for ( $i = 0; $i < $counter; ++$i ) { - $col = trim( $unique_key_array[ $i ] ); - if ( isset( $ins_data_assoc[ $col ] ) && $i === $counter - 1 ) { - $condition .= $col . '=' . $ins_data_assoc[ $col ] . ' OR '; - } elseif ( isset( $ins_data_assoc[ $col ] ) ) { - $condition .= $col . '=' . $ins_data_assoc[ $col ] . ' AND '; - } else { - continue; - } - } - } else { - $col = trim( $unique_key ); - if ( ! isset( $ins_data_assoc[ $col ] ) ) { - continue; - } - $condition .= $col . '=' . $ins_data_assoc[ $col ] . ' OR '; - } - } - $condition = rtrim( $condition, ' OR ' ); - $test_query = "SELECT * FROM {$table_name} WHERE {$condition}"; - $results = $_wpdb->query( $test_query ); - $_wpdb = null; - if ( 0 == $results ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison - $this->_query = "INSERT INTO $table_name $insert_data"; - return; - } - - $ins_array_assoc = array(); - - if ( preg_match( '/^\((.*)\)\\s*VALUES\\s*\((.*)\)$/im', $insert_data, $match_2 ) ) { - $col_array = explode( ',', $match_2[1] ); - $ins_array = explode( ',', $match_2[2] ); - $count = count( $col_array ); - for ( $i = 0; $i < $count; $i++ ) { - $col = trim( $col_array[ $i ] ); - $val = trim( $ins_array[ $i ] ); - $ins_array_assoc[ $col ] = $val; - } - } - $update_data = rtrim( $update_data, ';' ); - $tmp_array = explode( ',', $update_data ); - foreach ( $tmp_array as $pair ) { - list($col, $value) = explode( '=', $pair ); - $col = trim( $col ); - $value = trim( $value ); - $update_array_assoc[ $col ] = $value; - } - foreach ( $update_array_assoc as $key => &$value ) { - if ( preg_match( '/^VALUES\\s*\((.*)\)$/im', $value, $match_3 ) ) { - $col = trim( $match_3[1] ); - $value = $ins_array_assoc[ $col ]; - } - } - foreach ( $ins_array_assoc as $key => $val ) { - if ( in_array( $key, $unique_keys_for_check, true ) ) { - $where_array[] = $key . '=' . $val; - } - } - $update_strings = ''; - foreach ( $update_array_assoc as $key => $val ) { - if ( in_array( $key, $unique_keys_for_check, true ) ) { - $where_array[] = $key . '=' . $val; - } else { - $update_strings .= $key . '=' . $val . ','; - } - } - $update_strings = rtrim( $update_strings, ',' ); - $unique_where = array_unique( $where_array, SORT_REGULAR ); - $where_string = ' WHERE ' . implode( ' AND ', $unique_where ); - $update_query = 'UPDATE ' . $table_name . ' SET ' . $update_strings . $where_string; - $this->_query = $update_query; - } - } - } - - /** - * Method to rewrite BETWEEN A AND B clause. - * - * This clause is the same form as natural language, so we have to check if it is - * in the data or SQL statement. - * - * @access private - */ - private function rewrite_between() { - if ( ! $this->rewrite_between ) { - return; - } - $pattern = '/\\s*(CAST\([^\)]+?\)|[^\\s\(]*)?\\s*BETWEEN\\s*([^\\s]*)?\\s*AND\\s*([^\\s\)]*)?\\s*/ims'; - do { - if ( preg_match( $pattern, $this->_query, $match ) ) { - $column_name = trim( $match[1] ); - $min_value = trim( $match[2] ); - $max_value = trim( $match[3] ); - $max_value = rtrim( $max_value ); - $replacement = " ($column_name >= $min_value AND $column_name <= $max_value)"; - $this->_query = str_ireplace( $match[0], $replacement, $this->_query ); - } - $this->num_of_rewrite_between--; - } while ( $this->num_of_rewrite_between > 0 ); - } - - /** - * Method to handle ORDER BY FIELD() clause. - * - * When FIELD() function has column name to compare, we can't rewrite it with - * use defined functions. When this function detect column name in the argument, - * it creates another instance, does the query withuot ORDER BY clause and gives - * the result array sorted to the main instance. - * - * If FIELD() function doesn't have column name, it will use the user defined - * function. usort() function closure function to compare the items. - * - * @access private - */ - private function handle_orderby_field() { - if ( ! $this->orderby_field ) { - return; - } - global $wpdb; - $pattern = '/\\s+ORDER\\s+BY\\s+FIELD\\s*\(\\s*([^\)]+?)\\s*\)/i'; - if ( preg_match( $pattern, $this->_query, $match ) ) { - $params = explode( ',', $match[1] ); - $params = array_map( 'trim', $params ); - $tbl_col = array_shift( $params ); - $flipped = array_flip( $params ); - $tbl_name = substr( $tbl_col, 0, strpos( $tbl_col, '.' ) ); - $tbl_name = str_replace( $wpdb->prefix, '', $tbl_name ); - - if ( $tbl_name && in_array( $tbl_name, $wpdb->tables, true ) ) { - $query = str_replace( $match[0], '', $this->_query ); - $_wpdb = new Perflab_SQLite_DB(); - $results = $_wpdb->get_results( $query ); - $_wpdb = null; - usort( - $results, - function ( $a, $b ) use ( $flipped ) { - return $flipped[ $a->ID ] - $flipped[ $b->ID ]; - } - ); - } - $wpdb->dbh->pre_ordered_results = $results; - } - } - - /** - * Method to avoid DELETE with JOIN statement. - * - * The wp-admin/includes/upgrade.php contains a 'DELETE ... JOIN' statement. - * This query can't be replaced with regular expression or udf, so we - * replace all the statement with another. But this query was used in - * the very old version of WordPress when it was upgraded. So we won't - * have no chance that this method should be used. - * - * @access private - */ - private function delete_workaround() { - global $wpdb; - $pattern = "DELETE o1 FROM $wpdb->options AS o1 JOIN $wpdb->options AS o2"; - $pattern2 = "DELETE a, b FROM $wpdb->sitemeta AS a, $wpdb->sitemeta AS b"; - $rewritten = "DELETE FROM $wpdb->options WHERE option_id IN (SELECT MIN(option_id) FROM $wpdb->options GROUP BY option_name HAVING COUNT(*) > 1)"; - if ( stripos( $this->_query, $pattern ) !== false ) { - $this->_query = $rewritten; - } elseif ( stripos( $this->_query, $pattern2 ) !== false ) { - $time = time(); - $prep_query = "SELECT a.meta_id AS aid, b.meta_id AS bid FROM $wpdb->sitemeta AS a INNER JOIN $wpdb->sitemeta AS b ON a.meta_key='_site_transient_timeout_'||substr(b.meta_key, 17) WHERE b.meta_key='_site_transient_'||substr(a.meta_key, 25) AND a.meta_value < $time"; - $_wpdb = new Perflab_SQLite_DB(); - $ids = $_wpdb->get_results( $prep_query ); - foreach ( $ids as $id ) { - $ids_to_delete[] = $id->aid; - $ids_to_delete[] = $id->bid; - } - $rewritten = "DELETE FROM $wpdb->sitemeta WHERE meta_id IN (" . implode( ',', $ids_to_delete ) . ')'; - $this->_query = $rewritten; - } - } - - /** - * Method to suppress errors. - * - * When the query string is the one that this class can't manipulate, - * the query string is replaced with the one that always returns true - * and does nothing. - * - * @access private - */ - private function return_true() { - $this->_query = 'SELECT 1=1'; - } -} diff --git a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-pdo-engine.php b/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-pdo-engine.php deleted file mode 100644 index e72b5d27b4..0000000000 --- a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-pdo-engine.php +++ /dev/null @@ -1,1527 +0,0 @@ -client_info = SQLite3::version()['versionString']; - - register_shutdown_function( array( $this, '__destruct' ) ); - if ( ! is_file( FQDB ) ) { - $this->prepare_directory(); - } - $dsn = 'sqlite:' . FQDB; - if ( isset( $GLOBALS['@pdo'] ) ) { - $this->pdo = $GLOBALS['@pdo']; - $this->init(); - return; - } - - $locked = false; - $status = 0; - $err_message = ''; - do { - try { - $this->pdo = new PDO( $dsn, null, null, array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ) ); // phpcs:ignore WordPress.DB.RestrictedClasses - new Perflab_SQLite_PDO_User_Defined_Functions( $this->pdo ); - $GLOBALS['@pdo'] = $this->pdo; - } catch ( PDOException $ex ) { - $status = $ex->getCode(); - if ( 5 === $status || 6 === $status ) { - $locked = true; - } else { - $err_message = $ex->getMessage(); - } - } - } while ( $locked ); - - if ( $status > 0 ) { - $message = sprintf( - '
%s
%s
%s
', - __( 'Database initialization error!', 'performance-lab' ), - /* translators: %d: error code */ - sprintf( __( 'Code: %d', 'performance-lab' ), $status ), - /* translators: %s: error message */ - sprintf( __( 'Error Message: %s', 'performance-lab' ), $err_message ) - ); - $this->set_error( __LINE__, __FILE__, $message ); - - return false; - } - $this->init(); - } - - /** - * Destructor - * - * If SQLITE_MEM_DEBUG constant is defined, append information about - * memory usage into database/mem_debug.txt. - * - * This definition is changed since version 1.7. - * - * @return boolean - */ - function __destruct() { - if ( defined( 'SQLITE_MEM_DEBUG' ) && SQLITE_MEM_DEBUG ) { - $max = ini_get( 'memory_limit' ); - if ( is_null( $max ) ) { - $message = sprintf( - '[%s] Memory_limit is not set in php.ini file.', - gmdate( 'Y-m-d H:i:s', $_SERVER['REQUEST_TIME'] ) - ); - error_log( $message ); - return true; - } - if ( stripos( $max, 'M' ) !== false ) { - $max = (int) $max * MB_IN_BYTES; - } - $peak = memory_get_peak_usage( true ); - $used = round( (int) $peak / (int) $max * 100, 2 ); - if ( $used > 90 ) { - $message = sprintf( - "[%s] Memory peak usage warning: %s %% used. (max: %sM, now: %sM)\n", - gmdate( 'Y-m-d H:i:s', $_SERVER['REQUEST_TIME'] ), - $used, - $max, - $peak - ); - error_log( $message ); - } - } - - return true; - } - - /** - * Method to initialize database, executed in the constructor. - * - * It checks if WordPress is in the installing process and does the required - * jobs. SQLite library version specific settings are also in this function. - * - * Some developers use WP_INSTALLING constant for other purposes, if so, this - * function will do no harms. - */ - private function init() { - if ( version_compare( SQLite3::version()['versionString'], '3.7.11', '>=' ) ) { - $this->can_insert_multiple_rows = true; - } - $statement = $this->pdo->query( 'PRAGMA foreign_keys' ); - if ( $statement->fetchColumn( 0 ) == '0' ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison - $this->pdo->query( 'PRAGMA foreign_keys = ON' ); - } - } - - /** - * This method makes database directory and .htaccess file. - * - * It is executed only once when the installation begins. - */ - private function prepare_directory() { - global $wpdb; - $u = umask( 0000 ); - if ( ! is_dir( FQDBDIR ) ) { - if ( ! @mkdir( FQDBDIR, 0704, true ) ) { - umask( $u ); - $message = __( 'Unable to create the required directory! Please check your server settings.', 'performance-lab' ); - wp_die( $message, 'Error!' ); - } - } - if ( ! is_writable( FQDBDIR ) ) { - umask( $u ); - $message = __( 'Unable to create a file in the directory! Please check your server settings.', 'performance-lab' ); - wp_die( $message, 'Error!' ); - } - if ( ! is_file( FQDBDIR . '.htaccess' ) ) { - $fh = fopen( FQDBDIR . '.htaccess', 'w' ); - if ( ! $fh ) { - umask( $u ); - $message = __( 'Unable to create a file in the directory! Please check your server settings.', 'performance-lab' ); - echo $message; - - return false; - } - fwrite( $fh, 'DENY FROM ALL' ); - fclose( $fh ); - } - if ( ! is_file( FQDBDIR . 'index.php' ) ) { - $fh = fopen( FQDBDIR . 'index.php', 'w' ); - if ( ! $fh ) { - umask( $u ); - $message = __( 'Unable to create a file in the directory! Please check your server settings.', 'performance-lab' ); - echo $message; - - return false; - } - fwrite( $fh, '' ); - fclose( $fh ); - } - umask( $u ); - - return true; - } - - /** - * Method to execute query(). - * - * Divide the query types into seven different ones. That is to say: - * - * 1. SELECT SQL_CALC_FOUND_ROWS - * 2. INSERT - * 3. CREATE TABLE(INDEX) - * 4. ALTER TABLE - * 5. SHOW VARIABLES - * 6. DROP INDEX - * 7. THE OTHERS - * - * #1 is just a tricky play. See the private function handle_sql_count() in query.class.php. - * From #2 through #5 call different functions respectively. - * #6 call the ALTER TABLE query. - * #7 is a normal process: sequentially call prepare_query() and execute_query(). - * - * #1 process has been changed since version 1.5.1. - * - * @param string $statement Full SQL statement string. - * @param int $mode Not used. - * @param array ...$fetch_mode_args Not used. - * - * @return mixed according to the query type - * @see PDO::query() - */ - public function query( $statement, $mode = PDO::ATTR_DEFAULT_FETCH_MODE, ...$fetch_mode_args ) { // phpcs:ignore WordPress.DB.RestrictedClasses - $this->flush(); - - $this->queries[] = "Raw query:\n$statement"; - $res = $this->determine_query_type( $statement ); - if ( ! $res && defined( 'PDO_DEBUG' ) && PDO_DEBUG ) { - $bailout_string = sprintf( - /* translators: %s: SQL statement */ - '' . __( 'Sorry, we cannot determine the type of query that is requested (%s).', 'performance-lab' ) . '
', - $statement - ); - $this->set_error( __LINE__, __FUNCTION__, $bailout_string ); - } - switch ( strtolower( $this->query_type ) ) { - case 'set': - $this->return_value = false; - break; - - case 'foundrows': - $_column = array( 'FOUND_ROWS()' => '' ); - $column = array(); - if ( ! is_null( $this->found_rows_result ) ) { - $this->num_rows = $this->found_rows_result; - $_column['FOUND_ROWS()'] = $this->num_rows; - $column[] = new Perflab_SQLite_Object_Array( $_column ); - $this->results = $column; - $this->found_rows_result = null; - } - break; - - case 'insert': - if ( $this->can_insert_multiple_rows ) { - $this->execute_insert_query_new( $statement ); - break; - } - $this->execute_insert_query( $statement ); - break; - - case 'create': - $this->return_value = $this->execute_create_query( $statement ); - break; - - case 'alter': - $this->return_value = $this->execute_alter_query( $statement ); - break; - - case 'show_variables': - $this->return_value = $this->show_variables_workaround( $statement ); - break; - - case 'showstatus': - $this->return_value = $this->show_status_workaround( $statement ); - break; - - case 'drop_index': - $this->return_value = false; - if ( preg_match( '/^\\s*(DROP\\s*INDEX\\s*.*?)\\s*ON\\s*(.*)/im', $statement, $match ) ) { - $this->query_type = 'alter'; - $this->return_value = $this->execute_alter_query( 'ALTER TABLE ' . trim( $match[2] ) . ' ' . trim( $match[1] ) ); - } - break; - - default: - $engine = $this->prepare_engine( $this->query_type ); - $this->rewritten_query = $engine->rewrite_query( $statement, $this->query_type ); - if ( ! is_null( $this->pre_ordered_results ) ) { - $this->results = $this->pre_ordered_results; - $this->num_rows = count( $this->results ); - $this->return_value = $this->num_rows; - $this->pre_ordered_results = null; - break; - } - $this->queries[] = "Rewritten:\n$this->rewritten_query"; - $this->extract_variables(); - $prepared_query = $this->prepare_query(); - $this->execute_query( $prepared_query ); - if ( ! $this->is_error ) { - $this->process_results( $engine ); - } - break; - } - $debug_string = $this->get_debug_info(); - if ( defined( 'PDO_DEBUG' ) && PDO_DEBUG === true && $debug_string ) { - error_log( $debug_string ); - } - - return $this->return_value; - } - - /** - * Method to return inserted row id. - */ - public function get_insert_id() { - return $this->last_insert_id; - } - - /** - * Method to return the number of rows affected. - */ - public function get_affected_rows() { - return $this->affected_rows; - } - - /** - * Method to return the queried column names. - * - * These data are meaningless for SQLite. So they are dummy emulating - * MySQL columns data. - * - * @return array of the object - */ - public function get_columns() { - if ( ! empty( $this->results ) ) { - $primary_key = array( - 'meta_id', - 'comment_ID', - 'link_ID', - 'option_id', - 'blog_id', - 'option_name', - 'ID', - 'term_id', - 'object_id', - 'term_taxonomy_id', - 'umeta_id', - 'id', - ); - $unique_key = array( 'term_id', 'taxonomy', 'slug' ); - $data = array( - 'name' => '', // Column name. - 'table' => '', // Table name. - 'max_length' => 0, // Max length of the column. - 'not_null' => 1, // 1 if not null. - 'primary_key' => 0, // 1 if column has primary key. - 'unique_key' => 0, // 1 if column has unique key. - 'multiple_key' => 0, // 1 if column doesn't have unique key. - 'numeric' => 0, // 1 if column has numeric value. - 'blob' => 0, // 1 if column is blob. - 'type' => '', // Type of the column. - 'unsigned' => 0, // 1 if column is unsigned integer. - 'zerofill' => 0, // 1 if column is zero-filled. - ); - $table_name = ''; - if ( preg_match( '/\s*FROM\s*(.*)?\s*/i', $this->rewritten_query, $match ) ) { - $table_name = trim( $match[1] ); - } - foreach ( $this->results[0] as $key => $value ) { - $data['name'] = $key; - $data['table'] = $table_name; - if ( in_array( $key, $primary_key, true ) ) { - $data['primary_key'] = 1; - } elseif ( in_array( $key, $unique_key, true ) ) { - $data['unique_key'] = 1; - } else { - $data['multiple_key'] = 1; - } - $this->column_data[] = new Perflab_SQLite_Object_Array( $data ); - - // Reset data for next iteration. - $data['name'] = ''; - $data['table'] = ''; - $data['primary_key'] = 0; - $data['unique_key'] = 0; - $data['multiple_key'] = 0; - } - - return $this->column_data; - } - return null; - } - - /** - * Method to return the queried result data. - * - * @return mixed - */ - public function get_query_results() { - return $this->results; - } - - /** - * Method to return the number of rows from the queried result. - */ - public function get_num_rows() { - return $this->num_rows; - } - - /** - * Method to return the queried results according to the query types. - * - * @return mixed - */ - public function get_return_value() { - return $this->return_value; - } - - /** - * Method to return error messages. - * - * @return string - */ - public function get_error_message() { - if ( count( $this->error_messages ) === 0 ) { - $this->is_error = false; - $this->error_messages = array(); - return ''; - } - - if ( false === $this->is_error ) { - return ''; - } - - $output = '' . __( 'Queries made or created this session were:', 'performance-lab' ) . '
'; - $output .= '' . ob_get_contents() . ''; - ob_end_clean(); - - return $output; - } - - /** - * Method to return information about query string for debugging. - * - * @return string - */ - private function get_debug_info() { - $output = ''; - foreach ( $this->queries as $q ) { - $output .= $q . "\n"; - } - - return $output; - } - - /** - * Method to clear previous data. - */ - private function flush() { - $this->rewritten_query = ''; - $this->query_type = ''; - $this->results = null; - $this->_results = null; - $this->last_insert_id = null; - $this->affected_rows = null; - $this->column_data = array(); - $this->num_rows = null; - $this->return_value = null; - $this->extracted_variables = array(); - $this->error_messages = array(); - $this->is_error = false; - $this->queries = array(); - $this->param_num = 0; - } - - /** - * Method to include the apropreate class files. - * - * It is not a good habit to change the include files programmatically. - * Needs to be fixed some other way. - * - * @param string $query_type The query type (create, alter, etc). - * - * @return object reference to apropreate driver - */ - private function prepare_engine( $query_type = null ) { - if ( stripos( $query_type, 'create' ) !== false ) { - return new Perflab_SQLite_Create_Query(); - } - if ( stripos( $query_type, 'alter' ) !== false ) { - return new Perflab_SQLite_Alter_Query(); - } - return new Perflab_SQLite_PDO_Driver(); - } - - /** - * Method to create a PDO statement object from the query string. - * - * @return PDOStatement - */ - private function prepare_query() { - $this->queries[] = "Prepare:\n" . $this->prepared_query; - $reason = 0; - $message = ''; - $statement = null; - do { - try { - $statement = $this->pdo->prepare( $this->prepared_query ); - } catch ( PDOException $err ) { - $reason = $err->getCode(); - $message = $err->getMessage(); - } - } while ( 5 === $reason || 6 === $reason ); - - if ( $reason > 0 ) { - $err_message = sprintf( 'Problem preparing the PDO SQL Statement. Error was: %s', $message ); - $this->set_error( __LINE__, __FUNCTION__, $err_message ); - } - - return $statement; - } - - /** - * Method to execute PDO statement object. - * - * This function executes query and sets the variables to give back to WordPress. - * The variables are class fields. So if success, no return value. If failure, it - * returns void and stops. - * - * @param object $statement The PDO statement. - * - * @return boolean|void - */ - private function execute_query( $statement ) { - $reason = 0; - $message = ''; - if ( ! is_object( $statement ) ) { - return false; - } - if ( count( $this->extracted_variables ) > 0 ) { - $this->queries[] = "Executing:\n" . var_export( $this->extracted_variables, true ); - do { - if ( 'update' === $this->query_type || 'replace' === $this->query_type ) { - try { - $this->beginTransaction(); - $statement->execute( $this->extracted_variables ); - $this->commit(); - } catch ( PDOException $err ) { - $reason = $err->getCode(); - $message = $err->getMessage(); - $this->rollBack(); - } - } else { - try { - $statement->execute( $this->extracted_variables ); - } catch ( PDOException $err ) { - $reason = $err->getCode(); - $message = $err->getMessage(); - } - } - } while ( 5 == $reason || 6 == $reason ); // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison - } else { - $this->queries[] = 'Executing: (no parameters)'; - do { - if ( 'update' === $this->query_type || 'replace' === $this->query_type ) { - try { - $this->beginTransaction(); - $statement->execute(); - $this->commit(); - } catch ( PDOException $err ) { - $reason = $err->getCode(); - $message = $err->getMessage(); - $this->rollBack(); - } - } else { - try { - $statement->execute(); - } catch ( PDOException $err ) { - $reason = $err->getCode(); - $message = $err->getMessage(); - } - } - } while ( 5 == $reason || 6 == $reason ); // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison - } - if ( $reason > 0 ) { - $err_message = sprintf( 'Error while executing query! Error message was: %s', $message ); - $this->set_error( __LINE__, __FUNCTION__, $err_message ); - return false; - } - $this->_results = $statement->fetchAll( PDO::FETCH_OBJ ); // phpcs:ignore WordPress.DB.RestrictedClasses - - // Generate the results that $wpdb will want to see. - switch ( $this->query_type ) { - case 'insert': - case 'update': - case 'replace': - $this->last_insert_id = $this->pdo->lastInsertId(); - $this->affected_rows = $statement->rowCount(); - $this->return_value = $this->affected_rows; - break; - - case 'select': - case 'show': - case 'showcolumns': - case 'showindex': - case 'describe': - case 'desc': - case 'check': - case 'analyze': - $this->num_rows = count( $this->_results ); - $this->return_value = $this->num_rows; - break; - - case 'delete': - $this->affected_rows = $statement->rowCount(); - $this->return_value = $this->affected_rows; - break; - - case 'alter': - case 'drop': - case 'create': - case 'optimize': - case 'truncate': - $this->return_value = true; - if ( $this->is_error ) { - $this->return_value = false; - } - break; - } - } - - /** - * Method to extract field data to an array and prepare the query statement. - * - * If original SQL statement is CREATE query, this function does nothing. - */ - private function extract_variables() { - if ( 'create' === $this->query_type ) { - $this->prepared_query = $this->rewritten_query; - return; - } - - // Long queries can really kill this. - $pattern = '/(? 10000000 ) { - $query = preg_replace_callback( - $pattern, - array( $this, 'replace_variables_with_placeholders' ), - $this->rewritten_query - ); - } else { - do { - if ( $limit > 10000000 ) { - $this->set_error( __LINE__, __FUNCTION__, 'The query is too big to parse properly' ); - break; // No point in continuing execution, would get into a loop. - } - ini_set( 'pcre.backtrack_limit', $limit ); - $query = preg_replace_callback( - $pattern, - array( $this, 'replace_variables_with_placeholders' ), - $this->rewritten_query - ); - $limit = $limit * 10; - } while ( is_null( $query ) ); - - // Reset the pcre.backtrack_limit. - ini_set( 'pcre.backtrack_limit', $_limit ); - } - - if ( isset( $query ) ) { - $this->queries[] = "With Placeholders:\n" . $query; - $this->prepared_query = $query; - } - } - - /** - * Call back function to replace field data with PDO parameter. - * - * @param string $matches Matched string. - * - * @return string - */ - private function replace_variables_with_placeholders( $matches ) { - // Remove the WordPress escaping mechanism. - $param = stripslashes( $matches[0] ); - - // Remove trailing spaces. - $param = trim( $param ); - - // Remove the quotes at the end and the beginning. - if ( in_array( $param[ strlen( $param ) - 1 ], array( "'", '"' ), true ) ) { - $param = substr( $param, 0, -1 ); // End. - } - if ( in_array( $param[0], array( "'", '"' ), true ) ) { - $param = substr( $param, 1 ); // Start. - } - $key = ':param_' . $this->param_num++; - $this->extracted_variables[] = $param; - // Return the placeholder. - return ' ' . $key . ' '; - } - - /** - * Method to determine which query type the argument is. - * - * It takes the query string ,determines the type and returns the type string. - * If the query is the type that SQLite Integration can't executes, returns false. - * - * @param string $query The query string. - * - * @return boolean - */ - private function determine_query_type( $query ) { - $result = preg_match( - '/^\\s*(SET|EXPLAIN|PRAGMA|SELECT\\s*FOUND_ROWS|SELECT|INSERT|UPDATE|REPLACE|DELETE|ALTER|CREATE|DROP\\s*INDEX|DROP|SHOW\\s*\\w+\\s*\\w+\\s*|DESCRIBE|DESC|TRUNCATE|OPTIMIZE|CHECK|ANALYZE)/i', - $query, - $match - ); - - if ( ! $result ) { - return false; - } - $this->query_type = strtolower( $match[1] ); - if ( stripos( $this->query_type, 'found' ) !== false ) { - $this->query_type = 'foundrows'; - } - if ( stripos( $this->query_type, 'show' ) !== false ) { - if ( stripos( $this->query_type, 'show table status' ) !== false ) { - $this->query_type = 'showstatus'; - } elseif ( - stripos( $this->query_type, 'show tables' ) !== false || - stripos( $this->query_type, 'show full tables' ) !== false - ) { - $this->query_type = 'show'; - } elseif ( - stripos( $this->query_type, 'show columns' ) !== false || - stripos( $this->query_type, 'show fields' ) !== false || - stripos( $this->query_type, 'show full columns' ) !== false - ) { - $this->query_type = 'showcolumns'; - } elseif ( - stripos( $this->query_type, 'show index' ) !== false || - stripos( $this->query_type, 'show indexes' ) !== false || - stripos( $this->query_type, 'show keys' ) !== false - ) { - $this->query_type = 'showindex'; - } elseif ( - stripos( $this->query_type, 'show variables' ) !== false || - stripos( $this->query_type, 'show global variables' ) !== false || - stripos( $this->query_type, 'show session variables' ) !== false - ) { - $this->query_type = 'show_variables'; - } else { - return false; - } - } - if ( stripos( $this->query_type, 'drop index' ) !== false ) { - $this->query_type = 'drop_index'; - } - - return true; - } - - /** - * Method to execute INSERT query for SQLite version 3.7.11 or later. - * - * SQLite version 3.7.11 began to support multiple rows insert with values - * clause. This is for that version or later. - * - * @param string $query The query string. - */ - private function execute_insert_query_new( $query ) { - $engine = $this->prepare_engine( $this->query_type ); - $this->rewritten_query = $engine->rewrite_query( $query, $this->query_type ); - $this->queries[] = "Rewritten:\n" . $this->rewritten_query; - $this->extract_variables(); - $statement = $this->prepare_query(); - $this->execute_query( $statement ); - } - - /** - * Method to execute INSERT query for SQLite version 3.7.10 or lesser. - * - * It executes the INSERT query for SQLite version 3.7.10 or lesser. It is - * necessary to rewrite multiple row values. - * - * @param string $query The query string. - */ - private function execute_insert_query( $query ) { - global $wpdb; - $multi_insert = false; - $statement = null; - $engine = $this->prepare_engine( $this->query_type ); - if ( preg_match( '/(INSERT.*?VALUES\\s*)(\(.*\))/imsx', $query, $matched ) ) { - $query_prefix = $matched[1]; - $values_data = $matched[2]; - if ( stripos( $values_data, 'ON DUPLICATE KEY' ) !== false ) { - $exploded_parts = $values_data; - } elseif ( stripos( $query_prefix, "INSERT INTO $wpdb->comments" ) !== false ) { - $exploded_parts = $values_data; - } else { - $exploded_parts = $this->parse_multiple_inserts( $values_data ); - } - $count = count( $exploded_parts ); - if ( $count > 1 ) { - $multi_insert = true; - } - } - if ( $multi_insert ) { - $first = true; - foreach ( $exploded_parts as $value ) { - $suffix = ( substr( $value, -1, 1 ) === ')' ) ? '' : ')'; - - $query_string = $query_prefix . ' ' . $value . $suffix; - $this->rewritten_query = $engine->rewrite_query( $query_string, $this->query_type ); - $this->queries[] = "Rewritten:\n" . $this->rewritten_query; - $this->extracted_variables = array(); - $this->extract_variables(); - if ( $first ) { - $statement = $this->prepare_query(); - $this->execute_query( $statement ); - $first = false; - } else { - $this->execute_query( $statement ); - } - } - } else { - $this->rewritten_query = $engine->rewrite_query( $query, $this->query_type ); - $this->queries[] = "Rewritten:\n" . $this->rewritten_query; - $this->extract_variables(); - $statement = $this->prepare_query(); - $this->execute_query( $statement ); - } - } - - /** - * Method to help rewriting multiple row values insert query. - * - * It splits the values clause into an array to execute separately. - * - * @param string $values The values to be split. - * - * @return array - */ - private function parse_multiple_inserts( $values ) { - $tokens = preg_split( "/(''|(?prepare_engine( $this->query_type ); - $rewritten_query = $engine->rewrite_query( $query ); - $reason = 0; - $message = ''; - - try { - $this->beginTransaction(); - foreach ( (array) $rewritten_query as $single_query ) { - $this->queries[] = "Executing:\n" . $single_query; - $single_query = trim( $single_query ); - if ( empty( $single_query ) ) { - continue; - } - $this->pdo->exec( $single_query ); - } - $this->commit(); - } catch ( PDOException $err ) { - $reason = $err->getCode(); - $message = $err->getMessage(); - if ( 5 == $reason || 6 == $reason ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison - $this->commit(); - } else { - $this->rollBack(); - } - } - if ( $reason > 0 ) { - $err_message = sprintf( 'Problem in creating table or index. Error was: %s', $message ); - $this->set_error( __LINE__, __FUNCTION__, $err_message ); - - return false; - } - - return true; - } - - /** - * Method to execute ALTER TABLE query. - * - * @param string $query The query to execute. - * - * @return boolean - */ - private function execute_alter_query( $query ) { - $engine = $this->prepare_engine( $this->query_type ); - $reason = 0; - $message = ''; - $re_query = ''; - $rewritten_query = $engine->rewrite_query( $query, $this->query_type ); - if ( is_array( $rewritten_query ) && array_key_exists( 'recursion', $rewritten_query ) ) { - $re_query = $rewritten_query['recursion']; - unset( $rewritten_query['recursion'] ); - } - try { - $this->beginTransaction(); - if ( is_array( $rewritten_query ) ) { - foreach ( $rewritten_query as $single_query ) { - $this->queries[] = "Executing:\n" . $single_query; - $single_query = trim( $single_query ); - if ( empty( $single_query ) ) { - continue; - } - $this->pdo->exec( $single_query ); - } - } else { - $this->queries[] = "Executing:\n" . $rewritten_query; - $rewritten_query = trim( $rewritten_query ); - $this->pdo->exec( $rewritten_query ); - } - $this->commit(); - } catch ( PDOException $err ) { - $reason = $err->getCode(); - $message = $err->getMessage(); - if ( 5 == $reason || 6 == $reason ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison - $this->commit(); - usleep( 10000 ); - } else { - $this->rollBack(); - } - } - if ( '' !== $re_query ) { - $this->query( $re_query ); - } - if ( $reason > 0 ) { - $err_message = sprintf( 'Problem in executing alter query. Error was: %s', $message ); - $this->set_error( __LINE__, __FUNCTION__, $err_message ); - - return false; - } - - return true; - } - - /** - * Method to execute SHOW VARIABLES query - * - * This query is meaningless for SQLite. This function returns null data with some - * exceptions and only avoids the error message. - * - * @param string $query The query to execute. - * - * @return bool - */ - private function show_variables_workaround( $query ) { - $dummy_data = array( - 'Variable_name' => '', - 'Value' => null, - ); - $pattern = '/SHOW\\s*VARIABLES\\s*LIKE\\s*(.*)?$/im'; - if ( preg_match( $pattern, $query, $match ) ) { - $value = str_replace( "'", '', $match[1] ); - $dummy_data['Variable_name'] = trim( $value ); - // This is set for Wordfence Security Plugin. - $dummy_data['Value'] = ''; - if ( 'max_allowed_packet' === $value ) { - $dummy_data['Value'] = 1047552; - } - } - $_results[] = new Perflab_SQLite_Object_Array( $dummy_data ); - $this->results = $_results; - $this->num_rows = count( $this->results ); - $this->return_value = $this->num_rows; - - return true; - } - - /** - * Method to execute SHOW TABLE STATUS query. - * - * This query is meaningless for SQLite. This function returns dummy data. - * - * @param string $query The query to execute. - * - * @return bool - */ - private function show_status_workaround( $query ) { - $pattern = '/^SHOW\\s*TABLE\\s*STATUS\\s*LIKE\\s*(.*?)$/im'; - $table_name = ''; - if ( preg_match( $pattern, $query, $match ) ) { - $table_name = str_replace( "'", '', $match[1] ); - } - $dummy_data = array( - 'Name' => $table_name, - 'Engine' => '', - 'Version' => '', - 'Row_format' => '', - 'Rows' => 0, - 'Avg_row_length' => 0, - 'Data_length' => 0, - 'Max_data_length' => 0, - 'Index_length' => 0, - 'Data_free' => 0, - 'Auto_increment' => 0, - 'Create_time' => '', - 'Update_time' => '', - 'Check_time' => '', - 'Collation' => '', - 'Checksum' => '', - 'Create_options' => '', - 'Comment' => '', - ); - $_results[] = new Perflab_SQLite_Object_Array( $dummy_data ); - $this->results = $_results; - $this->num_rows = count( $this->results ); - $this->return_value = $this->num_rows; - - return true; - } - - /** - * Method to format the queried data to that of MySQL. - * - * @param string $engine Not used. - */ - private function process_results( $engine ) { - if ( in_array( $this->query_type, array( 'describe', 'desc', 'showcolumns' ), true ) ) { - $this->convert_to_columns_object(); - } elseif ( 'showindex' === $this->query_type ) { - $this->convert_to_index_object(); - } elseif ( in_array( $this->query_type, array( 'check', 'analyze' ), true ) ) { - $this->convert_result_check_or_analyze(); - } else { - $this->results = $this->_results; - } - } - - /** - * Method to format the error messages and put out to the file. - * - * When $wpdb::suppress_errors is set to true or $wpdb::show_errors is set to false, - * the error messages are ignored. - * - * @param string $line Where the error occurred. - * @param string $function Indicate the function name where the error occurred. - * @param string $message The message. - * - * @return boolean|void - */ - private function set_error( $line, $function, $message ) { - global $wpdb; - $this->errors[] = array( - 'line' => $line, - 'function' => $function, - ); - $this->error_messages[] = $message; - $this->is_error = true; - if ( $wpdb->suppress_errors || ! $wpdb->show_errors ) { - return false; - } - error_log( "Line $line, Function: $function, Message: $message" ); - } - - /** - * Method to change the queried data to PHP object format. - * - * It takes the associative array of query results and creates a numeric - * array of anonymous objects - * - * @access private - */ - private function convert_to_object() { - $_results = array(); - if ( count( $this->results ) === 0 ) { - echo $this->get_error_message(); - } else { - foreach ( $this->results as $row ) { - $_results[] = new Perflab_SQLite_Object_Array( $row ); - } - } - $this->results = $_results; - } - - /** - * Method to convert the SHOW COLUMNS query data to an object. - * - * It rewrites pragma results to mysql compatible array - * when query_type is describe, we use sqlite pragma function. - * - * @access private - */ - private function convert_to_columns_object() { - $_results = array(); - $_columns = array( // Field names MySQL SHOW COLUMNS returns. - 'Field' => '', - 'Type' => '', - 'Null' => '', - 'Key' => '', - 'Default' => '', - 'Extra' => '', - ); - if ( empty( $this->_results ) ) { - echo $this->get_error_message(); - } else { - foreach ( $this->_results as $row ) { - if ( ! is_object( $row ) ) { - continue; - } - if ( property_exists( $row, 'name' ) ) { - $_columns['Field'] = $row->name; - } - if ( property_exists( $row, 'type' ) ) { - $_columns['Type'] = $row->type; - } - if ( property_exists( $row, 'notnull' ) ) { - $_columns['Null'] = $row->notnull ? 'NO' : 'YES'; - } - if ( property_exists( $row, 'pk' ) ) { - $_columns['Key'] = $row->pk ? 'PRI' : ''; - } - if ( property_exists( $row, 'dflt_value' ) ) { - $_columns['Default'] = $row->dflt_value; - } - $_results[] = new Perflab_SQLite_Object_Array( $_columns ); - } - } - $this->results = $_results; - } - - /** - * Method to convert SHOW INDEX query data to PHP object. - * - * It rewrites the result of SHOW INDEX to the Object compatible with MySQL - * added the WHERE clause manipulation (ver 1.3.1) - * - * @access private - */ - private function convert_to_index_object() { - $_results = array(); - $_columns = array( - 'Table' => '', - 'Non_unique' => '', // Unique -> 0, not unique -> 1. - 'Key_name' => '', // The name of the index. - 'Seq_in_index' => '', // Column sequence number in the index. begins at 1. - 'Column_name' => '', - 'Collation' => '', // A(scend) or NULL. - 'Cardinality' => '', - 'Sub_part' => '', // Set to NULL. - 'Packed' => '', // How to pack key or else NULL. - 'Null' => '', // If column contains null, YES. If not, NO. - 'Index_type' => '', // BTREE, FULLTEXT, HASH, RTREE. - 'Comment' => '', - ); - if ( 0 === count( $this->_results ) ) { - echo $this->get_error_message(); - } else { - foreach ( $this->_results as $row ) { - if ( 'table' === $row->type && ! stripos( $row->sql, 'primary' ) ) { - continue; - } - if ( 'index' === $row->type && stripos( $row->name, 'sqlite_autoindex' ) !== false ) { - continue; - } - switch ( $row->type ) { - case 'table': - $pattern1 = '/^\\s*PRIMARY.*\((.*)\)/im'; - $pattern2 = '/^\\s*(\\w+)?\\s*.*PRIMARY.*(?!\()/im'; - if ( preg_match( $pattern1, $row->sql, $match ) ) { - $col_name = trim( $match[1] ); - $_columns['Key_name'] = 'PRIMARY'; - $_columns['Non_unique'] = 0; - $_columns['Column_name'] = $col_name; - } elseif ( preg_match( $pattern2, $row->sql, $match ) ) { - $col_name = trim( $match[1] ); - $_columns['Key_name'] = 'PRIMARY'; - $_columns['Non_unique'] = 0; - $_columns['Column_name'] = $col_name; - } - break; - - case 'index': - $_columns['Non_unique'] = 1; - if ( stripos( $row->sql, 'unique' ) !== false ) { - $_columns['Non_unique'] = 0; - } - if ( preg_match( '/^.*\((.*)\)/i', $row->sql, $match ) ) { - $col_name = str_replace( "'", '', $match[1] ); - $_columns['Column_name'] = trim( $col_name ); - } - $_columns['Key_name'] = $row->name; - break; - - } - $_columns['Table'] = $row->tbl_name; - $_columns['Collation'] = null; - $_columns['Cardinality'] = 0; - $_columns['Sub_part'] = null; - $_columns['Packed'] = null; - $_columns['Null'] = 'NO'; - $_columns['Index_type'] = 'BTREE'; - $_columns['Comment'] = ''; - $_results[] = new Perflab_SQLite_Object_Array( $_columns ); - } - if ( stripos( $this->queries[0], 'WHERE' ) !== false ) { - preg_match( '/WHERE\\s*(.*)$/im', $this->queries[0], $match ); - list($key, $value) = explode( '=', $match[1] ); - $key = trim( $key ); - $value = trim( preg_replace( "/[\';]/", '', $value ) ); - foreach ( $_results as $result ) { - if ( ! empty( $result->$key ) && is_scalar( $result->$key ) && stripos( $value, $result->$key ) !== false ) { - unset( $_results ); - $_results[] = $result; - break; - } - } - } - } - - $this->results = $_results; - } - - /** - * Method to the CHECK query data to an object. - * - * @access private - */ - private function convert_result_check_or_analyze() { - $is_check = 'check' === $this->query_type; - $_results[] = new Perflab_SQLite_Object_Array( - array( - 'Table' => '', - 'Op' => $is_check ? 'check' : 'analyze', - 'Msg_type' => 'status', - 'Msg_text' => $is_check ? __( 'OK', 'performance-lab' ) : __( 'Table is already up to date', 'performance-lab' ), - ) - ); - $this->results = $_results; - } - - /** - * Method to check SQLite library version. - * - * This is used for checking if SQLite can execute multiple rows insert. - * - * @return version number string or 0 - * @access private - */ - private function get_sqlite_version() { - try { - $statement = $this->pdo->prepare( 'SELECT sqlite_version()' ); - $statement->execute(); - $result = $statement->fetch( PDO::FETCH_NUM ); // phpcs:ignore WordPress.DB.RestrictedClasses - - return $result[0]; - } catch ( PDOException $err ) { - return '0'; - } - } - - /** - * Method to call PDO::beginTransaction(). - * - * @see PDO::beginTransaction() - * @return boolean - */ - public function beginTransaction() { - if ( $this->has_active_transaction ) { - return false; - } - $this->has_active_transaction = $this->pdo->beginTransaction(); - return $this->has_active_transaction; - } - - /** - * Method to call PDO::commit(). - * - * @see PDO::commit() - * @return void - */ - public function commit() { - $this->pdo->commit(); - $this->has_active_transaction = false; - } - - /** - * Method to call PDO::rollBack(). - * - * @see PDO::rollBack() - * @return void - */ - public function rollBack() { - $this->pdo->rollBack(); - $this->has_active_transaction = false; - } -} diff --git a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-db.php b/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-db.php similarity index 85% rename from modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-db.php rename to modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-db.php index bee6b09285..f3a066aed1 100644 --- a/modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-db.php +++ b/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-db.php @@ -11,14 +11,14 @@ * * It also rewrites some methods that use mysql specific functions. */ -class Perflab_SQLite_DB extends wpdb { +class WP_SQLite_DB extends wpdb { /** * Database Handle * * @access protected * - * @var Perflab_SQLite_PDO_Engine + * @var WP_SQLite_Translator */ protected $dbh; @@ -29,6 +29,7 @@ class Perflab_SQLite_DB extends wpdb { */ public function __construct() { parent::__construct( '', '', '', '' ); + $this->charset = 'utf8mb4'; } /** @@ -45,6 +46,20 @@ public function __construct() { 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'; + } + /** * Method to dummy out wpdb::set_sql_mode() * @@ -55,6 +70,16 @@ public function set_charset( $dbh, $charset = null, $collate = null ) { 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. + */ + public function close() { + return true; + } + /** * Method to select the database connection. * @@ -133,11 +158,10 @@ public function print_error( $str = '' ) { wp_load_translations_early(); $caller = $this->get_caller(); - $caller = $caller ? $caller : __( '(unknown)', 'performance-lab' ); + $caller = $caller ? $caller : '(unknown)'; $error_str = sprintf( - /* translators: 1: Database error message, 2: SQL query, 3: Caller. */ - __( 'WordPress database error %1$s for query %2$s made by %3$s', 'performance-lab' ), + 'WordPress database error %1$s for query %2$s made by %3$s', $str, $this->last_query, $caller @@ -162,8 +186,7 @@ public function print_error( $str = '' ) { $query = htmlspecialchars( $this->last_query, ENT_QUOTES ); printf( - /* translators: 1: Database error message, 2: SQL query. */ - '
' . __( 'WordPress database error: [%1$s] %2$s', 'performance-lab' ) . '
WordPress database error: [%1$s] %2$s
' . $query . '
'
);
@@ -199,9 +222,22 @@ public function flush() {
* @return void
*/
public function db_connect( $allow_bail = true ) {
+ if ( $this->dbh ) {
+ return;
+ }
$this->init_charset();
- $this->dbh = new Perflab_SQLite_PDO_Engine();
- $this->ready = true;
+
+ $pdo = null;
+ if ( isset( $GLOBALS['@pdo'] ) ) {
+ $pdo = $GLOBALS['@pdo'];
+ }
+ $this->dbh = new WP_SQLite_Translator( $pdo );
+ $this->last_error = $this->dbh->get_error_message();
+ if ( $this->last_error ) {
+ return false;
+ }
+ $GLOBALS['@pdo'] = $this->dbh->get_pdo();
+ $this->ready = true;
}
/**
@@ -251,12 +287,12 @@ public function query( $query ) {
}
$this->last_error = $this->dbh->get_error_message();
- if ( $this->last_error && ( ! defined( 'WP_INSTALLING' ) || ! WP_INSTALLING ) ) {
+ if ( $this->last_error ) {
$this->print_error( $this->last_error );
return false;
}
- if ( preg_match( '/^\\s*(create|alter|truncate|drop|optimize)\\s*/i', $query ) ) {
+ if ( preg_match( '/^\\s*(set|create|alter|truncate|drop|optimize)\\s*/i', $query ) ) {
return $this->dbh->get_return_value();
}
diff --git a/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-lexer.php b/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-lexer.php
new file mode 100644
index 0000000000..d16fa8a3d9
--- /dev/null
+++ b/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-lexer.php
@@ -0,0 +1,2580 @@
+
+ */
+ public static $operators = array(
+
+ /*
+ * Some operators (*, =) may have ambiguous flags, because they depend on
+ * the context they are being used in.
+ * For example: 1. SELECT * FROM table; # SQL specific (wildcard)
+ * SELECT 2 * 3; # arithmetic
+ * 2. SELECT * FROM table WHERE foo = 'bar';
+ * SET @i = 0;
+ */
+
+ // @see WP_SQLite_Token::FLAG_OPERATOR_ARITHMETIC
+ '%' => 1,
+ '*' => 1,
+ '+' => 1,
+ '-' => 1,
+ '/' => 1,
+
+ // @see WP_SQLite_Token::FLAG_OPERATOR_LOGICAL
+ '!' => 2,
+ '!=' => 2,
+ '&&' => 2,
+ '<' => 2,
+ '<=' => 2,
+ '<=>' => 2,
+ '<>' => 2,
+ '=' => 2,
+ '>' => 2,
+ '>=' => 2,
+ '||' => 2,
+
+ // @see WP_SQLite_Token::FLAG_OPERATOR_BITWISE
+ '&' => 4,
+ '<<' => 4,
+ '>>' => 4,
+ '^' => 4,
+ '|' => 4,
+ '~' => 4,
+
+ // @see WP_SQLite_Token::FLAG_OPERATOR_ASSIGNMENT
+ ':=' => 8,
+
+ // @see WP_SQLite_Token::FLAG_OPERATOR_SQL
+ '(' => 16,
+ ')' => 16,
+ '.' => 16,
+ ',' => 16,
+ ';' => 16,
+ );
+
+ /**
+ * List of keywords.
+ *
+ * The value associated to each keyword represents its flags.
+ *
+ * @see WP_SQLite_Token::FLAG_KEYWORD_RESERVED
+ * WP_SQLite_Token::FLAG_KEYWORD_COMPOSED
+ * WP_SQLite_Token::FLAG_KEYWORD_DATA_TYPE
+ * ∂WP_SQLite_Token::FLAG_KEYWORD_KEY
+ * WP_SQLite_Token::FLAG_KEYWORD_FUNCTION
+ *
+ * @var array
- * new Perflab_SQLite_PDO_User_Defined_Functions(ref_to_pdo_obj);
+ * new WP_SQLite_PDO_User_Defined_Functions(ref_to_pdo_obj);
*
*
* This automatically enables ref_to_pdo_obj to replace the function in the SQL statement
* to the ones defined here.
*/
-class Perflab_SQLite_PDO_User_Defined_Functions {
+class WP_SQLite_PDO_User_Defined_Functions {
/**
* The class constructor
@@ -47,31 +47,27 @@ public function __construct( $pdo ) {
*/
private $functions = array(
'month' => 'month',
+ 'monthnum' => 'month',
'year' => 'year',
'day' => 'day',
+ 'hour' => 'hour',
+ 'minute' => 'minute',
+ 'second' => 'second',
+ 'week' => 'week',
+ 'weekday' => 'weekday',
+ 'dayofweek' => 'dayofweek',
+ 'dayofmonth' => 'dayofmonth',
'unix_timestamp' => 'unix_timestamp',
'now' => 'now',
- 'char_length' => 'char_length',
'md5' => 'md5',
'curdate' => 'curdate',
'rand' => 'rand',
- 'substring' => 'substring',
- 'dayofmonth' => 'day',
- 'second' => 'second',
- 'minute' => 'minute',
- 'hour' => 'hour',
- 'date_format' => 'dateformat',
'from_unixtime' => 'from_unixtime',
- 'date_add' => 'date_add',
- 'date_sub' => 'date_sub',
- 'adddate' => 'date_add',
- 'subdate' => 'date_sub',
'localtime' => 'now',
'localtimestamp' => 'now',
'isnull' => 'isnull',
'if' => '_if',
- 'regexpp' => 'regexp',
- 'concat' => 'concat',
+ 'regexp' => 'regexp',
'field' => 'field',
'log' => 'log',
'least' => 'least',
@@ -90,39 +86,6 @@ public function __construct( $pdo ) {
'version' => 'version',
);
- /**
- * Method to extract the month value from the date.
- *
- * @param string $field representing the date formatted as 0000-00-00.
- *
- * @return string representing the number of the month between 1 and 12.
- */
- public function month( $field ) {
- return gmdate( 'n', strtotime( $field ) );
- }
-
- /**
- * Method to extract the year value from the date.
- *
- * @param string $field representing the date formatted as 0000-00-00.
- *
- * @return string representing the number of the year.
- */
- public function year( $field ) {
- return gmdate( 'Y', strtotime( $field ) );
- }
-
- /**
- * Method to extract the day value from the date.
- *
- * @param string $field Representing the date formatted as 0000-00-00.
- *
- * @return string representing the number of the day of the month from 1 and 31.
- */
- public function day( $field ) {
- return gmdate( 'j', strtotime( $field ) );
- }
-
/**
* Method to return the unix timestamp.
*
@@ -138,48 +101,13 @@ public function unix_timestamp( $field = null ) {
return is_null( $field ) ? time() : strtotime( $field );
}
- /**
- * Method to emulate MySQL SECOND() function.
- *
- * @param string $field Representing the time formatted as '00:00:00'.
- *
- * @return number of unsigned integer
- */
- public function second( $field ) {
- return intval( gmdate( 's', strtotime( $field ) ) );
- }
-
- /**
- * Method to emulate MySQL MINUTE() function.
- *
- * @param string $field Representing the time formatted as '00:00:00'.
- *
- * @return number of unsigned integer
- */
- public function minute( $field ) {
- return intval( gmdate( 'i', strtotime( $field ) ) );
- }
-
- /**
- * Method to emulate MySQL HOUR() function.
- *
- * @param string $time Representing the time formatted as '00:00:00'.
- *
- * @return number
- */
- public function hour( $time ) {
- list($hours) = explode( ':', $time );
-
- return intval( $hours );
- }
-
/**
* Method to emulate MySQL FROM_UNIXTIME() function.
*
- * @param integer $field The unix timestamp.
- * @param string $format Indicate the way of formatting(optional).
+ * @param int $field The unix timestamp.
+ * @param string $format Indicate the way of formatting(optional).
*
- * @return string formatted as '0000-00-00 00:00:00'.
+ * @return string
*/
public function from_unixtime( $field, $format = null ) {
// Convert to ISO time.
@@ -206,17 +134,6 @@ public function curdate() {
return gmdate( 'Y-m-d' );
}
- /**
- * Method to emulate MySQL CHAR_LENGTH() function.
- *
- * @param string $field The string to be measured.
- *
- * @return int unsigned integer for the length of the argument.
- */
- public function char_length( $field ) {
- return strlen( $field );
- }
-
/**
* Method to emulate MySQL MD5() function.
*
@@ -244,22 +161,6 @@ public function rand() {
return mt_rand( 0, 1 );
}
- /**
- * Method to emulate MySQL SUBSTRING() function.
- *
- * This function rewrites the function name to SQLite compatible substr(),
- * which can manipulate UTF-8 characters.
- *
- * @param string $text The text to be processed.
- * @param integer $pos Representing the start point.
- * @param integer $len Representing the length of the substring(optional).
- *
- * @return string
- */
- public function substring( $text, $pos, $len = null ) {
- return "substr($text, $pos, $len)";
- }
-
/**
* Method to emulate MySQL DATEFORMAT() function.
*
@@ -309,146 +210,216 @@ public function dateformat( $date, $format ) {
}
/**
- * Method to emulate MySQL DATE_ADD() function.
- *
- * This function adds the time value of $interval expression to $date.
- * $interval is a single quoted strings rewritten by SQLiteQueryDriver::rewrite_query().
- * It is calculated in the private function derive_interval().
+ * Method to extract the month value from the date.
*
- * @param string $date representing the start date.
- * @param string $interval representing the expression of the time to add.
+ * @param string $field Representing the date formatted as 0000-00-00.
*
- * @return string date formatted as '0000-00-00 00:00:00'.
+ * @return string Representing the number of the month between 1 and 12.
*/
- public function date_add( $date, $interval ) {
- $interval = $this->derive_interval( $interval );
- switch ( strtolower( $date ) ) {
- case 'curdate()':
- $date_object = new DateTime( $this->curdate() );
- $date_object->add( new DateInterval( $interval ) );
- return $date_object->format( 'Y-m-d' );
+ public function month( $field ) {
+ /*
+ * From https://www.php.net/manual/en/datetime.format.php:
+ *
+ * n - Numeric representation of a month, without leading zeros.
+ * 1 through 12
+ */
+ return intval( gmdate( 'n', strtotime( $field ) ) );
+ }
- case 'now()':
- $date_object = new DateTime( $this->now() );
- $date_object->add( new DateInterval( $interval ) );
- return $date_object->format( 'Y-m-d H:i:s' );
+ /**
+ * Method to extract the year value from the date.
+ *
+ * @param string $field Representing the date formatted as 0000-00-00.
+ *
+ * @return string Representing the number of the year.
+ */
+ public function year( $field ) {
+ /*
+ * From https://www.php.net/manual/en/datetime.format.php:
+ *
+ * Y - A full numeric representation of a year, 4 digits.
+ */
+ return intval( gmdate( 'Y', strtotime( $field ) ) );
+ }
- default:
- $date_object = new DateTime( $date );
- $date_object->add( new DateInterval( $interval ) );
- return $date_object->format( 'Y-m-d H:i:s' );
- }
+ /**
+ * Method to extract the day value from the date.
+ *
+ * @param string $field Representing the date formatted as 0000-00-00.
+ *
+ * @return string Representing the number of the day of the month from 1 and 31.
+ */
+ public function day( $field ) {
+ /*
+ * From https://www.php.net/manual/en/datetime.format.php:
+ *
+ * j - Day of the month without leading zeros.
+ * 1 to 31.
+ */
+ return intval( gmdate( 'j', strtotime( $field ) ) );
}
/**
- * Method to emulate MySQL DATE_SUB() function.
+ * Method to emulate MySQL SECOND() function.
*
- * This function subtracts the time value of $interval expression from $date.
- * $interval is a single quoted strings rewritten by SQLiteQueryDriver::rewrite_query().
- * It is calculated in the private function derive_interval().
+ * @see https://www.php.net/manual/en/datetime.format.php
*
- * @param string $date representing the start date.
- * @param string $interval representing the expression of the time to subtract.
+ * @param string $field Representing the time formatted as '00:00:00'.
*
- * @return string date formatted as '0000-00-00 00:00:00'.
+ * @return number Unsigned integer
*/
- public function date_sub( $date, $interval ) {
- $interval = $this->derive_interval( $interval );
- switch ( strtolower( $date ) ) {
- case 'curdate()':
- $date_object = new DateTime( $this->curdate() );
- $date_object->sub( new DateInterval( $interval ) );
- return $date_object->format( 'Y-m-d' );
-
- case 'now()':
- $date_object = new DateTime( $this->now() );
- $date_object->sub( new DateInterval( $interval ) );
- return $date_object->format( 'Y-m-d H:i:s' );
+ public function second( $field ) {
+ /*
+ * From https://www.php.net/manual/en/datetime.format.php:
+ *
+ * s - Seconds, with leading zeros (00 to 59)
+ */
+ return intval( gmdate( 's', strtotime( $field ) ) );
+ }
- default:
- $date_object = new DateTime( $date );
- $date_object->sub( new DateInterval( $interval ) );
- return $date_object->format( 'Y-m-d H:i:s' );
- }
+ /**
+ * Method to emulate MySQL MINUTE() function.
+ *
+ * @param string $field Representing the time formatted as '00:00:00'.
+ *
+ * @return int
+ */
+ public function minute( $field ) {
+ /*
+ * From https://www.php.net/manual/en/datetime.format.php:
+ *
+ * i - Minutes with leading zeros.
+ * 00 to 59.
+ */
+ return intval( gmdate( 'i', strtotime( $field ) ) );
}
/**
- * Method to calculate the interval time between two dates value.
+ * Method to emulate MySQL HOUR() function.
*
- * @access private
+ * Returns the hour for time, in 24-hour format, from 0 to 23.
+ * Importantly, midnight is 0, not 24.
*
- * @param string $interval white space separated expression.
+ * @param string $time Representing the time formatted, like '14:08:12'.
*
- * @return string representing the time to add or substract.
+ * @return int
*/
- private function derive_interval( $interval ) {
- $interval = trim( substr( trim( $interval ), 8 ) );
- $parts = explode( ' ', $interval );
- foreach ( $parts as $part ) {
- if ( ! empty( $part ) ) {
- $_parts[] = $part;
- }
- }
- $type = strtolower( end( $_parts ) );
- switch ( $type ) {
- case 'second':
- return 'PT' . $_parts[0] . 'S';
-
- case 'minute':
- return 'PT' . $_parts[0] . 'M';
-
- case 'hour':
- return 'PT' . $_parts[0] . 'H';
-
- case 'day':
- return 'P' . $_parts[0] . 'D';
-
- case 'week':
- return 'P' . $_parts[0] . 'W';
-
- case 'month':
- return 'P' . $_parts[0] . 'M';
-
- case 'year':
- return 'P' . $_parts[0] . 'Y';
-
- case 'minute_second':
- list($minutes, $seconds) = explode( ':', $_parts[0] );
- return 'PT' . $minutes . 'M' . $seconds . 'S';
-
- case 'hour_second':
- list($hours, $minutes, $seconds) = explode( ':', $_parts[0] );
- return 'PT' . $hours . 'H' . $minutes . 'M' . $seconds . 'S';
-
- case 'hour_minute':
- list($hours, $minutes) = explode( ':', $_parts[0] );
- return 'PT' . $hours . 'H' . $minutes . 'M';
-
- case 'day_second':
- $days = intval( $_parts[0] );
- list($hours, $minutes, $seconds) = explode( ':', $_parts[1] );
- return 'P' . $days . 'D' . 'T' . $hours . 'H' . $minutes . 'M' . $seconds . 'S';
-
- case 'day_minute':
- $days = intval( $_parts[0] );
- list($hours, $minutes) = explode( ':', $parts[1] );
- return 'P' . $days . 'D' . 'T' . $hours . 'H' . $minutes . 'M';
+ public function hour( $time ) {
+ /*
+ * From https://www.php.net/manual/en/datetime.format.php:
+ *
+ * H 24-hour format of an hour with leading zeros.
+ * 00 through 23.
+ */
+ return intval( gmdate( 'H', strtotime( $time ) ) );
+ }
+
+ /**
+ * Covers MySQL WEEK() function.
+ *
+ * Always assumes $mode = 1.
+ *
+ * @TODO: Support other modes.
+ *
+ * From https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_week:
+ *
+ * > Returns the week number for date. The two-argument form of WEEK()
+ * > enables you to specify whether the week starts on Sunday or Monday
+ * > and whether the return value should be in the range from 0 to 53
+ * > or from 1 to 53. If the mode argument is omitted, the value of the
+ * > default_week_format system variable is used.
+ * >
+ * > The following table describes how the mode argument works:
+ * >
+ * > Mode First day of week Range Week 1 is the first week …
+ * > 0 Sunday 0-53 with a Sunday in this year
+ * > 1 Monday 0-53 with 4 or more days this year
+ * > 2 Sunday 1-53 with a Sunday in this year
+ * > 3 Monday 1-53 with 4 or more days this year
+ * > 4 Sunday 0-53 with 4 or more days this year
+ * > 5 Monday 0-53 with a Monday in this year
+ * > 6 Sunday 1-53 with 4 or more days this year
+ * > 7 Monday 1-53 with a Monday in this year
+ *
+ * @param string $field Representing the date.
+ * @param int $mode The mode argument.
+ */
+ public function week( $field, $mode ) {
+ /*
+ * From https://www.php.net/manual/en/datetime.format.php:
+ *
+ * W - ISO-8601 week number of year, weeks starting on Monday.
+ * Example: 42 (the 42nd week in the year)
+ *
+ * Week 1 is the first week with a Thursday in it.
+ */
+ return intval( gmdate( 'W', strtotime( $field ) ) );
+ }
+
+ /**
+ * Simulates WEEKDAY() function in MySQL.
+ *
+ * Returns the day of the week as an integer.
+ * The days of the week are numbered 0 to 6:
+ * * 0 for Monday
+ * * 1 for Tuesday
+ * * 2 for Wednesday
+ * * 3 for Thursday
+ * * 4 for Friday
+ * * 5 for Saturday
+ * * 6 for Sunday
+ *
+ * @param string $field Representing the date.
+ *
+ * @return int
+ */
+ public function weekday( $field ) {
+ /*
+ * date('N') returns 1 (for Monday) through 7 (for Sunday)
+ * That's one more than MySQL.
+ * Let's subtract one to make it compatible.
+ */
+ return intval( gmdate( 'N', strtotime( $field ) ) ) - 1;
+ }
- case 'day_hour':
- $days = intval( $_parts[0] );
- $hours = intval( $_parts[1] );
- return 'P' . $days . 'D' . 'T' . $hours . 'H';
+ /**
+ * Method to emulate MySQL DAYOFMONTH() function.
+ *
+ * @see https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_dayofmonth
+ *
+ * @param string $field Representing the date.
+ *
+ * @return int Returns the day of the month for date as a number in the range 1 to 31.
+ */
+ public function dayofmonth( $field ) {
+ return intval( gmdate( 'j', strtotime( $field ) ) );
+ }
- case 'year_month':
- list($years, $months) = explode( '-', $_parts[0] );
- return 'P' . $years . 'Y' . $months . 'M';
- }
- return '';
+ /**
+ * Method to emulate MySQL DAYOFWEEK() function.
+ *
+ * > Returns the weekday index for date (1 = Sunday, 2 = Monday, …, 7 = Saturday).
+ * > These index values correspond to the ODBC standard. Returns NULL if date is NULL.
+ *
+ * @param string $field Representing the date.
+ *
+ * @return int Returns the weekday index for date (1 = Sunday, 2 = Monday, …, 7 = Saturday).
+ */
+ public function dayofweek( $field ) {
+ /**
+ * From https://www.php.net/manual/en/datetime.format.php:
+ *
+ * `w` – Numeric representation of the day of the week
+ * 0 (for Sunday) through 6 (for Saturday)
+ */
+ return intval( gmdate( 'w', strtotime( $field ) ) ) + 1;
}
/**
* Method to emulate MySQL DATE() function.
*
+ * @see https://www.php.net/manual/en/datetime.format.php
+ *
* @param string $date formatted as unix time.
*
* @return string formatted as '0000-00-00'.
@@ -488,40 +459,39 @@ public function _if( $expression, $true, $false ) {
/**
* Method to emulate MySQL REGEXP() function.
*
- * @param string $field Haystack.
* @param string $pattern Regular expression to match.
+ * @param string $field Haystack.
*
* @return integer 1 if matched, 0 if not matched.
*/
- public function regexp( $field, $pattern ) {
+ public function regexp( $pattern, $field ) {
+ /*
+ * If the original query says REGEXP BINARY
+ * the comparison is byte-by-byte and letter casing now
+ * matters since lower- and upper-case letters have different
+ * byte codes.
+ *
+ * The REGEXP function can't be easily made to accept two
+ * parameters, so we'll have to use a hack to get around this.
+ *
+ * If the first character of the pattern is a null byte, we'll
+ * remove it and make the comparison case-sensitive. This should
+ * be reasonably safe since PHP does not allow null bytes in
+ * regular expressions anyway.
+ */
+ if ( "\x00" === $pattern[0] ) {
+ $pattern = substr( $pattern, 1 );
+ $flags = '';
+ } else {
+ // Otherwise, the search is case-insensitive.
+ $flags = 'i';
+ }
$pattern = str_replace( '/', '\/', $pattern );
- $pattern = '/' . $pattern . '/i';
+ $pattern = '/' . $pattern . '/' . $flags;
return preg_match( $pattern, $field );
}
- /**
- * Method to emulate MySQL CONCAT() function.
- *
- * SQLite does have CONCAT() function, but it has a different syntax from MySQL.
- * So this function must be manipulated here.
- *
- * @return null|string Return null if the argument is null, or a concatenated string if the argument is given.
- */
- public function concat() {
- $return_value = '';
- $args_num = func_num_args();
- $args_list = func_get_args();
- for ( $i = 0; $i < $args_num; $i++ ) {
- if ( is_null( $args_list[ $i ] ) ) {
- return null;
- }
- $return_value .= $args_list[ $i ];
- }
-
- return $return_value;
- }
-
/**
* Method to emulate MySQL FIELD() function.
*
@@ -532,18 +502,13 @@ public function concat() {
* @return int
*/
public function field() {
- global $wpdb;
$num_args = func_num_args();
if ( $num_args < 2 || is_null( func_get_arg( 0 ) ) ) {
return 0;
}
$arg_list = func_get_args();
- $search_string = array_shift( $arg_list );
- $str_to_check = substr( $search_string, 0, strpos( $search_string, '.' ) );
- $str_to_check = str_replace( $wpdb->prefix, '', $str_to_check );
- if ( $str_to_check && in_array( trim( $str_to_check ), $wpdb->tables, true ) ) {
- return 0;
- }
+ $search_string = strtolower( array_shift( $arg_list ) );
+
for ( $i = 0; $i < $num_args - 1; $i++ ) {
if ( strtolower( $arg_list[ $i ] ) === $search_string ) {
return $i + 1;
@@ -597,7 +562,7 @@ public function log() {
public function least() {
$arg_list = func_get_args();
- return "min($arg_list)";
+ return min( $arg_list );
}
/**
@@ -610,7 +575,7 @@ public function least() {
public function greatest() {
$arg_list = func_get_args();
- return "max($arg_list)";
+ return max( $arg_list );
}
/**
diff --git a/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-query-rewriter.php b/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-query-rewriter.php
new file mode 100644
index 0000000000..b085315a95
--- /dev/null
+++ b/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-query-rewriter.php
@@ -0,0 +1,343 @@
+input_tokens = $input_tokens;
+ $this->max = count( $input_tokens );
+ }
+
+ /**
+ * Returns the updated query.
+ *
+ * @return string
+ */
+ public function get_updated_query() {
+ $query = '';
+ foreach ( $this->output_tokens as $token ) {
+ $query .= $token->token;
+ }
+ return $query;
+ }
+
+ /**
+ * Add a token to the output.
+ *
+ * @param WP_SQLite_Token $token Token object.
+ */
+ public function add( $token ) {
+ if ( $token ) {
+ $this->output_tokens[] = $token;
+ }
+ }
+
+ /**
+ * Add multiple tokens to the output.
+ *
+ * @param WP_SQLite_Token[] $tokens Array of token objects.
+ */
+ public function add_many( $tokens ) {
+ $this->output_tokens = array_merge( $this->output_tokens, $tokens );
+ }
+
+ /**
+ * Replaces all tokens.
+ *
+ * @param WP_SQLite_Token[] $tokens Array of token objects.
+ */
+ public function replace_all( $tokens ) {
+ $this->output_tokens = $tokens;
+ }
+
+ /**
+ * Peek at the next tokens and return one that matches the given criteria.
+ *
+ * @param array $query Optional. Search query.
+ * [
+ * 'type' => string|null, // Token type.
+ * 'flags' => int|null, // Token flags.
+ * 'values' => string|null, // Token values.
+ * ].
+ *
+ * @return WP_SQLite_Token
+ */
+ public function peek( $query = array() ) {
+ $type = isset( $query['type'] ) ? $query['type'] : null;
+ $flags = isset( $query['flags'] ) ? $query['flags'] : null;
+ $values = isset( $query['value'] )
+ ? ( is_array( $query['value'] ) ? $query['value'] : array( $query['value'] ) )
+ : null;
+
+ $i = $this->index;
+ while ( ++$i < $this->max ) {
+ if ( $this->input_tokens[ $i ]->matches( $type, $flags, $values ) ) {
+ return $this->input_tokens[ $i ];
+ }
+ }
+ }
+
+ /**
+ * Move forward and return the next tokens that match the given criteria.
+ *
+ * @param int $nth The nth token to return.
+ *
+ * @return WP_SQLite_Token
+ */
+ public function peek_nth( $nth ) {
+ $found = 0;
+ for ( $i = $this->index + 1;$i < $this->max;$i++ ) {
+ $token = $this->input_tokens[ $i ];
+ if ( ! $token->is_semantically_void() ) {
+ ++$found;
+ }
+ if ( $found === $nth ) {
+ return $this->input_tokens[ $i ];
+ }
+ }
+ }
+
+ /**
+ * Consume all the tokens.
+ *
+ * @param array $query Search query.
+ *
+ * @return void
+ */
+ public function consume_all( $query = array() ) {
+ while ( $this->consume( $query ) ) {
+ // Do nothing.
+ }
+ }
+
+ /**
+ * Consume the next tokens and return one that matches the given criteria.
+ *
+ * @param array $query Search query.
+ * [
+ * 'type' => null, // Optional. Token type.
+ * 'flags' => null, // Optional. Token flags.
+ * 'values' => null, // Optional. Token values.
+ * ].
+ *
+ * @return WP_SQLite_Token|null
+ */
+ public function consume( $query = array() ) {
+ $tokens = $this->move_forward( $query );
+ $this->output_tokens = array_merge( $this->output_tokens, $tokens );
+ return $this->token;
+ }
+
+ /**
+ * Drop the last consumed token and return it.
+ *
+ * @return WP_SQLite_Token|null
+ */
+ public function drop_last() {
+ return array_pop( $this->output_tokens );
+ }
+
+ /**
+ * Skip over the next tokens and return one that matches the given criteria.
+ *
+ * @param array $query Search query.
+ * [
+ * 'type' => null, // Optional. Token type.
+ * 'flags' => null, // Optional. Token flags.
+ * 'values' => null, // Optional. Token values.
+ * ].
+ *
+ * @return WP_SQLite_Token|null
+ */
+ public function skip( $query = array() ) {
+ $this->skip_and_return_all( $query );
+ return $this->token;
+ }
+
+ /**
+ * Skip over the next tokens until one matches the given criteria,
+ * and return all the skipped tokens.
+ *
+ * @param array $query Search query.
+ * [
+ * 'type' => null, // Optional. Token type.
+ * 'flags' => null, // Optional. Token flags.
+ * 'values' => null, // Optional. Token values.
+ * ].
+ *
+ * @return WP_SQLite_Token[]
+ */
+ public function skip_and_return_all( $query = array() ) {
+ $tokens = $this->move_forward( $query );
+
+ /*
+ * When skipping over whitespaces, make sure to consume
+ * at least one to avoid SQL syntax errors.
+ */
+ foreach ( $tokens as $token ) {
+ if ( $token->matches( WP_SQLite_Token::TYPE_WHITESPACE ) ) {
+ $this->add( $token );
+ break;
+ }
+ }
+
+ return $tokens;
+ }
+
+ /**
+ * Returns the next tokens that match the given criteria.
+ *
+ * @param array $query Search query.
+ * [
+ * 'type' => string|null, // Optional. Token type.
+ * 'flags' => int|null, // Optional. Token flags.
+ * 'values' => string|null, // Optional. Token values.
+ * ].
+ *
+ * @return array
+ */
+ private function move_forward( $query = array() ) {
+ $type = isset( $query['type'] ) ? $query['type'] : null;
+ $flags = isset( $query['flags'] ) ? $query['flags'] : null;
+ $values = isset( $query['value'] )
+ ? ( is_array( $query['value'] ) ? $query['value'] : array( $query['value'] ) )
+ : null;
+ $depth = isset( $query['depth'] ) ? $query['depth'] : null;
+
+ $buffered = array();
+ while ( true ) {
+ if ( ++$this->index >= $this->max ) {
+ $this->token = null;
+ $this->call_stack = array();
+ break;
+ }
+ $this->token = $this->input_tokens[ $this->index ];
+ $this->update_call_stack();
+ $buffered[] = $this->token;
+ if (
+ ( null === $depth || $this->depth === $depth )
+ && $this->token->matches( $type, $flags, $values )
+ ) {
+ break;
+ }
+ }
+
+ return $buffered;
+ }
+
+ /**
+ * Returns the last call stack element.
+ *
+ * @return array|null
+ */
+ public function last_call_stack_element() {
+ return count( $this->call_stack ) ? $this->call_stack[ count( $this->call_stack ) - 1 ] : null;
+ }
+
+ /**
+ * Updates the call stack.
+ *
+ * @return void
+ */
+ private function update_call_stack() {
+ if ( $this->token->flags & WP_SQLite_Token::FLAG_KEYWORD_FUNCTION ) {
+ $this->last_function_call = $this->token->value;
+ }
+ if ( WP_SQLite_Token::TYPE_OPERATOR === $this->token->type ) {
+ switch ( $this->token->value ) {
+ case '(':
+ if ( $this->last_function_call ) {
+ array_push(
+ $this->call_stack,
+ array(
+ 'function' => $this->last_function_call,
+ 'depth' => $this->depth,
+ )
+ );
+ $this->last_function_call = null;
+ }
+ ++$this->depth;
+ break;
+
+ case ')':
+ --$this->depth;
+ $call_parent = $this->last_call_stack_element();
+ if (
+ $call_parent &&
+ $call_parent['depth'] === $this->depth
+ ) {
+ array_pop( $this->call_stack );
+ }
+ break;
+ }
+ }
+ }
+}
diff --git a/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-token.php b/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-token.php
new file mode 100644
index 0000000000..70e2428306
--- /dev/null
+++ b/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-token.php
@@ -0,0 +1,328 @@
+, !==, etc.
+ * Bitwise operators: &, |, ^, etc.
+ * Assignment operators: =, +=, -=, etc.
+ * SQL specific operators: . (e.g. .. WHERE database.table ..),
+ * * (e.g. SELECT * FROM ..)
+ */
+ const TYPE_OPERATOR = 2;
+
+ /**
+ * Spaces, tabs, new lines, etc.
+ */
+ const TYPE_WHITESPACE = 3;
+
+ /**
+ * Any type of legal comment.
+ *
+ * Bash (#), C (/* *\/) or SQL (--) comments:
+ *
+ * -- SQL-comment
+ *
+ * #Bash-like comment
+ *
+ * /*C-like comment*\/
+ *
+ * or:
+ *
+ * /*C-like
+ * comment*\/
+ *
+ * Backslashes were added to respect PHP's comments syntax.
+ */
+ const TYPE_COMMENT = 4;
+
+ /**
+ * Boolean values: true or false.
+ */
+ const TYPE_BOOL = 5;
+
+ /**
+ * Numbers: 4, 0x8, 15.16, 23e42, etc.
+ */
+ const TYPE_NUMBER = 6;
+
+ /**
+ * Literal strings: 'string', "test".
+ * Some of these strings are actually symbols.
+ */
+ const TYPE_STRING = 7;
+
+ /**
+ * Database, table names, variables, etc.
+ * For example: ```SELECT `foo`, `bar` FROM `database`.`table`;```.
+ */
+ const TYPE_SYMBOL = 8;
+
+ /**
+ * Delimits an unknown string.
+ * For example: ```SELECT * FROM test;```, `test` is a delimiter.
+ */
+ const TYPE_DELIMITER = 9;
+
+ /**
+ * Labels in LOOP statement, ITERATE statement etc.
+ * For example (only for begin label):
+ * begin_label: BEGIN [statement_list] END [end_label]
+ * begin_label: LOOP [statement_list] END LOOP [end_label]
+ * begin_label: REPEAT [statement_list] ... END REPEAT [end_label]
+ * begin_label: WHILE ... DO [statement_list] END WHILE [end_label].
+ */
+ const TYPE_LABEL = 10;
+
+ // Flags that describe the tokens in more detail.
+ // All keywords must have flag 1 so `Context::isKeyword` method doesn't
+ // require strict comparison.
+ const FLAG_KEYWORD_RESERVED = 2;
+ const FLAG_KEYWORD_COMPOSED = 4;
+ const FLAG_KEYWORD_DATA_TYPE = 8;
+ const FLAG_KEYWORD_KEY = 16;
+ const FLAG_KEYWORD_FUNCTION = 32;
+
+ // Numbers related flags.
+ const FLAG_NUMBER_HEX = 1;
+ const FLAG_NUMBER_FLOAT = 2;
+ const FLAG_NUMBER_APPROXIMATE = 4;
+ const FLAG_NUMBER_NEGATIVE = 8;
+ const FLAG_NUMBER_BINARY = 16;
+
+ // Strings related flags.
+ const FLAG_STRING_SINGLE_QUOTES = 1;
+ const FLAG_STRING_DOUBLE_QUOTES = 2;
+
+ // Comments related flags.
+ const FLAG_COMMENT_BASH = 1;
+ const FLAG_COMMENT_C = 2;
+ const FLAG_COMMENT_SQL = 4;
+ const FLAG_COMMENT_MYSQL_CMD = 8;
+
+ // Operators related flags.
+ const FLAG_OPERATOR_ARITHMETIC = 1;
+ const FLAG_OPERATOR_LOGICAL = 2;
+ const FLAG_OPERATOR_BITWISE = 4;
+ const FLAG_OPERATOR_ASSIGNMENT = 8;
+ const FLAG_OPERATOR_SQL = 16;
+
+ // Symbols related flags.
+ const FLAG_SYMBOL_VARIABLE = 1;
+ const FLAG_SYMBOL_BACKTICK = 2;
+ const FLAG_SYMBOL_USER = 4;
+ const FLAG_SYMBOL_SYSTEM = 8;
+ const FLAG_SYMBOL_PARAMETER = 16;
+
+ /**
+ * The token it its raw string representation.
+ *
+ * @var string
+ */
+ public $token;
+
+ /**
+ * The value this token contains (i.e. token after some evaluation).
+ *
+ * @var mixed
+ */
+ public $value;
+
+ /**
+ * The keyword value this token contains, always uppercase.
+ *
+ * @var mixed|string|null
+ */
+ public $keyword;
+
+ /**
+ * The type of this token.
+ *
+ * @var int
+ */
+ public $type;
+
+ /**
+ * The flags of this token.
+ *
+ * @var int
+ */
+ public $flags;
+
+ /**
+ * The position in the initial string where this token started.
+ *
+ * The position is counted in chars, not bytes, so you should
+ * use mb_* functions to properly handle utf-8 multibyte chars.
+ *
+ * @var int|null
+ */
+ public $position;
+
+ /**
+ * Constructor.
+ *
+ * @param string $token The value of the token.
+ * @param int $type The type of the token.
+ * @param int $flags The flags of the token.
+ */
+ public function __construct( $token, $type = 0, $flags = 0 ) {
+ $this->token = $token;
+ $this->type = $type;
+ $this->flags = $flags;
+ $this->keyword = null;
+ $this->value = $this->extract();
+ }
+
+ /**
+ * Check if the token matches the given parameters.
+ *
+ * @param int|null $type The type of the token.
+ * @param int|null $flags The flags of the token.
+ * @param array|null $values The values of the token.
+ *
+ * @return bool
+ */
+ public function matches( $type = null, $flags = null, $values = null ) {
+ if ( null === $type && null === $flags && ( null === $values || array() === $values ) ) {
+ return ! $this->is_semantically_void();
+ }
+
+ return (
+ ( null === $type || $this->type === $type )
+ && ( null === $flags || ( $this->flags & $flags ) )
+ && ( null === $values || in_array( strtoupper( $this->value ), $values, true ) )
+ );
+ }
+
+ /**
+ * Check if the token is semantically void (i.e. whitespace or comment).
+ *
+ * @return bool
+ */
+ public function is_semantically_void() {
+ return $this->matches( self::TYPE_WHITESPACE ) || $this->matches( self::TYPE_COMMENT );
+ }
+
+ /**
+ * Does little processing to the token to extract a value.
+ *
+ * If no processing can be done it will return the initial string.
+ *
+ * @return mixed
+ */
+ private function extract() {
+ switch ( $this->type ) {
+ case self::TYPE_KEYWORD:
+ $this->keyword = strtoupper( $this->token );
+ if ( ! ( $this->flags & self::FLAG_KEYWORD_RESERVED ) ) {
+ /*
+ * Unreserved keywords should stay the way they are
+ * because they might represent field names.
+ */
+ return $this->token;
+ }
+
+ return $this->keyword;
+
+ case self::TYPE_WHITESPACE:
+ return ' ';
+
+ case self::TYPE_BOOL:
+ return strtoupper( $this->token ) === 'TRUE';
+
+ case self::TYPE_NUMBER:
+ $ret = str_replace( '--', '', $this->token ); // e.g. ---42 === -42.
+ if ( $this->flags & self::FLAG_NUMBER_HEX ) {
+ if ( $this->flags & self::FLAG_NUMBER_NEGATIVE ) {
+ $ret = str_replace( '-', '', $this->token );
+ $ret = -hexdec( $ret );
+ } else {
+ $ret = hexdec( $ret );
+ }
+ } elseif ( ( $this->flags & self::FLAG_NUMBER_APPROXIMATE ) || ( $this->flags & self::FLAG_NUMBER_FLOAT ) ) {
+ $ret = (float) $ret;
+ } elseif ( ! ( $this->flags & self::FLAG_NUMBER_BINARY ) ) {
+ $ret = (int) $ret;
+ }
+
+ return $ret;
+
+ case self::TYPE_STRING:
+ // Trims quotes.
+ $str = $this->token;
+ $str = mb_substr( $str, 1, -1, 'UTF-8' );
+
+ // Removes surrounding quotes.
+ $quote = $this->token[0];
+ $str = str_replace( $quote . $quote, $quote, $str );
+
+ /*
+ * Finally unescapes the string.
+ *
+ * `stripcslashes` replaces escape sequences with their
+ * representation.
+ */
+ $str = stripcslashes( $str );
+
+ return $str;
+
+ case self::TYPE_SYMBOL:
+ $str = $this->token;
+ if ( isset( $str[0] ) && ( '@' === $str[0] ) ) {
+ /*
+ * `mb_strlen($str)` must be used instead of `null` because
+ * in PHP 5.3- the `null` parameter isn't handled correctly.
+ */
+ $str = mb_substr(
+ $str,
+ ! empty( $str[1] ) && ( '@' === $str[1] ) ? 2 : 1,
+ mb_strlen( $str ),
+ 'UTF-8'
+ );
+ }
+
+ if ( isset( $str[0] ) && ( ':' === $str[0] ) ) {
+ $str = mb_substr( $str, 1, mb_strlen( $str ), 'UTF-8' );
+ }
+
+ if ( isset( $str[0] ) && ( ( '`' === $str[0] ) || ( '"' === $str[0] ) || ( '\'' === $str[0] ) ) ) {
+ $quote = $str[0];
+ $str = str_replace( $quote . $quote, $quote, $str );
+ $str = mb_substr( $str, 1, -1, 'UTF-8' );
+ }
+
+ return $str;
+ }
+
+ return $this->token;
+ }
+}
diff --git a/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-translator.php b/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-translator.php
new file mode 100644
index 0000000000..afe3161fc3
--- /dev/null
+++ b/modules/database/sqlite/wp-includes/sqlite/class-wp-sqlite-translator.php
@@ -0,0 +1,3624 @@
+ 'integer',
+ 'bool' => 'integer',
+ 'boolean' => 'integer',
+ 'tinyint' => 'integer',
+ 'smallint' => 'integer',
+ 'mediumint' => 'integer',
+ 'int' => 'integer',
+ 'integer' => 'integer',
+ 'bigint' => 'integer',
+ 'float' => 'real',
+ 'double' => 'real',
+ 'decimal' => 'real',
+ 'dec' => 'real',
+ 'numeric' => 'real',
+ 'fixed' => 'real',
+ 'date' => 'text',
+ 'datetime' => 'text',
+ 'timestamp' => 'text',
+ 'time' => 'text',
+ 'year' => 'text',
+ 'char' => 'text',
+ 'varchar' => 'text',
+ 'binary' => 'integer',
+ 'varbinary' => 'blob',
+ 'tinyblob' => 'blob',
+ 'tinytext' => 'text',
+ 'blob' => 'blob',
+ 'text' => 'text',
+ 'mediumblob' => 'blob',
+ 'mediumtext' => 'text',
+ 'longblob' => 'blob',
+ 'longtext' => 'text',
+ 'geomcollection' => 'text',
+ 'geometrycollection' => 'text',
+ );
+
+ /**
+ * The MySQL to SQLite date formats translation.
+ *
+ * Maps MySQL formats to SQLite strftime() formats.
+ *
+ * For MySQL formats, see:
+ * * https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_date-format
+ *
+ * For SQLite formats, see:
+ * * https://www.sqlite.org/lang_datefunc.html
+ * * https://strftime.org/
+ *
+ * @var array
+ */
+ private $mysql_date_format_to_sqlite_strftime = array(
+ '%a' => '%D',
+ '%b' => '%M',
+ '%c' => '%n',
+ '%D' => '%jS',
+ '%d' => '%d',
+ '%e' => '%j',
+ '%H' => '%H',
+ '%h' => '%h',
+ '%I' => '%h',
+ '%i' => '%M',
+ '%j' => '%z',
+ '%k' => '%G',
+ '%l' => '%g',
+ '%M' => '%F',
+ '%m' => '%m',
+ '%p' => '%A',
+ '%r' => '%h:%i:%s %A',
+ '%S' => '%s',
+ '%s' => '%s',
+ '%T' => '%H:%i:%s',
+ '%U' => '%W',
+ '%u' => '%W',
+ '%V' => '%W',
+ '%v' => '%W',
+ '%W' => '%l',
+ '%w' => '%w',
+ '%X' => '%Y',
+ '%x' => '%o',
+ '%Y' => '%Y',
+ '%y' => '%y',
+ );
+
+ /**
+ * Number of rows found by the last SELECT query.
+ *
+ * @var int
+ */
+ private $last_select_found_rows;
+
+ /**
+ * Number of rows found by the last SQL_CALC_FOUND_ROW query.
+ *
+ * @var int integer
+ */
+ private $last_sql_calc_found_rows = null;
+
+ /**
+ * The query rewriter.
+ *
+ * @var WP_SQLite_Query_Rewriter
+ */
+ private $rewriter;
+
+ /**
+ * Last executed MySQL query.
+ *
+ * @var string
+ */
+ public $mysql_query;
+
+ /**
+ * A list of executed SQLite queries.
+ *
+ * @var array
+ */
+ public $executed_sqlite_queries = array();
+
+ /**
+ * The columns to insert.
+ *
+ * @var array
+ */
+ private $insert_columns = array();
+
+ /**
+ * Class variable to store the result of the query.
+ *
+ * @access private
+ *
+ * @var array reference to the PHP object
+ */
+ private $results = null;
+
+ /**
+ * Class variable to check if there is an error.
+ *
+ * @var boolean
+ */
+ public $is_error = false;
+
+ /**
+ * Class variable to store the file name and function to cause error.
+ *
+ * @access private
+ *
+ * @var array
+ */
+ private $errors;
+
+ /**
+ * Class variable to store the error messages.
+ *
+ * @access private
+ *
+ * @var array
+ */
+ private $error_messages = array();
+
+ /**
+ * Class variable to store the affected row id.
+ *
+ * @var int integer
+ * @access private
+ */
+ private $last_insert_id;
+
+ /**
+ * Class variable to store the number of rows affected.
+ *
+ * @var int integer
+ */
+ private $affected_rows;
+
+ /**
+ * Class variable to store the queried column info.
+ *
+ * @var array
+ */
+ private $column_data;
+
+ /**
+ * Variable to emulate MySQL affected row.
+ *
+ * @var integer
+ */
+ private $num_rows;
+
+ /**
+ * Return value from query().
+ *
+ * Each query has its own return value.
+ *
+ * @var mixed
+ */
+ private $return_value;
+
+ /**
+ * Variable to keep track of nested transactions level.
+ *
+ * @var number
+ */
+ private $transaction_level = 0;
+
+ /**
+ * Value returned by the last exec().
+ *
+ * @var mixed
+ */
+ private $last_exec_returned;
+
+ /**
+ * The PDO fetch mode passed to query().
+ *
+ * @var mixed
+ */
+ private $pdo_fetch_mode;
+
+ /**
+ * The last reserved keyword seen in an SQL query.
+ *
+ * @var mixed
+ */
+ private $last_reserved_keyword;
+
+ /**
+ * True if a VACUUM operation should be done on shutdown,
+ * to handle OPTIMIZE TABLE and similar operations.
+ *
+ * @var bool
+ */
+ private $vacuum_requested = false;
+
+ /**
+ * True if the present query is metadata
+ *
+ * @var bool
+ */
+ private $is_information_schema_query = false;
+
+ /**
+ * True if a GROUP BY clause is detected.
+ *
+ * @var bool
+ */
+ private $has_group_by = false;
+
+ /**
+ * 0 if no LIKE is in progress, otherwise counts nested parentheses.
+ *
+ * @todo A generic stack of expression would scale better. There's already a call_stack in WP_SQLite_Query_Rewriter.
+ * @var int
+ */
+ private $like_expression_nesting = 0;
+
+ /**
+ * 0 if no LIKE is in progress, otherwise counts nested parentheses.
+ *
+ * @var int
+ */
+ private $like_escape_count = 0;
+
+ /**
+ * Associative array with list of system (non-WordPress) tables.
+ *
+ * @var array [tablename => tablename]
+ */
+ private $sqlite_system_tables = array();
+
+ /**
+ * Constructor.
+ *
+ * Create PDO object, set user defined functions and initialize other settings.
+ * Don't use parent::__construct() because this class does not only returns
+ * PDO instance but many others jobs.
+ *
+ * @param PDO $pdo The PDO object.
+ */
+ public function __construct( $pdo = null ) {
+ if ( ! $pdo ) {
+ if ( ! is_file( FQDB ) ) {
+ $this->prepare_directory();
+ }
+
+ $locked = false;
+ $status = 0;
+ $err_message = '';
+ do {
+ try {
+ $options = array(
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // phpcs:ignore WordPress.DB.RestrictedClasses
+ PDO::ATTR_STRINGIFY_FETCHES => true, // phpcs:ignore WordPress.DB.RestrictedClasses
+ PDO::ATTR_TIMEOUT => 5, // phpcs:ignore WordPress.DB.RestrictedClasses
+ );
+
+ $dsn = 'sqlite:' . FQDB;
+ $pdo = new PDO( $dsn, null, null, $options ); // phpcs:ignore WordPress.DB.RestrictedClasses
+ } catch ( PDOException $ex ) {
+ $status = $ex->getCode();
+ if ( self::SQLITE_BUSY === $status || self::SQLITE_LOCKED === $status ) {
+ $locked = true;
+ } else {
+ $err_message = $ex->getMessage();
+ }
+ }
+ } while ( $locked );
+
+ if ( $status > 0 ) {
+ $message = sprintf(
+ '%s
%s
%s
', + 'Database initialization error!', + "Code: $status", + "Error Message: $err_message" + ); + $this->is_error = true; + $this->error_messages[] = $message; + return; + } + } + + new WP_SQLite_PDO_User_Defined_Functions( $pdo ); + + // MySQL data comes across stringified by default. + $pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO + $pdo->query( WP_SQLite_Translator::CREATE_DATA_TYPES_CACHE_TABLE ); + + /* + * A list of system tables lets us emulate information_schema + * queries without returning extra tables. + */ + $this->sqlite_system_tables ['sqlite_sequence'] = 'sqlite_sequence'; + $this->sqlite_system_tables [ self::DATA_TYPES_CACHE_TABLE ] = self::DATA_TYPES_CACHE_TABLE; + + $this->pdo = $pdo; + + // Fixes a warning in the site-health screen. + $this->client_info = SQLite3::version()['versionString']; + + register_shutdown_function( array( $this, '__destruct' ) ); + + // WordPress happens to use no foreign keys. + $statement = $this->pdo->query( 'PRAGMA foreign_keys' ); + if ( $statement->fetchColumn( 0 ) == '0' ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison + $this->pdo->query( 'PRAGMA foreign_keys = ON' ); + } + $this->pdo->query( 'PRAGMA encoding="UTF-8";' ); + } + + /** + * Destructor + * + * If SQLITE_MEM_DEBUG constant is defined, append information about + * memory usage into database/mem_debug.txt. + * + * This definition is changed since version 1.7. + */ + function __destruct() { + if ( defined( 'SQLITE_MEM_DEBUG' ) && SQLITE_MEM_DEBUG ) { + $max = ini_get( 'memory_limit' ); + if ( is_null( $max ) ) { + $message = sprintf( + '[%s] Memory_limit is not set in php.ini file.', + gmdate( 'Y-m-d H:i:s', $_SERVER['REQUEST_TIME'] ) + ); + error_log( $message ); + return; + } + if ( stripos( $max, 'M' ) !== false ) { + $max = (int) $max * MB_IN_BYTES; + } + $peak = memory_get_peak_usage( true ); + $used = round( (int) $peak / (int) $max * 100, 2 ); + if ( $used > 90 ) { + $message = sprintf( + "[%s] Memory peak usage warning: %s %% used. (max: %sM, now: %sM)\n", + gmdate( 'Y-m-d H:i:s', $_SERVER['REQUEST_TIME'] ), + $used, + $max, + $peak + ); + error_log( $message ); + } + } + } + + /** + * Get the PDO object. + * + * @return PDO + */ + public function get_pdo() { + return $this->pdo; + } + + /** + * Method to return inserted row id. + */ + public function get_insert_id() { + return $this->last_insert_id; + } + + /** + * Method to return the number of rows affected. + */ + public function get_affected_rows() { + return $this->affected_rows; + } + + /** + * This method makes database directory and .htaccess file. + * + * It is executed only once when the installation begins. + */ + private function prepare_directory() { + global $wpdb; + $u = umask( 0000 ); + if ( ! is_dir( FQDBDIR ) ) { + if ( ! @mkdir( FQDBDIR, 0704, true ) ) { + umask( $u ); + wp_die( 'Unable to create the required directory! Please check your server settings.', 'Error!' ); + } + } + if ( ! is_writable( FQDBDIR ) ) { + umask( $u ); + $message = 'Unable to create a file in the directory! Please check your server settings.'; + wp_die( $message, 'Error!' ); + } + if ( ! is_file( FQDBDIR . '.htaccess' ) ) { + $fh = fopen( FQDBDIR . '.htaccess', 'w' ); + if ( ! $fh ) { + umask( $u ); + echo 'Unable to create a file in the directory! Please check your server settings.'; + + return false; + } + fwrite( $fh, 'DENY FROM ALL' ); + fclose( $fh ); + } + if ( ! is_file( FQDBDIR . 'index.php' ) ) { + $fh = fopen( FQDBDIR . 'index.php', 'w' ); + if ( ! $fh ) { + umask( $u ); + echo 'Unable to create a file in the directory! Please check your server settings.'; + + return false; + } + fwrite( $fh, '' ); + fclose( $fh ); + } + umask( $u ); + + return true; + } + + /** + * Method to execute query(). + * + * Divide the query types into seven different ones. That is to say: + * + * 1. SELECT SQL_CALC_FOUND_ROWS + * 2. INSERT + * 3. CREATE TABLE(INDEX) + * 4. ALTER TABLE + * 5. SHOW VARIABLES + * 6. DROP INDEX + * 7. THE OTHERS + * + * #1 is just a tricky play. See the private function handle_sql_count() in query.class.php. + * From #2 through #5 call different functions respectively. + * #6 call the ALTER TABLE query. + * #7 is a normal process: sequentially call prepare_query() and execute_query(). + * + * #1 process has been changed since version 1.5.1. + * + * @param string $statement Full SQL statement string. + * @param int $mode Not used. + * @param array ...$fetch_mode_args Not used. + * + * @see PDO::query() + * + * @throws Exception If the query could not run. + * @throws PDOException If the translated query could not run. + * + * @return mixed according to the query type + */ + public function query( $statement, $mode = PDO::FETCH_OBJ, ...$fetch_mode_args ) { // phpcs:ignore WordPress.DB.RestrictedClasses + $this->flush(); + if ( function_exists( 'apply_filters' ) ) { + /** + * Filters queries before they are translated and run. + * + * Return a non-null value to cause query() to return early with that result. + * Use this filter to intercept queries that don't work correctly in SQLite. + * + * From within the filter you can do + * function filter_sql ($result, $translator, $statement, $mode, $fetch_mode_args) { + * if ( intercepting this query ) { + * return $translator->execute_sqlite_query( $statement ); + * } + * return $result; + * } + * + * @param null|array $result Default null to continue with the query. + * @param object $translator The translator object. You can call $translator->execute_sqlite_query(). + * @param string $statement The statement passed. + * @param int $mode Fetch mode: PDO::FETCH_OBJ, PDO::FETCH_CLASS, etc. + * @param array $fetch_mode_args Variable arguments passed to query. + * + * @returns null|array Null to proceed, or an array containing a resultset. + * @since 2.1.0 + */ + $pre = apply_filters( 'pre_query_sqlite_db', null, $this, $statement, $mode, $fetch_mode_args ); + if ( null !== $pre ) { + return $pre; + } + } + $this->pdo_fetch_mode = $mode; + $this->mysql_query = $statement; + if ( + preg_match( '/^\s*START TRANSACTION/i', $statement ) + || preg_match( '/^\s*BEGIN/i', $statement ) + ) { + return $this->begin_transaction(); + } + if ( preg_match( '/^\s*COMMIT/i', $statement ) ) { + return $this->commit(); + } + if ( preg_match( '/^\s*ROLLBACK/i', $statement ) ) { + return $this->rollback(); + } + + try { + // Perform all the queries in a nested transaction. + $this->begin_transaction(); + + do { + $error = null; + try { + $this->execute_mysql_query( + $statement + ); + } catch ( PDOException $error ) { + if ( $error->getCode() !== self::SQLITE_BUSY ) { + throw $error; + } + } + } while ( $error ); + + // Commit the nested transaction. + $this->commit(); + return $this->return_value; + } catch ( Exception $err ) { + // Rollback the nested transaction. + $this->rollback(); + if ( defined( 'PDO_DEBUG' ) && PDO_DEBUG === true ) { + throw $err; + } + return $this->handle_error( $err ); + } + } + + /** + * Method to return the queried column names. + * + * These data are meaningless for SQLite. So they are dummy emulating + * MySQL columns data. + * + * @return array|null of the object + */ + public function get_columns() { + if ( ! empty( $this->results ) ) { + $primary_key = array( + 'meta_id', + 'comment_ID', + 'link_ID', + 'option_id', + 'blog_id', + 'option_name', + 'ID', + 'term_id', + 'object_id', + 'term_taxonomy_id', + 'umeta_id', + 'id', + ); + $unique_key = array( 'term_id', 'taxonomy', 'slug' ); + $data = array( + 'name' => '', // Column name. + 'table' => '', // Table name. + 'max_length' => 0, // Max length of the column. + 'not_null' => 1, // 1 if not null. + 'primary_key' => 0, // 1 if column has primary key. + 'unique_key' => 0, // 1 if column has unique key. + 'multiple_key' => 0, // 1 if column doesn't have unique key. + 'numeric' => 0, // 1 if column has numeric value. + 'blob' => 0, // 1 if column is blob. + 'type' => '', // Type of the column. + 'int' => 0, // 1 if column is int integer. + 'zerofill' => 0, // 1 if column is zero-filled. + ); + $table_name = ''; + $sql = ''; + $query = end( $this->executed_sqlite_queries ); + if ( $query ) { + $sql = $query['sql']; + } + if ( preg_match( '/\s*FROM\s*(.*)?\s*/i', $sql, $match ) ) { + $table_name = trim( $match[1] ); + } + foreach ( $this->results[0] as $key => $value ) { + $data['name'] = $key; + $data['table'] = $table_name; + if ( in_array( $key, $primary_key, true ) ) { + $data['primary_key'] = 1; + } elseif ( in_array( $key, $unique_key, true ) ) { + $data['unique_key'] = 1; + } else { + $data['multiple_key'] = 1; + } + $this->column_data[] = json_decode( json_encode( $data ) ); + + // Reset data for next iteration. + $data['name'] = ''; + $data['table'] = ''; + $data['primary_key'] = 0; + $data['unique_key'] = 0; + $data['multiple_key'] = 0; + } + + return $this->column_data; + } + return null; + } + + /** + * Method to return the queried result data. + * + * @return mixed + */ + public function get_query_results() { + return $this->results; + } + + /** + * Method to return the number of rows from the queried result. + */ + public function get_num_rows() { + return $this->num_rows; + } + + /** + * Method to return the queried results according to the query types. + * + * @return mixed + */ + public function get_return_value() { + return $this->return_value; + } + + /** + * Executes a MySQL query in SQLite. + * + * @param string $query The query. + * + * @throws Exception If the query is not supported. + */ + private function execute_mysql_query( $query ) { + $tokens = ( new WP_SQLite_Lexer( $query ) )->tokens; + $this->rewriter = new WP_SQLite_Query_Rewriter( $tokens ); + $query_type = $this->rewriter->peek()->value; + + switch ( $query_type ) { + case 'ALTER': + $this->execute_alter(); + break; + + case 'CREATE': + $this->execute_create(); + break; + + case 'SELECT': + $this->execute_select(); + break; + + case 'INSERT': + case 'REPLACE': + $this->execute_insert_or_replace(); + break; + + case 'UPDATE': + $this->execute_update(); + break; + + case 'DELETE': + $this->execute_delete(); + break; + + case 'CALL': + case 'SET': + /* + * It would be lovely to support at least SET autocommit, + * but I don't think that is even possible with SQLite. + */ + $this->results = 0; + break; + + case 'TRUNCATE': + $this->execute_truncate(); + break; + + case 'BEGIN': + case 'START TRANSACTION': + $this->results = $this->begin_transaction(); + break; + + case 'COMMIT': + $this->results = $this->commit(); + break; + + case 'ROLLBACK': + $this->results = $this->rollback(); + break; + + case 'DROP': + $this->execute_drop(); + break; + + case 'SHOW': + $this->execute_show(); + break; + + case 'DESCRIBE': + $this->execute_describe(); + break; + + case 'CHECK': + $this->execute_check(); + break; + + case 'OPTIMIZE': + case 'REPAIR': + case 'ANALYZE': + $this->execute_optimize( $query_type ); + break; + + default: + throw new Exception( 'Unknown query type: ' . $query_type ); + } + } + + /** + * Executes a MySQL CREATE TABLE query in SQLite. + * + * @throws Exception If the query is not supported. + */ + private function execute_create_table() { + $table = $this->parse_create_table(); + + $definitions = array(); + foreach ( $table->fields as $field ) { + /* + * Do not include the inline PRIMARY KEY definition + * if there is more than one primary key. + */ + if ( $field->primary_key && count( $table->primary_key ) > 1 ) { + $field->primary_key = false; + } + if ( $field->auto_increment && count( $table->primary_key ) > 1 ) { + throw new Exception( 'Cannot combine AUTOINCREMENT and multiple primary keys in SQLite' ); + } + + $definitions[] = $this->make_sqlite_field_definition( $field ); + $this->update_data_type_cache( + $table->name, + $field->name, + $field->mysql_data_type + ); + } + + if ( count( $table->primary_key ) > 1 ) { + $definitions[] = 'PRIMARY KEY ("' . implode( '", "', $table->primary_key ) . '")'; + } + + $create_query = ( + $table->create_table . + '"' . $table->name . '" (' . "\n" . + implode( ",\n", $definitions ) . + ')' + ); + $this->execute_sqlite_query( $create_query ); + $this->results = $this->last_exec_returned; + $this->return_value = $this->results; + + foreach ( $table->constraints as $constraint ) { + $index_type = $this->mysql_index_type_to_sqlite_type( $constraint->value ); + $unique = ''; + if ( 'UNIQUE INDEX' === $index_type ) { + $unique = 'UNIQUE '; + } + $index_name = "{$table->name}__{$constraint->name}"; + $this->execute_sqlite_query( + "CREATE $unique INDEX \"$index_name\" ON \"{$table->name}\" (\"" . implode( '", "', $constraint->columns ) . '")' + ); + $this->update_data_type_cache( + $table->name, + $index_name, + $constraint->value + ); + } + } + + /** + * Parse the CREATE TABLE query. + * + * @return stdClass Structured data. + */ + private function parse_create_table() { + $this->rewriter = clone $this->rewriter; + $result = new stdClass(); + $result->create_table = null; + $result->name = null; + $result->fields = array(); + $result->constraints = array(); + $result->primary_key = array(); + + /* + * The query starts with CREATE TABLE [IF NOT EXISTS]. + * Consume everything until the table name. + */ + while ( true ) { + $token = $this->rewriter->consume(); + if ( ! $token ) { + break; + } + // The table name is the first non-keyword token. + if ( WP_SQLite_Token::TYPE_KEYWORD !== $token->type ) { + // Store the table name for later. + $result->name = $this->normalize_column_name( $token->value ); + + // Drop the table name and store the CREATE TABLE command. + $this->rewriter->drop_last(); + $result->create_table = $this->rewriter->get_updated_query(); + break; + } + } + + /* + * Move to the opening parenthesis: + * CREATE TABLE wp_options ( + * ^ here. + */ + $this->rewriter->skip( + array( + 'type' => WP_SQLite_Token::TYPE_OPERATOR, + 'value' => '(', + ) + ); + + /* + * We're in the table definition now. + * Read everything until the closing parenthesis. + */ + $declarations_depth = $this->rewriter->depth; + do { + /* + * We want to capture a rewritten line of the query. + * Let's clear any data we might have captured so far. + */ + $this->rewriter->replace_all( array() ); + + /* + * Decide how to parse the current line. We expect either: + * + * Field definition, e.g.: + * `my_field` varchar(255) NOT NULL DEFAULT 'foo' + * Constraint definition, e.g.: + * PRIMARY KEY (`my_field`) + * + * Lexer does not seem to reliably understand whether the + * first token is a field name or a reserved keyword, so + * instead we'll check whether the second non-whitespace + * token is a data type. + */ + $second_token = $this->rewriter->peek_nth( 2 ); + + if ( $second_token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_DATA_TYPE + ) ) { + $result->fields[] = $this->parse_mysql_create_table_field(); + } else { + $result->constraints[] = $this->parse_mysql_create_table_constraint(); + } + + /* + * If we're back at the initial depth, we're done. + * Also, MySQL supports a trailing comma – if we see one, + * then we're also done. + */ + } while ( + $token + && $this->rewriter->depth >= $declarations_depth + && $this->rewriter->peek()->token !== ')' + ); + + // Merge all the definitions of the primary key. + foreach ( $result->constraints as $k => $constraint ) { + if ( 'PRIMARY' === $constraint->value ) { + $result->primary_key = array_merge( + $result->primary_key, + $constraint->columns + ); + unset( $result->constraints[ $k ] ); + } + } + + // Inline primary key in a field definition. + foreach ( $result->fields as $k => $field ) { + if ( $field->primary_key ) { + $result->primary_key[] = $field->name; + } elseif ( in_array( $field->name, $result->primary_key, true ) ) { + $field->primary_key = true; + } + } + + // Remove duplicates. + $result->primary_key = array_unique( $result->primary_key ); + + return $result; + } + + /** + * Parses a CREATE TABLE query. + * + * @throws Exception If the query is not supported. + * + * @return stdClass + */ + private function parse_mysql_create_table_field() { + $result = new stdClass(); + $result->name = ''; + $result->sqlite_data_type = ''; + $result->not_null = false; + $result->default = null; + $result->auto_increment = false; + $result->primary_key = false; + + $field_name_token = $this->rewriter->skip(); // Field name. + $this->rewriter->add( new WP_SQLite_Token( "\n", WP_SQLite_Token::TYPE_WHITESPACE ) ); + $result->name = $this->normalize_column_name( $field_name_token->value ); + + $definition_depth = $this->rewriter->depth; + + $skip_mysql_data_type_parts = $this->skip_mysql_data_type(); + $result->sqlite_data_type = $skip_mysql_data_type_parts[0]; + $result->mysql_data_type = $skip_mysql_data_type_parts[1]; + + // Look for the NOT NULL and AUTO_INCREMENT flags. + while ( true ) { + $token = $this->rewriter->skip(); + if ( ! $token ) { + break; + } + if ( $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED, + array( 'NOT NULL' ) + ) ) { + $result->not_null = true; + continue; + } + + if ( $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED, + array( 'PRIMARY KEY' ) + ) ) { + $result->primary_key = true; + continue; + } + + if ( $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + null, + array( 'AUTO_INCREMENT' ) + ) ) { + $result->primary_key = true; + $result->auto_increment = true; + continue; + } + + if ( $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_FUNCTION, + array( 'DEFAULT' ) + ) ) { + $result->default = $this->rewriter->consume()->token; + continue; + } + + if ( $this->is_create_table_field_terminator( $token, $definition_depth ) ) { + $this->rewriter->add( $token ); + break; + } + } + + return $result; + } + + /** + * Translate field definitions. + * + * @param stdClass $field Field definition. + * + * @return string + */ + private function make_sqlite_field_definition( $field ) { + $definition = '"' . $field->name . '" ' . $field->sqlite_data_type; + if ( $field->auto_increment ) { + $definition .= ' PRIMARY KEY AUTOINCREMENT'; + } elseif ( $field->primary_key ) { + $definition .= ' PRIMARY KEY '; + } + if ( $field->not_null ) { + $definition .= ' NOT NULL'; + } + if ( null !== $field->default ) { + $definition .= ' DEFAULT ' . $field->default; + } + + /* + * In MySQL, text fields are case-insensitive by default. + * COLLATE NOCASE emulates the same behavior in SQLite. + */ + if ( 'text' === $field->sqlite_data_type ) { + $definition .= ' COLLATE NOCASE'; + } + return $definition; + } + + /** + * Parses a CREATE TABLE constraint. + * + * @throws Exception If the query is not supported. + * + * @return stdClass + */ + private function parse_mysql_create_table_constraint() { + $result = new stdClass(); + $result->name = ''; + $result->value = ''; + $result->columns = array(); + + $definition_depth = $this->rewriter->depth; + $constraint = $this->rewriter->peek(); + if ( ! $constraint->matches( WP_SQLite_Token::TYPE_KEYWORD ) ) { + /* + * Not a constraint declaration, but we're not finished + * with the table declaration yet. + */ + throw new Exception( 'Unexpected token in MySQL query: ' . $this->rewriter->peek()->value ); + } + + $result->value = $this->normalize_mysql_index_type( $constraint->value ); + if ( $result->value ) { + $this->rewriter->skip(); // Constraint type. + if ( 'PRIMARY' !== $result->value ) { + $result->name = $this->rewriter->skip()->value; + } + + $constraint_depth = $this->rewriter->depth; + $this->rewriter->skip(); // `(` + do { + $result->columns[] = $this->normalize_column_name( $this->rewriter->skip()->value ); + $paren_maybe = $this->rewriter->peek(); + if ( $paren_maybe && '(' === $paren_maybe->token ) { + $this->rewriter->skip(); + $this->rewriter->skip(); + $this->rewriter->skip(); + } + $this->rewriter->skip(); // `,` or `)` + } while ( $this->rewriter->depth > $constraint_depth ); + } + + do { + $token = $this->rewriter->skip(); + } while ( ! $this->is_create_table_field_terminator( $token, $definition_depth ) ); + + return $result; + } + + /** + * Checks if the current token is the terminator of a CREATE TABLE field. + * + * @param WP_SQLite_Token $token The current token. + * @param int $definition_depth The initial depth. + * @param int|null $current_depth The current depth. + * + * @return bool + */ + private function is_create_table_field_terminator( $token, $definition_depth, $current_depth = null ) { + if ( null === $current_depth ) { + $current_depth = $this->rewriter->depth; + } + return ( + // Reached the end of the query. + null === $token + + // The field-terminating ",". + || ( + $current_depth === $definition_depth && + WP_SQLite_Token::TYPE_OPERATOR === $token->type && + ',' === $token->value + ) + + // The definitions-terminating ")". + || $current_depth === $definition_depth - 1 + + // The query-terminating ";". + || ( + WP_SQLite_Token::TYPE_DELIMITER === $token->type && + ';' === $token->value + ) + ); + } + + /** + * Executes a DELETE statement. + * + * @throws Exception If the table could not be found. + */ + private function execute_delete() { + $this->rewriter->consume(); // DELETE. + + // Process expressions and extract bound parameters. + $params = array(); + while ( true ) { + $token = $this->rewriter->peek(); + if ( ! $token ) { + break; + } + + $this->remember_last_reserved_keyword( $token ); + + if ( + $this->extract_bound_parameter( $token, $params ) + || $this->translate_expression( $token ) + ) { + continue; + } + + $this->rewriter->consume(); + } + $this->rewriter->consume_all(); + + $updated_query = $this->rewriter->get_updated_query(); + + // Perform DELETE-specific translations. + + // Naive rewriting of DELETE JOIN query. + // @TODO: Actually rewrite the query instead of using a hardcoded workaround. + if ( str_contains( $updated_query, ' JOIN ' ) ) { + $table_prefix = isset( $GLOBALS['table_prefix'] ) ? $GLOBALS['table_prefix'] : 'wp_'; + $this->execute_sqlite_query( + "DELETE FROM {$table_prefix}options WHERE option_id IN (SELECT MIN(option_id) FROM {$table_prefix}options GROUP BY option_name HAVING COUNT(*) > 1)" + ); + $this->set_result_from_affected_rows(); + return; + } + + $rewriter = new WP_SQLite_Query_Rewriter( $this->rewriter->output_tokens ); + + $comma = $rewriter->peek( + array( + 'type' => WP_SQLite_Token::TYPE_OPERATOR, + 'value' => ',', + ) + ); + $from = $rewriter->peek( + array( + 'type' => WP_SQLite_Token::TYPE_KEYWORD, + 'value' => 'FROM', + ) + ); + // The DELETE query targets a single table if there's no comma before the FROM. + if ( ! $comma || ! $from || $comma->position >= $from->position ) { + $this->execute_sqlite_query( + $updated_query, + $params + ); + $this->set_result_from_affected_rows(); + return; + } + + // The DELETE query targets multiple tables – rewrite it into a + // SELECT to fetch the IDs of the rows to delete, then delete them + // using a separate DELETE query. + + $table_name = $rewriter->skip()->value; + $rewriter->add( new WP_SQLite_Token( 'SELECT', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ) ); + + /* + * Get table name. + */ + $from = $rewriter->peek( + array( + 'type' => WP_SQLite_Token::TYPE_KEYWORD, + 'value' => 'FROM', + ) + ); + $index = array_search( $from, $rewriter->input_tokens, true ); + for ( $i = $index + 1; $i < $rewriter->max; $i++ ) { + // Assume the table name is the first token after FROM. + if ( ! $rewriter->input_tokens[ $i ]->is_semantically_void() ) { + $table_name = $rewriter->input_tokens[ $i ]->value; + break; + } + } + if ( ! $table_name ) { + throw new Exception( 'Could not find table name for dual delete query.' ); + } + + /* + * Now, let's figure out the primary key name. + * This assumes that all listed table names are the same. + */ + $q = $this->execute_sqlite_query( 'SELECT l.name FROM pragma_table_info("' . $table_name . '") as l WHERE l.pk = 1;' ); + $pk_name = $q->fetch()['name']; + + /* + * Good, we can finally create the SELECT query. + * Let's rewrite DELETE a, b FROM ... to SELECT a.id, b.id FROM ... + */ + $alias_nb = 0; + while ( true ) { + $token = $rewriter->consume(); + if ( WP_SQLite_Token::TYPE_KEYWORD === $token->type && 'FROM' === $token->value ) { + break; + } + + /* + * Between DELETE and FROM we only expect commas and table aliases. + * If it's not a comma, it must be a table alias. + */ + if ( ',' !== $token->value ) { + // Insert .id AS id_1 after the table alias. + $rewriter->add_many( + array( + new WP_SQLite_Token( '.', WP_SQLite_Token::TYPE_OPERATOR, WP_SQLite_Token::FLAG_OPERATOR_SQL ), + new WP_SQLite_Token( $pk_name, WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( 'AS', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( 'id_' . $alias_nb, WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ), + ) + ); + ++$alias_nb; + } + } + $rewriter->consume_all(); + + // Select the IDs to delete. + $select = $rewriter->get_updated_query(); + $stmt = $this->execute_sqlite_query( $select ); + $stmt->execute( $params ); + $rows = $stmt->fetchAll(); + $ids_to_delete = array(); + foreach ( $rows as $id ) { + $ids_to_delete[] = $id['id_0']; + $ids_to_delete[] = $id['id_1']; + } + + $query = ( + count( $ids_to_delete ) + ? "DELETE FROM {$table_name} WHERE {$pk_name} IN (" . implode( ',', $ids_to_delete ) . ')' + : "DELETE FROM {$table_name} WHERE 0=1" + ); + $this->execute_sqlite_query( $query ); + $this->set_result_from_affected_rows( + count( $ids_to_delete ) + ); + } + + /** + * Executes a SELECT statement. + */ + private function execute_select() { + $this->rewriter->consume(); // SELECT. + + $params = array(); + $table_name = null; + $has_sql_calc_found_rows = false; + + // Consume and record the table name. + while ( true ) { + $token = $this->rewriter->peek(); + if ( ! $token ) { + break; + } + + $this->remember_last_reserved_keyword( $token ); + + if ( ! $table_name ) { + $table_name = $this->peek_table_name( $token ); + } + + if ( $this->skip_sql_calc_found_rows( $token ) ) { + $has_sql_calc_found_rows = true; + continue; + } + + if ( + $this->extract_bound_parameter( $token, $params ) + || $this->translate_expression( $token ) + ) { + continue; + } + + $this->rewriter->consume(); + } + $this->rewriter->consume_all(); + + $updated_query = $this->rewriter->get_updated_query(); + + if ( $table_name && str_starts_with( strtolower( $table_name ), 'information_schema' ) ) { + $this->is_information_schema_query = true; + $updated_query = $this->get_information_schema_query( $updated_query ); + $params = array(); + } elseif ( + strpos( $updated_query, '@@SESSION.sql_mode' ) !== false + || strpos( $updated_query, 'CONVERT( ' ) !== false + ) { + /* + * If the query contains a function that is not supported by SQLite, + * return a dummy select. This check must be done after the query + * has been rewritten to use parameters to avoid false positives + * on queries such as `SELECT * FROM table WHERE field='CONVERT('`. + */ + $updated_query = 'SELECT 1=0'; + $params = array(); + } elseif ( $has_sql_calc_found_rows ) { + // Emulate SQL_CALC_FOUND_ROWS for now. + $query = $updated_query; + // We make the data for next SELECT FOUND_ROWS() statement. + $unlimited_query = preg_replace( '/\\bLIMIT\\s\d+(?:\s*,\s*\d+)?$/imsx', '', $query ); + $stmt = $this->execute_sqlite_query( $unlimited_query ); + $stmt->execute( $params ); + $this->last_sql_calc_found_rows = count( $stmt->fetchAll() ); + } + + // Emulate FOUND_ROWS() by counting the rows in the result set. + if ( strpos( $updated_query, 'FOUND_ROWS(' ) !== false ) { + $last_found_rows = ( $this->last_sql_calc_found_rows ? $this->last_sql_calc_found_rows : 0 ) . ''; + $updated_query = "SELECT {$last_found_rows} AS `FOUND_ROWS()`"; + } + + $stmt = $this->execute_sqlite_query( $updated_query, $params ); + if ( $this->is_information_schema_query ) { + $this->set_results_from_fetched_data( + $this->strip_sqlite_system_tables( + $stmt->fetchAll( $this->pdo_fetch_mode ) + ) + ); + } else { + $this->set_results_from_fetched_data( + $stmt->fetchAll( $this->pdo_fetch_mode ) + ); + } + } + + /** + * Executes a TRUNCATE statement. + */ + private function execute_truncate() { + $this->rewriter->skip(); // TRUNCATE. + $this->rewriter->skip(); // TABLE. + $this->rewriter->add( new WP_SQLite_Token( 'DELETE', WP_SQLite_Token::TYPE_KEYWORD ) ); + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); + $this->rewriter->add( new WP_SQLite_Token( 'FROM', WP_SQLite_Token::TYPE_KEYWORD ) ); + $this->rewriter->consume_all(); + $this->execute_sqlite_query( $this->rewriter->get_updated_query() ); + $this->results = true; + $this->return_value = true; + } + + /** + * Executes a DESCRIBE statement. + * + * @throws PDOException When the table is not found. + */ + private function execute_describe() { + $this->rewriter->skip(); + $table_name = $this->rewriter->consume()->value; + $stmt = $this->execute_sqlite_query( + "SELECT + `name` as `Field`, + ( + CASE `notnull` + WHEN 0 THEN 'YES' + WHEN 1 THEN 'NO' + END + ) as `Null`, + IFNULL( + d.`mysql_type`, + ( + CASE `type` + WHEN 'INTEGER' THEN 'int' + WHEN 'TEXT' THEN 'text' + WHEN 'BLOB' THEN 'blob' + WHEN 'REAL' THEN 'real' + ELSE `type` + END + ) + ) as `Type`, + TRIM(`dflt_value`, \"'\") as `Default`, + '' as Extra, + ( + CASE `pk` + WHEN 0 THEN '' + ELSE 'PRI' + END + ) as `Key` + FROM pragma_table_info(\"$table_name\") p + LEFT JOIN " . self::DATA_TYPES_CACHE_TABLE . " d + ON d.`table` = \"$table_name\" + AND d.`column_or_index` = p.`name` + ; + " + ); + $this->set_results_from_fetched_data( + $stmt->fetchAll( $this->pdo_fetch_mode ) + ); + if ( ! $this->results ) { + throw new PDOException( 'Table not found' ); + } + } + + /** + * Executes an UPDATE statement. + */ + private function execute_update() { + $this->rewriter->consume(); // Update. + + $params = array(); + while ( true ) { + $token = $this->rewriter->peek(); + if ( ! $token ) { + break; + } + + $this->remember_last_reserved_keyword( $token ); + + if ( + $this->extract_bound_parameter( $token, $params ) + || $this->translate_expression( $token ) + ) { + continue; + } + + $this->rewriter->consume(); + } + $this->rewriter->consume_all(); + + $updated_query = $this->rewriter->get_updated_query(); + $this->execute_sqlite_query( $updated_query, $params ); + $this->set_result_from_affected_rows(); + } + + /** + * Executes a INSERT or REPLACE statement. + */ + private function execute_insert_or_replace() { + $params = array(); + $is_in_duplicate_section = false; + $table_name = null; + + $this->rewriter->consume(); // INSERT or REPLACE. + + // Consume the query type. + if ( 'IGNORE' === $this->rewriter->peek()->value ) { + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); + $this->rewriter->add( new WP_SQLite_Token( 'OR', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ) ); + $this->rewriter->consume(); // IGNORE. + } + + // Consume and record the table name. + $this->insert_columns = array(); + $this->rewriter->consume(); // INTO. + $table_name = $this->rewriter->consume()->value; // Table name. + + /* + * A list of columns is given if the opening parenthesis + * is earlier than the VALUES keyword. + */ + $paren = $this->rewriter->peek( + array( + 'type' => WP_SQLite_Token::TYPE_OPERATOR, + 'value' => '(', + ) + ); + $values = $this->rewriter->peek( + array( + 'type' => WP_SQLite_Token::TYPE_KEYWORD, + 'value' => 'VALUES', + ) + ); + if ( $paren && $values && $paren->position <= $values->position ) { + $this->rewriter->consume( + array( + 'type' => WP_SQLite_Token::TYPE_OPERATOR, + 'value' => '(', + ) + ); + while ( true ) { + $token = $this->rewriter->consume(); + if ( $token->matches( WP_SQLite_Token::TYPE_OPERATOR, null, array( ')' ) ) ) { + break; + } + if ( ! $token->matches( WP_SQLite_Token::TYPE_OPERATOR ) ) { + $this->insert_columns[] = $token->value; + } + } + } + + while ( true ) { + $token = $this->rewriter->peek(); + if ( ! $token ) { + break; + } + + $this->remember_last_reserved_keyword( $token ); + + if ( + ( $is_in_duplicate_section && $this->translate_values_function( $token ) ) + || $this->extract_bound_parameter( $token, $params ) + || $this->translate_expression( $token ) + ) { + continue; + } + + if ( $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + null, + array( 'DUPLICATE' ) + ) + ) { + $is_in_duplicate_section = true; + $this->translate_on_duplicate_key( $table_name ); + continue; + } + + $this->rewriter->consume(); + } + + $this->rewriter->consume_all(); + + $updated_query = $this->rewriter->get_updated_query(); + $this->execute_sqlite_query( $updated_query, $params ); + $this->set_result_from_affected_rows(); + $this->last_insert_id = $this->pdo->lastInsertId(); + if ( is_numeric( $this->last_insert_id ) ) { + $this->last_insert_id = (int) $this->last_insert_id; + } + } + + /** + * Preprocesses a string literal. + * + * @param string $value The string literal. + * + * @return string The preprocessed string literal. + */ + private function preprocess_string_literal( $value ) { + /* + * The code below converts the date format to one preferred by SQLite. + * + * MySQL accepts ISO 8601 date strings: 'YYYY-MM-DDTHH:MM:SSZ' + * SQLite prefers a slightly different format: 'YYYY-MM-DD HH:MM:SS' + * + * SQLite date and time functions can understand the ISO 8601 notation, but + * lookups don't. To keep the lookups working, we need to store all dates + * in UTC without the "T" and "Z" characters. + * + * Caveat: It will adjust every string that matches the pattern, not just dates. + * + * In theory, we could only adjust semantic dates, e.g. the data inserted + * to a date column or compared against a date column. + * + * In practice, this is hard because dates are just text – SQLite has no separate + * datetime field. We'd need to cache the MySQL data type from the original + * CREATE TABLE query and then keep refreshing the cache after each ALTER TABLE query. + * + * That's a lot of complexity that's perhaps not worth it. Let's just convert + * everything for now. The regexp assumes "Z" is always at the end of the string, + * which is true in the unit test suite, but there could also be a timezone offset + * like "+00:00" or "+01:00". We could add support for that later if needed. + */ + if ( 1 === preg_match( '/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})Z$/', $value, $matches ) ) { + $value = $matches[1] . ' ' . $matches[2]; + } + + /* + * Mimic MySQL's behavior and truncate invalid dates. + * + * "2020-12-41 14:15:27" becomes "0000-00-00 00:00:00" + * + * WARNING: We have no idea whether the truncated value should + * be treated as a date in the first place. + * In SQLite dates are just strings. This could be a perfectly + * valid string that just happens to contain a date-like value. + * + * At the same time, WordPress seems to rely on MySQL's behavior + * and even tests for it in Tests_Post_wpInsertPost::test_insert_empty_post_date. + * Let's truncate the dates for now. + * + * In the future, let's update WordPress to do its own date validation + * and stop relying on this MySQL feature, + */ + if ( 1 === preg_match( '/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})$/', $value, $matches ) ) { + /* + * Calling strtotime("0000-00-00 00:00:00") in 32-bit environments triggers + * an "out of integer range" warning – let's avoid that call for the popular + * case of "zero" dates. + */ + if ( '0000-00-00 00:00:00' !== $value && false === strtotime( $value ) ) { + $value = '0000-00-00 00:00:00'; + } + } + return $value; + } + + /** + * Preprocesses a LIKE expression. + * + * @param WP_SQLite_Token $token The token to preprocess. + * @return string + */ + private function preprocess_like_expr( &$token ) { + /* + * This code handles escaped wildcards in LIKE clauses. + * If we are within a LIKE experession, we look for \_ and \%, the + * escaped LIKE wildcards, the ones where we want a literal, not a + * wildcard match. We change the \ escape for an ASCII \x1a (SUB) character, + * so the \ characters won't get munged. + * These \_ and \% escape sequences are in the token name, because + * the lexer has already done stripcslashes on the value. + */ + if ( $this->like_expression_nesting > 0 ) { + /* Remove the quotes around the name. */ + $unescaped_value = mb_substr( $token->token, 1, -1, 'UTF-8' ); + if ( str_contains( $unescaped_value, '\_' ) || str_contains( $unescaped_value, '\%' ) ) { + $this->like_escape_count ++; + return str_replace( + array( '\_', '\%' ), + array( self::LIKE_ESCAPE_CHAR . '_', self::LIKE_ESCAPE_CHAR . '%' ), + $unescaped_value + ); + } + } + return $token->value; + } + /** + * Translate CAST() function when we want to cast to BINARY. + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function translate_cast_as_binary( $token ) { + if ( ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_DATA_TYPE, + array( 'BINARY' ) + ) + ) { + return false; + } + + $call_parent = $this->rewriter->last_call_stack_element(); + if ( + ! $call_parent + || 'CAST' !== $call_parent['function'] + ) { + return false; + } + + // Rewrite AS BINARY to AS BLOB inside CAST() calls. + $this->rewriter->skip(); + $this->rewriter->add( new WP_SQLite_Token( 'BLOB', $token->type, $token->flags ) ); + return true; + } + + /** + * Translates an expression in an SQL statement if the token is the start of an expression. + * + * @param WP_SQLite_Token $token The first token of an expression. + * + * @return bool True if the expression was translated successfully, false otherwise. + */ + private function translate_expression( $token ) { + return ( + $this->skip_from_dual( $token ) + || $this->translate_concat_function( $token ) + || $this->translate_concat_comma_to_pipes( $token ) + || $this->translate_function_aliases( $token ) + || $this->translate_cast_as_binary( $token ) + || $this->translate_date_add_sub( $token ) + || $this->translate_date_format( $token ) + || $this->translate_interval( $token ) + || $this->translate_regexp_functions( $token ) + || $this->capture_group_by( $token ) + || $this->translate_ungrouped_having( $token ) + || $this->translate_like_escape( $token ) + ); + } + + /** + * Skips the `FROM DUAL` clause in the SQL statement. + * + * @param WP_SQLite_Token $token The token to check for the `FROM DUAL` clause. + * + * @return bool True if the `FROM DUAL` clause was skipped, false otherwise. + */ + private function skip_from_dual( $token ) { + if ( + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED, + array( 'FROM' ) + ) + ) { + return false; + } + $from_table = $this->rewriter->peek_nth( 2 )->value; + if ( 'DUAL' !== strtoupper( $from_table ) ) { + return false; + } + + // FROM DUAL is a MySQLism that means "no tables". + $this->rewriter->skip(); + $this->rewriter->skip(); + return true; + } + + /** + * Peeks at the table name in the SQL statement. + * + * @param WP_SQLite_Token $token The token to check for the table name. + * + * @return string|bool The table name if it was found, false otherwise. + */ + private function peek_table_name( $token ) { + if ( + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED, + array( 'FROM' ) + ) + ) { + return false; + } + $table_name = $this->rewriter->peek_nth( 2 )->value; + if ( 'dual' === strtolower( $table_name ) ) { + return false; + } + return $table_name; + } + + /** + * Skips the `SQL_CALC_FOUND_ROWS` keyword in the SQL statement. + * + * @param WP_SQLite_Token $token The token to check for the `SQL_CALC_FOUND_ROWS` keyword. + * + * @return bool True if the `SQL_CALC_FOUND_ROWS` keyword was skipped, false otherwise. + */ + private function skip_sql_calc_found_rows( $token ) { + if ( + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + null, + array( 'SQL_CALC_FOUND_ROWS' ) + ) + ) { + return false; + } + $this->rewriter->skip(); + return true; + } + + /** + * Remembers the last reserved keyword encountered in the SQL statement. + * + * @param WP_SQLite_Token $token The token to check for the reserved keyword. + */ + private function remember_last_reserved_keyword( $token ) { + if ( + $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED + ) + ) { + $this->last_reserved_keyword = $token->value; + } + } + + /** + * Extracts the bound parameter from the given token and adds it to the `$params` array. + * + * @param WP_SQLite_Token $token The token to extract the bound parameter from. + * @param array $params An array of parameters to be bound to the SQL statement. + * + * @return bool True if the parameter was extracted successfully, false otherwise. + */ + private function extract_bound_parameter( $token, &$params ) { + if ( ! $token->matches( + WP_SQLite_Token::TYPE_STRING, + WP_SQLite_Token::FLAG_STRING_SINGLE_QUOTES + ) + || 'AS' === $this->last_reserved_keyword + ) { + return false; + } + + $param_name = ':param' . count( $params ); + $value = $this->preprocess_like_expr( $token ); + $value = $this->preprocess_string_literal( $value ); + $params[ $param_name ] = $value; + $this->rewriter->skip(); + $this->rewriter->add( new WP_SQLite_Token( $param_name, WP_SQLite_Token::TYPE_STRING, WP_SQLite_Token::FLAG_STRING_SINGLE_QUOTES ) ); + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); + return true; + } + + /** + * Translate CONCAT() function. + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function translate_concat_function( $token ) { + if ( + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_FUNCTION, + array( 'CONCAT' ) + ) + ) { + return false; + } + + /* + * Skip the CONCAT function but leave the parentheses. + * There is another code block below that replaces the + * , operators between the CONCAT arguments with ||. + */ + $this->rewriter->skip(); + return true; + } + + /** + * Translate CONCAT() function arguments. + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function translate_concat_comma_to_pipes( $token ) { + if ( ! $token->matches( + WP_SQLite_Token::TYPE_OPERATOR, + WP_SQLite_Token::FLAG_OPERATOR_SQL, + array( ',' ) + ) + ) { + return false; + } + + $call_parent = $this->rewriter->last_call_stack_element(); + if ( + ! $call_parent + || 'CONCAT' !== $call_parent['function'] + ) { + return false; + } + + // Rewrite commas to || in CONCAT() calls. + $this->rewriter->skip(); + $this->rewriter->add( new WP_SQLite_Token( '||', WP_SQLite_Token::TYPE_OPERATOR ) ); + return true; + } + + /** + * Translate DATE_ADD() and DATE_SUB() functions. + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function translate_date_add_sub( $token ) { + if ( + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_FUNCTION, + array( 'DATE_ADD', 'DATE_SUB' ) + ) + ) { + return false; + } + + $this->rewriter->skip(); + $this->rewriter->add( new WP_SQLite_Token( 'DATETIME', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_FUNCTION ) ); + return true; + } + + /** + * Convert function aliases. + * + * @param object $token The current token. + * + * @return bool False when no match, true when this function consumes the token. + * + * @todo LENGTH and CHAR_LENGTH aren't always the same in MySQL for utf8 characters. They are in SQLite. + */ + private function translate_function_aliases( $token ) { + if ( ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_FUNCTION, + array( 'SUBSTRING', 'CHAR_LENGTH' ) + ) + ) { + return false; + } + switch ( $token->value ) { + case 'SUBSTRING': + $name = 'SUBSTR'; + break; + case 'CHAR_LENGTH': + $name = 'LENGTH'; + break; + default: + $name = $token->value; + break; + } + $this->rewriter->skip(); + $this->rewriter->add( new WP_SQLite_Token( $name, $token->type, $token->flags ) ); + + return true; + } + + /** + * Translate VALUES() function. + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function translate_values_function( $token ) { + if ( + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_FUNCTION, + array( 'VALUES' ) + ) + ) { + return false; + } + + /* + * Rewrite: VALUES(`option_name`) + * to: excluded.option_name + */ + $this->rewriter->skip(); + $this->rewriter->add( new WP_SQLite_Token( 'excluded', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ) ); + $this->rewriter->add( new WP_SQLite_Token( '.', WP_SQLite_Token::TYPE_OPERATOR ) ); + + $this->rewriter->skip(); // Skip the opening `(`. + // Consume the column name. + $this->rewriter->consume( + array( + 'type' => WP_SQLite_Token::TYPE_OPERATOR, + 'value' => ')', + ) + ); + // Drop the consumed ')' token. + $this->rewriter->drop_last(); + return true; + } + + /** + * Translate DATE_FORMAT() function. + * + * @param WP_SQLite_Token $token The token to translate. + * + * @throws Exception If the token is not a DATE_FORMAT() function. + * + * @return bool + */ + private function translate_date_format( $token ) { + if ( + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_FUNCTION, + array( 'DATE_FORMAT' ) + ) + ) { + return false; + } + + // Rewrite DATE_FORMAT( `post_date`, '%Y-%m-%d' ) to STRFTIME( '%Y-%m-%d', `post_date` ). + + // Skip the DATE_FORMAT function name. + $this->rewriter->skip(); + // Skip the opening `(`. + $this->rewriter->skip(); + + // Skip the first argument so we can read the second one. + $first_arg = $this->rewriter->skip_and_return_all( + array( + 'type' => WP_SQLite_Token::TYPE_OPERATOR, + 'value' => ',', + ) + ); + + // Make sure we actually found the comma. + $comma = array_pop( $first_arg ); + if ( ',' !== $comma->value ) { + throw new Exception( 'Could not parse the DATE_FORMAT() call' ); + } + + // Skip the second argument but capture the token. + $format = $this->rewriter->skip()->value; + $new_format = strtr( $format, $this->mysql_date_format_to_sqlite_strftime ); + if ( ! $new_format ) { + throw new Exception( "Could not translate a DATE_FORMAT() format to STRFTIME format ($format)" ); + } + + /* + * MySQL supports comparing strings and floats, e.g. + * + * > SELECT '00.42' = 0.4200 + * 1 + * + * SQLite does not support that. At the same time, + * WordPress likes to filter dates by comparing numeric + * outputs of DATE_FORMAT() to floats, e.g.: + * + * -- Filter by hour and minutes + * DATE_FORMAT( + * STR_TO_DATE('2014-10-21 00:42:29', '%Y-%m-%d %H:%i:%s'), + * '%H.%i' + * ) = 0.4200; + * + * Let's cast the STRFTIME() output to a float if + * the date format is typically used for string + * to float comparisons. + * + * In the future, let's update WordPress to avoid comparing + * strings and floats. + */ + $cast_to_float = '%H.%i' === $format; + if ( $cast_to_float ) { + $this->rewriter->add( new WP_SQLite_Token( 'CAST', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_FUNCTION ) ); + $this->rewriter->add( new WP_SQLite_Token( '(', WP_SQLite_Token::TYPE_OPERATOR ) ); + } + + $this->rewriter->add( new WP_SQLite_Token( 'STRFTIME', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_FUNCTION ) ); + $this->rewriter->add( new WP_SQLite_Token( '(', WP_SQLite_Token::TYPE_OPERATOR ) ); + $this->rewriter->add( new WP_SQLite_Token( "'$new_format'", WP_SQLite_Token::TYPE_STRING ) ); + $this->rewriter->add( new WP_SQLite_Token( ',', WP_SQLite_Token::TYPE_OPERATOR ) ); + + // Add the buffered tokens back to the stream. + $this->rewriter->add_many( $first_arg ); + + // Consume the closing ')'. + $this->rewriter->consume( + array( + 'type' => WP_SQLite_Token::TYPE_OPERATOR, + 'value' => ')', + ) + ); + + if ( $cast_to_float ) { + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); + $this->rewriter->add( new WP_SQLite_Token( 'as', WP_SQLite_Token::TYPE_OPERATOR ) ); + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); + $this->rewriter->add( new WP_SQLite_Token( 'FLOAT', WP_SQLite_Token::TYPE_KEYWORD ) ); + $this->rewriter->add( new WP_SQLite_Token( ')', WP_SQLite_Token::TYPE_OPERATOR ) ); + } + + return true; + } + + /** + * Translate INTERVAL keyword with DATE_ADD() and DATE_SUB(). + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function translate_interval( $token ) { + if ( + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + null, + array( 'INTERVAL' ) + ) + ) { + return false; + } + // Skip the INTERVAL keyword from the output stream. + $this->rewriter->skip(); + + $num = $this->rewriter->skip()->value; + $unit = $this->rewriter->skip()->value; + + /* + * In MySQL, we say: + * DATE_ADD(d, INTERVAL 1 YEAR) + * DATE_SUB(d, INTERVAL 1 YEAR) + * + * In SQLite, we say: + * DATE(d, '+1 YEAR') + * DATE(d, '-1 YEAR') + * + * The sign of the interval is determined by the date_* function + * that is closest in the call stack. + * + * Let's find it. + */ + $interval_op = '+'; // Default to adding. + for ( $j = count( $this->rewriter->call_stack ) - 1; $j >= 0; $j-- ) { + $call = $this->rewriter->call_stack[ $j ]; + if ( 'DATE_ADD' === $call['function'] ) { + $interval_op = '+'; + break; + } + if ( 'DATE_SUB' === $call['function'] ) { + $interval_op = '-'; + break; + } + } + + $this->rewriter->add( new WP_SQLite_Token( "'{$interval_op}$num $unit'", WP_SQLite_Token::TYPE_STRING ) ); + return true; + } + + /** + * Translate REGEXP and RLIKE keywords. + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function translate_regexp_functions( $token ) { + if ( + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + null, + array( 'REGEXP', 'RLIKE' ) + ) + ) { + return false; + } + $this->rewriter->skip(); + $this->rewriter->add( new WP_SQLite_Token( 'REGEXP', WP_SQLite_Token::TYPE_KEYWORD ) ); + + $next = $this->rewriter->peek(); + + /* + * If the query says REGEXP BINARY, the comparison is byte-by-byte + * and letter casing matters – lowercase and uppercase letters are + * represented using different byte codes. + * + * The REGEXP function can't be easily made to accept two + * parameters, so we'll have to use a hack to get around this. + * + * If the first character of the pattern is a null byte, we'll + * remove it and make the comparison case-sensitive. This should + * be reasonably safe since PHP does not allow null bytes in + * regular expressions anyway. + */ + if ( $next->matches( WP_SQLite_Token::TYPE_KEYWORD, null, array( 'BINARY' ) ) ) { + // Skip the "BINARY" keyword. + $this->rewriter->skip(); + // Prepend a null byte to the pattern. + $this->rewriter->add_many( + array( + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( 'char', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_FUNCTION ), + new WP_SQLite_Token( '(', WP_SQLite_Token::TYPE_OPERATOR ), + new WP_SQLite_Token( '0', WP_SQLite_Token::TYPE_NUMBER ), + new WP_SQLite_Token( ')', WP_SQLite_Token::TYPE_OPERATOR ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( '||', WP_SQLite_Token::TYPE_OPERATOR ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + ) + ); + } + return true; + } + + /** + * Detect GROUP BY. + * + * @todo edgecase Fails on a statement with GROUP BY nested in an outer HAVING without GROUP BY. + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function capture_group_by( $token ) { + if ( + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED, + array( 'GROUP' ) + ) + ) { + return false; + } + $next = $this->rewriter->peek_nth( 2 )->value; + if ( 'BY' !== strtoupper( $next ) ) { + return false; + } + + $this->has_group_by = true; + + return false; + } + + /** + * Translate WHERE something HAVING something to WHERE something AND something. + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function translate_ungrouped_having( $token ) { + if ( + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED, + array( 'HAVING' ) + ) + ) { + return false; + } + if ( $this->has_group_by ) { + return false; + } + $this->rewriter->skip(); + $this->rewriter->add( new WP_SQLite_Token( 'AND', WP_SQLite_Token::TYPE_KEYWORD ) ); + + return true; + } + + /** + * Rewrite LIKE '\_whatever' as LIKE '\_whatever' ESCAPE '\' . + * + * We look for keyword LIKE. On seeing it we set a flag. + * If the flag is set, we emit ESCAPE '\' before the next keyword. + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function translate_like_escape( $token ) { + + if ( 0 === $this->like_expression_nesting ) { + $is_like = $token->matches( WP_SQLite_Token::TYPE_KEYWORD, null, array( 'LIKE' ) ); + /* is this the LIKE keyword? If so set the flag. */ + if ( $is_like ) { + $this->like_expression_nesting = 1; + } + } else { + /* open parenthesis during LIKE parameter, count it. */ + if ( $token->matches( WP_SQLite_Token::TYPE_OPERATOR, null, array( '(' ) ) ) { + $this->like_expression_nesting ++; + + return false; + } + + /* close parenthesis matching open parenthesis during LIKE parameter, count it. */ + if ( $this->like_expression_nesting > 1 && $token->matches( WP_SQLite_Token::TYPE_OPERATOR, null, array( ')' ) ) ) { + $this->like_expression_nesting --; + + return false; + } + + /* a keyword, a commo, a semicolon, the end of the statement, or a close parenthesis */ + $is_like_finished = $token->matches( WP_SQLite_Token::TYPE_KEYWORD ) + || $token->matches( WP_SQLite_Token::TYPE_DELIMITER, null, array( ';' ) ) || ( WP_SQLite_Token::TYPE_DELIMITER === $token->type && null === $token->value ) + || $token->matches( WP_SQLite_Token::TYPE_OPERATOR, null, array( ')', ',' ) ); + + if ( $is_like_finished ) { + /* + * Here we have another keyword encountered with the LIKE in progress. + * Emit the ESCAPE clause. + */ + if ( $this->like_escape_count > 0 ) { + /* If we need the ESCAPE clause emit it. */ + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_DELIMITER ) ); + $this->rewriter->add( new WP_SQLite_Token( 'ESCAPE', WP_SQLite_Token::TYPE_KEYWORD ) ); + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_DELIMITER ) ); + $this->rewriter->add( new WP_SQLite_Token( "'" . self::LIKE_ESCAPE_CHAR . "'", WP_SQLite_Token::TYPE_STRING ) ); + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_DELIMITER ) ); + } + $this->like_escape_count = 0; + $this->like_expression_nesting = 0; + } + } + + return false; + } + + /** + * Rewrite a query from the MySQL information_schema. + * + * @param string $updated_query The query to rewrite. + * + * @return string The query for use by SQLite + */ + private function get_information_schema_query( $updated_query ) { + // @TODO: Actually rewrite the columns. + $normalized_query = preg_replace( '/\s+/', ' ', strtolower( $updated_query ) ); + if ( str_contains( $normalized_query, 'bytes' ) ) { + // Count rows per table. + $tables = + $this->execute_sqlite_query( "SELECT name as `table_name` FROM sqlite_master WHERE type='table' ORDER BY name" )->fetchAll(); + $tables = $this->strip_sqlite_system_tables( $tables ); + + $rows = '(CASE '; + foreach ( $tables as $table ) { + $table_name = $table['table_name']; + $count = $this->execute_sqlite_query( "SELECT COUNT(1) as `count` FROM $table_name" )->fetch(); + $rows .= " WHEN name = '$table_name' THEN {$count['count']} "; + } + $rows .= 'ELSE 0 END) '; + $updated_query = + "SELECT name as `table_name`, $rows as `rows`, 0 as `bytes` FROM sqlite_master WHERE type='table' ORDER BY name"; + } elseif ( str_contains( $normalized_query, 'count(*)' ) && ! str_contains( $normalized_query, 'table_name =' ) ) { + // @TODO This is a guess that the caller wants a count of tables. + $list = array(); + foreach ( $this->sqlite_system_tables as $system_table => $name ) { + $list [] = "'" . $system_table . "'"; + } + $list = implode( ', ', $list ); + $sql = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name NOT IN ($list)"; + $table_count = $this->execute_sqlite_query( $sql )->fetch(); + $updated_query = 'SELECT ' . $table_count[0] . ' AS num'; + + $this->is_information_schema_query = false; + } else { + $updated_query = + "SELECT name as `table_name`, 'myisam' as `engine`, 0 as `data_length`, 0 as `index_length`, 0 as `data_free` FROM sqlite_master WHERE type='table' ORDER BY name"; + } + + return $updated_query; + } + + /** + * Remove system table rows from resultsets of information_schema tables. + * + * @param array $tables The result set. + * + * @return array The filtered result set. + */ + private function strip_sqlite_system_tables( $tables ) { + return array_values( + array_filter( + $tables, + function ( $table ) { + $table_name = property_exists( $table, 'Name' ) ? $table->Name : $table->table_name; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + return ! array_key_exists( $table_name, $this->sqlite_system_tables ); + }, + ARRAY_FILTER_USE_BOTH + ) + ); + } + + /** + * Translate the ON DUPLICATE KEY UPDATE clause. + * + * @param string $table_name The table name. + * + * @return void + */ + private function translate_on_duplicate_key( $table_name ) { + /* + * Rewrite: + * ON DUPLICATE KEY UPDATE `option_name` = VALUES(`option_name`) + * to: + * ON CONFLICT(ip) DO UPDATE SET option_name = excluded.option_name + */ + + // Find the conflicting column. + $pk_columns = array(); + foreach ( $this->get_primary_keys( $table_name ) as $row ) { + $pk_columns[] = $row['name']; + } + + $unique_columns = array(); + foreach ( $this->get_keys( $table_name, true ) as $row ) { + foreach ( $row['columns'] as $column ) { + $unique_columns[] = $column['name']; + } + } + + // Guess the conflict column based on the query details. + + // 1. Listed INSERT columns that are either PK or UNIQUE. + $conflict_columns = array_intersect( + $this->insert_columns, + array_merge( $pk_columns, $unique_columns ) + ); + // 2. Composite Primary Key columns. + if ( ! $conflict_columns && count( $pk_columns ) > 1 ) { + $conflict_columns = $pk_columns; + } + // 3. The first unique column. + if ( ! $conflict_columns && count( $unique_columns ) > 0 ) { + $conflict_columns = array( $unique_columns[0] ); + } + // 4. Regular Primary Key column. + if ( ! $conflict_columns ) { + $conflict_columns = $pk_columns; + } + + /* + * If we still haven't found any conflict column, we + * can't rewrite the ON DUPLICATE KEY statement. + * Let's default to a regular INSERT to mimic MySQL + * which would still insert the row without throwing + * an error. + */ + if ( ! $conflict_columns ) { + // Drop the consumed "ON". + $this->rewriter->drop_last(); + // Skip over "DUPLICATE", "KEY", and "UPDATE". + $this->rewriter->skip(); + $this->rewriter->skip(); + $this->rewriter->skip(); + while ( $this->rewriter->skip() ) { + // Skip over the rest of the query. + } + return; + } + + // Skip over "DUPLICATE", "KEY", and "UPDATE". + $this->rewriter->skip(); + $this->rewriter->skip(); + $this->rewriter->skip(); + + // Add the CONFLICT keyword. + $this->rewriter->add( new WP_SQLite_Token( 'CONFLICT', WP_SQLite_Token::TYPE_KEYWORD ) ); + + // Add "(MySQL query:
' . PHP_EOL; + $output .= '' . $this->mysql_query . '
' . PHP_EOL; + $output .= 'Queries made or created this session were:
' . PHP_EOL; + $output .= 'Backtrace:
' . PHP_EOL; + $output .= '' . $e->getTraceAsString() . '' . PHP_EOL; + } + + return $output; + } + + /** + * Executes a query in SQLite. + * + * @param mixed $sql The query to execute. + * @param mixed $params The parameters to bind to the query. + * @throws PDOException If the query could not be executed. + * @return object { + * The result of the query. + * + * @type PDOStatement $stmt The executed statement + * @type * $result The value returned by $stmt. + * } + */ + public function execute_sqlite_query( $sql, $params = array() ) { + $this->executed_sqlite_queries[] = array( + 'sql' => $sql, + 'params' => $params, + ); + + $stmt = $this->pdo->prepare( $sql ); + if ( false === $stmt || null === $stmt ) { + $this->last_exec_returned = null; + $info = $this->pdo->errorInfo(); + $this->last_sqlite_error = $info[0] . ' ' . $info[2]; + throw new PDOException( implode( ' ', array( 'Error:', $info[0], $info[2], 'SQLite:', $sql ) ), $info[1] ); + } + $returned = $stmt->execute( $params ); + $this->last_exec_returned = $returned; + if ( ! $returned ) { + $info = $stmt->errorInfo(); + $this->last_sqlite_error = $info[0] . ' ' . $info[2]; + throw new PDOException( implode( ' ', array( 'Error:', $info[0], $info[2], 'SQLite:', $sql ) ), $info[1] ); + } + + return $stmt; + } + + /** + * Method to set the results from the fetched data. + * + * @param array $data The data to set. + */ + private function set_results_from_fetched_data( $data ) { + if ( null === $this->results ) { + $this->results = $data; + } + if ( is_array( $this->results ) ) { + $this->num_rows = count( $this->results ); + $this->last_select_found_rows = count( $this->results ); + } + $this->return_value = $this->results; + } + + /** + * Method to set the results from the affected rows. + * + * @param int|null $override Override the affected rows. + */ + private function set_result_from_affected_rows( $override = null ) { + /* + * SELECT CHANGES() is a workaround for the fact that + * $stmt->rowCount() returns "0" (zero) with the + * SQLite driver at all times. + * Source: https://www.php.net/manual/en/pdostatement.rowcount.php + */ + if ( null === $override ) { + $this->affected_rows = (int) $this->execute_sqlite_query( 'select changes()' )->fetch()[0]; + } else { + $this->affected_rows = $override; + } + $this->return_value = $this->affected_rows; + $this->num_rows = $this->affected_rows; + $this->results = $this->affected_rows; + } + + /** + * Method to clear previous data. + */ + private function flush() { + $this->mysql_query = ''; + $this->results = null; + $this->last_exec_returned = null; + $this->last_insert_id = null; + $this->affected_rows = null; + $this->column_data = array(); + $this->num_rows = null; + $this->return_value = null; + $this->error_messages = array(); + $this->is_error = false; + $this->executed_sqlite_queries = array(); + $this->like_expression_nesting = 0; + $this->like_escape_count = 0; + $this->is_information_schema_query = false; + $this->has_group_by = false; + } + + /** + * Begin a new transaction or nested transaction. + * + * @return boolean + */ + public function begin_transaction() { + $success = false; + try { + if ( 0 === $this->transaction_level ) { + $this->execute_sqlite_query( 'BEGIN' ); + } else { + $this->execute_sqlite_query( 'SAVEPOINT LEVEL' . $this->transaction_level ); + } + $success = $this->last_exec_returned; + } finally { + if ( $success ) { + ++$this->transaction_level; + } + } + return $success; + } + + /** + * Commit the current transaction or nested transaction. + * + * @return boolean True on success, false on failure. + */ + public function commit() { + if ( 0 === $this->transaction_level ) { + return false; + } + + --$this->transaction_level; + if ( 0 === $this->transaction_level ) { + $this->execute_sqlite_query( 'COMMIT' ); + } else { + $this->execute_sqlite_query( 'RELEASE SAVEPOINT LEVEL' . $this->transaction_level ); + } + return $this->last_exec_returned; + } + + /** + * Rollback the current transaction or nested transaction. + * + * @return boolean True on success, false on failure. + */ + public function rollback() { + if ( 0 === $this->transaction_level ) { + return false; + } + + --$this->transaction_level; + if ( 0 === $this->transaction_level ) { + $this->execute_sqlite_query( 'ROLLBACK' ); + } else { + $this->execute_sqlite_query( 'ROLLBACK TO SAVEPOINT LEVEL' . $this->transaction_level ); + } + return $this->last_exec_returned; + } +} diff --git a/modules/database/sqlite/wp-includes/sqlite/db.php b/modules/database/sqlite/wp-includes/sqlite/db.php index 4480b9155a..04242628dd 100644 --- a/modules/database/sqlite/wp-includes/sqlite/db.php +++ b/modules/database/sqlite/wp-includes/sqlite/db.php @@ -9,8 +9,8 @@ // Require the constants file. require_once dirname( dirname( __DIR__ ) ) . '/constants.php'; -// Bail early if DATABASE_TYPE is not defined as sqlite. -if ( ! defined( 'DATABASE_TYPE' ) || 'sqlite' !== DATABASE_TYPE ) { +// Bail early if DB_ENGINE is not defined as sqlite. +if ( ! defined( 'DB_ENGINE' ) || 'sqlite' !== DB_ENGINE ) { return; } @@ -42,13 +42,12 @@ ); } -require_once __DIR__ . '/class-perflab-sqlite-pdo-user-defined-functions.php'; -require_once __DIR__ . '/class-perflab-sqlite-pdo-engine.php'; -require_once __DIR__ . '/class-perflab-sqlite-object-array.php'; -require_once __DIR__ . '/class-perflab-sqlite-db.php'; -require_once __DIR__ . '/class-perflab-sqlite-pdo-driver.php'; -require_once __DIR__ . '/class-perflab-sqlite-create-query.php'; -require_once __DIR__ . '/class-perflab-sqlite-alter-query.php'; +require_once __DIR__ . '/class-wp-sqlite-lexer.php'; +require_once __DIR__ . '/class-wp-sqlite-query-rewriter.php'; +require_once __DIR__ . '/class-wp-sqlite-translator.php'; +require_once __DIR__ . '/class-wp-sqlite-token.php'; +require_once __DIR__ . '/class-wp-sqlite-pdo-user-defined-functions.php'; +require_once __DIR__ . '/class-wp-sqlite-db.php'; require_once __DIR__ . '/install-functions.php'; -$GLOBALS['wpdb'] = new Perflab_SQLite_DB(); +$GLOBALS['wpdb'] = new WP_SQLite_DB(); diff --git a/modules/database/sqlite/wp-includes/sqlite/install-functions.php b/modules/database/sqlite/wp-includes/sqlite/install-functions.php index e7442ffab4..f541e3794f 100644 --- a/modules/database/sqlite/wp-includes/sqlite/install-functions.php +++ b/modules/database/sqlite/wp-includes/sqlite/install-functions.php @@ -14,14 +14,14 @@ * @since 1.8.0 * * @return boolean + * + * @throws PDOException If the database connection fails. */ -function perflab_sqlite_make_db_sqlite() { +function sqlite_make_db_sqlite() { include_once ABSPATH . 'wp-admin/includes/schema.php'; - $index_array = array(); $table_schemas = wp_get_db_schema(); $queries = explode( ';', $table_schemas ); - $query_parser = new Perflab_SQLite_Create_Query(); try { $pdo = new PDO( 'sqlite:' . FQDB, null, null, array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ) ); // phpcs:ignore WordPress.DB.RestrictedClasses } catch ( PDOException $err ) { @@ -31,66 +31,36 @@ function perflab_sqlite_make_db_sqlite() { wp_die( $message, 'Database Error!' ); } + $translator = new WP_SQLite_Translator( $pdo, $GLOBALS['table_prefix'] ); + $query = null; + try { - $pdo->beginTransaction(); + $translator->begin_transaction(); foreach ( $queries as $query ) { $query = trim( $query ); if ( empty( $query ) ) { continue; } - $rewritten_query = $query_parser->rewrite_query( $query ); - if ( is_array( $rewritten_query ) ) { - $table_query = array_shift( $rewritten_query ); - $index_queries = $rewritten_query; - $table_query = trim( $table_query ); - $pdo->exec( $table_query ); - } else { - $rewritten_query = trim( $rewritten_query ); - $pdo->exec( $rewritten_query ); - } - } - $pdo->commit(); - if ( $index_queries ) { - // $query_parser rewrites KEY to INDEX, so we don't need KEY pattern. - $pattern = '/CREATE\\s*(UNIQUE\\s*INDEX|INDEX)\\s*IF\\s*NOT\\s*EXISTS\\s*(\\w+)?\\s*.*/im'; - $pdo->beginTransaction(); - foreach ( $index_queries as $index_query ) { - preg_match( $pattern, $index_query, $match ); - $index_name = trim( $match[2] ); - if ( in_array( $index_name, $index_array, true ) ) { - $r = rand( 0, 50 ); - $replacement = $index_name . "_$r"; - $index_query = str_ireplace( - 'EXISTS ' . $index_name, - 'EXISTS ' . $replacement, - $index_query - ); - } else { - $index_array[] = $index_name; - } - $pdo->exec( $index_query ); + + $result = $translator->query( $query ); + if ( false === $result ) { + throw new PDOException( $translator->get_error_message() ); } - $pdo->commit(); } + $translator->commit(); } catch ( PDOException $err ) { $err_data = $err->errorInfo; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $err_code = $err_data[1]; - if ( 5 == $err_code || 6 == $err_code ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison - // If the database is locked, commit again. - $pdo->commit(); - } else { - $pdo->rollBack(); - $message = sprintf( - 'Error occurred while creating tables or indexes...