├── version.php ├── load.php ├── wp-includes ├── sqlite-ast │ ├── class-wp-sqlite-driver-exception.php │ ├── class-wp-sqlite-information-schema-exception.php │ ├── class-wp-sqlite-connection.php │ ├── class-wp-sqlite-configurator.php │ └── class-wp-sqlite-information-schema-reconstructor.php ├── mysql │ ├── class-wp-mysql-parser.php │ └── class-wp-mysql-token.php ├── parser │ ├── class-wp-parser-token.php │ ├── class-wp-parser.php │ ├── class-wp-parser-grammar.php │ └── class-wp-parser-node.php └── sqlite │ ├── db.php │ ├── install-functions.php │ ├── class-wp-sqlite-query-rewriter.php │ ├── class-wp-sqlite-token.php │ ├── class-wp-sqlite-db.php │ └── class-wp-sqlite-pdo-user-defined-functions.php ├── php-polyfills.php ├── constants.php ├── db.copy ├── readme.txt ├── deactivate.php ├── admin-notices.php ├── health-check.php ├── integrations └── query-monitor │ ├── plugin.php │ └── boot.php ├── activate.php ├── admin-page.php └── LICENSE /version.php: -------------------------------------------------------------------------------- 1 | code = $code; 27 | $this->driver = $driver; 28 | } 29 | 30 | public function getDriver(): WP_SQLite_Driver { 31 | return $this->driver; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /wp-includes/mysql/class-wp-mysql-parser.php: -------------------------------------------------------------------------------- 1 | next_query() ) { 22 | * $ast = $parser->get_query_ast(); 23 | * if ( ! $ast ) { 24 | * // The parsing failed. 25 | * } 26 | * // The query was successfully parsed. 27 | * } 28 | * 29 | * @return bool Whether a query was successfully parsed. 30 | */ 31 | public function next_query(): bool { 32 | if ( $this->position >= count( $this->tokens ) ) { 33 | return false; 34 | } 35 | $this->current_ast = $this->parse(); 36 | return true; 37 | } 38 | 39 | /** 40 | * Get the current query AST. 41 | * 42 | * When no query has been parsed yet, the parsing failed, or the end of the 43 | * input was reached, this method returns null. 44 | * 45 | * @see WP_MySQL_Parser::next_query() for usage example. 46 | * 47 | * @return WP_Parser_Node|null The current query AST, or null if no query was parsed. 48 | */ 49 | public function get_query_ast(): ?WP_Parser_Node { 50 | return $this->current_ast; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /php-polyfills.php: -------------------------------------------------------------------------------- 1 | id = $id; 55 | $this->start = $start; 56 | $this->length = $length; 57 | $this->input = $input; 58 | } 59 | 60 | /** 61 | * Get the raw bytes of the token from the input. 62 | * 63 | * @return string The token bytes. 64 | */ 65 | public function get_bytes(): string { 66 | return substr( $this->input, $this->start, $this->length ); 67 | } 68 | 69 | /** 70 | * Get the real unquoted value of the token. 71 | * 72 | * @return string The token value. 73 | */ 74 | public function get_value(): string { 75 | return $this->get_bytes(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /db.copy: -------------------------------------------------------------------------------- 1 | delete( WP_CONTENT_DIR . '/db.php' ); 29 | // Flush the cache again to mitigate a possible race condition. 30 | wp_cache_flush(); 31 | } 32 | 33 | // Run an action on `shutdown`, to deactivate the option in the MySQL database. 34 | add_action( 35 | 'shutdown', 36 | function () { 37 | global $table_prefix; 38 | 39 | // Get credentials for the MySQL database. 40 | $dbuser = defined( 'DB_USER' ) ? DB_USER : ''; 41 | $dbpassword = defined( 'DB_PASSWORD' ) ? DB_PASSWORD : ''; 42 | $dbname = defined( 'DB_NAME' ) ? DB_NAME : ''; 43 | $dbhost = defined( 'DB_HOST' ) ? DB_HOST : ''; 44 | 45 | // Init a connection to the MySQL database. 46 | $wpdb_mysql = new wpdb( $dbuser, $dbpassword, $dbname, $dbhost ); 47 | $wpdb_mysql->set_prefix( $table_prefix ); 48 | 49 | // Get the perflab options, remove the database/sqlite module and update the option. 50 | $row = $wpdb_mysql->get_row( $wpdb_mysql->prepare( "SELECT option_value FROM $wpdb_mysql->options WHERE option_name = %s LIMIT 1", 'active_plugins' ) ); 51 | if ( is_object( $row ) ) { 52 | $value = maybe_unserialize( $row->option_value ); 53 | if ( is_array( $value ) ) { 54 | $value_flipped = array_flip( $value ); 55 | $items = array_reverse( explode( DIRECTORY_SEPARATOR, SQLITE_MAIN_FILE ) ); 56 | $item = $items[1] . DIRECTORY_SEPARATOR . $items[0]; 57 | unset( $value_flipped[ $item ] ); 58 | $value = array_flip( $value_flipped ); 59 | $wpdb_mysql->update( $wpdb_mysql->options, array( 'option_value' => maybe_serialize( $value ) ), array( 'option_name' => 'active_plugins' ) ); 60 | } 61 | } 62 | }, 63 | PHP_INT_MAX 64 | ); 65 | // Flush any persistent cache. 66 | wp_cache_flush(); 67 | } 68 | register_deactivation_hook( SQLITE_MAIN_FILE, 'sqlite_plugin_remove_db_file' ); // Remove db.php file on plugin deactivation. 69 | -------------------------------------------------------------------------------- /admin-notices.php: -------------------------------------------------------------------------------- 1 | base ) && 'settings_page_sqlite-integration' === $current_screen->base ) { 19 | return; 20 | } 21 | 22 | // If PDO SQLite is not loaded, bail early. 23 | if ( ! extension_loaded( 'pdo_sqlite' ) ) { 24 | printf( 25 | '

%s

', 26 | esc_html__( 'The SQLite Integration plugin is active, but the PDO SQLite extension is missing from your server. Please make sure that PDO SQLite is enabled in your PHP installation.', 'sqlite-database-integration' ) 27 | ); 28 | return; 29 | } 30 | 31 | /* 32 | * If the SQLITE_DB_DROPIN_VERSION constant is not defined 33 | * but there's a db.php file in the wp-content directory, then the module can't be activated. 34 | * The module should not have been activated in the first place 35 | * (there's a check in the can-load.php file), but this is a fallback check. 36 | */ 37 | if ( file_exists( WP_CONTENT_DIR . '/db.php' ) && ! defined( 'SQLITE_DB_DROPIN_VERSION' ) ) { 38 | printf( 39 | '

%s

', 40 | sprintf( 41 | /* translators: 1: SQLITE_DB_DROPIN_VERSION constant, 2: db.php drop-in path */ 42 | __( 'The SQLite Integration module is active, but the %1$s constant is missing. It appears you already have another %2$s file present on your site. ', 'sqlite-database-integration' ), 43 | 'SQLITE_DB_DROPIN_VERSION', 44 | '' . esc_html( basename( WP_CONTENT_DIR ) ) . '/db.php' 45 | ) 46 | ); 47 | 48 | return; 49 | } 50 | 51 | if ( file_exists( WP_CONTENT_DIR . '/db.php' ) ) { 52 | return; 53 | } 54 | 55 | if ( ! wp_is_writable( WP_CONTENT_DIR ) ) { 56 | printf( 57 | '

%s

', 58 | esc_html__( 'The SQLite Integration plugin is active, but the wp-content/db.php file is missing and the wp-content directory is not writable. Please ensure the wp-content folder is writable, then deactivate the plugin and try again.', 'sqlite-database-integration' ) 59 | ); 60 | return; 61 | } 62 | // The dropin db.php is missing. 63 | printf( 64 | '

%s

', 65 | sprintf( 66 | /* translators: 1: db.php drop-in path, 2: Admin URL to deactivate the module */ 67 | __( 'The SQLite Integration plugin is active, but the %1$s file is missing. Please deactivate the plugin and re-activate it to try again.', 'sqlite-database-integration' ), 68 | '' . esc_html( basename( WP_CONTENT_DIR ) ) . '/db.php', 69 | esc_url( admin_url( 'plugins.php' ) ) 70 | ) 71 | ); 72 | } 73 | add_action( 'admin_notices', 'sqlite_plugin_admin_notice' ); // Add the admin notices. 74 | 75 | // Remove the PL-plugin admin notices for SQLite. 76 | remove_action( 'admin_notices', 'perflab_sqlite_plugin_admin_notice' ); 77 | -------------------------------------------------------------------------------- /wp-includes/sqlite/db.php: -------------------------------------------------------------------------------- 1 | %1$s

%2$s

', 28 | 'PHP PDO Extension is not loaded', 29 | 'Your PHP installation appears to be missing the PDO extension which is required for this version of WordPress and the type of database you have specified.' 30 | ) 31 | ), 32 | 'PHP PDO Extension is not loaded.' 33 | ); 34 | } 35 | 36 | if ( ! extension_loaded( 'pdo_sqlite' ) ) { 37 | wp_die( 38 | new WP_Error( 39 | 'pdo_driver_not_loaded', 40 | sprintf( 41 | '

%1$s

%2$s

', 42 | 'PDO Driver for SQLite is missing', 43 | 'Your PHP installation appears not to have the right PDO drivers loaded. These are required for this version of WordPress and the type of database you have specified.' 44 | ) 45 | ), 46 | 'PDO Driver for SQLite is missing.' 47 | ); 48 | } 49 | 50 | require_once __DIR__ . '/class-wp-sqlite-lexer.php'; 51 | require_once __DIR__ . '/class-wp-sqlite-query-rewriter.php'; 52 | require_once __DIR__ . '/class-wp-sqlite-translator.php'; 53 | require_once __DIR__ . '/class-wp-sqlite-token.php'; 54 | require_once __DIR__ . '/class-wp-sqlite-pdo-user-defined-functions.php'; 55 | require_once __DIR__ . '/class-wp-sqlite-db.php'; 56 | require_once __DIR__ . '/install-functions.php'; 57 | 58 | /** 59 | * The DB_NAME constant is required by the new SQLite driver. 60 | * 61 | * There are some existing projects in which the DB_NAME constant is missing in 62 | * wp-config.php. To enable easier early adoption and testing of the new SQLite 63 | * driver, let's allow using a default database name when DB_NAME is not set. 64 | * 65 | * TODO: For version 3.0, enforce the DB_NAME constant and remove the fallback. 66 | */ 67 | if ( defined( 'DB_NAME' ) && '' !== DB_NAME ) { 68 | $db_name = DB_NAME; 69 | } else { 70 | $db_name = apply_filters( 'wp_sqlite_default_db_name', 'database_name_here' ); 71 | } 72 | 73 | /* 74 | * Debug: Cross-check with MySQL. 75 | * This is for debugging purpose only and requires files 76 | * that are present in the GitHub repository 77 | * but not the plugin published on WordPress.org. 78 | */ 79 | $crosscheck_tests_file_path = dirname( __DIR__, 2 ) . '/tests/class-wp-sqlite-crosscheck-db.php'; 80 | if ( defined( 'SQLITE_DEBUG_CROSSCHECK' ) && SQLITE_DEBUG_CROSSCHECK && file_exists( $crosscheck_tests_file_path ) ) { 81 | require_once $crosscheck_tests_file_path; 82 | $GLOBALS['wpdb'] = new WP_SQLite_Crosscheck_DB( $db_name ); 83 | } else { 84 | $GLOBALS['wpdb'] = new WP_SQLite_DB( $db_name ); 85 | 86 | // Boot the Query Monitor plugin if it is active. 87 | require_once dirname( __DIR__, 2 ) . '/integrations/query-monitor/boot.php'; 88 | } 89 | -------------------------------------------------------------------------------- /health-check.php: -------------------------------------------------------------------------------- 1 | 'DB_ENGINE', 22 | 'value' => ( defined( 'DB_ENGINE' ) ? DB_ENGINE : __( 'Undefined', 'sqlite-database-integration' ) ), 23 | 'debug' => ( defined( 'DB_ENGINE' ) ? DB_ENGINE : 'undefined' ), 24 | ); 25 | 26 | $info['wp-database']['fields']['db_engine'] = array( 27 | 'label' => __( 'Database type', 'sqlite-database-integration' ), 28 | 'value' => 'sqlite' === $db_engine ? 'SQLite' : 'MySQL/MariaDB', 29 | ); 30 | 31 | if ( 'sqlite' === $db_engine ) { 32 | $info['wp-database']['fields']['database_version'] = array( 33 | 'label' => __( 'SQLite version', 'sqlite-database-integration' ), 34 | 'value' => $info['wp-database']['fields']['server_version'] ?? null, 35 | ); 36 | 37 | $info['wp-database']['fields']['database_file'] = array( 38 | 'label' => __( 'Database file', 'sqlite-database-integration' ), 39 | 'value' => FQDB, 40 | 'private' => true, 41 | ); 42 | 43 | $info['wp-database']['fields']['database_size'] = array( 44 | 'label' => __( 'Database size', 'sqlite-database-integration' ), 45 | 'value' => size_format( filesize( FQDB ) ), 46 | ); 47 | 48 | unset( $info['wp-database']['fields']['extension'] ); 49 | unset( $info['wp-database']['fields']['server_version'] ); 50 | unset( $info['wp-database']['fields']['client_version'] ); 51 | unset( $info['wp-database']['fields']['database_host'] ); 52 | unset( $info['wp-database']['fields']['database_user'] ); 53 | unset( $info['wp-database']['fields']['database_name'] ); 54 | unset( $info['wp-database']['fields']['database_charset'] ); 55 | unset( $info['wp-database']['fields']['database_collate'] ); 56 | unset( $info['wp-database']['fields']['max_allowed_packet'] ); 57 | unset( $info['wp-database']['fields']['max_connections'] ); 58 | } 59 | 60 | return $info; 61 | } 62 | add_filter( 'debug_information', 'sqlite_plugin_filter_debug_data' ); // Filter debug data in site-health screen. 63 | 64 | /** 65 | * Filter site_status tests in site-health screen. 66 | * 67 | * When the plugin gets merged in wp-core, these should be merged in src/wp-admin/includes/class-wp-site-health.php 68 | * 69 | * @param array $tests The tests. 70 | * @return array 71 | */ 72 | function sqlite_plugin_filter_site_status_tests( $tests ) { 73 | $db_engine = defined( 'DB_ENGINE' ) && 'sqlite' === DB_ENGINE ? 'sqlite' : 'mysql'; 74 | 75 | if ( 'sqlite' === $db_engine ) { 76 | unset( $tests['direct']['utf8mb4_support'] ); 77 | unset( $tests['direct']['sql_server'] ); 78 | unset( $tests['direct']['persistent_object_cache'] ); // Throws an error because DB_NAME is not defined. 79 | } 80 | 81 | return $tests; 82 | } 83 | add_filter( 'site_status_tests', 'sqlite_plugin_filter_site_status_tests' ); 84 | -------------------------------------------------------------------------------- /integrations/query-monitor/plugin.php: -------------------------------------------------------------------------------- 1 | $row The row data. 19 | * @param array $cols The column names. 20 | * @return void 21 | */ 22 | protected function output_query_row( array $row, array $cols ) { 23 | // Capture the query row HTML. 24 | ob_start(); 25 | parent::output_query_row( $row, $cols ); 26 | $data = ob_get_length() > 0 ? ob_get_clean() : ''; 27 | 28 | // Get the corresponding SQLite queries. 29 | global $wpdb; 30 | static $query_index = 0; 31 | $sqlite_queries = $wpdb->queries[ $query_index ]['sqlite_queries'] ?? array(); 32 | $sqlite_query_count = count( $sqlite_queries ); 33 | $query_index += 1; 34 | 35 | // Build the SQLite info HTML. 36 | $sqlite_info = sprintf( 37 | '
Executed %d SQLite %s:
', 38 | $sqlite_query_count, 39 | 1 === $sqlite_query_count ? 'Query' : 'Queries' 40 | ); 41 | $sqlite_info .= '
    '; 42 | foreach ( $sqlite_queries as $query ) { 43 | $sqlite_info .= '
  1. '; 44 | $sqlite_info .= '' . str_replace( '
    ', '', self::format_sql( $query['sql'] ) ) . '
    '; 45 | $sqlite_info .= '
  2. '; 46 | } 47 | $sqlite_info .= '
'; 48 | 49 | // Inject toggle button and SQLite info into the query row HTML. 50 | $toggle_button = ''; 51 | $toggle_content = sprintf( '', $sqlite_info ); 52 | 53 | $data = str_replace( 'qm-row-sql', 'qm-row-sql qm-has-toggle', $data ); 54 | $data = preg_replace( 55 | '/()(.*?)(<\/td>)/s', 56 | implode( 57 | array( 58 | '$1', 59 | str_replace( '$', '\\$', $toggle_button ), 60 | '$2', 61 | str_replace( '$', '\\$', $toggle_content ), 62 | '$3', 63 | ) 64 | ), 65 | $data 66 | ); 67 | echo $data; 68 | } 69 | } 70 | 71 | // Remove the default Query Monitor class and replace it with the custom one. 72 | remove_filter( 'qm/outputter/html', 'register_qm_output_html_db_queries', 20 ); 73 | 74 | /** 75 | * Register the custom HTML output class. 76 | * 77 | * @param array $output 78 | * @param QM_Collectors $collectors 79 | * @return array 80 | */ 81 | function register_sqlite_qm_output_html_db_queries( array $output, $collectors ) { 82 | $collector = QM_Collectors::get( 'db_queries' ); 83 | if ( $collector ) { 84 | $output['db_queries'] = new SQLite_QM_Output_Html_DB_Queries( $collector ); 85 | } 86 | return $output; 87 | } 88 | 89 | add_filter( 'qm/outputter/html', 'register_sqlite_qm_output_html_db_queries', 20, 2 ); 90 | -------------------------------------------------------------------------------- /wp-includes/parser/class-wp-parser.php: -------------------------------------------------------------------------------- 1 | grammar = $grammar; 18 | $this->tokens = $tokens; 19 | $this->position = 0; 20 | } 21 | 22 | public function parse() { 23 | // @TODO: Make the starting rule lookup non-grammar-specific. 24 | $query_rule_id = $this->grammar->get_rule_id( 'query' ); 25 | $ast = $this->parse_recursive( $query_rule_id ); 26 | return false === $ast ? null : $ast; 27 | } 28 | 29 | private function parse_recursive( $rule_id ) { 30 | $is_terminal = $rule_id <= $this->grammar->highest_terminal_id; 31 | if ( $is_terminal ) { 32 | if ( $this->position >= count( $this->tokens ) ) { 33 | return false; 34 | } 35 | 36 | if ( WP_Parser_Grammar::EMPTY_RULE_ID === $rule_id ) { 37 | return true; 38 | } 39 | 40 | if ( $this->tokens[ $this->position ]->id === $rule_id ) { 41 | ++$this->position; 42 | return $this->tokens[ $this->position - 1 ]; 43 | } 44 | return false; 45 | } 46 | 47 | $branches = $this->grammar->rules[ $rule_id ]; 48 | if ( ! count( $branches ) ) { 49 | return false; 50 | } 51 | 52 | // Bale out from processing the current branch if none of its rules can 53 | // possibly match the current token. 54 | if ( isset( $this->grammar->lookahead_is_match_possible[ $rule_id ] ) ) { 55 | $token_id = $this->tokens[ $this->position ]->id; 56 | if ( 57 | ! isset( $this->grammar->lookahead_is_match_possible[ $rule_id ][ $token_id ] ) && 58 | ! isset( $this->grammar->lookahead_is_match_possible[ $rule_id ][ WP_Parser_Grammar::EMPTY_RULE_ID ] ) 59 | ) { 60 | return false; 61 | } 62 | } 63 | 64 | $rule_name = $this->grammar->rule_names[ $rule_id ]; 65 | $starting_position = $this->position; 66 | foreach ( $branches as $branch ) { 67 | $this->position = $starting_position; 68 | $node = new WP_Parser_Node( $rule_id, $rule_name ); 69 | $branch_matches = true; 70 | foreach ( $branch as $subrule_id ) { 71 | $subnode = $this->parse_recursive( $subrule_id ); 72 | if ( false === $subnode ) { 73 | $branch_matches = false; 74 | break; 75 | } elseif ( true === $subnode ) { 76 | /* 77 | * The subrule was matched without actually matching a token. 78 | * This means a special empty "ε" (epsilon) rule was matched. 79 | * An "ε" rule in a grammar matches an empty input of 0 bytes. 80 | * It is used to represent optional grammar productions. 81 | */ 82 | continue; 83 | } elseif ( is_array( $subnode ) && 0 === count( $subnode ) ) { 84 | continue; 85 | } 86 | if ( is_array( $subnode ) && ! count( $subnode ) ) { 87 | continue; 88 | } 89 | if ( isset( $this->grammar->fragment_ids[ $subrule_id ] ) ) { 90 | $node->merge_fragment( $subnode ); 91 | } else { 92 | $node->append_child( $subnode ); 93 | } 94 | } 95 | 96 | // Negative lookahead for INTO after a valid SELECT statement. 97 | // If we match a SELECT statement, but there is an INTO keyword after it, 98 | // we're in the wrong branch and need to leave matching to a later rule. 99 | // @TODO: Extract this to the "WP_MySQL_Parser" class, or add support 100 | // for right-associative rules, which could solve this. 101 | // See: https://github.com/mysql/mysql-workbench/blob/8.0.38/library/parsers/grammars/MySQLParser.g4#L994 102 | // See: https://github.com/antlr/antlr4/issues/488 103 | $la = $this->tokens[ $this->position ] ?? null; 104 | if ( $la && 'selectStatement' === $rule_name && WP_MySQL_Lexer::INTO_SYMBOL === $la->id ) { 105 | $branch_matches = false; 106 | } 107 | 108 | if ( true === $branch_matches ) { 109 | break; 110 | } 111 | } 112 | 113 | if ( ! $branch_matches ) { 114 | $this->position = $starting_position; 115 | return false; 116 | } 117 | 118 | if ( ! $node->has_child() ) { 119 | return true; 120 | } 121 | 122 | return $node; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /activate.php: -------------------------------------------------------------------------------- 1 | base ) && 'settings_page_sqlite-integration' === $current_screen->base ) { 37 | return; 38 | } 39 | if ( isset( $_GET['confirm-install'] ) && wp_verify_nonce( $_GET['_wpnonce'], 'sqlite-install' ) ) { 40 | 41 | // Handle upgrading from the performance-lab plugin. 42 | if ( isset( $_GET['upgrade-from-pl'] ) ) { 43 | global $wp_filesystem; 44 | require_once ABSPATH . '/wp-admin/includes/file.php'; 45 | // Delete the previous db.php file. 46 | $wp_filesystem->delete( WP_CONTENT_DIR . '/db.php' ); 47 | // Deactivate the performance-lab SQLite module. 48 | $pl_option_name = defined( 'PERFLAB_MODULES_SETTING' ) ? PERFLAB_MODULES_SETTING : 'perflab_modules_settings'; 49 | $pl_option = get_option( $pl_option_name, array() ); 50 | unset( $pl_option['database/sqlite'] ); 51 | update_option( $pl_option_name, $pl_option ); 52 | } 53 | sqlite_plugin_copy_db_file(); 54 | // WordPress will automatically redirect to the install screen here. 55 | wp_redirect( admin_url() ); 56 | exit; 57 | } 58 | } 59 | add_action( 'admin_init', 'sqlite_activation' ); 60 | 61 | // Flush the cache at the last moment before the redirect. 62 | add_filter( 63 | 'x_redirect_by', 64 | function ( $result ) { 65 | wp_cache_flush(); 66 | return $result; 67 | }, 68 | PHP_INT_MAX, 69 | 1 70 | ); 71 | 72 | /** 73 | * Add the db.php file in wp-content. 74 | * 75 | * When the plugin gets merged in wp-core, this is not to be ported. 76 | */ 77 | function sqlite_plugin_copy_db_file() { 78 | // Bail early if the PDO SQLite extension is not loaded. 79 | if ( ! extension_loaded( 'pdo_sqlite' ) ) { 80 | return; 81 | } 82 | 83 | $destination = WP_CONTENT_DIR . '/db.php'; 84 | 85 | /* 86 | * When an existing "db.php" drop-in is detected, let's check if it's a known 87 | * plugin that we can continue supporting even when we override the drop-in. 88 | */ 89 | $override_db_dropin = false; 90 | if ( file_exists( $destination ) ) { 91 | // Check for the Query Monitor plugin. 92 | // When "QM_DB" exists, it must have been loaded via the "db.php" file. 93 | if ( class_exists( 'QM_DB', false ) ) { 94 | $override_db_dropin = true; 95 | } 96 | 97 | if ( $override_db_dropin ) { 98 | require_once ABSPATH . '/wp-admin/includes/file.php'; 99 | global $wp_filesystem; 100 | if ( ! $wp_filesystem ) { 101 | WP_Filesystem(); 102 | } 103 | $wp_filesystem->delete( $destination ); 104 | } 105 | } 106 | 107 | // Place database drop-in if not present yet, except in case there is 108 | // another database drop-in present already. 109 | if ( ! defined( 'SQLITE_DB_DROPIN_VERSION' ) && ! file_exists( $destination ) ) { 110 | // Init the filesystem to allow copying the file. 111 | global $wp_filesystem; 112 | 113 | require_once ABSPATH . '/wp-admin/includes/file.php'; 114 | 115 | // Init the filesystem if needed, then copy the file, replacing contents as needed. 116 | if ( ( $wp_filesystem || WP_Filesystem() ) && $wp_filesystem->touch( $destination ) ) { 117 | 118 | // Get the db.copy.php file contents, replace placeholders and write it to the destination. 119 | $file_contents = str_replace( 120 | array( 121 | '{SQLITE_IMPLEMENTATION_FOLDER_PATH}', 122 | '{SQLITE_PLUGIN}', 123 | ), 124 | array( 125 | __DIR__, 126 | str_replace( WP_PLUGIN_DIR . '/', '', SQLITE_MAIN_FILE ), 127 | ), 128 | file_get_contents( __DIR__ . '/db.copy' ) 129 | ); 130 | 131 | $wp_filesystem->put_contents( $destination, $file_contents ); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /integrations/query-monitor/boot.php: -------------------------------------------------------------------------------- 1 | options ) { 114 | global $table_prefix; 115 | $wpdb->set_prefix( $table_prefix ?? '' ); 116 | } 117 | 118 | $query_monitor_active = false; 119 | try { 120 | // Make sure no errors are displayed when the query fails. 121 | $show_errors = $wpdb->hide_errors(); 122 | $value = $wpdb->get_row( 123 | $wpdb->prepare( 124 | "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", 125 | 'active_plugins' 126 | ) 127 | ); 128 | $wpdb->show_errors( $show_errors ); 129 | 130 | if ( null !== $value ) { 131 | $query_monitor_active = in_array( 132 | 'query-monitor/query-monitor.php', 133 | unserialize( $value->option_value ), 134 | true 135 | ); 136 | } 137 | } catch ( Throwable $e ) { 138 | return; 139 | } 140 | 141 | if ( ! $query_monitor_active ) { 142 | return; 143 | } 144 | 145 | // Load Query Monitor eagerly (as per the original "db.php" file). 146 | require_once $qm_php; 147 | 148 | if ( ! QM_PHP::version_met() ) { 149 | return; 150 | } 151 | 152 | if ( ! file_exists( "{$qm_dir}/vendor/autoload.php" ) ) { 153 | add_action( 'all_admin_notices', 'QM_PHP::vendor_nope' ); 154 | return; 155 | } 156 | 157 | require_once "{$qm_dir}/vendor/autoload.php"; 158 | 159 | if ( ! class_exists( 'QM_Backtrace' ) ) { 160 | return; 161 | } 162 | 163 | if ( ! defined( 'SAVEQUERIES' ) ) { 164 | define( 'SAVEQUERIES', true ); 165 | } 166 | 167 | // Mark the Query Monitor integration as loaded. 168 | define( 'SQLITE_QUERY_MONITOR_LOADED', true ); 169 | -------------------------------------------------------------------------------- /wp-includes/sqlite-ast/class-wp-sqlite-information-schema-exception.php: -------------------------------------------------------------------------------- 1 | type = $type; 48 | $this->data = $data; 49 | } 50 | 51 | /** 52 | * Get the type of the exception. 53 | * 54 | * @return string The type of the exception. 55 | */ 56 | public function get_type(): string { 57 | return $this->type; 58 | } 59 | 60 | /** 61 | * Get the data associated with the exception. 62 | * 63 | * @return array The data associated with the exception. 64 | */ 65 | public function get_data(): array { 66 | return $this->data; 67 | } 68 | 69 | /** 70 | * Create a duplicate table name exception. 71 | * 72 | * @param string $table_name The name of the affected table. 73 | * @return self The exception instance. 74 | */ 75 | public static function duplicate_table_name( string $table_name ): WP_SQLite_Information_Schema_Exception { 76 | return new self( 77 | self::TYPE_DUPLICATE_TABLE_NAME, 78 | sprintf( "Table '%s' already exists.", $table_name ), 79 | array( 'table_name' => $table_name ) 80 | ); 81 | } 82 | 83 | /** 84 | * Create a duplicate column name exception. 85 | * 86 | * @param string $column_name The name of the affected column. 87 | * @return self The exception instance. 88 | */ 89 | public static function duplicate_column_name( string $column_name ): WP_SQLite_Information_Schema_Exception { 90 | return new self( 91 | self::TYPE_DUPLICATE_COLUMN_NAME, 92 | sprintf( "Column '%s' already exists.", $column_name ), 93 | array( 'column_name' => $column_name ) 94 | ); 95 | } 96 | 97 | /** 98 | * Create a duplicate key name exception. 99 | * 100 | * @param string $key_name The name of the affected key. 101 | * @return self The exception instance. 102 | */ 103 | public static function duplicate_key_name( string $key_name ): WP_SQLite_Information_Schema_Exception { 104 | return new self( 105 | self::TYPE_DUPLICATE_KEY_NAME, 106 | sprintf( "Key '%s' already exists.", $key_name ), 107 | array( 'key_name' => $key_name ) 108 | ); 109 | } 110 | 111 | /** 112 | * Create a key column not found exception. 113 | * 114 | * @param string $column_name The name of the affected column. 115 | * @return self The exception instance. 116 | */ 117 | public static function key_column_not_found( string $column_name ): WP_SQLite_Information_Schema_Exception { 118 | return new self( 119 | self::TYPE_KEY_COLUMN_NOT_FOUND, 120 | sprintf( "Key column '%s' doesn't exist in table.", $column_name ), 121 | array( 'column_name' => $column_name ) 122 | ); 123 | } 124 | 125 | /** 126 | * Create a constraint does not exist exception. 127 | * 128 | * @param string $name The name of the affected constraint. 129 | * @return self The exception instance. 130 | */ 131 | public static function constraint_does_not_exist( string $name ): WP_SQLite_Information_Schema_Exception { 132 | return new self( 133 | self::TYPE_CONSTRAINT_DOES_NOT_EXIST, 134 | sprintf( "Constraint '%s' does not exist.", $name ), 135 | array( 'name' => $name ) 136 | ); 137 | } 138 | 139 | /** 140 | * Create a multiple constraints with name exception. 141 | * 142 | * @param string $name The name of the affected constraint. 143 | * @return self The exception instance. 144 | */ 145 | public static function multiple_constraints_with_name( string $name ): WP_SQLite_Information_Schema_Exception { 146 | return new self( 147 | self::TYPE_MULTIPLE_CONSTRAINTS_WITH_NAME, 148 | sprintf( "Table has multiple constraints with the name '%s'. Please use constraint specific 'DROP' clause.", $name ), 149 | array( 'name' => $name ) 150 | ); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /wp-includes/parser/class-wp-parser-grammar.php: -------------------------------------------------------------------------------- 1 | inflate( $rules ); 38 | } 39 | 40 | public function get_rule_name( $rule_id ) { 41 | return $this->rule_names[ $rule_id ]; 42 | } 43 | 44 | public function get_rule_id( $rule_name ) { 45 | return array_search( $rule_name, $this->rule_names, true ); 46 | } 47 | 48 | /** 49 | * Inflate the grammar to an internal representation optimized for parsing. 50 | * 51 | * The input grammar is a compressed PHP array to minimize the file size. 52 | * Every rule and token in the compressed grammar is encoded as an integer. 53 | */ 54 | private function inflate( $grammar ) { 55 | $this->lowest_non_terminal_id = $grammar['rules_offset']; 56 | $this->highest_terminal_id = $this->lowest_non_terminal_id - 1; 57 | 58 | foreach ( $grammar['rules_names'] as $rule_index => $rule_name ) { 59 | $this->rule_names[ $rule_index + $grammar['rules_offset'] ] = $rule_name; 60 | $this->rules[ $rule_index + $grammar['rules_offset'] ] = array(); 61 | 62 | /** 63 | * Treat all intermediate rules as fragments to inline before returning 64 | * the final parse tree to the API consumer. 65 | * 66 | * The original grammar was too difficult to parse with rules like: 67 | * 68 | * query ::= EOF | ((simpleStatement | beginWork) ((SEMICOLON_SYMBOL EOF?) | EOF)) 69 | * 70 | * We've factored rule fragments, such as `EOF?`, into separate rules, such as `%EOF_zero_or_one`. 71 | * This is super useful for parsing, but it limits the API consumer's ability to 72 | * reason about the parse tree. 73 | * 74 | * Fragments are intermediate rules that are not part of the original grammar. 75 | * They are prefixed with a "%" to be distinguished from the original rules. 76 | */ 77 | if ( '%' === $rule_name[0] ) { 78 | $this->fragment_ids[ $rule_index + $grammar['rules_offset'] ] = true; 79 | } 80 | } 81 | 82 | $this->rules = array(); 83 | foreach ( $grammar['grammar'] as $rule_index => $branches ) { 84 | $rule_id = $rule_index + $grammar['rules_offset']; 85 | $this->rules[ $rule_id ] = $branches; 86 | } 87 | 88 | /** 89 | * Compute a rule => [token => true] lookup table for each rule 90 | * that starts with a terminal OR with another rule that already 91 | * has a lookahead mapping. 92 | * 93 | * This is similar to left-factoring the grammar, even if not quite 94 | * the same. 95 | * 96 | * This enables us to quickly bail out from checking branches that 97 | * cannot possibly match the current token. This increased the parser 98 | * speed by a whopping 80%! 99 | * 100 | * @TODO: Explore these possible next steps: 101 | * 102 | * * Compute a rule => [token => branch[]] list lookup table and only 103 | * process the branches that have a chance of matching the current token. 104 | * * Actually left-factor the grammar as much as possible. This, however, 105 | * could inflate the serialized grammar size. 106 | */ 107 | // 5 iterations seem to give us all the speed gains we can get from this. 108 | for ( $i = 0; $i < 5; $i++ ) { 109 | foreach ( $grammar['grammar'] as $rule_index => $branches ) { 110 | $rule_id = $rule_index + $grammar['rules_offset']; 111 | if ( isset( $this->lookahead_is_match_possible[ $rule_id ] ) ) { 112 | continue; 113 | } 114 | $rule_lookup = array(); 115 | $first_symbol_can_be_expanded_to_all_terminals = true; 116 | foreach ( $branches as $branch ) { 117 | $terminals = false; 118 | $branch_starts_with_terminal = $branch[0] < $this->lowest_non_terminal_id; 119 | if ( $branch_starts_with_terminal ) { 120 | $terminals = array( $branch[0] ); 121 | } elseif ( isset( $this->lookahead_is_match_possible[ $branch[0] ] ) ) { 122 | $terminals = array_keys( $this->lookahead_is_match_possible[ $branch[0] ] ); 123 | } 124 | 125 | if ( false === $terminals ) { 126 | $first_symbol_can_be_expanded_to_all_terminals = false; 127 | break; 128 | } 129 | foreach ( $terminals as $terminal ) { 130 | $rule_lookup[ $terminal ] = true; 131 | } 132 | } 133 | if ( $first_symbol_can_be_expanded_to_all_terminals ) { 134 | $this->lookahead_is_match_possible[ $rule_id ] = $rule_lookup; 135 | } 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /wp-includes/sqlite-ast/class-wp-sqlite-connection.php: -------------------------------------------------------------------------------- 1 | pdo = $options['pdo']; 75 | } else { 76 | if ( ! isset( $options['path'] ) || ! is_string( $options['path'] ) ) { 77 | throw new InvalidArgumentException( 'Option "path" is required when "connection" is not provided.' ); 78 | } 79 | $pdo_class = PHP_VERSION_ID >= 80400 ? PDO\SQLite::class : PDO::class; 80 | $this->pdo = new $pdo_class( 'sqlite:' . $options['path'] ); 81 | } 82 | 83 | // Throw exceptions on error. 84 | $this->pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION ); 85 | 86 | // Configure SQLite timeout. 87 | if ( isset( $options['timeout'] ) && is_int( $options['timeout'] ) ) { 88 | $timeout = $options['timeout']; 89 | } else { 90 | $timeout = self::DEFAULT_SQLITE_TIMEOUT; 91 | } 92 | $this->pdo->setAttribute( PDO::ATTR_TIMEOUT, $timeout ); 93 | 94 | // Return all values (except null) as strings. 95 | $this->pdo->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); 96 | 97 | // Configure SQLite journal mode. 98 | $journal_mode = $options['journal_mode'] ?? null; 99 | if ( $journal_mode && in_array( $journal_mode, self::SQLITE_JOURNAL_MODES, true ) ) { 100 | $this->query( 'PRAGMA journal_mode = ' . $journal_mode ); 101 | } 102 | } 103 | 104 | /** 105 | * Execute a query in SQLite. 106 | * 107 | * @param string $sql The query to execute. 108 | * @param array $params The query parameters. 109 | * @throws PDOException When the query execution fails. 110 | * @return PDOStatement The PDO statement object. 111 | */ 112 | public function query( string $sql, array $params = array() ): PDOStatement { 113 | if ( $this->query_logger ) { 114 | ( $this->query_logger )( $sql, $params ); 115 | } 116 | $stmt = $this->pdo->prepare( $sql ); 117 | $stmt->execute( $params ); 118 | return $stmt; 119 | } 120 | 121 | /** 122 | * Returns the ID of the last inserted row. 123 | * 124 | * @return string The ID of the last inserted row. 125 | */ 126 | public function get_last_insert_id(): string { 127 | return $this->pdo->lastInsertId(); 128 | } 129 | 130 | /** 131 | * Quote a value for use in a query. 132 | * 133 | * @param mixed $value The value to quote. 134 | * @param int $type The type of the value. 135 | * @return string The quoted value. 136 | */ 137 | public function quote( $value, int $type = PDO::PARAM_STR ): string { 138 | return $this->pdo->quote( $value, $type ); 139 | } 140 | 141 | /** 142 | * Quote an SQLite identifier. 143 | * 144 | * Wraps the identifier in backticks and escapes backtick characters within. 145 | * 146 | * --- 147 | * 148 | * Quoted identifiers in SQLite are represented by string constants: 149 | * 150 | * A string constant is formed by enclosing the string in single quotes ('). 151 | * A single quote within the string can be encoded by putting two single 152 | * quotes in a row - as in Pascal. C-style escapes using the backslash 153 | * character are not supported because they are not standard SQL. 154 | * 155 | * See: https://www.sqlite.org/lang_expr.html#literal_values_constants_ 156 | * 157 | * Although sparsely documented, this applies to backtick and double quoted 158 | * string constants as well, so only the quote character needs to be escaped. 159 | * 160 | * For more details, see the grammar for SQLite table and column names: 161 | * 162 | * - https://github.com/sqlite/sqlite/blob/873fc5dff2a781251f2c9bd2c791a5fac45b7a2b/src/tokenize.c#L395-L419 163 | * - https://github.com/sqlite/sqlite/blob/873fc5dff2a781251f2c9bd2c791a5fac45b7a2b/src/parse.y#L321-L338 164 | * 165 | * --- 166 | * 167 | * We use backtick quotes instead of the SQL standard double quotes, due to 168 | * an SQLite quirk causing double-quoted strings to be accepted as literals: 169 | * 170 | * This misfeature means that a misspelled double-quoted identifier will 171 | * be interpreted as a string literal, rather than generating an error. 172 | * 173 | * See: https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted 174 | * 175 | * @param string $unquoted_identifier The unquoted identifier value. 176 | * @return string The quoted identifier value. 177 | */ 178 | public function quote_identifier( string $unquoted_identifier ): string { 179 | return '`' . str_replace( '`', '``', $unquoted_identifier ) . '`'; 180 | } 181 | 182 | /** 183 | * Get the PDO object. 184 | * 185 | * @return PDO 186 | */ 187 | public function get_pdo(): PDO { 188 | return $this->pdo; 189 | } 190 | 191 | /** 192 | * Set a logger for the queries. 193 | * 194 | * @param callable(string, array): void $logger A query logger callback. 195 | */ 196 | public function set_query_logger( callable $logger ): void { 197 | $this->query_logger = $logger; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /wp-includes/mysql/class-wp-mysql-token.php: -------------------------------------------------------------------------------- 1 | sql_mode_no_backslash_escapes_enabled = $sql_mode_no_backslash_escapes_enabled; 35 | } 36 | 37 | /** 38 | * Get the name of the token. 39 | * 40 | * This method is intended to be used only for testing and debugging purposes, 41 | * when tokens need to be presented by their names in a human-readable form. 42 | * It should not be used in production code, as it's not performance-optimized. 43 | * 44 | * @return string The token name. 45 | */ 46 | public function get_name(): string { 47 | $name = WP_MySQL_Lexer::get_token_name( $this->id ); 48 | if ( null === $name ) { 49 | $name = 'UNKNOWN'; 50 | } 51 | return $name; 52 | } 53 | 54 | /** 55 | * Get the real unquoted value of the token. 56 | * 57 | * @return string The token value. 58 | */ 59 | public function get_value(): string { 60 | $value = $this->get_bytes(); 61 | if ( 62 | WP_MySQL_Lexer::SINGLE_QUOTED_TEXT === $this->id 63 | || WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT === $this->id 64 | || WP_MySQL_Lexer::BACK_TICK_QUOTED_ID === $this->id 65 | ) { 66 | // Remove bounding quotes. 67 | $quote = $value[0]; 68 | $value = substr( $value, 1, -1 ); 69 | 70 | /* 71 | * When the NO_BACKSLASH_ESCAPES SQL mode is enabled, we only need to 72 | * handle escaped bounding quotes, as the other characters preserve 73 | * their literal values. 74 | */ 75 | if ( $this->sql_mode_no_backslash_escapes_enabled ) { 76 | return str_replace( $quote . $quote, $quote, $value ); 77 | } 78 | 79 | /** 80 | * Unescape MySQL escape sequences. 81 | * 82 | * MySQL string literals use backslash as an escape character, and 83 | * the string bounding quotes can also be escaped by being doubled. 84 | * 85 | * The escaping is done according to the following rules: 86 | * 87 | * 1. Some special character escape sequences are recognized. 88 | * For example, "\n" is a newline character, "\0" is ASCII NULL. 89 | * 2. A specific treatment is applied to "\%" and "\_" sequences. 90 | * This is due to their special meaning for pattern matching. 91 | * 3. Other backslash-prefixed characters resolve to their literal 92 | * values. For example, "\x" represents "x", "\\" represents "\". 93 | * 94 | * Despite looking similar, these rules are different from the C-style 95 | * string escaping, so we cannot use "strip(c)slashes()" in this case. 96 | * 97 | * See: https://dev.mysql.com/doc/refman/8.4/en/string-literals.html 98 | */ 99 | $backslash = chr( 92 ); 100 | $replacements = array( 101 | /* 102 | * MySQL special character escape sequences. 103 | */ 104 | ( $backslash . '0' ) => chr( 0 ), // An ASCII NULL character (\0). 105 | ( $backslash . "'" ) => chr( 39 ), // A single quote character ('). 106 | ( $backslash . '"' ) => chr( 34 ), // A double quote character ("). 107 | ( $backslash . 'b' ) => chr( 8 ), // A backspace character. 108 | ( $backslash . 'n' ) => chr( 10 ), // A newline (linefeed) character (\n). 109 | ( $backslash . 'r' ) => chr( 13 ), // A carriage return character (\r). 110 | ( $backslash . 't' ) => chr( 9 ), // A tab character (\t). 111 | ( $backslash . 'Z' ) => chr( 26 ), // An ASCII 26 (Control+Z) character. 112 | 113 | /* 114 | * Normalize escaping of "%" and "_" characters. 115 | * 116 | * MySQL has unusual handling for "\%" and "\_" in all string literals. 117 | * While other sequences follow the C-style escaping ("\?" is "?", etc.), 118 | * "\%" resolves to "\%" and "\_" resolves to "\_" (unlike in C strings). 119 | * 120 | * This means that "\%" behaves like "\\%", and "\_" behaves like "\\_". 121 | * To preserve this behavior, we need to add a second backslash here. 122 | * 123 | * From https://dev.mysql.com/doc/refman/8.4/en/string-literals.html: 124 | * > The \% and \_ sequences are used to search for literal instances 125 | * > of % and _ in pattern-matching contexts where they would otherwise 126 | * > be interpreted as wildcard characters. If you use \% or \_ outside 127 | * > of pattern-matching contexts, they evaluate to the strings \% and 128 | * > \_, not to % and _. 129 | */ 130 | ( $backslash . '%' ) => $backslash . $backslash . '%', 131 | ( $backslash . '_' ) => $backslash . $backslash . '_', 132 | 133 | /* 134 | * Preserve a double backslash as-is, so that the trailing backslash 135 | * is not consumed as the beginning of an escape sequence like "\n". 136 | * 137 | * Resolving "\\" to "\" will be handled in the next step, where all 138 | * other backslash-prefixed characters resolve to their literal values. 139 | */ 140 | ( $backslash . $backslash ) 141 | => $backslash . $backslash, 142 | 143 | /* 144 | * The bounding quotes can also be escaped by being doubled. 145 | */ 146 | ( $quote . $quote ) => $quote, 147 | ); 148 | 149 | /* 150 | * Apply the replacements. 151 | * 152 | * It is important to use "strtr()" and not "str_replace()", because 153 | * "str_replace()" applies replacements one after another, modifying 154 | * intermediate changes rather than just the original string: 155 | * 156 | * - str_replace( [ 'a', 'b' ], [ 'b', 'c' ], 'ab' ); // 'cc' (bad) 157 | * - strtr( 'ab', [ 'a' => 'b', 'b' => 'c' ] ); // 'bc' (good) 158 | */ 159 | $value = strtr( $value, $replacements ); 160 | 161 | /* 162 | * A backslash with any other character represents the character itself. 163 | * That is, \x evaluates to x, \\ evaluates to \, and \🙂 evaluates to 🙂. 164 | */ 165 | $preg_quoted_backslash = preg_quote( $backslash ); 166 | $value = preg_replace( "/$preg_quoted_backslash(.)/u", '$1', $value ); 167 | } 168 | return $value; 169 | } 170 | 171 | /** 172 | * Get the token representation as a string. 173 | * 174 | * This method is intended to be used only for testing and debugging purposes, 175 | * when tokens need to be presented in a human-readable form. It should not 176 | * be used in production code, as it's not performance-optimized. 177 | * 178 | * @return string 179 | */ 180 | public function __toString(): string { 181 | return $this->get_value() . '<' . $this->id . ',' . $this->get_name() . '>'; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /admin-page.php: -------------------------------------------------------------------------------- 1 | 45 |
46 |

47 |
48 | 49 |
50 | 51 |
52 |

53 |
54 |

55 | deactivate the plugin. Alternatively, you can manually delete the %2$s file from your server.', 'sqlite-database-integration' ), 59 | esc_url( admin_url( 'plugins.php' ) ), 60 | '' . esc_html( basename( WP_CONTENT_DIR ) ) . '/db.php' 61 | ); 62 | ?> 63 |

64 | 65 |
66 |

67 |
68 | 69 | 70 |
71 |

72 | ' . esc_html( basename( WP_CONTENT_DIR ) ) . '/db.php' 77 | ); 78 | ?> 79 |

80 |
81 | 82 | ' . esc_html( basename( WP_CONTENT_DIR ) ) . '/db.php' 87 | ); 88 | ?> 89 | 90 | 91 |
92 |

93 | ' . esc_html( basename( WP_CONTENT_DIR ) ) . '/db.php' 98 | ); 99 | ?> 100 |

101 |
102 | 103 | 104 |
105 |

106 | ' . esc_html( basename( WP_CONTENT_DIR ) ) . '' 111 | ); 112 | ?> 113 |

114 |
115 | 116 |
117 |

118 |
119 |

120 |

121 | 122 | 123 |

124 | NOTE: 125 | ' . esc_html( basename( WP_CONTENT_DIR ) ) . '/db.php' 130 | ); 131 | ?> 132 | 133 | 134 |

135 | 136 | 137 |

138 | 139 | 140 | 141 |
142 | ' . __( 'Database: SQLite', 'sqlite-database-integration' ) . $suffix . ''; 160 | } elseif ( stripos( $wpdb->db_server_info(), 'maria' ) !== false ) { 161 | $title = '' . __( 'Database: MariaDB', 'sqlite-database-integration' ) . ''; 162 | } else { 163 | $title = '' . __( 'Database: MySQL', 'sqlite-database-integration' ) . ''; 164 | } 165 | 166 | $args = array( 167 | 'id' => 'sqlite-db-integration', 168 | 'parent' => 'top-secondary', 169 | 'title' => $title, 170 | 'href' => esc_url( admin_url( 'options-general.php?page=sqlite-integration' ) ), 171 | 'meta' => false, 172 | ); 173 | $admin_bar->add_node( $args ); 174 | } 175 | add_action( 'admin_bar_menu', 'sqlite_plugin_adminbar_item', 999 ); 176 | -------------------------------------------------------------------------------- /wp-includes/sqlite/install-functions.php: -------------------------------------------------------------------------------- 1 | = 80400 ? PDO\SQLite::class : PDO::class; // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO 29 | $pdo = new $pdo_class( 'sqlite:' . FQDB, null, null, array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ) ); // phpcs:ignore WordPress.DB.RestrictedClasses 30 | } catch ( PDOException $err ) { 31 | $err_data = $err->errorInfo; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase 32 | $message = 'Database connection error!
'; 33 | $message .= sprintf( 'Error message is: %s', $err_data[2] ); 34 | wp_die( $message, 'Database Error!' ); 35 | } 36 | 37 | if ( defined( 'WP_SQLITE_AST_DRIVER' ) && WP_SQLITE_AST_DRIVER ) { 38 | $translator = new WP_SQLite_Driver( 39 | new WP_SQLite_Connection( array( 'pdo' => $pdo ) ), 40 | $wpdb->dbname 41 | ); 42 | } else { 43 | $translator = new WP_SQLite_Translator( $pdo ); 44 | } 45 | $query = null; 46 | 47 | try { 48 | $translator->begin_transaction(); 49 | foreach ( $queries as $query ) { 50 | $query = trim( $query ); 51 | if ( empty( $query ) ) { 52 | continue; 53 | } 54 | 55 | $result = $translator->query( $query ); 56 | if ( false === $result ) { 57 | throw new PDOException( $translator->get_error_message() ); 58 | } 59 | } 60 | $translator->commit(); 61 | } catch ( PDOException $err ) { 62 | $err_data = $err->errorInfo; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase 63 | $err_code = $err_data[1]; 64 | $translator->rollback(); 65 | $message = sprintf( 66 | 'Error occurred while creating tables or indexes...
Query was: %s
', 67 | var_export( $query, true ) 68 | ); 69 | $message .= sprintf( 'Error message is: %s', $err_data[2] ); 70 | wp_die( $message, 'Database Error!' ); 71 | } 72 | 73 | /* 74 | * Debug: Cross-check with MySQL. 75 | * This is for debugging purpose only and requires files 76 | * that are present in the GitHub repository 77 | * but not the plugin published on WordPress.org. 78 | */ 79 | if ( defined( 'SQLITE_DEBUG_CROSSCHECK' ) && SQLITE_DEBUG_CROSSCHECK ) { 80 | $host = DB_HOST; 81 | $port = 3306; 82 | if ( str_contains( $host, ':' ) ) { 83 | $host_parts = explode( ':', $host ); 84 | $host = $host_parts[0]; 85 | $port = $host_parts[1]; 86 | } 87 | $dsn = 'mysql:host=' . $host . '; port=' . $port . '; dbname=' . DB_NAME; 88 | $pdo_class = PHP_VERSION_ID >= 80400 ? PDO\MySQL::class : PDO::class; // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO 89 | $pdo_mysql = new $pdo_class( $dsn, DB_USER, DB_PASSWORD, array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ) ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO 90 | $pdo_mysql->query( 'SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";' ); 91 | $pdo_mysql->query( 'SET time_zone = "+00:00";' ); 92 | foreach ( $queries as $query ) { 93 | $query = trim( $query ); 94 | if ( empty( $query ) ) { 95 | continue; 96 | } 97 | try { 98 | $pdo_mysql->beginTransaction(); 99 | $pdo_mysql->query( $query ); 100 | } catch ( PDOException $err ) { 101 | $err_data = $err->errorInfo; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase 102 | $err_code = $err_data[1]; 103 | // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual 104 | if ( 5 == $err_code || 6 == $err_code ) { 105 | // If the database is locked, commit again. 106 | $pdo_mysql->commit(); 107 | } else { 108 | $pdo_mysql->rollBack(); 109 | $message = sprintf( 110 | 'Error occurred while creating tables or indexes...
Query was: %s
', 111 | var_export( $query, true ) 112 | ); 113 | $message .= sprintf( 'Error message is: %s', $err_data[2] ); 114 | wp_die( $message, 'Database Error!' ); 115 | } 116 | } 117 | } 118 | } 119 | 120 | $pdo = null; 121 | 122 | return true; 123 | } 124 | 125 | if ( ! function_exists( 'wp_install' ) ) { 126 | /** 127 | * Installs the site. 128 | * 129 | * Runs the required functions to set up and populate the database, 130 | * including primary admin user and initial options. 131 | * 132 | * @since 1.0.0 133 | * 134 | * @param string $blog_title Site title. 135 | * @param string $user_name User's username. 136 | * @param string $user_email User's email. 137 | * @param bool $is_public Whether the site is public. 138 | * @param string $deprecated Optional. Not used. 139 | * @param string $user_password Optional. User's chosen password. Default empty (random password). 140 | * @param string $language Optional. Language chosen. Default empty. 141 | * @return array { 142 | * Data for the newly installed site. 143 | * 144 | * @type string $url The URL of the site. 145 | * @type int $user_id The ID of the site owner. 146 | * @type string $password The password of the site owner, if their user account didn't already exist. 147 | * @type string $password_message The explanatory message regarding the password. 148 | * } 149 | */ 150 | function wp_install( $blog_title, $user_name, $user_email, $is_public, $deprecated = '', $user_password = '', $language = '' ) { 151 | if ( ! empty( $deprecated ) ) { 152 | _deprecated_argument( __FUNCTION__, '2.6.0' ); 153 | } 154 | 155 | wp_check_mysql_version(); 156 | wp_cache_flush(); 157 | /* SQLite changes: Replace the call to make_db_current_silent() with sqlite_make_db_sqlite(). */ 158 | sqlite_make_db_sqlite(); // phpcs:ignore PHPCompatibility.Extensions.RemovedExtensions.sqliteRemoved 159 | populate_options(); 160 | populate_roles(); 161 | 162 | update_option( 'blogname', $blog_title ); 163 | update_option( 'admin_email', $user_email ); 164 | update_option( 'blog_public', $is_public ); 165 | 166 | // Freshness of site - in the future, this could get more specific about actions taken, perhaps. 167 | update_option( 'fresh_site', 1 ); 168 | 169 | if ( $language ) { 170 | update_option( 'WPLANG', $language ); 171 | } 172 | 173 | $guessurl = wp_guess_url(); 174 | 175 | update_option( 'siteurl', $guessurl ); 176 | 177 | // If not a public site, don't ping. 178 | if ( ! $is_public ) { 179 | update_option( 'default_pingback_flag', 0 ); 180 | } 181 | 182 | /* 183 | * Create default user. If the user already exists, the user tables are 184 | * being shared among sites. Just set the role in that case. 185 | */ 186 | $user_id = username_exists( $user_name ); 187 | $user_password = trim( $user_password ); 188 | $email_password = false; 189 | $user_created = false; 190 | 191 | if ( ! $user_id && empty( $user_password ) ) { 192 | $user_password = wp_generate_password( 12, false ); 193 | $message = __( 'Note that password carefully! It is a random password that was generated just for you.', 'sqlite-database-integration' ); 194 | $user_id = wp_create_user( $user_name, $user_password, $user_email ); 195 | update_user_meta( $user_id, 'default_password_nag', true ); 196 | $email_password = true; 197 | $user_created = true; 198 | } elseif ( ! $user_id ) { 199 | // Password has been provided. 200 | $message = '' . __( 'Your chosen password.', 'sqlite-database-integration' ) . ''; 201 | $user_id = wp_create_user( $user_name, $user_password, $user_email ); 202 | $user_created = true; 203 | } else { 204 | $message = __( 'User already exists. Password inherited.', 'sqlite-database-integration' ); 205 | } 206 | 207 | $user = new WP_User( $user_id ); 208 | $user->set_role( 'administrator' ); 209 | 210 | if ( $user_created ) { 211 | $user->user_url = $guessurl; 212 | wp_update_user( $user ); 213 | } 214 | 215 | wp_install_defaults( $user_id ); 216 | 217 | wp_install_maybe_enable_pretty_permalinks(); 218 | 219 | flush_rewrite_rules(); 220 | 221 | wp_new_blog_notification( $blog_title, $guessurl, $user_id, ( $email_password ? $user_password : __( 'The password you chose during installation.', 'sqlite-database-integration' ) ) ); 222 | 223 | wp_cache_flush(); 224 | 225 | /** 226 | * Fires after a site is fully installed. 227 | * 228 | * @since 3.9.0 229 | * 230 | * @param WP_User $user The site owner. 231 | */ 232 | do_action( 'wp_install', $user ); 233 | 234 | return array( 235 | 'url' => $guessurl, 236 | 'user_id' => $user_id, 237 | 'password' => $user_password, 238 | 'password_message' => $message, 239 | ); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /wp-includes/sqlite/class-wp-sqlite-query-rewriter.php: -------------------------------------------------------------------------------- 1 | input_tokens = $input_tokens; 76 | $this->max = count( $input_tokens ); 77 | } 78 | 79 | /** 80 | * Returns the updated query. 81 | * 82 | * @return string 83 | */ 84 | public function get_updated_query() { 85 | $query = ''; 86 | foreach ( $this->output_tokens as $token ) { 87 | $query .= $token->token; 88 | } 89 | return $query; 90 | } 91 | 92 | /** 93 | * Add a token to the output. 94 | * 95 | * @param WP_SQLite_Token $token Token object. 96 | */ 97 | public function add( $token ) { 98 | if ( $token ) { 99 | $this->output_tokens[] = $token; 100 | } 101 | } 102 | 103 | /** 104 | * Add multiple tokens to the output. 105 | * 106 | * @param WP_SQLite_Token[] $tokens Array of token objects. 107 | */ 108 | public function add_many( $tokens ) { 109 | $this->output_tokens = array_merge( $this->output_tokens, $tokens ); 110 | } 111 | 112 | /** 113 | * Replaces all tokens. 114 | * 115 | * @param WP_SQLite_Token[] $tokens Array of token objects. 116 | */ 117 | public function replace_all( $tokens ) { 118 | $this->output_tokens = $tokens; 119 | } 120 | 121 | /** 122 | * Peek at the next tokens and return one that matches the given criteria. 123 | * 124 | * @param array $query Optional. Search query. 125 | * [ 126 | * 'type' => string|null, // Token type. 127 | * 'flags' => int|null, // Token flags. 128 | * 'values' => string|null, // Token values. 129 | * ]. 130 | * 131 | * @return WP_SQLite_Token 132 | */ 133 | public function peek( $query = array() ) { 134 | $type = isset( $query['type'] ) ? $query['type'] : null; 135 | $flags = isset( $query['flags'] ) ? $query['flags'] : null; 136 | $values = isset( $query['value'] ) 137 | ? ( is_array( $query['value'] ) ? $query['value'] : array( $query['value'] ) ) 138 | : null; 139 | 140 | $i = $this->index; 141 | while ( ++$i < $this->max ) { 142 | if ( $this->input_tokens[ $i ]->matches( $type, $flags, $values ) ) { 143 | return $this->input_tokens[ $i ]; 144 | } 145 | } 146 | } 147 | 148 | /** 149 | * Move forward and return the next tokens that match the given criteria. 150 | * 151 | * @param int $nth The nth token to return. 152 | * 153 | * @return WP_SQLite_Token 154 | */ 155 | public function peek_nth( $nth ) { 156 | $found = 0; 157 | for ( $i = $this->index + 1;$i < $this->max;$i++ ) { 158 | $token = $this->input_tokens[ $i ]; 159 | if ( ! $token->is_semantically_void() ) { 160 | ++$found; 161 | } 162 | if ( $found === $nth ) { 163 | return $this->input_tokens[ $i ]; 164 | } 165 | } 166 | } 167 | 168 | /** 169 | * Consume all the tokens. 170 | * 171 | * @param array $query Search query. 172 | * 173 | * @return void 174 | */ 175 | public function consume_all( $query = array() ) { 176 | while ( $this->consume( $query ) ) { 177 | // Do nothing. 178 | } 179 | } 180 | 181 | /** 182 | * Consume the next tokens and return one that matches the given criteria. 183 | * 184 | * @param array $query Search query. 185 | * [ 186 | * 'type' => null, // Optional. Token type. 187 | * 'flags' => null, // Optional. Token flags. 188 | * 'values' => null, // Optional. Token values. 189 | * ]. 190 | * 191 | * @return WP_SQLite_Token|null 192 | */ 193 | public function consume( $query = array() ) { 194 | $tokens = $this->move_forward( $query ); 195 | $this->output_tokens = array_merge( $this->output_tokens, $tokens ); 196 | return $this->token; 197 | } 198 | 199 | /** 200 | * Drop the last consumed token and return it. 201 | * 202 | * @return WP_SQLite_Token|null 203 | */ 204 | public function drop_last() { 205 | return array_pop( $this->output_tokens ); 206 | } 207 | 208 | /** 209 | * Skip over the next tokens and return one that matches the given criteria. 210 | * 211 | * @param array $query Search query. 212 | * [ 213 | * 'type' => null, // Optional. Token type. 214 | * 'flags' => null, // Optional. Token flags. 215 | * 'values' => null, // Optional. Token values. 216 | * ]. 217 | * 218 | * @return WP_SQLite_Token|null 219 | */ 220 | public function skip( $query = array() ) { 221 | $this->skip_and_return_all( $query ); 222 | return $this->token; 223 | } 224 | 225 | /** 226 | * Skip over the next tokens until one matches the given criteria, 227 | * and return all the skipped tokens. 228 | * 229 | * @param array $query Search query. 230 | * [ 231 | * 'type' => null, // Optional. Token type. 232 | * 'flags' => null, // Optional. Token flags. 233 | * 'values' => null, // Optional. Token values. 234 | * ]. 235 | * 236 | * @return WP_SQLite_Token[] 237 | */ 238 | public function skip_and_return_all( $query = array() ) { 239 | $tokens = $this->move_forward( $query ); 240 | 241 | /* 242 | * When skipping over whitespaces, make sure to consume 243 | * at least one to avoid SQL syntax errors. 244 | */ 245 | foreach ( $tokens as $token ) { 246 | if ( $token->matches( WP_SQLite_Token::TYPE_WHITESPACE ) ) { 247 | $this->add( $token ); 248 | break; 249 | } 250 | } 251 | 252 | return $tokens; 253 | } 254 | 255 | /** 256 | * Returns the next tokens that match the given criteria. 257 | * 258 | * @param array $query Search query. 259 | * [ 260 | * 'type' => string|null, // Optional. Token type. 261 | * 'flags' => int|null, // Optional. Token flags. 262 | * 'values' => string|null, // Optional. Token values. 263 | * ]. 264 | * 265 | * @return array 266 | */ 267 | private function move_forward( $query = array() ) { 268 | $type = isset( $query['type'] ) ? $query['type'] : null; 269 | $flags = isset( $query['flags'] ) ? $query['flags'] : null; 270 | $values = isset( $query['value'] ) 271 | ? ( is_array( $query['value'] ) ? $query['value'] : array( $query['value'] ) ) 272 | : null; 273 | $depth = isset( $query['depth'] ) ? $query['depth'] : null; 274 | 275 | $buffered = array(); 276 | while ( true ) { 277 | if ( ++$this->index >= $this->max ) { 278 | $this->token = null; 279 | $this->call_stack = array(); 280 | break; 281 | } 282 | $this->token = $this->input_tokens[ $this->index ]; 283 | $this->update_call_stack(); 284 | $buffered[] = $this->token; 285 | if ( 286 | ( null === $depth || $this->depth === $depth ) 287 | && $this->token->matches( $type, $flags, $values ) 288 | ) { 289 | break; 290 | } 291 | } 292 | 293 | return $buffered; 294 | } 295 | 296 | /** 297 | * Returns the last call stack element. 298 | * 299 | * @return array|null 300 | */ 301 | public function last_call_stack_element() { 302 | return count( $this->call_stack ) ? $this->call_stack[ count( $this->call_stack ) - 1 ] : null; 303 | } 304 | 305 | /** 306 | * Updates the call stack. 307 | * 308 | * @return void 309 | */ 310 | private function update_call_stack() { 311 | if ( $this->token->flags & WP_SQLite_Token::FLAG_KEYWORD_FUNCTION ) { 312 | $this->last_function_call = $this->token->value; 313 | } 314 | if ( WP_SQLite_Token::TYPE_OPERATOR === $this->token->type ) { 315 | switch ( $this->token->value ) { 316 | case '(': 317 | if ( $this->last_function_call ) { 318 | array_push( 319 | $this->call_stack, 320 | array( 321 | 'function' => $this->last_function_call, 322 | 'depth' => $this->depth, 323 | ) 324 | ); 325 | $this->last_function_call = null; 326 | } 327 | ++$this->depth; 328 | break; 329 | 330 | case ')': 331 | --$this->depth; 332 | $call_parent = $this->last_call_stack_element(); 333 | if ( 334 | $call_parent && 335 | $call_parent['depth'] === $this->depth 336 | ) { 337 | array_pop( $this->call_stack ); 338 | } 339 | break; 340 | } 341 | } 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /wp-includes/sqlite/class-wp-sqlite-token.php: -------------------------------------------------------------------------------- 1 | , !==, etc. 35 | * Bitwise operators: &, |, ^, etc. 36 | * Assignment operators: =, +=, -=, etc. 37 | * SQL specific operators: . (e.g. .. WHERE database.table ..), 38 | * * (e.g. SELECT * FROM ..) 39 | */ 40 | const TYPE_OPERATOR = 2; 41 | 42 | /** 43 | * Spaces, tabs, new lines, etc. 44 | */ 45 | const TYPE_WHITESPACE = 3; 46 | 47 | /** 48 | * Any type of legal comment. 49 | * 50 | * Bash (#), C (/* *\/) or SQL (--) comments: 51 | * 52 | * -- SQL-comment 53 | * 54 | * #Bash-like comment 55 | * 56 | * /*C-like comment*\/ 57 | * 58 | * or: 59 | * 60 | * /*C-like 61 | * comment*\/ 62 | * 63 | * Backslashes were added to respect PHP's comments syntax. 64 | */ 65 | const TYPE_COMMENT = 4; 66 | 67 | /** 68 | * Boolean values: true or false. 69 | */ 70 | const TYPE_BOOL = 5; 71 | 72 | /** 73 | * Numbers: 4, 0x8, 15.16, 23e42, etc. 74 | */ 75 | const TYPE_NUMBER = 6; 76 | 77 | /** 78 | * Literal strings: 'string', "test". 79 | * Some of these strings are actually symbols. 80 | */ 81 | const TYPE_STRING = 7; 82 | 83 | /** 84 | * Database, table names, variables, etc. 85 | * For example: ```SELECT `foo`, `bar` FROM `database`.`table`;```. 86 | */ 87 | const TYPE_SYMBOL = 8; 88 | 89 | /** 90 | * Delimits an unknown string. 91 | * For example: ```SELECT * FROM test;```, `test` is a delimiter. 92 | */ 93 | const TYPE_DELIMITER = 9; 94 | 95 | /** 96 | * Labels in LOOP statement, ITERATE statement etc. 97 | * For example (only for begin label): 98 | * begin_label: BEGIN [statement_list] END [end_label] 99 | * begin_label: LOOP [statement_list] END LOOP [end_label] 100 | * begin_label: REPEAT [statement_list] ... END REPEAT [end_label] 101 | * begin_label: WHILE ... DO [statement_list] END WHILE [end_label]. 102 | */ 103 | const TYPE_LABEL = 10; 104 | 105 | // Flags that describe the tokens in more detail. 106 | // All keywords must have flag 1 so `Context::isKeyword` method doesn't 107 | // require strict comparison. 108 | const FLAG_KEYWORD_RESERVED = 2; 109 | const FLAG_KEYWORD_COMPOSED = 4; 110 | const FLAG_KEYWORD_DATA_TYPE = 8; 111 | const FLAG_KEYWORD_KEY = 16; 112 | const FLAG_KEYWORD_FUNCTION = 32; 113 | 114 | // Numbers related flags. 115 | const FLAG_NUMBER_HEX = 1; 116 | const FLAG_NUMBER_FLOAT = 2; 117 | const FLAG_NUMBER_APPROXIMATE = 4; 118 | const FLAG_NUMBER_NEGATIVE = 8; 119 | const FLAG_NUMBER_BINARY = 16; 120 | 121 | // Strings related flags. 122 | const FLAG_STRING_SINGLE_QUOTES = 1; 123 | const FLAG_STRING_DOUBLE_QUOTES = 2; 124 | 125 | // Comments related flags. 126 | const FLAG_COMMENT_BASH = 1; 127 | const FLAG_COMMENT_C = 2; 128 | const FLAG_COMMENT_SQL = 4; 129 | const FLAG_COMMENT_MYSQL_CMD = 8; 130 | 131 | // Operators related flags. 132 | const FLAG_OPERATOR_ARITHMETIC = 1; 133 | const FLAG_OPERATOR_LOGICAL = 2; 134 | const FLAG_OPERATOR_BITWISE = 4; 135 | const FLAG_OPERATOR_ASSIGNMENT = 8; 136 | const FLAG_OPERATOR_SQL = 16; 137 | 138 | // Symbols related flags. 139 | const FLAG_SYMBOL_VARIABLE = 1; 140 | const FLAG_SYMBOL_BACKTICK = 2; 141 | const FLAG_SYMBOL_USER = 4; 142 | const FLAG_SYMBOL_SYSTEM = 8; 143 | const FLAG_SYMBOL_PARAMETER = 16; 144 | 145 | /** 146 | * The token it its raw string representation. 147 | * 148 | * @var string 149 | */ 150 | public $token; 151 | 152 | /** 153 | * The value this token contains (i.e. token after some evaluation). 154 | * 155 | * @var mixed 156 | */ 157 | public $value; 158 | 159 | /** 160 | * The keyword value this token contains, always uppercase. 161 | * 162 | * @var mixed|string|null 163 | */ 164 | public $keyword = null; 165 | 166 | /** 167 | * The type of this token. 168 | * 169 | * @var int 170 | */ 171 | public $type; 172 | 173 | /** 174 | * The flags of this token. 175 | * 176 | * @var int 177 | */ 178 | public $flags; 179 | 180 | /** 181 | * The position in the initial string where this token started. 182 | * 183 | * The position is counted in chars, not bytes, so you should 184 | * use mb_* functions to properly handle utf-8 multibyte chars. 185 | * 186 | * @var int|null 187 | */ 188 | public $position; 189 | 190 | /** 191 | * Constructor. 192 | * 193 | * @param string $token The value of the token. 194 | * @param int $type The type of the token. 195 | * @param int $flags The flags of the token. 196 | */ 197 | public function __construct( $token, $type = 0, $flags = 0 ) { 198 | $this->token = $token; 199 | $this->type = $type; 200 | $this->flags = $flags; 201 | $this->value = $this->extract(); 202 | } 203 | 204 | /** 205 | * Check if the token matches the given parameters. 206 | * 207 | * @param int|null $type The type of the token. 208 | * @param int|null $flags The flags of the token. 209 | * @param array|null $values The values of the token. 210 | * 211 | * @return bool 212 | */ 213 | public function matches( $type = null, $flags = null, $values = null ) { 214 | if ( null === $type && null === $flags && ( null === $values || array() === $values ) ) { 215 | return ! $this->is_semantically_void(); 216 | } 217 | 218 | return ( 219 | ( null === $type || $this->type === $type ) 220 | && ( null === $flags || ( $this->flags & $flags ) ) 221 | && ( null === $values || in_array( strtoupper( $this->value ?? '' ), $values, true ) ) 222 | ); 223 | } 224 | 225 | /** 226 | * Check if the token is semantically void (i.e. whitespace or comment). 227 | * 228 | * @return bool 229 | */ 230 | public function is_semantically_void() { 231 | return $this->matches( self::TYPE_WHITESPACE ) || $this->matches( self::TYPE_COMMENT ); 232 | } 233 | 234 | /** 235 | * Does little processing to the token to extract a value. 236 | * 237 | * If no processing can be done it will return the initial string. 238 | * 239 | * @return mixed 240 | */ 241 | private function extract() { 242 | switch ( $this->type ) { 243 | case self::TYPE_KEYWORD: 244 | $this->keyword = strtoupper( $this->token ?? '' ); 245 | if ( ! ( $this->flags & self::FLAG_KEYWORD_RESERVED ) ) { 246 | /* 247 | * Unreserved keywords should stay the way they are 248 | * because they might represent field names. 249 | */ 250 | return $this->token; 251 | } 252 | 253 | return $this->keyword; 254 | 255 | case self::TYPE_WHITESPACE: 256 | return ' '; 257 | 258 | case self::TYPE_BOOL: 259 | return strtoupper( $this->token ?? '' ) === 'TRUE'; 260 | 261 | case self::TYPE_NUMBER: 262 | $ret = str_replace( '--', '', $this->token ); // e.g. ---42 === -42. 263 | if ( $this->flags & self::FLAG_NUMBER_HEX ) { 264 | $ret = str_replace( array( '-', '+' ), '', $this->token ); 265 | if ( $this->flags & self::FLAG_NUMBER_NEGATIVE ) { 266 | $ret = -hexdec( $ret ); 267 | } else { 268 | $ret = hexdec( $ret ); 269 | } 270 | } elseif ( ( $this->flags & self::FLAG_NUMBER_APPROXIMATE ) || ( $this->flags & self::FLAG_NUMBER_FLOAT ) ) { 271 | $ret = (float) $ret; 272 | } elseif ( ! ( $this->flags & self::FLAG_NUMBER_BINARY ) ) { 273 | $ret = (int) $ret; 274 | } 275 | 276 | return $ret; 277 | 278 | case self::TYPE_STRING: 279 | // Trims quotes. 280 | $str = $this->token; 281 | $str = mb_substr( $str, 1, -1, 'UTF-8' ); 282 | 283 | // Removes surrounding quotes. 284 | $quote = $this->token[0]; 285 | $str = str_replace( $quote . $quote, $quote, $str ); 286 | 287 | /* 288 | * Finally unescapes the string. 289 | * 290 | * `stripcslashes` replaces escape sequences with their 291 | * representation. 292 | */ 293 | $str = stripcslashes( $str ); 294 | 295 | return $str; 296 | 297 | case self::TYPE_SYMBOL: 298 | $str = $this->token; 299 | if ( isset( $str[0] ) && ( '@' === $str[0] ) ) { 300 | /* 301 | * `mb_strlen($str)` must be used instead of `null` because 302 | * in PHP 5.3- the `null` parameter isn't handled correctly. 303 | */ 304 | $str = mb_substr( 305 | $str, 306 | ! empty( $str[1] ) && ( '@' === $str[1] ) ? 2 : 1, 307 | mb_strlen( $str ), 308 | 'UTF-8' 309 | ); 310 | } 311 | 312 | if ( isset( $str[0] ) && ( ':' === $str[0] ) ) { 313 | $str = mb_substr( $str, 1, mb_strlen( $str ), 'UTF-8' ); 314 | } 315 | 316 | if ( isset( $str[0] ) && ( ( '`' === $str[0] ) || ( '"' === $str[0] ) || ( '\'' === $str[0] ) ) ) { 317 | $quote = $str[0]; 318 | $str = str_replace( $quote . $quote, $quote, $str ); 319 | $str = mb_substr( $str, 1, -1, 'UTF-8' ); 320 | } 321 | 322 | return $str; 323 | } 324 | 325 | return $this->token; 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /wp-includes/sqlite-ast/class-wp-sqlite-configurator.php: -------------------------------------------------------------------------------- 1 | driver = $driver; 46 | $this->schema_builder = $schema_builder; 47 | $this->schema_reconstructor = new WP_SQLite_Information_Schema_Reconstructor( 48 | $driver, 49 | $schema_builder 50 | ); 51 | } 52 | 53 | /** 54 | * Ensure that the SQLite database is configured. 55 | * 56 | * This method checks if the database is configured for the latest SQLite 57 | * driver version, and if it is not, it will configure the database. 58 | */ 59 | public function ensure_database_configured(): void { 60 | $version = SQLITE_DRIVER_VERSION; 61 | $db_version = $this->driver->get_saved_driver_version(); 62 | if ( version_compare( $version, $db_version ) > 0 ) { 63 | $this->configure_database(); 64 | } 65 | } 66 | 67 | /** 68 | * Configure the SQLite database. 69 | * 70 | * This method creates tables used for emulating MySQL behaviors in SQLite, 71 | * and populates them with necessary data. When it is used with an already 72 | * configured database, it will update the configuration as per the current 73 | * SQLite driver version and attempt to repair any configuration corruption. 74 | */ 75 | public function configure_database(): void { 76 | // Use an EXCLUSIVE transaction to prevent multiple connections 77 | // from attempting to configure the database at the same time. 78 | $this->driver->execute_sqlite_query( 'BEGIN EXCLUSIVE TRANSACTION' ); 79 | try { 80 | $this->ensure_global_variables_table(); 81 | $this->schema_builder->ensure_information_schema_tables(); 82 | $this->schema_reconstructor->ensure_correct_information_schema(); 83 | $this->save_current_driver_version(); 84 | $this->ensure_database_data(); 85 | } catch ( Throwable $e ) { 86 | $this->driver->execute_sqlite_query( 'ROLLBACK' ); 87 | throw $e; 88 | } 89 | $this->driver->execute_sqlite_query( 'COMMIT' ); 90 | } 91 | 92 | /** 93 | * Ensure that the global variables table exists. 94 | * 95 | * This method configures a database table to store MySQL global variables 96 | * and other internal configuration values. 97 | */ 98 | private function ensure_global_variables_table(): void { 99 | $this->driver->execute_sqlite_query( 100 | sprintf( 101 | 'CREATE TABLE IF NOT EXISTS %s (name TEXT PRIMARY KEY, value TEXT)', 102 | $this->driver->get_connection()->quote_identifier( 103 | WP_SQLite_Driver::GLOBAL_VARIABLES_TABLE_NAME 104 | ) 105 | ) 106 | ); 107 | } 108 | 109 | /** 110 | * Ensure that the database data is correctly populated. 111 | * 112 | * This method ensures that the "INFORMATION_SCHEMA.SCHEMATA" table contains 113 | * records for both the "INFORMATION_SCHEMA" database and the user database. 114 | * At the moment, only a single user database is supported. 115 | * 116 | * Additionally, this method ensures that the user database name is stored 117 | * correctly in all the information schema tables. 118 | */ 119 | public function ensure_database_data(): void { 120 | // Get all databases from the "SCHEMATA" table. 121 | $schemata_table = $this->schema_builder->get_table_name( false, 'schemata' ); 122 | $databases = $this->driver->execute_sqlite_query( 123 | sprintf( 124 | 'SELECT SCHEMA_NAME FROM %s', 125 | $this->driver->get_connection()->quote_identifier( $schemata_table ) 126 | ) 127 | )->fetchAll( PDO::FETCH_COLUMN ); // phpcs:disable WordPress.DB.RestrictedClasses.mysql__PDO 128 | 129 | // Ensure that the "INFORMATION_SCHEMA" database record exists. 130 | if ( ! in_array( 'information_schema', $databases, true ) ) { 131 | $this->driver->execute_sqlite_query( 132 | sprintf( 133 | 'INSERT INTO %s (SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME) VALUES (?, ?, ?)', 134 | $this->driver->get_connection()->quote_identifier( $schemata_table ) 135 | ), 136 | // The "INFORMATION_SCHEMA" database stays on "utf8mb3" even in MySQL 8 and 9. 137 | array( 'information_schema', 'utf8mb3', 'utf8mb3_general_ci' ) 138 | ); 139 | } 140 | 141 | // Get the existing user database name. 142 | $existing_user_db_name = null; 143 | foreach ( $databases as $database ) { 144 | if ( 'information_schema' !== strtolower( $database ) ) { 145 | $existing_user_db_name = $database; 146 | break; 147 | } 148 | } 149 | 150 | // Ensure that the user database record exists. 151 | if ( null === $existing_user_db_name ) { 152 | $existing_user_db_name = WP_SQLite_Information_Schema_Builder::SAVED_DATABASE_NAME; 153 | $this->driver->execute_sqlite_query( 154 | sprintf( 155 | 'INSERT INTO %s (SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME) VALUES (?, ?, ?)', 156 | $this->driver->get_connection()->quote_identifier( $schemata_table ) 157 | ), 158 | // @TODO: This should probably be version-dependent. 159 | // Before MySQL 8, the default was different. 160 | array( $existing_user_db_name, 'utf8mb4', 'utf8mb4_0900_ai_ci' ) 161 | ); 162 | } 163 | 164 | // Migrate from older versions without dynamic database names. 165 | $saved_database_name = WP_SQLite_Information_Schema_Builder::SAVED_DATABASE_NAME; 166 | if ( $saved_database_name !== $existing_user_db_name ) { 167 | // INFORMATION_SCHEMA.SCHEMATA 168 | $this->driver->execute_sqlite_query( 169 | sprintf( 170 | "UPDATE %s SET SCHEMA_NAME = ? WHERE SCHEMA_NAME != 'information_schema'", 171 | $this->driver->get_connection()->quote_identifier( $schemata_table ) 172 | ), 173 | array( $saved_database_name ) 174 | ); 175 | 176 | // INFORMATION_SCHEMA.TABLES 177 | $tables_table = $this->schema_builder->get_table_name( false, 'tables' ); 178 | $this->driver->execute_sqlite_query( 179 | sprintf( 180 | "UPDATE %s SET TABLE_SCHEMA = ? WHERE TABLE_SCHEMA != 'information_schema'", 181 | $this->driver->get_connection()->quote_identifier( $tables_table ) 182 | ), 183 | array( $saved_database_name ) 184 | ); 185 | 186 | // INFORMATION_SCHEMA.COLUMNS 187 | $columns_table = $this->schema_builder->get_table_name( false, 'columns' ); 188 | $this->driver->execute_sqlite_query( 189 | sprintf( 190 | "UPDATE %s SET TABLE_SCHEMA = ? WHERE TABLE_SCHEMA != 'information_schema'", 191 | $this->driver->get_connection()->quote_identifier( $columns_table ) 192 | ), 193 | array( $saved_database_name ) 194 | ); 195 | 196 | // INFORMATION_SCHEMA.STATISTICS 197 | $statistics_table = $this->schema_builder->get_table_name( false, 'statistics' ); 198 | $this->driver->execute_sqlite_query( 199 | sprintf( 200 | "UPDATE %s SET TABLE_SCHEMA = ?, INDEX_SCHEMA = ? WHERE TABLE_SCHEMA != 'information_schema'", 201 | $this->driver->get_connection()->quote_identifier( $statistics_table ) 202 | ), 203 | array( $saved_database_name, $saved_database_name ) 204 | ); 205 | 206 | // INFORMATION_SCHEMA.TABLE_CONSTRAINTS 207 | $table_constraints_table = $this->schema_builder->get_table_name( false, 'table_constraints' ); 208 | $this->driver->execute_sqlite_query( 209 | sprintf( 210 | "UPDATE %s SET TABLE_SCHEMA = ?, CONSTRAINT_SCHEMA = ? WHERE TABLE_SCHEMA != 'information_schema'", 211 | $this->driver->get_connection()->quote_identifier( $table_constraints_table ) 212 | ), 213 | array( $saved_database_name, $saved_database_name ) 214 | ); 215 | 216 | // INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS 217 | $referential_constraints_table = $this->schema_builder->get_table_name( false, 'referential_constraints' ); 218 | $this->driver->execute_sqlite_query( 219 | sprintf( 220 | "UPDATE %s SET CONSTRAINT_SCHEMA = ?, UNIQUE_CONSTRAINT_SCHEMA = ? WHERE CONSTRAINT_SCHEMA != 'information_schema'", 221 | $this->driver->get_connection()->quote_identifier( $referential_constraints_table ) 222 | ), 223 | array( $saved_database_name, $saved_database_name ) 224 | ); 225 | 226 | // INFORMATION_SCHEMA.KEY_COLUMN_USAGE 227 | $key_column_usage_table = $this->schema_builder->get_table_name( false, 'key_column_usage' ); 228 | $this->driver->execute_sqlite_query( 229 | sprintf( 230 | "UPDATE %s 231 | SET 232 | TABLE_SCHEMA = ?, 233 | CONSTRAINT_SCHEMA = ?, 234 | REFERENCED_TABLE_SCHEMA = IIF(REFERENCED_TABLE_SCHEMA IS NULL, NULL, ?) 235 | WHERE TABLE_SCHEMA != 'information_schema'", 236 | $this->driver->get_connection()->quote_identifier( $key_column_usage_table ) 237 | ), 238 | array( $saved_database_name, $saved_database_name, $saved_database_name ) 239 | ); 240 | 241 | // INFORMATION_SCHEMA.CHECK_CONSTRAINTS 242 | $check_constraints_table = $this->schema_builder->get_table_name( false, 'check_constraints' ); 243 | $this->driver->execute_sqlite_query( 244 | sprintf( 245 | "UPDATE %s SET CONSTRAINT_SCHEMA = ? WHERE CONSTRAINT_SCHEMA != 'information_schema'", 246 | $this->driver->get_connection()->quote_identifier( $check_constraints_table ) 247 | ), 248 | array( $saved_database_name ) 249 | ); 250 | } 251 | } 252 | 253 | /** 254 | * Save the current SQLite driver version. 255 | * 256 | * This method saves the current SQLite driver version to the database. 257 | */ 258 | private function save_current_driver_version(): void { 259 | $this->driver->execute_sqlite_query( 260 | sprintf( 261 | 'INSERT INTO %s (name, value) VALUES (?, ?) ON CONFLICT(name) DO UPDATE SET value = ?', 262 | $this->driver->get_connection()->quote_identifier( 263 | WP_SQLite_Driver::GLOBAL_VARIABLES_TABLE_NAME 264 | ) 265 | ), 266 | array( 267 | WP_SQLite_Driver::DRIVER_VERSION_VARIABLE_NAME, 268 | SQLITE_DRIVER_VERSION, 269 | SQLITE_DRIVER_VERSION, 270 | ) 271 | ); 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /wp-includes/parser/class-wp-parser-node.php: -------------------------------------------------------------------------------- 1 | rule_id = $rule_id; 22 | $this->rule_name = $rule_name; 23 | } 24 | 25 | public function append_child( $node ) { 26 | $this->children[] = $node; 27 | } 28 | 29 | /** 30 | * Flatten the matched rule fragments as if their children were direct 31 | * descendants of the current rule. 32 | * 33 | * What are rule fragments? 34 | * 35 | * When we initially parse the grammar file, it has compound rules such 36 | * as this one: 37 | * 38 | * query ::= EOF | ((simpleStatement | beginWork) ((SEMICOLON_SYMBOL EOF?) | EOF)) 39 | * 40 | * Building a parser that can understand such rules is way more complex than building 41 | * a parser that only follows simple rules, so we flatten those compound rules into 42 | * simpler ones. The above rule would be flattened to: 43 | * 44 | * query ::= EOF | %query0 45 | * %query0 ::= %%query01 %%query02 46 | * %%query01 ::= simpleStatement | beginWork 47 | * %%query02 ::= SEMICOLON_SYMBOL EOF_zero_or_one | EOF 48 | * EOF_zero_or_one ::= EOF | ε 49 | * 50 | * This factorization happens in "convert-grammar.php". 51 | * 52 | * "Fragments" are intermediate artifacts whose names are not in the original grammar. 53 | * They are extremely useful for the parser, but the API consumer should never have to 54 | * worry about them. Fragment names start with a percent sign ("%"). 55 | * 56 | * The code below inlines every fragment back in its parent rule. 57 | * 58 | * We could optimize this. The current $match may be discarded later on so any inlining 59 | * effort here would be wasted. However, inlining seems cheap and doing it bottom-up here 60 | * is **much** easier than reprocessing the parse tree top-down later on. 61 | * 62 | * The following parse tree: 63 | * 64 | * [ 65 | * 'query' => [ 66 | * [ 67 | * '%query01' => [ 68 | * [ 69 | * 'simpleStatement' => [ 70 | * MySQLToken(MySQLLexer::WITH_SYMBOL, 'WITH') 71 | * ], 72 | * '%query02' => [ 73 | * [ 74 | * 'simpleStatement' => [ 75 | * MySQLToken(MySQLLexer::WITH_SYMBOL, 'WITH') 76 | * ] 77 | * ], 78 | * ] 79 | * ] 80 | * ] 81 | * ] 82 | * ] 83 | * 84 | * Would be inlined as: 85 | * 86 | * [ 87 | * 'query' => [ 88 | * [ 89 | * 'simpleStatement' => [ 90 | * MySQLToken(MySQLLexer::WITH_SYMBOL, 'WITH') 91 | * ] 92 | * ], 93 | * [ 94 | * 'simpleStatement' => [ 95 | * MySQLToken(MySQLLexer::WITH_SYMBOL, 'WITH') 96 | * ] 97 | * ] 98 | * ] 99 | * ] 100 | */ 101 | public function merge_fragment( $node ) { 102 | $this->children = array_merge( $this->children, $node->children ); 103 | } 104 | 105 | /** 106 | * Check if this node has any child nodes or tokens. 107 | * 108 | * @return bool True if this node has any child nodes or tokens, false otherwise. 109 | */ 110 | public function has_child(): bool { 111 | return count( $this->children ) > 0; 112 | } 113 | 114 | /** 115 | * Check if this node has any child nodes. 116 | * 117 | * @param string|null $rule_name Optional. A node rule name to check for. 118 | * @return bool True if any child nodes are found, false otherwise. 119 | */ 120 | public function has_child_node( ?string $rule_name = null ): bool { 121 | foreach ( $this->children as $child ) { 122 | if ( 123 | $child instanceof WP_Parser_Node 124 | && ( null === $rule_name || $child->rule_name === $rule_name ) 125 | ) { 126 | return true; 127 | } 128 | } 129 | return false; 130 | } 131 | 132 | /** 133 | * Check if this node has any child tokens. 134 | * 135 | * @param int|null $token_id Optional. A token ID to check for. 136 | * @return bool True if any child tokens are found, false otherwise. 137 | */ 138 | public function has_child_token( ?int $token_id = null ): bool { 139 | foreach ( $this->children as $child ) { 140 | if ( 141 | $child instanceof WP_Parser_Token 142 | && ( null === $token_id || $child->id === $token_id ) 143 | ) { 144 | return true; 145 | } 146 | } 147 | return false; 148 | } 149 | 150 | /** 151 | * Get the first child node or token of this node. 152 | * 153 | * @return WP_Parser_Node|WP_Parser_Token|null The first child node or token; 154 | * null when no children are found. 155 | */ 156 | public function get_first_child() { 157 | return $this->children[0] ?? null; 158 | } 159 | 160 | /** 161 | * Get the first child node of this node. 162 | * 163 | * @param string|null $rule_name Optional. A node rule name to check for. 164 | * @return WP_Parser_Node|null The first matching child node; null when no children are found. 165 | */ 166 | public function get_first_child_node( ?string $rule_name = null ): ?WP_Parser_Node { 167 | foreach ( $this->children as $child ) { 168 | if ( 169 | $child instanceof WP_Parser_Node 170 | && ( null === $rule_name || $child->rule_name === $rule_name ) 171 | ) { 172 | return $child; 173 | } 174 | } 175 | return null; 176 | } 177 | 178 | /** 179 | * Get the first child token of this node. 180 | * 181 | * @param int|null $token_id Optional. A token ID to check for. 182 | * @return WP_Parser_Token|null The first matching child token; null when no children are found. 183 | */ 184 | public function get_first_child_token( ?int $token_id = null ): ?WP_Parser_Token { 185 | foreach ( $this->children as $child ) { 186 | if ( 187 | $child instanceof WP_Parser_Token 188 | && ( null === $token_id || $child->id === $token_id ) 189 | ) { 190 | return $child; 191 | } 192 | } 193 | return null; 194 | } 195 | 196 | /** 197 | * Get the first descendant node of this node. 198 | * 199 | * The node children are traversed recursively in a depth-first order until 200 | * a matching descendant node is found, or the entire subtree is searched. 201 | * 202 | * @param string|null $rule_name Optional. A node rule name to check for. 203 | * @return WP_Parser_Node|null The first matching descendant node; null when no descendants are found. 204 | */ 205 | public function get_first_descendant_node( ?string $rule_name = null ): ?WP_Parser_Node { 206 | for ( $i = 0; $i < count( $this->children ); $i++ ) { 207 | $child = $this->children[ $i ]; 208 | if ( ! $child instanceof WP_Parser_Node ) { 209 | continue; 210 | } 211 | if ( null === $rule_name || $child->rule_name === $rule_name ) { 212 | return $child; 213 | } 214 | $node = $child->get_first_descendant_node( $rule_name ); 215 | if ( $node ) { 216 | return $node; 217 | } 218 | } 219 | return null; 220 | } 221 | 222 | /** 223 | * Get the first descendant token of this node. 224 | * 225 | * The node children are traversed recursively in a depth-first order until 226 | * a matching descendant token is found, or the entire subtree is searched. 227 | * 228 | * @param int|null $token_id Optional. A token ID to check for. 229 | * @return WP_Parser_Token|null The first matching descendant token; null when no descendants are found. 230 | */ 231 | public function get_first_descendant_token( ?int $token_id = null ): ?WP_Parser_Token { 232 | for ( $i = 0; $i < count( $this->children ); $i++ ) { 233 | $child = $this->children[ $i ]; 234 | if ( $child instanceof WP_Parser_Token ) { 235 | if ( null === $token_id || $child->id === $token_id ) { 236 | return $child; 237 | } 238 | } else { 239 | $token = $child->get_first_descendant_token( $token_id ); 240 | if ( $token ) { 241 | return $token; 242 | } 243 | } 244 | } 245 | return null; 246 | } 247 | 248 | /** 249 | * Get all children of this node. 250 | * 251 | * @return array An array of all child nodes and tokens of this node. 252 | */ 253 | public function get_children(): array { 254 | return $this->children; 255 | } 256 | 257 | /** 258 | * Get all child nodes of this node. 259 | * 260 | * @param string|null $rule_name Optional. A node rule name to check for. 261 | * @return WP_Parser_Node[] An array of all matching child nodes. 262 | */ 263 | public function get_child_nodes( ?string $rule_name = null ): array { 264 | $nodes = array(); 265 | foreach ( $this->children as $child ) { 266 | if ( 267 | $child instanceof WP_Parser_Node 268 | && ( null === $rule_name || $child->rule_name === $rule_name ) 269 | ) { 270 | $nodes[] = $child; 271 | } 272 | } 273 | return $nodes; 274 | } 275 | 276 | /** 277 | * Get all child tokens of this node. 278 | * 279 | * @param int|null $token_id Optional. A token ID to check for. 280 | * @return WP_Parser_Token[] An array of all matching child tokens. 281 | */ 282 | public function get_child_tokens( ?int $token_id = null ): array { 283 | $tokens = array(); 284 | foreach ( $this->children as $child ) { 285 | if ( 286 | $child instanceof WP_Parser_Token 287 | && ( null === $token_id || $child->id === $token_id ) 288 | ) { 289 | $tokens[] = $child; 290 | } 291 | } 292 | return $tokens; 293 | } 294 | 295 | /** 296 | * Get all descendants of this node. 297 | * 298 | * The descendants are collected using a depth-first pre-order NLR traversal. 299 | * This produces a natural ordering that corresponds to the original input. 300 | * 301 | * @return array An array of all descendant nodes and tokens of this node. 302 | */ 303 | public function get_descendants(): array { 304 | $descendants = array(); 305 | foreach ( $this->children as $child ) { 306 | if ( $child instanceof WP_Parser_Node ) { 307 | $descendants[] = $child; 308 | $descendants = array_merge( $descendants, $child->get_descendants() ); 309 | } else { 310 | $descendants[] = $child; 311 | } 312 | } 313 | return $descendants; 314 | } 315 | 316 | /** 317 | * Get all descendant nodes of this node. 318 | * 319 | * The descendants are collected using a depth-first pre-order NLR traversal. 320 | * This produces a natural ordering that corresponds to the original input. 321 | * All matching nodes are collected during the traversal. 322 | * 323 | * @param string|null $rule_name Optional. A node rule name to check for. 324 | * @return WP_Parser_Node[] An array of all matching descendant nodes. 325 | */ 326 | public function get_descendant_nodes( ?string $rule_name = null ): array { 327 | $nodes = array(); 328 | foreach ( $this->children as $child ) { 329 | if ( ! $child instanceof WP_Parser_Node ) { 330 | continue; 331 | } 332 | if ( null === $rule_name || $child->rule_name === $rule_name ) { 333 | $nodes[] = $child; 334 | } 335 | $nodes = array_merge( $nodes, $child->get_descendant_nodes( $rule_name ) ); 336 | } 337 | return $nodes; 338 | } 339 | 340 | /** 341 | * Get all descendant tokens of this node. 342 | * 343 | * The descendants are collected using a depth-first pre-order NLR traversal. 344 | * This produces a natural ordering that corresponds to the original input. 345 | * All matching tokens are collected during the traversal. 346 | * 347 | * @param int|null $token_id Optional. A token ID to check for. 348 | * @return WP_Parser_Token[] An array of all matching descendant tokens. 349 | */ 350 | public function get_descendant_tokens( ?int $token_id = null ): array { 351 | $tokens = array(); 352 | foreach ( $this->children as $child ) { 353 | if ( $child instanceof WP_Parser_Token ) { 354 | if ( null === $token_id || $child->id === $token_id ) { 355 | $tokens[] = $child; 356 | } 357 | } else { 358 | $tokens = array_merge( $tokens, $child->get_descendant_tokens( $token_id ) ); 359 | } 360 | } 361 | return $tokens; 362 | } 363 | 364 | /** 365 | * Get the byte offset in the input string where this node begins. 366 | * 367 | * @return int The byte offset in the input string where this node begins. 368 | */ 369 | public function get_start(): int { 370 | return $this->get_first_descendant_token()->start; 371 | } 372 | 373 | /** 374 | * Get the byte length of this node in the input string. 375 | * 376 | * @return int The byte length of this node in the input string. 377 | */ 378 | public function get_length(): int { 379 | $tokens = $this->get_descendant_tokens(); 380 | $first_token = $tokens[0]; 381 | $last_token = $tokens[ count( $tokens ) - 1 ]; 382 | return $last_token->start + $last_token->length - $first_token->start; 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /wp-includes/sqlite/class-wp-sqlite-db.php: -------------------------------------------------------------------------------- 1 | db_connect()". 46 | * 2. The database connection call initializes the SQLite driver. 47 | * 3. The SQLite driver initializes and runs "WP_SQLite_Configurator". 48 | * 4. The configurator uses "WP_SQLite_Information_Schema_Reconstructor", 49 | * which requires "wp-admin/includes/schema.php" when in WordPress. 50 | * 5. The "wp-admin/includes/schema.php" requires the "$wpdb" global, 51 | * which creates a circular dependency. 52 | */ 53 | $GLOBALS['wpdb'] = $this; 54 | 55 | parent::__construct( '', '', $dbname, '' ); 56 | $this->charset = 'utf8mb4'; 57 | } 58 | 59 | /** 60 | * Method to set character set for the database. 61 | * 62 | * This overrides wpdb::set_charset(), only to dummy out the MySQL function. 63 | * 64 | * @see wpdb::set_charset() 65 | * 66 | * @param resource $dbh The resource given by mysql_connect. 67 | * @param string $charset Optional. The character set. Default null. 68 | * @param string $collate Optional. The collation. Default null. 69 | */ 70 | public function set_charset( $dbh, $charset = null, $collate = null ) { 71 | } 72 | 73 | /** 74 | * Method to get the character set for the database. 75 | * Hardcoded to utf8mb4 for now. 76 | * 77 | * @param string $table The table name. 78 | * @param string $column The column name. 79 | * 80 | * @return string The character set. 81 | */ 82 | public function get_col_charset( $table, $column ) { 83 | // Hardcoded for now. 84 | return 'utf8mb4'; 85 | } 86 | 87 | /** 88 | * Changes the current SQL mode, and ensures its WordPress compatibility. 89 | * 90 | * If no modes are passed, it will ensure the current MySQL server modes are compatible. 91 | * 92 | * This overrides wpdb::set_sql_mode() while closely mirroring its implementation. 93 | * 94 | * @param array $modes Optional. A list of SQL modes to set. Default empty array. 95 | */ 96 | public function set_sql_mode( $modes = array() ) { 97 | if ( ! $this->dbh instanceof WP_SQLite_Driver ) { 98 | return; 99 | } 100 | 101 | if ( empty( $modes ) ) { 102 | $result = $this->dbh->query( 'SELECT @@SESSION.sql_mode' ); 103 | if ( ! isset( $result[0] ) ) { 104 | return; 105 | } 106 | 107 | $modes_str = $result[0]->{'@@SESSION.sql_mode'}; 108 | if ( empty( $modes_str ) ) { 109 | return; 110 | } 111 | $modes = explode( ',', $modes_str ); 112 | } 113 | 114 | $modes = array_change_key_case( $modes, CASE_UPPER ); 115 | 116 | /** 117 | * Filters the list of incompatible SQL modes to exclude. 118 | * 119 | * @since 3.9.0 120 | * 121 | * @param array $incompatible_modes An array of incompatible modes. 122 | */ 123 | $incompatible_modes = (array) apply_filters( 'incompatible_sql_modes', $this->incompatible_modes ); 124 | 125 | foreach ( $modes as $i => $mode ) { 126 | if ( in_array( $mode, $incompatible_modes, true ) ) { 127 | unset( $modes[ $i ] ); 128 | } 129 | } 130 | $modes_str = implode( ',', $modes ); 131 | 132 | $this->dbh->query( "SET SESSION sql_mode='$modes_str'" ); 133 | } 134 | 135 | /** 136 | * Closes the current database connection. 137 | * Noop in SQLite. 138 | * 139 | * @return bool True to indicate the connection was successfully closed. 140 | */ 141 | public function close() { 142 | return true; 143 | } 144 | 145 | /** 146 | * Method to select the database connection. 147 | * 148 | * This overrides wpdb::select(), only to dummy out the MySQL function. 149 | * 150 | * @see wpdb::select() 151 | * 152 | * @param string $db MySQL database name. Not used. 153 | * @param resource|null $dbh Optional link identifier. 154 | */ 155 | public function select( $db, $dbh = null ) { 156 | $this->ready = true; 157 | } 158 | 159 | /** 160 | * Method to escape characters. 161 | * 162 | * This overrides wpdb::_real_escape() to avoid using mysql_real_escape_string(). 163 | * 164 | * @see wpdb::_real_escape() 165 | * 166 | * @param string $data The string to escape. 167 | * 168 | * @return string escaped 169 | */ 170 | public function _real_escape( $data ) { 171 | if ( ! is_scalar( $data ) ) { 172 | return ''; 173 | } 174 | $escaped = addslashes( $data ); 175 | return $this->add_placeholder_escape( $escaped ); 176 | } 177 | 178 | /** 179 | * Method to dummy out wpdb::esc_like() function. 180 | * 181 | * WordPress 4.0.0 introduced esc_like() function that adds backslashes to %, 182 | * underscore and backslash, which is not interpreted as escape character 183 | * by SQLite. So we override it and dummy out this function. 184 | * 185 | * @param string $text The raw text to be escaped. The input typed by the user should have no 186 | * extra or deleted slashes. 187 | * 188 | * @return string Text in the form of a LIKE phrase. The output is not SQL safe. Call $wpdb::prepare() 189 | * or real_escape next. 190 | */ 191 | public function esc_like( $text ) { 192 | // The new driver adds "ESCAPE '\\'" to every LIKE expression by default. 193 | // We only need to overload this function to a no-op for the old driver. 194 | if ( $this->dbh instanceof WP_SQLite_Driver ) { 195 | return parent::esc_like( $text ); 196 | } 197 | return $text; 198 | } 199 | 200 | /** 201 | * Prints SQL/DB error. 202 | * 203 | * This overrides wpdb::print_error() while closely mirroring its implementation. 204 | * 205 | * @global array $EZSQL_ERROR Stores error information of query and error string. 206 | * 207 | * @param string $str The error to display. 208 | * @return void|false Void if the showing of errors is enabled, false if disabled. 209 | */ 210 | public function print_error( $str = '' ) { 211 | global $EZSQL_ERROR; 212 | 213 | if ( ! $str ) { 214 | $str = $this->last_error; 215 | } 216 | 217 | $EZSQL_ERROR[] = array( 218 | 'query' => $this->last_query, 219 | 'error_str' => $str, 220 | ); 221 | 222 | if ( $this->suppress_errors ) { 223 | return false; 224 | } 225 | 226 | $caller = $this->get_caller(); 227 | if ( $caller ) { 228 | // Not translated, as this will only appear in the error log. 229 | $error_str = sprintf( 'WordPress database error %1$s for query %2$s made by %3$s', $str, $this->last_query, $caller ); 230 | } else { 231 | $error_str = sprintf( 'WordPress database error %1$s for query %2$s', $str, $this->last_query ); 232 | } 233 | 234 | error_log( $error_str ); 235 | 236 | // Are we showing errors? 237 | if ( ! $this->show_errors ) { 238 | return false; 239 | } 240 | 241 | wp_load_translations_early(); 242 | 243 | // If there is an error then take note of it. 244 | if ( is_multisite() ) { 245 | $msg = sprintf( 246 | "%s [%s]\n%s\n", 247 | __( 'WordPress database error:' ), 248 | $str, 249 | $this->last_query 250 | ); 251 | 252 | if ( defined( 'ERRORLOGFILE' ) ) { 253 | error_log( $msg, 3, ERRORLOGFILE ); 254 | } 255 | if ( defined( 'DIEONDBERROR' ) ) { 256 | wp_die( $msg ); 257 | } 258 | } else { 259 | $str = htmlspecialchars( $str, ENT_QUOTES ); 260 | $query = htmlspecialchars( $this->last_query, ENT_QUOTES ); 261 | 262 | printf( 263 | '

%s [%s]
%s

', 264 | __( 'WordPress database error:' ), 265 | $str, 266 | $query 267 | ); 268 | } 269 | } 270 | 271 | /** 272 | * Method to flush cached data. 273 | * 274 | * This overrides wpdb::flush(). This is not necessarily overridden, because 275 | * $result will never be resource. 276 | * 277 | * @see wpdb::flush 278 | */ 279 | public function flush() { 280 | $this->last_result = array(); 281 | $this->col_info = null; 282 | $this->last_query = null; 283 | $this->rows_affected = 0; 284 | $this->num_rows = 0; 285 | $this->last_error = ''; 286 | $this->result = null; 287 | } 288 | 289 | /** 290 | * Method to do the database connection. 291 | * 292 | * This overrides wpdb::db_connect() to avoid using MySQL function. 293 | * 294 | * @see wpdb::db_connect() 295 | * 296 | * @param bool $allow_bail Not used. 297 | * @return void 298 | */ 299 | public function db_connect( $allow_bail = true ) { 300 | if ( $this->dbh ) { 301 | return; 302 | } 303 | $this->init_charset(); 304 | 305 | $pdo = null; 306 | if ( isset( $GLOBALS['@pdo'] ) ) { 307 | $pdo = $GLOBALS['@pdo']; 308 | } 309 | if ( defined( 'WP_SQLITE_AST_DRIVER' ) && WP_SQLITE_AST_DRIVER ) { 310 | if ( null === $this->dbname || '' === $this->dbname ) { 311 | $this->bail( 312 | 'The database name was not set. The SQLite driver requires a database name to be set to emulate MySQL information schema tables.', 313 | 'db_connect_fail' 314 | ); 315 | return false; 316 | } 317 | 318 | require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-grammar.php'; 319 | require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser.php'; 320 | require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-node.php'; 321 | require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-token.php'; 322 | require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-token.php'; 323 | require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-lexer.php'; 324 | require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-parser.php'; 325 | require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-connection.php'; 326 | require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-configurator.php'; 327 | require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver.php'; 328 | require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php'; 329 | require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php'; 330 | require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-exception.php'; 331 | require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-reconstructor.php'; 332 | $this->ensure_database_directory( FQDB ); 333 | 334 | try { 335 | $connection = new WP_SQLite_Connection( 336 | array( 337 | 'pdo' => $pdo, 338 | 'path' => FQDB, 339 | 'journal_mode' => defined( 'SQLITE_JOURNAL_MODE' ) ? SQLITE_JOURNAL_MODE : null, 340 | ) 341 | ); 342 | $this->dbh = new WP_SQLite_Driver( $connection, $this->dbname ); 343 | $GLOBALS['@pdo'] = $this->dbh->get_connection()->get_pdo(); 344 | } catch ( Throwable $e ) { 345 | $this->last_error = $this->format_error_message( $e ); 346 | } 347 | } else { 348 | $this->dbh = new WP_SQLite_Translator( $pdo ); 349 | $this->last_error = $this->dbh->get_error_message(); 350 | $GLOBALS['@pdo'] = $this->dbh->get_pdo(); 351 | } 352 | if ( $this->last_error ) { 353 | return false; 354 | } 355 | $this->ready = true; 356 | $this->set_sql_mode(); 357 | } 358 | 359 | /** 360 | * Method to dummy out wpdb::check_connection() 361 | * 362 | * @param bool $allow_bail Not used. 363 | * 364 | * @return bool 365 | */ 366 | public function check_connection( $allow_bail = true ) { 367 | return true; 368 | } 369 | 370 | /** 371 | * Prepares a SQL query for safe execution. 372 | * 373 | * See "wpdb::prepare()". This override only fixes a WPDB test issue. 374 | * 375 | * @param string $query Query statement with `sprintf()`-like placeholders. 376 | * @param array|mixed $args The array of variables or the first variable to substitute. 377 | * @param mixed ...$args Further variables to substitute when using individual arguments. 378 | * @return string|void Sanitized query string, if there is a query to prepare. 379 | */ 380 | public function prepare( $query, ...$args ) { 381 | /* 382 | * Sync "$allow_unsafe_unquoted_parameters" with the WPDB parent property. 383 | * This is only needed because some WPDB tests are accessing the private 384 | * property externally via PHP reflection. This should be fixed WP tests. 385 | */ 386 | $wpdb_allow_unsafe_unquoted_parameters = $this->__get( 'allow_unsafe_unquoted_parameters' ); 387 | if ( $wpdb_allow_unsafe_unquoted_parameters !== $this->allow_unsafe_unquoted_parameters ) { 388 | $property = new ReflectionProperty( 'wpdb', 'allow_unsafe_unquoted_parameters' ); 389 | $property->setAccessible( true ); 390 | $property->setValue( $this, $this->allow_unsafe_unquoted_parameters ); 391 | $property->setAccessible( false ); 392 | } 393 | 394 | return parent::prepare( $query, ...$args ); 395 | } 396 | 397 | /** 398 | * Performs a database query. 399 | * 400 | * This overrides wpdb::query() while closely mirroring its implementation. 401 | * 402 | * @see wpdb::query() 403 | * 404 | * @param string $query Database query. 405 | * 406 | * @param string $query Database query. 407 | * @return int|bool Boolean true for CREATE, ALTER, TRUNCATE and DROP queries. Number of rows 408 | * affected/selected for all other queries. Boolean false on error. 409 | */ 410 | public function query( $query ) { 411 | // Query Monitor integration: 412 | $query_monitor_active = defined( 'SQLITE_QUERY_MONITOR_LOADED' ) && SQLITE_QUERY_MONITOR_LOADED; 413 | if ( $query_monitor_active && $this->show_errors ) { 414 | $this->hide_errors(); 415 | } 416 | 417 | if ( ! $this->ready ) { 418 | return false; 419 | } 420 | 421 | $query = apply_filters( 'query', $query ); 422 | 423 | if ( ! $query ) { 424 | $this->insert_id = 0; 425 | return false; 426 | } 427 | 428 | $this->flush(); 429 | 430 | // Log how the function was called. 431 | $this->func_call = "\$db->query(\"$query\")"; 432 | 433 | // Keep track of the last query for debug. 434 | $this->last_query = $query; 435 | 436 | // Save the query count before running another query. 437 | $last_query_count = count( $this->queries ?? array() ); 438 | 439 | /* 440 | * @TODO: WPDB uses "$this->check_current_query" to check table/column 441 | * charset and strip all invalid characters from the query. 442 | * This is an involved process that we can bypass for SQLite, 443 | * if we simply strip all invalid UTF-8 characters from the query. 444 | * 445 | * To do so, mb_convert_encoding can be used with an optional 446 | * fallback to a htmlspecialchars method. E.g.: 447 | * https://github.com/nette/utils/blob/be534713c227aeef57ce1883fc17bc9f9e29eca2/src/Utils/Strings.php#L42 448 | */ 449 | $this->_do_query( $query ); 450 | 451 | if ( $this->last_error ) { 452 | // Clear insert_id on a subsequent failed insert. 453 | if ( $this->insert_id && preg_match( '/^\s*(insert|replace)\s/i', $query ) ) { 454 | $this->insert_id = 0; 455 | } 456 | 457 | $this->print_error(); 458 | return false; 459 | } 460 | 461 | if ( preg_match( '/^\s*(create|alter|truncate|drop)\s/i', $query ) ) { 462 | $return_val = true; 463 | } elseif ( preg_match( '/^\s*(insert|delete|update|replace)\s/i', $query ) ) { 464 | if ( $this->dbh instanceof WP_SQLite_Driver ) { 465 | $this->rows_affected = $this->dbh->get_last_return_value(); 466 | } else { 467 | $this->rows_affected = $this->dbh->get_affected_rows(); 468 | } 469 | 470 | // Take note of the insert_id. 471 | if ( preg_match( '/^\s*(insert|replace)\s/i', $query ) ) { 472 | $this->insert_id = $this->dbh->get_insert_id(); 473 | } 474 | 475 | // Return number of rows affected. 476 | $return_val = $this->rows_affected; 477 | } else { 478 | $num_rows = 0; 479 | 480 | if ( is_array( $this->result ) ) { 481 | $this->last_result = $this->result; 482 | $num_rows = count( $this->result ); 483 | } 484 | 485 | // Log and return the number of rows selected. 486 | $this->num_rows = $num_rows; 487 | $return_val = $num_rows; 488 | } 489 | 490 | // Query monitor integration: 491 | if ( $query_monitor_active && class_exists( 'QM_Backtrace' ) ) { 492 | if ( did_action( 'qm/cease' ) ) { 493 | $this->queries = array(); 494 | } 495 | 496 | $i = $last_query_count; 497 | if ( ! isset( $this->queries[ $i ] ) ) { 498 | return $return_val; 499 | } 500 | 501 | $this->queries[ $i ]['trace'] = new QM_Backtrace(); 502 | if ( ! isset( $this->queries[ $i ][3] ) ) { 503 | $this->queries[ $i ][3] = $this->time_start; 504 | } 505 | 506 | if ( $this->last_error && ! $this->suppress_errors ) { 507 | $this->queries[ $i ]['result'] = new WP_Error( 'qmdb', $this->last_error ); 508 | } else { 509 | $this->queries[ $i ]['result'] = (int) $return_val; 510 | } 511 | 512 | // Add SQLite query data. 513 | if ( $this->dbh instanceof WP_SQLite_Driver ) { 514 | $this->queries[ $i ]['sqlite_queries'] = $this->dbh->get_last_sqlite_queries(); 515 | } else { 516 | $this->queries[ $i ]['sqlite_queries'] = $this->dbh->executed_sqlite_queries; 517 | } 518 | } 519 | return $return_val; 520 | } 521 | 522 | /** 523 | * Internal function to perform the SQLite query call. 524 | * 525 | * This closely mirrors wpdb::_do_query(). 526 | * 527 | * @see wpdb::_do_query() 528 | * 529 | * @param string $query The query to run. 530 | */ 531 | private function _do_query( $query ) { 532 | if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { 533 | $this->timer_start(); 534 | } 535 | 536 | try { 537 | $this->result = $this->dbh->query( $query ); 538 | } catch ( Throwable $e ) { 539 | $this->last_error = $this->format_error_message( $e ); 540 | } 541 | 542 | if ( $this->dbh instanceof WP_SQLite_Translator ) { 543 | $this->last_error = $this->dbh->get_error_message(); 544 | } 545 | 546 | ++$this->num_queries; 547 | 548 | if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) { 549 | $this->log_query( 550 | $query, 551 | $this->timer_stop(), 552 | $this->get_caller(), 553 | $this->time_start, 554 | array() 555 | ); 556 | } 557 | } 558 | 559 | /** 560 | * Method to set the class variable $col_info. 561 | * 562 | * This overrides wpdb::load_col_info(), which uses a mysql function. 563 | * 564 | * @see wpdb::load_col_info() 565 | */ 566 | protected function load_col_info() { 567 | if ( $this->col_info ) { 568 | return; 569 | } 570 | if ( $this->dbh instanceof WP_SQLite_Driver ) { 571 | $this->col_info = array(); 572 | foreach ( $this->dbh->get_last_column_meta() as $column ) { 573 | $this->col_info[] = (object) array( 574 | 'name' => $column['name'], 575 | 'orgname' => $column['mysqli:orgname'], 576 | 'table' => $column['table'], 577 | 'orgtable' => $column['mysqli:orgtable'], 578 | 'def' => '', // Unused, always ''. 579 | 'db' => $column['mysqli:db'], 580 | 'catalog' => 'def', // Unused, always 'def'. 581 | 'max_length' => 0, // As of PHP 8.1, this is always 0. 582 | 'length' => $column['len'], 583 | 'charsetnr' => $column['mysqli:charsetnr'], 584 | 'flags' => $column['mysqli:flags'], 585 | 'type' => $column['mysqli:type'], 586 | 'decimals' => $column['precision'], 587 | ); 588 | } 589 | } else { 590 | $this->col_info = $this->dbh->get_columns(); 591 | } 592 | } 593 | 594 | /** 595 | * Method to return what the database can do. 596 | * 597 | * This overrides wpdb::has_cap() to avoid using MySQL functions. 598 | * SQLite supports subqueries, but not support collation, group_concat and set_charset. 599 | * 600 | * @see wpdb::has_cap() 601 | * 602 | * @param string $db_cap The feature to check for. Accepts 'collation', 603 | * 'group_concat', 'subqueries', 'set_charset', 604 | * 'utf8mb4', or 'utf8mb4_520'. 605 | * 606 | * @return bool Whether the database feature is supported, false otherwise. 607 | */ 608 | public function has_cap( $db_cap ) { 609 | return 'subqueries' === strtolower( $db_cap ); 610 | } 611 | 612 | /** 613 | * Method to return database version number. 614 | * 615 | * This overrides wpdb::db_version() to avoid using MySQL function. 616 | * It returns mysql version number, but it means nothing for SQLite. 617 | * So it return the newest mysql version. 618 | * 619 | * @see wpdb::db_version() 620 | */ 621 | public function db_version() { 622 | return '8.0'; 623 | } 624 | 625 | /** 626 | * Returns the version of the SQLite engine. 627 | * 628 | * @return string SQLite engine version as a string. 629 | */ 630 | public function db_server_info() { 631 | return $this->dbh->get_sqlite_version(); 632 | } 633 | 634 | /** 635 | * Make sure the SQLite database directory exists and is writable. 636 | * Create .htaccess and index.php files to prevent direct access. 637 | * 638 | * @param string $database_path The path to the SQLite database file. 639 | */ 640 | private function ensure_database_directory( string $database_path ) { 641 | $dir = dirname( $database_path ); 642 | 643 | // Set the umask to 0000 to apply permissions exactly as specified. 644 | // A non-zero umask affects new file and directory permissions. 645 | $umask = umask( 0 ); 646 | 647 | // Ensure database directory. 648 | if ( ! is_dir( $dir ) ) { 649 | if ( ! @mkdir( $dir, 0700, true ) ) { 650 | wp_die( sprintf( 'Failed to create database directory: %s', $dir ), 'Error!' ); 651 | } 652 | } 653 | if ( ! is_writable( $dir ) ) { 654 | wp_die( sprintf( 'Database directory is not writable: %s', $dir ), 'Error!' ); 655 | } 656 | 657 | // Ensure .htaccess file to prevent direct access. 658 | $path = $dir . DIRECTORY_SEPARATOR . '.htaccess'; 659 | if ( ! is_file( $path ) ) { 660 | $result = file_put_contents( $path, 'DENY FROM ALL', LOCK_EX ); 661 | if ( false === $result ) { 662 | wp_die( sprintf( 'Failed to create file: %s', $path ), 'Error!' ); 663 | } 664 | chmod( $path, 0600 ); 665 | } 666 | 667 | // Ensure index.php file to prevent direct access. 668 | $path = $dir . DIRECTORY_SEPARATOR . 'index.php'; 669 | if ( ! is_file( $path ) ) { 670 | $result = file_put_contents( $path, '', LOCK_EX ); 671 | if ( false === $result ) { 672 | wp_die( sprintf( 'Failed to create file: %s', $path ), 'Error!' ); 673 | } 674 | chmod( $path, 0600 ); 675 | } 676 | 677 | // Restore the original umask value. 678 | umask( $umask ); 679 | } 680 | 681 | 682 | /** 683 | * Format SQLite driver error message. 684 | * 685 | * @return string 686 | */ 687 | private function format_error_message( Throwable $e ) { 688 | $output = '
 
' . PHP_EOL; 689 | 690 | // Queries. 691 | if ( $e instanceof WP_SQLite_Driver_Exception ) { 692 | $driver = $e->getDriver(); 693 | 694 | $output .= '
' . PHP_EOL; 695 | $output .= '

MySQL query:

' . PHP_EOL; 696 | $output .= '

' . $driver->get_last_mysql_query() . '

' . PHP_EOL; 697 | $output .= '

Queries made or created this session were:

' . PHP_EOL; 698 | $output .= '
    ' . PHP_EOL; 699 | foreach ( $driver->get_last_sqlite_queries() as $q ) { 700 | $message = "Executing: {$q['sql']} | " . ( $q['params'] ? 'parameters: ' . implode( ', ', $q['params'] ) : '(no parameters)' ); 701 | $output .= '
  1. ' . htmlspecialchars( $message ) . '
  2. ' . PHP_EOL; 702 | } 703 | $output .= '
' . PHP_EOL; 704 | $output .= '
' . PHP_EOL; 705 | } 706 | 707 | // Message. 708 | $output .= '
' . PHP_EOL; 709 | $output .= $e->getMessage() . PHP_EOL; 710 | $output .= '
' . PHP_EOL; 711 | 712 | // Backtrace. 713 | $output .= '

Backtrace:

' . PHP_EOL; 714 | $output .= '
' . $e->getTraceAsString() . '
' . PHP_EOL; 715 | return $output; 716 | } 717 | } 718 | -------------------------------------------------------------------------------- /wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php: -------------------------------------------------------------------------------- 1 | 17 | * new WP_SQLite_PDO_User_Defined_Functions(ref_to_pdo_obj); 18 | * 19 | * 20 | * This automatically enables ref_to_pdo_obj to replace the function in the SQL statement 21 | * to the ones defined here. 22 | */ 23 | class WP_SQLite_PDO_User_Defined_Functions { 24 | 25 | /** 26 | * Registers the user defined functions for SQLite to a PDO instance. 27 | * The functions are registered using PDO::sqliteCreateFunction(). 28 | * 29 | * @param PDO|PDO\SQLite $pdo The PDO object. 30 | */ 31 | public static function register_for( $pdo ): self { 32 | $instance = new self(); 33 | foreach ( $instance->functions as $f => $t ) { 34 | if ( $pdo instanceof PDO\SQLite ) { 35 | $pdo->createFunction( $f, array( $instance, $t ) ); 36 | } else { 37 | $pdo->sqliteCreateFunction( $f, array( $instance, $t ) ); 38 | } 39 | } 40 | return $instance; 41 | } 42 | 43 | /** 44 | * Array to define MySQL function => function defined with PHP. 45 | * 46 | * Replaced functions must be public. 47 | * 48 | * @var array 49 | */ 50 | private $functions = array( 51 | 'throw' => 'throw', 52 | 'month' => 'month', 53 | 'monthnum' => 'month', 54 | 'year' => 'year', 55 | 'day' => 'day', 56 | 'hour' => 'hour', 57 | 'minute' => 'minute', 58 | 'second' => 'second', 59 | 'week' => 'week', 60 | 'weekday' => 'weekday', 61 | 'dayofweek' => 'dayofweek', 62 | 'dayofmonth' => 'dayofmonth', 63 | 'unix_timestamp' => 'unix_timestamp', 64 | 'now' => 'now', 65 | 'md5' => 'md5', 66 | 'curdate' => 'curdate', 67 | 'rand' => 'rand', 68 | 'from_unixtime' => 'from_unixtime', 69 | 'localtime' => 'now', 70 | 'localtimestamp' => 'now', 71 | 'isnull' => 'isnull', 72 | 'if' => '_if', 73 | 'regexp' => 'regexp', 74 | 'field' => 'field', 75 | 'log' => 'log', 76 | 'least' => 'least', 77 | 'greatest' => 'greatest', 78 | 'get_lock' => 'get_lock', 79 | 'release_lock' => 'release_lock', 80 | 'ucase' => 'ucase', 81 | 'lcase' => 'lcase', 82 | 'unhex' => 'unhex', 83 | 'inet_ntoa' => 'inet_ntoa', 84 | 'inet_aton' => 'inet_aton', 85 | 'datediff' => 'datediff', 86 | 'locate' => 'locate', 87 | 'utc_date' => 'utc_date', 88 | 'utc_time' => 'utc_time', 89 | 'utc_timestamp' => 'utc_timestamp', 90 | 'version' => 'version', 91 | 92 | // Internal helper functions. 93 | '_helper_like_to_glob_pattern' => '_helper_like_to_glob_pattern', 94 | ); 95 | 96 | /** 97 | * A helper function to throw an error from SQLite expressions. 98 | * 99 | * @param string $message The error message. 100 | * 101 | * @throws Exception The error message. 102 | * @return void 103 | */ 104 | public function throw( $message ): void { 105 | throw new Exception( $message ); 106 | } 107 | 108 | /** 109 | * Method to return the unix timestamp. 110 | * 111 | * Used without an argument, it returns PHP time() function (total seconds passed 112 | * from '1970-01-01 00:00:00' GMT). Used with the argument, it changes the value 113 | * to the timestamp. 114 | * 115 | * @param string $field Representing the date formatted as '0000-00-00 00:00:00'. 116 | * 117 | * @return number of unsigned integer 118 | */ 119 | public function unix_timestamp( $field = null ) { 120 | return is_null( $field ) ? time() : strtotime( $field ); 121 | } 122 | 123 | /** 124 | * Method to emulate MySQL FROM_UNIXTIME() function. 125 | * 126 | * @param int $field The unix timestamp. 127 | * @param string $format Indicate the way of formatting(optional). 128 | * 129 | * @return string 130 | */ 131 | public function from_unixtime( $field, $format = null ) { 132 | // Convert to ISO time. 133 | $date = gmdate( 'Y-m-d H:i:s', $field ); 134 | 135 | return is_null( $format ) ? $date : $this->dateformat( $date, $format ); 136 | } 137 | 138 | /** 139 | * Method to emulate MySQL NOW() function. 140 | * 141 | * @return string representing current time formatted as '0000-00-00 00:00:00'. 142 | */ 143 | public function now() { 144 | return gmdate( 'Y-m-d H:i:s' ); 145 | } 146 | 147 | /** 148 | * Method to emulate MySQL CURDATE() function. 149 | * 150 | * @return string representing current time formatted as '0000-00-00'. 151 | */ 152 | public function curdate() { 153 | return gmdate( 'Y-m-d' ); 154 | } 155 | 156 | /** 157 | * Method to emulate MySQL MD5() function. 158 | * 159 | * @param string $field The string to be hashed. 160 | * 161 | * @return string of the md5 hash value of the argument. 162 | */ 163 | public function md5( $field ) { 164 | return md5( $field ); 165 | } 166 | 167 | /** 168 | * Method to emulate MySQL RAND() function. 169 | * 170 | * SQLite does have a random generator, but it is called RANDOM() and returns random 171 | * number between -9223372036854775808 and +9223372036854775807. So we substitute it 172 | * with PHP random generator. 173 | * 174 | * This function uses mt_rand() which is four times faster than rand() and returns 175 | * the random number between 0 and 1. 176 | * 177 | * @return int 178 | */ 179 | public function rand() { 180 | return mt_rand( 0, 1 ); 181 | } 182 | 183 | /** 184 | * Method to emulate MySQL DATEFORMAT() function. 185 | * 186 | * @param string $date Formatted as '0000-00-00' or datetime as '0000-00-00 00:00:00'. 187 | * @param string $format The string format. 188 | * 189 | * @return string formatted according to $format 190 | */ 191 | public function dateformat( $date, $format ) { 192 | $mysql_php_date_formats = array( 193 | '%a' => 'D', 194 | '%b' => 'M', 195 | '%c' => 'n', 196 | '%D' => 'jS', 197 | '%d' => 'd', 198 | '%e' => 'j', 199 | '%H' => 'H', 200 | '%h' => 'h', 201 | '%I' => 'h', 202 | '%i' => 'i', 203 | '%j' => 'z', 204 | '%k' => 'G', 205 | '%l' => 'g', 206 | '%M' => 'F', 207 | '%m' => 'm', 208 | '%p' => 'A', 209 | '%r' => 'h:i:s A', 210 | '%S' => 's', 211 | '%s' => 's', 212 | '%T' => 'H:i:s', 213 | '%U' => 'W', 214 | '%u' => 'W', 215 | '%V' => 'W', 216 | '%v' => 'W', 217 | '%W' => 'l', 218 | '%w' => 'w', 219 | '%X' => 'Y', 220 | '%x' => 'o', 221 | '%Y' => 'Y', 222 | '%y' => 'y', 223 | ); 224 | 225 | $time = strtotime( $date ); 226 | $format = strtr( $format, $mysql_php_date_formats ); 227 | 228 | return gmdate( $format, $time ); 229 | } 230 | 231 | /** 232 | * Method to extract the month value from the date. 233 | * 234 | * @param string $field Representing the date formatted as 0000-00-00. 235 | * 236 | * @return string Representing the number of the month between 1 and 12. 237 | */ 238 | public function month( $field ) { 239 | /* 240 | * From https://www.php.net/manual/en/datetime.format.php: 241 | * 242 | * n - Numeric representation of a month, without leading zeros. 243 | * 1 through 12 244 | */ 245 | return intval( gmdate( 'n', strtotime( $field ) ) ); 246 | } 247 | 248 | /** 249 | * Method to extract the year value from the date. 250 | * 251 | * @param string $field Representing the date formatted as 0000-00-00. 252 | * 253 | * @return string Representing the number of the year. 254 | */ 255 | public function year( $field ) { 256 | /* 257 | * From https://www.php.net/manual/en/datetime.format.php: 258 | * 259 | * Y - A full numeric representation of a year, 4 digits. 260 | */ 261 | return intval( gmdate( 'Y', strtotime( $field ) ) ); 262 | } 263 | 264 | /** 265 | * Method to extract the day value from the date. 266 | * 267 | * @param string $field Representing the date formatted as 0000-00-00. 268 | * 269 | * @return string Representing the number of the day of the month from 1 and 31. 270 | */ 271 | public function day( $field ) { 272 | /* 273 | * From https://www.php.net/manual/en/datetime.format.php: 274 | * 275 | * j - Day of the month without leading zeros. 276 | * 1 to 31. 277 | */ 278 | return intval( gmdate( 'j', strtotime( $field ) ) ); 279 | } 280 | 281 | /** 282 | * Method to emulate MySQL SECOND() function. 283 | * 284 | * @see https://www.php.net/manual/en/datetime.format.php 285 | * 286 | * @param string $field Representing the time formatted as '00:00:00'. 287 | * 288 | * @return number Unsigned integer 289 | */ 290 | public function second( $field ) { 291 | /* 292 | * From https://www.php.net/manual/en/datetime.format.php: 293 | * 294 | * s - Seconds, with leading zeros (00 to 59) 295 | */ 296 | return intval( gmdate( 's', strtotime( $field ) ) ); 297 | } 298 | 299 | /** 300 | * Method to emulate MySQL MINUTE() function. 301 | * 302 | * @param string $field Representing the time formatted as '00:00:00'. 303 | * 304 | * @return int 305 | */ 306 | public function minute( $field ) { 307 | /* 308 | * From https://www.php.net/manual/en/datetime.format.php: 309 | * 310 | * i - Minutes with leading zeros. 311 | * 00 to 59. 312 | */ 313 | return intval( gmdate( 'i', strtotime( $field ) ) ); 314 | } 315 | 316 | /** 317 | * Method to emulate MySQL HOUR() function. 318 | * 319 | * Returns the hour for time, in 24-hour format, from 0 to 23. 320 | * Importantly, midnight is 0, not 24. 321 | * 322 | * @param string $time Representing the time formatted, like '14:08:12'. 323 | * 324 | * @return int 325 | */ 326 | public function hour( $time ) { 327 | /* 328 | * From https://www.php.net/manual/en/datetime.format.php: 329 | * 330 | * H 24-hour format of an hour with leading zeros. 331 | * 00 through 23. 332 | */ 333 | return intval( gmdate( 'H', strtotime( $time ) ) ); 334 | } 335 | 336 | /** 337 | * Covers MySQL WEEK() function. 338 | * 339 | * Always assumes $mode = 1. 340 | * 341 | * @TODO: Support other modes. 342 | * 343 | * From https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_week: 344 | * 345 | * > Returns the week number for date. The two-argument form of WEEK() 346 | * > enables you to specify whether the week starts on Sunday or Monday 347 | * > and whether the return value should be in the range from 0 to 53 348 | * > or from 1 to 53. If the mode argument is omitted, the value of the 349 | * > default_week_format system variable is used. 350 | * > 351 | * > The following table describes how the mode argument works: 352 | * > 353 | * > Mode First day of week Range Week 1 is the first week … 354 | * > 0 Sunday 0-53 with a Sunday in this year 355 | * > 1 Monday 0-53 with 4 or more days this year 356 | * > 2 Sunday 1-53 with a Sunday in this year 357 | * > 3 Monday 1-53 with 4 or more days this year 358 | * > 4 Sunday 0-53 with 4 or more days this year 359 | * > 5 Monday 0-53 with a Monday in this year 360 | * > 6 Sunday 1-53 with 4 or more days this year 361 | * > 7 Monday 1-53 with a Monday in this year 362 | * 363 | * @param string $field Representing the date. 364 | * @param int $mode The mode argument. 365 | */ 366 | public function week( $field, $mode ) { 367 | /* 368 | * From https://www.php.net/manual/en/datetime.format.php: 369 | * 370 | * W - ISO-8601 week number of year, weeks starting on Monday. 371 | * Example: 42 (the 42nd week in the year) 372 | * 373 | * Week 1 is the first week with a Thursday in it. 374 | */ 375 | return intval( gmdate( 'W', strtotime( $field ) ) ); 376 | } 377 | 378 | /** 379 | * Simulates WEEKDAY() function in MySQL. 380 | * 381 | * Returns the day of the week as an integer. 382 | * The days of the week are numbered 0 to 6: 383 | * * 0 for Monday 384 | * * 1 for Tuesday 385 | * * 2 for Wednesday 386 | * * 3 for Thursday 387 | * * 4 for Friday 388 | * * 5 for Saturday 389 | * * 6 for Sunday 390 | * 391 | * @param string $field Representing the date. 392 | * 393 | * @return int 394 | */ 395 | public function weekday( $field ) { 396 | /* 397 | * date('N') returns 1 (for Monday) through 7 (for Sunday) 398 | * That's one more than MySQL. 399 | * Let's subtract one to make it compatible. 400 | */ 401 | return intval( gmdate( 'N', strtotime( $field ) ) ) - 1; 402 | } 403 | 404 | /** 405 | * Method to emulate MySQL DAYOFMONTH() function. 406 | * 407 | * @see https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_dayofmonth 408 | * 409 | * @param string $field Representing the date. 410 | * 411 | * @return int Returns the day of the month for date as a number in the range 1 to 31. 412 | */ 413 | public function dayofmonth( $field ) { 414 | return intval( gmdate( 'j', strtotime( $field ) ) ); 415 | } 416 | 417 | /** 418 | * Method to emulate MySQL DAYOFWEEK() function. 419 | * 420 | * > Returns the weekday index for date (1 = Sunday, 2 = Monday, …, 7 = Saturday). 421 | * > These index values correspond to the ODBC standard. Returns NULL if date is NULL. 422 | * 423 | * @param string $field Representing the date. 424 | * 425 | * @return int Returns the weekday index for date (1 = Sunday, 2 = Monday, …, 7 = Saturday). 426 | */ 427 | public function dayofweek( $field ) { 428 | /** 429 | * From https://www.php.net/manual/en/datetime.format.php: 430 | * 431 | * `w` – Numeric representation of the day of the week 432 | * 0 (for Sunday) through 6 (for Saturday) 433 | */ 434 | return intval( gmdate( 'w', strtotime( $field ) ) ) + 1; 435 | } 436 | 437 | /** 438 | * Method to emulate MySQL DATE() function. 439 | * 440 | * @see https://www.php.net/manual/en/datetime.format.php 441 | * 442 | * @param string $date formatted as unix time. 443 | * 444 | * @return string formatted as '0000-00-00'. 445 | */ 446 | public function date( $date ) { 447 | return gmdate( 'Y-m-d', strtotime( $date ) ); 448 | } 449 | 450 | /** 451 | * Method to emulate MySQL ISNULL() function. 452 | * 453 | * This function returns true if the argument is null, and true if not. 454 | * 455 | * @param mixed $field The field to be tested. 456 | * 457 | * @return boolean 458 | */ 459 | public function isnull( $field ) { 460 | return is_null( $field ); 461 | } 462 | 463 | /** 464 | * Method to emulate MySQL IF() function. 465 | * 466 | * As 'IF' is a reserved word for PHP, function name must be changed. 467 | * 468 | * @param mixed $expression The statement to be evaluated as true or false. 469 | * @param mixed $truthy Statement or value returned if $expression is true. 470 | * @param mixed $falsy Statement or value returned if $expression is false. 471 | * 472 | * @return mixed 473 | */ 474 | public function _if( $expression, $truthy, $falsy ) { 475 | return ( true === $expression ) ? $truthy : $falsy; 476 | } 477 | 478 | /** 479 | * Method to emulate MySQL REGEXP() function. 480 | * 481 | * @param string $pattern Regular expression to match. 482 | * @param string $field Haystack. 483 | * 484 | * @return integer 1 if matched, 0 if not matched. 485 | */ 486 | public function regexp( $pattern, $field ) { 487 | /* 488 | * If the original query says REGEXP BINARY 489 | * the comparison is byte-by-byte and letter casing now 490 | * matters since lower- and upper-case letters have different 491 | * byte codes. 492 | * 493 | * The REGEXP function can't be easily made to accept two 494 | * parameters, so we'll have to use a hack to get around this. 495 | * 496 | * If the first character of the pattern is a null byte, we'll 497 | * remove it and make the comparison case-sensitive. This should 498 | * be reasonably safe since PHP does not allow null bytes in 499 | * regular expressions anyway. 500 | */ 501 | if ( "\x00" === $pattern[0] ) { 502 | $pattern = substr( $pattern, 1 ); 503 | $flags = ''; 504 | } else { 505 | // Otherwise, the search is case-insensitive. 506 | $flags = 'i'; 507 | } 508 | $pattern = str_replace( '/', '\/', $pattern ); 509 | $pattern = '/' . $pattern . '/' . $flags; 510 | 511 | return preg_match( $pattern, $field ); 512 | } 513 | 514 | /** 515 | * Method to emulate MySQL FIELD() function. 516 | * 517 | * This function gets the list argument and compares the first item to all the others. 518 | * If the same value is found, it returns the position of that value. If not, it 519 | * returns 0. 520 | * 521 | * @return int 522 | */ 523 | public function field() { 524 | $num_args = func_num_args(); 525 | if ( $num_args < 2 || is_null( func_get_arg( 0 ) ) ) { 526 | return 0; 527 | } 528 | $arg_list = func_get_args(); 529 | $search_string = strtolower( array_shift( $arg_list ) ); 530 | 531 | for ( $i = 0; $i < $num_args - 1; $i++ ) { 532 | if ( strtolower( $arg_list[ $i ] ) === $search_string ) { 533 | return $i + 1; 534 | } 535 | } 536 | 537 | return 0; 538 | } 539 | 540 | /** 541 | * Method to emulate MySQL LOG() function. 542 | * 543 | * Used with one argument, it returns the natural logarithm of X. 544 | * 545 | * LOG(X) 546 | * 547 | * Used with two arguments, it returns the natural logarithm of X base B. 548 | * 549 | * LOG(B, X) 550 | * 551 | * In this case, it returns the value of log(X) / log(B). 552 | * 553 | * Used without an argument, it returns false. This returned value will be 554 | * rewritten to 0, because SQLite doesn't understand true/false value. 555 | * 556 | * @return double|null 557 | */ 558 | public function log() { 559 | $num_args = func_num_args(); 560 | if ( 1 === $num_args ) { 561 | $arg1 = func_get_arg( 0 ); 562 | 563 | return log( $arg1 ); 564 | } 565 | if ( 2 === $num_args ) { 566 | $arg1 = func_get_arg( 0 ); 567 | $arg2 = func_get_arg( 1 ); 568 | 569 | return log( $arg1 ) / log( $arg2 ); 570 | } 571 | return null; 572 | } 573 | 574 | /** 575 | * Method to emulate MySQL LEAST() function. 576 | * 577 | * This function rewrites the function name to SQLite compatible function name. 578 | * 579 | * @return mixed 580 | */ 581 | public function least() { 582 | $arg_list = func_get_args(); 583 | 584 | return min( $arg_list ); 585 | } 586 | 587 | /** 588 | * Method to emulate MySQL GREATEST() function. 589 | * 590 | * This function rewrites the function name to SQLite compatible function name. 591 | * 592 | * @return mixed 593 | */ 594 | public function greatest() { 595 | $arg_list = func_get_args(); 596 | 597 | return max( $arg_list ); 598 | } 599 | 600 | /** 601 | * Method to dummy out MySQL GET_LOCK() function. 602 | * 603 | * This function is meaningless in SQLite, so we do nothing. 604 | * 605 | * @param string $name Not used. 606 | * @param integer $timeout Not used. 607 | * 608 | * @return string 609 | */ 610 | public function get_lock( $name, $timeout ) { 611 | return '1=1'; 612 | } 613 | 614 | /** 615 | * Method to dummy out MySQL RELEASE_LOCK() function. 616 | * 617 | * This function is meaningless in SQLite, so we do nothing. 618 | * 619 | * @param string $name Not used. 620 | * 621 | * @return string 622 | */ 623 | public function release_lock( $name ) { 624 | return '1=1'; 625 | } 626 | 627 | /** 628 | * Method to emulate MySQL UCASE() function. 629 | * 630 | * This is MySQL alias for upper() function. This function rewrites it 631 | * to SQLite compatible name upper(). 632 | * 633 | * @param string $content String to be converted to uppercase. 634 | * 635 | * @return string SQLite compatible function name. 636 | */ 637 | public function ucase( $content ) { 638 | return "upper($content)"; 639 | } 640 | 641 | /** 642 | * Method to emulate MySQL LCASE() function. 643 | * 644 | * This is MySQL alias for lower() function. This function rewrites it 645 | * to SQLite compatible name lower(). 646 | * 647 | * @param string $content String to be converted to lowercase. 648 | * 649 | * @return string SQLite compatible function name. 650 | */ 651 | public function lcase( $content ) { 652 | return "lower($content)"; 653 | } 654 | 655 | /** 656 | * Method to emulate MySQL UNHEX() function. 657 | * 658 | * For a string argument str, UNHEX(str) interprets each pair of characters 659 | * in the argument as a hexadecimal number and converts it to the byte represented 660 | * by the number. The return value is a binary string. 661 | * 662 | * @param string $number Number to be unhexed. 663 | * 664 | * @return string Binary string 665 | */ 666 | public function unhex( $number ) { 667 | return pack( 'H*', $number ); 668 | } 669 | 670 | /** 671 | * Method to emulate MySQL INET_NTOA() function. 672 | * 673 | * This function gets 4 or 8 bytes integer and turn it into the network address. 674 | * 675 | * @param integer $num Long integer. 676 | * 677 | * @return string 678 | */ 679 | public function inet_ntoa( $num ) { 680 | return long2ip( $num ); 681 | } 682 | 683 | /** 684 | * Method to emulate MySQL INET_ATON() function. 685 | * 686 | * This function gets the network address and turns it into integer. 687 | * 688 | * @param string $addr Network address. 689 | * 690 | * @return int long integer 691 | */ 692 | public function inet_aton( $addr ) { 693 | return absint( ip2long( $addr ) ); 694 | } 695 | 696 | /** 697 | * Method to emulate MySQL DATEDIFF() function. 698 | * 699 | * This function compares two dates value and returns the difference. 700 | * 701 | * @param string $start Start date. 702 | * @param string $end End date. 703 | * 704 | * @return string 705 | */ 706 | public function datediff( $start, $end ) { 707 | $start_date = new DateTime( $start ); 708 | $end_date = new DateTime( $end ); 709 | $interval = $end_date->diff( $start_date, false ); 710 | 711 | return $interval->format( '%r%a' ); 712 | } 713 | 714 | /** 715 | * Method to emulate MySQL LOCATE() function. 716 | * 717 | * This function returns the position if $substr is found in $str. If not, 718 | * it returns 0. If mbstring extension is loaded, mb_strpos() function is 719 | * used. 720 | * 721 | * @param string $substr Needle. 722 | * @param string $str Haystack. 723 | * @param integer $pos Position. 724 | * 725 | * @return integer 726 | */ 727 | public function locate( $substr, $str, $pos = 0 ) { 728 | if ( ! extension_loaded( 'mbstring' ) ) { 729 | $val = strpos( $str, $substr, $pos ); 730 | if ( false !== $val ) { 731 | return $val + 1; 732 | } 733 | return 0; 734 | } 735 | $val = mb_strpos( $str, $substr, $pos ); 736 | if ( false !== $val ) { 737 | return $val + 1; 738 | } 739 | return 0; 740 | } 741 | 742 | /** 743 | * Method to return GMT date in the string format. 744 | * 745 | * @return string formatted GMT date 'dddd-mm-dd' 746 | */ 747 | public function utc_date() { 748 | return gmdate( 'Y-m-d', time() ); 749 | } 750 | 751 | /** 752 | * Method to return GMT time in the string format. 753 | * 754 | * @return string formatted GMT time '00:00:00' 755 | */ 756 | public function utc_time() { 757 | return gmdate( 'H:i:s', time() ); 758 | } 759 | 760 | /** 761 | * Method to return GMT time stamp in the string format. 762 | * 763 | * @return string formatted GMT timestamp 'yyyy-mm-dd 00:00:00' 764 | */ 765 | public function utc_timestamp() { 766 | return gmdate( 'Y-m-d H:i:s', time() ); 767 | } 768 | 769 | /** 770 | * Method to return MySQL version. 771 | * 772 | * This function only returns the current newest version number of MySQL, 773 | * because it is meaningless for SQLite database. 774 | * 775 | * @return string representing the version number: major_version.minor_version 776 | */ 777 | public function version() { 778 | return '5.5'; 779 | } 780 | 781 | /** 782 | * A helper to covert LIKE pattern to a GLOB pattern for "LIKE BINARY" support. 783 | 784 | * @TODO: Some of the MySQL string specifics described below are likely to 785 | * affect also other patterns than just "LIKE BINARY". We should 786 | * consider applying some of the conversions more broadly. 787 | * 788 | * @param string $pattern 789 | * @return string 790 | */ 791 | public function _helper_like_to_glob_pattern( $pattern ) { 792 | if ( null === $pattern ) { 793 | return null; 794 | } 795 | 796 | /* 797 | * 1. Escape characters that have special meaning in GLOB patterns. 798 | * 799 | * We need to: 800 | * 1. Escape "]" as "[]]" to avoid interpreting "[...]" as a character class. 801 | * 2. Escape "*" as "[*]" (must be after 1 to avoid being escaped). 802 | * 3. Escape "?" as "[?]" (must be after 1 to avoid being escaped). 803 | */ 804 | $pattern = str_replace( ']', '[]]', $pattern ); 805 | $pattern = str_replace( '*', '[*]', $pattern ); 806 | $pattern = str_replace( '?', '[?]', $pattern ); 807 | 808 | /* 809 | * 2. Convert LIKE wildcards to GLOB wildcards ("%" -> "*", "_" -> "?"). 810 | * 811 | * We need to convert them only when they don't follow any backslashes, 812 | * or when they follow an even number of backslashes (as "\\" is "\"). 813 | */ 814 | $pattern = preg_replace( '/(^|[^\\\\](?:\\\\{2})*)%/', '$1*', $pattern ); 815 | $pattern = preg_replace( '/(^|[^\\\\](?:\\\\{2})*)_/', '$1?', $pattern ); 816 | 817 | /* 818 | * 3. Unescape LIKE escape sequences. 819 | * 820 | * While in MySQL LIKE patterns, a backslash is usually used to escape 821 | * special characters ("%", "_", and "\"), it works with all characters. 822 | * 823 | * That is: 824 | * SELECT '\\x' prints '\x', but LIKE '\\x' is equivalent to LIKE 'x'. 825 | * 826 | * This is true also for multi-byte characters: 827 | * SELECT '\\©' prints '\©', but LIKE '\\©' is equivalent to LIKE '©'. 828 | * 829 | * However, the multi-byte behavior is likely to depend on the charset. 830 | * For now, we'll assume UTF-8 and thus the "u" modifier for the regex. 831 | */ 832 | $pattern = preg_replace( '/\\\\(.)/u', '$1', $pattern ); 833 | 834 | return $pattern; 835 | } 836 | } 837 | -------------------------------------------------------------------------------- /wp-includes/sqlite-ast/class-wp-sqlite-information-schema-reconstructor.php: -------------------------------------------------------------------------------- 1 | driver = $driver; 51 | $this->connection = $driver->get_connection(); 52 | $this->schema_builder = $schema_builder; 53 | } 54 | 55 | /** 56 | * Ensure that the MySQL INFORMATION_SCHEMA data in SQLite is correct. 57 | * 58 | * This method checks if the MySQL INFORMATION_SCHEMA data in SQLite is correct, 59 | * and if it is not, it will reconstruct missing data and remove stale values. 60 | */ 61 | public function ensure_correct_information_schema(): void { 62 | $sqlite_tables = $this->get_sqlite_table_names(); 63 | $information_schema_tables = $this->get_information_schema_table_names(); 64 | 65 | // In WordPress, use "wp_get_db_schema()" to reconstruct WordPress tables. 66 | $wp_tables = $this->get_wp_create_table_statements(); 67 | 68 | // Reconstruct information schema records for tables that don't have them. 69 | foreach ( $sqlite_tables as $table ) { 70 | if ( ! in_array( $table, $information_schema_tables, true ) ) { 71 | if ( isset( $wp_tables[ $table ] ) ) { 72 | // WordPress core table (as returned by "wp_get_db_schema()"). 73 | $ast = $wp_tables[ $table ]; 74 | } else { 75 | // Other table (a WordPress plugin or unrelated to WordPress). 76 | $sql = $this->generate_create_table_statement( $table ); 77 | $ast = $this->driver->create_parser( $sql )->parse(); 78 | if ( null === $ast ) { 79 | throw new WP_SQLite_Driver_Exception( $this->driver, 'Failed to parse the MySQL query.' ); 80 | } 81 | } 82 | 83 | /* 84 | * First, let's make sure we clean up all related data. This fixes 85 | * partial data corruption, such as when a table record is missing, 86 | * but some related column, index, or constraint records are stored. 87 | */ 88 | $this->record_drop_table( $table ); 89 | 90 | $this->schema_builder->record_create_table( $ast ); 91 | } 92 | } 93 | 94 | // Remove information schema records for tables that don't exist. 95 | foreach ( $information_schema_tables as $table ) { 96 | if ( ! in_array( $table, $sqlite_tables, true ) ) { 97 | $this->record_drop_table( $table ); 98 | } 99 | } 100 | } 101 | 102 | /** 103 | * Record a DROP TABLE statement in the information schema. 104 | * 105 | * This removes a table record from the information schema, as well as all 106 | * column, index, and constraint records that are related to the table. 107 | * 108 | * @param string $table_name The name of the table to drop. 109 | */ 110 | private function record_drop_table( string $table_name ): void { 111 | $sql = sprintf( 'DROP TABLE %s', $this->connection->quote_identifier( $table_name ) ); // TODO: mysql quote 112 | $ast = $this->driver->create_parser( $sql )->parse(); 113 | if ( null === $ast ) { 114 | throw new WP_SQLite_Driver_Exception( $this->driver, 'Failed to parse the MySQL query.' ); 115 | } 116 | $this->schema_builder->record_drop_table( 117 | $ast->get_first_descendant_node( 'dropStatement' ) 118 | ); 119 | } 120 | 121 | /** 122 | * Get the names of all existing tables in the SQLite database. 123 | * 124 | * @return string[] The names of tables in the SQLite database. 125 | */ 126 | private function get_sqlite_table_names(): array { 127 | return $this->driver->execute_sqlite_query( 128 | " 129 | SELECT name 130 | FROM sqlite_schema 131 | WHERE type = 'table' 132 | AND name != ? 133 | AND name NOT LIKE ? ESCAPE '\' 134 | AND name NOT LIKE ? ESCAPE '\' 135 | ORDER BY name 136 | ", 137 | array( 138 | '_mysql_data_types_cache', 139 | 'sqlite\_%', 140 | str_replace( '_', '\_', WP_SQLite_Driver::RESERVED_PREFIX ) . '%', 141 | ) 142 | )->fetchAll( PDO::FETCH_COLUMN ); 143 | } 144 | 145 | /** 146 | * Get the names of all tables recorded in the information schema. 147 | * 148 | * @return string[] The names of tables in the information schema. 149 | */ 150 | private function get_information_schema_table_names(): array { 151 | $tables_table = $this->schema_builder->get_table_name( false, 'tables' ); 152 | return $this->driver->execute_sqlite_query( 153 | sprintf( 154 | 'SELECT table_name FROM %s ORDER BY table_name', 155 | $this->connection->quote_identifier( $tables_table ) 156 | ) 157 | )->fetchAll( PDO::FETCH_COLUMN ); 158 | } 159 | 160 | /** 161 | * Get a map of parsed CREATE TABLE statements for WordPress tables. 162 | * 163 | * When reconstructing the information schema data for WordPress tables, we 164 | * can use the "wp_get_db_schema()" function to get accurate CREATE TABLE 165 | * statements. This method parses the result of "wp_get_db_schema()" into 166 | * an array of parsed CREATE TABLE statements indexed by the table names. 167 | * 168 | * @return array The WordPress CREATE TABLE statements. 169 | */ 170 | private function get_wp_create_table_statements(): array { 171 | // Bail out when not in a WordPress environment. 172 | if ( ! defined( 'ABSPATH' ) ) { 173 | return array(); 174 | } 175 | 176 | /* 177 | * In WP CLI, $wpdb may not be set. In that case, we can't load the schema. 178 | * We need to bail out and use the standard non-WordPress-specific behavior. 179 | */ 180 | global $wpdb; 181 | if ( ! isset( $wpdb ) ) { 182 | // Outside of WP CLI, let's trigger a warning. 183 | if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) { 184 | trigger_error( 'The $wpdb global is not initialized.', E_USER_WARNING ); 185 | } 186 | return array(); 187 | } 188 | 189 | // Ensure the "wp_get_db_schema()" function is defined. 190 | if ( file_exists( ABSPATH . 'wp-admin/includes/schema.php' ) ) { 191 | require_once ABSPATH . 'wp-admin/includes/schema.php'; 192 | } 193 | if ( ! function_exists( 'wp_get_db_schema' ) ) { 194 | throw new Exception( 'The "wp_get_db_schema()" function was not defined.' ); 195 | } 196 | 197 | /* 198 | * At this point, WPDB may not yet be initialized, as we're configuring 199 | * the database connection. Let's only populate the table names using 200 | * the "$table_prefix" global so we can get correct table names. 201 | */ 202 | global $table_prefix; 203 | $wpdb->set_prefix( $table_prefix ); 204 | 205 | // Get schema for global tables. 206 | $schema = wp_get_db_schema( 'global' ); 207 | 208 | // For multisite installs, add schema definitions for all sites. 209 | if ( is_multisite() ) { 210 | /* 211 | * We need to use a database query over the "get_sites()" function, 212 | * as WPDB may not yet initialized. Moreover, we need to get the IDs 213 | * of all existing blogs, independent of any filters and actions that 214 | * could possibly alter the results of a "get_sites()" call. 215 | */ 216 | $blog_ids = $this->driver->execute_sqlite_query( 217 | sprintf( 218 | 'SELECT blog_id FROM %s', 219 | $this->connection->quote_identifier( $wpdb->blogs ) 220 | ) 221 | )->fetchAll( PDO::FETCH_COLUMN ); 222 | foreach ( $blog_ids as $blog_id ) { 223 | $schema .= wp_get_db_schema( 'blog', (int) $blog_id ); 224 | } 225 | } else { 226 | // For single site installs, add schema for the main site. 227 | $schema .= wp_get_db_schema( 'blog' ); 228 | } 229 | 230 | // Parse the schema. 231 | $parser = $this->driver->create_parser( $schema ); 232 | $wp_tables = array(); 233 | while ( $parser->next_query() ) { 234 | $ast = $parser->get_query_ast(); 235 | if ( null === $ast ) { 236 | throw new WP_SQLite_Driver_Exception( $this->driver, 'Failed to parse the MySQL query.' ); 237 | } 238 | 239 | $create_node = $ast->get_first_descendant_node( 'createStatement' ); 240 | if ( $create_node && $create_node->has_child_node( 'createTable' ) ) { 241 | $name_node = $create_node->get_first_descendant_node( 'tableName' ); 242 | $name = $this->unquote_mysql_identifier( 243 | substr( $schema, $name_node->get_start(), $name_node->get_length() ) 244 | ); 245 | 246 | $wp_tables[ $name ] = $create_node; 247 | } 248 | } 249 | return $wp_tables; 250 | } 251 | 252 | /** 253 | * Generate a MySQL CREATE TABLE statement from an SQLite table definition. 254 | * 255 | * @param string $table_name The name of the table. 256 | * @return string The CREATE TABLE statement. 257 | */ 258 | private function generate_create_table_statement( string $table_name ): string { 259 | // Columns. 260 | $columns = $this->driver->execute_sqlite_query( 261 | sprintf( 262 | 'PRAGMA table_xinfo(%s)', 263 | $this->connection->quote_identifier( $table_name ) 264 | ) 265 | )->fetchAll( PDO::FETCH_ASSOC ); 266 | 267 | $definitions = array(); 268 | $column_types = array(); 269 | foreach ( $columns as $column ) { 270 | $mysql_type = $this->get_cached_mysql_data_type( $table_name, $column['name'] ); 271 | if ( null === $mysql_type ) { 272 | $mysql_type = $this->get_mysql_column_type( $column['type'] ); 273 | } 274 | $definitions[] = $this->generate_column_definition( $table_name, $column ); 275 | $column_types[ $column['name'] ] = $mysql_type; 276 | } 277 | 278 | // Primary key. 279 | $pk_columns = array(); 280 | foreach ( $columns as $column ) { 281 | // A position of the column in the primary key, starting from index 1. 282 | // A value of 0 means that the column is not part of the primary key. 283 | $pk_position = (int) $column['pk']; 284 | if ( 0 !== $pk_position ) { 285 | $pk_columns[ $pk_position ] = $column['name']; 286 | } 287 | } 288 | 289 | // Sort the columns by their position in the primary key. 290 | ksort( $pk_columns ); 291 | 292 | if ( count( $pk_columns ) > 0 ) { 293 | $quoted_pk_columns = array(); 294 | foreach ( $pk_columns as $pk_column ) { 295 | $quoted_pk_columns[] = $this->connection->quote_identifier( $pk_column ); 296 | } 297 | $definitions[] = sprintf( 'PRIMARY KEY (%s)', implode( ', ', $quoted_pk_columns ) ); 298 | } 299 | 300 | // Indexes and keys. 301 | $keys = $this->driver->execute_sqlite_query( 302 | sprintf( 303 | 'PRAGMA index_list(%s)', 304 | $this->connection->quote_identifier( $table_name ) 305 | ) 306 | )->fetchAll( PDO::FETCH_ASSOC ); 307 | 308 | foreach ( $keys as $key ) { 309 | // Skip the internal index that SQLite may create for a primary key. 310 | // In MySQL, no explicit index needs to be defined for a primary key. 311 | if ( 'pk' === $key['origin'] ) { 312 | continue; 313 | } 314 | $definitions[] = $this->generate_key_definition( $table_name, $key, $column_types ); 315 | } 316 | 317 | return sprintf( 318 | "CREATE TABLE %s (\n %s\n)", 319 | $this->connection->quote_identifier( $table_name ), 320 | implode( ",\n ", $definitions ) 321 | ); 322 | } 323 | 324 | /** 325 | * Generate a MySQL column definition from an SQLite column information. 326 | * 327 | * This method generates a MySQL column definition from SQLite column data. 328 | * 329 | * @param string $table_name The name of the table. 330 | * @param array $column_info The SQLite column information. 331 | * @return string The MySQL column definition. 332 | */ 333 | private function generate_column_definition( string $table_name, array $column_info ): string { 334 | $definition = array(); 335 | $definition[] = $this->connection->quote_identifier( $column_info['name'] ); 336 | 337 | // Data type. 338 | $mysql_type = $this->get_cached_mysql_data_type( $table_name, $column_info['name'] ); 339 | if ( null === $mysql_type ) { 340 | $mysql_type = $this->get_mysql_column_type( $column_info['type'] ); 341 | } 342 | 343 | /** 344 | * Correct some column types based on their default values: 345 | * 1. In MySQL, non-datetime columns can't have a timestamp default. 346 | * Let's use DATETIME when default is set to CURRENT_TIMESTAMP. 347 | * 2. In MySQL, TEXT and BLOB columns can't have a default value. 348 | * Let's use VARCHAR(65535) and VARBINARY(65535) when default is set. 349 | */ 350 | $default = $this->generate_column_default( $mysql_type, $column_info['dflt_value'] ); 351 | if ( 'CURRENT_TIMESTAMP' === $default ) { 352 | $mysql_type = 'datetime'; 353 | } elseif ( 'text' === $mysql_type && null !== $default ) { 354 | $mysql_type = 'varchar(65535)'; 355 | } elseif ( 'blob' === $mysql_type && null !== $default ) { 356 | $mysql_type = 'varbinary(65535)'; 357 | } 358 | 359 | $definition[] = $mysql_type; 360 | 361 | // NULL/NOT NULL. 362 | if ( '1' === $column_info['notnull'] ) { 363 | $definition[] = 'NOT NULL'; 364 | } 365 | 366 | // Auto increment. 367 | $is_auto_increment = false; 368 | if ( '0' !== $column_info['pk'] ) { 369 | $is_auto_increment = $this->driver->execute_sqlite_query( 370 | 'SELECT 1 FROM sqlite_schema WHERE tbl_name = ? AND sql LIKE ?', 371 | array( $table_name, '%AUTOINCREMENT%' ) 372 | )->fetchColumn(); 373 | 374 | if ( $is_auto_increment ) { 375 | $definition[] = 'AUTO_INCREMENT'; 376 | } 377 | } 378 | 379 | // Default value. 380 | if ( null !== $default && ! $is_auto_increment ) { 381 | $definition[] = 'DEFAULT ' . $default; 382 | } 383 | 384 | return implode( ' ', $definition ); 385 | } 386 | 387 | /** 388 | * Generate a MySQL key definition from an SQLite key information. 389 | * 390 | * This method generates a MySQL key definition from SQLite key data. 391 | * 392 | * @param string $table_name The name of the table. 393 | * @param array $key_info The SQLite key information. 394 | * @param array $column_types The MySQL data types of the columns. 395 | * @return string The MySQL key definition. 396 | */ 397 | private function generate_key_definition( string $table_name, array $key_info, array $column_types ): string { 398 | $definition = array(); 399 | 400 | // Key type. 401 | $cached_type = $this->get_cached_mysql_data_type( $table_name, $key_info['name'] ); 402 | if ( 'FULLTEXT' === $cached_type ) { 403 | $definition[] = 'FULLTEXT KEY'; 404 | } elseif ( 'SPATIAL' === $cached_type ) { 405 | $definition[] = 'SPATIAL KEY'; 406 | } elseif ( 'UNIQUE' === $cached_type || '1' === $key_info['unique'] ) { 407 | $definition[] = 'UNIQUE KEY'; 408 | } else { 409 | $definition[] = 'KEY'; 410 | } 411 | 412 | // Key name. 413 | $name = $key_info['name']; 414 | 415 | /* 416 | * The SQLite driver prefixes index names with "{$table_name}__" to avoid 417 | * naming conflicts among tables in SQLite. We need to remove the prefix. 418 | */ 419 | if ( str_starts_with( $name, "{$table_name}__" ) ) { 420 | $name = substr( $name, strlen( "{$table_name}__" ) ); 421 | } 422 | 423 | /** 424 | * SQLite creates automatic internal indexes for primary and unique keys, 425 | * naming them in format "sqlite_autoindex_{$table_name}_{$index_id}". 426 | * For these internal indexes, we need to skip their name, so that in 427 | * the generated MySQL definition, they follow implicit MySQL naming. 428 | */ 429 | if ( ! str_starts_with( $name, 'sqlite_autoindex_' ) ) { 430 | $definition[] = $this->connection->quote_identifier( $name ); 431 | } 432 | 433 | // Key columns. 434 | $key_columns = $this->driver->execute_sqlite_query( 435 | sprintf( 436 | 'PRAGMA index_info(%s)', 437 | $this->connection->quote_identifier( $key_info['name'] ) 438 | ) 439 | )->fetchAll( PDO::FETCH_ASSOC ); 440 | $cols = array(); 441 | foreach ( $key_columns as $column ) { 442 | /* 443 | * Extract type and length from column data type definition. 444 | * 445 | * This is required when the column data type is inferred from the 446 | * '_mysql_data_types_cache' table, which stores the data type in 447 | * the format "type(length)", such as "varchar(255)". 448 | */ 449 | $max_prefix_length = 100; 450 | $type = strtolower( $column_types[ $column['name'] ] ); 451 | $parts = explode( '(', $type ); 452 | $column_type = $parts[0]; 453 | $column_length = isset( $parts[1] ) ? (int) $parts[1] : null; 454 | 455 | /* 456 | * Add an index column prefix length, if needed. 457 | * 458 | * This is required for "text" and "blob" types for columns inferred 459 | * directly from the SQLite schema, and for the following types for 460 | * columns inferred from the '_mysql_data_types_cache' table: 461 | * char, varchar 462 | * text, tinytext, mediumtext, longtext 463 | * blob, tinyblob, mediumblob, longblob 464 | * varbinary 465 | */ 466 | if ( 467 | str_ends_with( $column_type, 'char' ) 468 | || str_ends_with( $column_type, 'text' ) 469 | || str_ends_with( $column_type, 'blob' ) 470 | || str_starts_with( $column_type, 'var' ) 471 | ) { 472 | $cols[] = sprintf( 473 | '%s(%d)', 474 | $this->connection->quote_identifier( $column['name'] ), 475 | min( $column_length ?? $max_prefix_length, $max_prefix_length ) 476 | ); 477 | } else { 478 | $cols[] = $this->connection->quote_identifier( $column['name'] ); 479 | } 480 | } 481 | 482 | $definition[] = '(' . implode( ', ', $cols ) . ')'; 483 | return implode( ' ', $definition ); 484 | } 485 | 486 | /** 487 | * Generate a MySQL default value from an SQLite default value. 488 | * 489 | * @param string $mysql_type The MySQL data type of the column. 490 | * @param string|null $default_value The default value of the SQLite column. 491 | * @return string|null The default value, or null if the column has no default value. 492 | */ 493 | private function generate_column_default( string $mysql_type, ?string $default_value ): ?string { 494 | if ( null === $default_value || '' === $default_value ) { 495 | return null; 496 | } 497 | $mysql_type = strtolower( $mysql_type ); 498 | 499 | /* 500 | * In MySQL, geometry columns can't have a default value. 501 | * 502 | * Geometry columns are saved as TEXT in SQLite, and in an older version 503 | * of the SQLite driver, TEXT columns were assigned a default value of ''. 504 | */ 505 | if ( 'geomcollection' === $mysql_type || 'geometrycollection' === $mysql_type ) { 506 | return null; 507 | } 508 | 509 | /* 510 | * In MySQL, date/time columns can't have a default value of ''. 511 | * 512 | * Date/time columns are saved as TEXT in SQLite, and in an older version 513 | * of the SQLite driver, TEXT columns were assigned a default value of ''. 514 | */ 515 | if ( 516 | "''" === $default_value 517 | && in_array( $mysql_type, array( 'datetime', 'date', 'time', 'timestamp', 'year' ), true ) 518 | ) { 519 | return null; 520 | } 521 | 522 | /** 523 | * Convert SQLite default values to MySQL default values. 524 | * 525 | * See: 526 | * - https://www.sqlite.org/syntax/column-constraint.html 527 | * - https://www.sqlite.org/syntax/literal-value.html 528 | * - https://www.sqlite.org/lang_expr.html#literal_values_constants_ 529 | */ 530 | 531 | // Quoted string literal. E.g.: 'abc', "abc", `abc` 532 | $first_byte = $default_value[0] ?? null; 533 | if ( '"' === $first_byte || "'" === $first_byte || '`' === $first_byte ) { 534 | $value = substr( $default_value, 1, -1 ); 535 | $value = str_replace( $first_byte . $first_byte, $first_byte, $value ); 536 | return $this->quote_mysql_utf8_string_literal( $value ); 537 | } 538 | 539 | // Normalize the default value for easier comparison. 540 | $uppercase_default_value = strtoupper( $default_value ); 541 | 542 | // NULL, TRUE, FALSE. 543 | if ( 'NULL' === $uppercase_default_value ) { 544 | // DEFAULT NULL is the same as no default value. 545 | return null; 546 | } elseif ( 'TRUE' === $uppercase_default_value ) { 547 | return '1'; 548 | } elseif ( 'FALSE' === $uppercase_default_value ) { 549 | return '0'; 550 | } 551 | 552 | // Date/time values. 553 | if ( 'CURRENT_TIMESTAMP' === $uppercase_default_value ) { 554 | return 'CURRENT_TIMESTAMP'; 555 | } elseif ( 'CURRENT_DATE' === $uppercase_default_value ) { 556 | return null; // Not supported in MySQL. 557 | } elseif ( 'CURRENT_TIME' === $uppercase_default_value ) { 558 | return null; // Not supported in MySQL. 559 | } 560 | 561 | // SQLite supports underscores in all numeric literals. 562 | $no_underscore_default_value = str_replace( '_', '', $default_value ); 563 | 564 | // Numeric literals. E.g.: 123, 1.23, -1.23, 1e3, 1.2e-3 565 | if ( is_numeric( $no_underscore_default_value ) ) { 566 | return $no_underscore_default_value; 567 | } 568 | 569 | // HEX literals (numeric). E.g.: 0x1a2f, 0X1A2F 570 | $value = filter_var( $no_underscore_default_value, FILTER_VALIDATE_INT, FILTER_FLAG_ALLOW_HEX ); 571 | if ( false !== $value ) { 572 | return $value; 573 | } 574 | 575 | // BLOB literals (string). E.g.: x'1a2f', X'1A2F' 576 | // Checking the prefix is enough as SQLite doesn't allow malformed values. 577 | if ( str_starts_with( $uppercase_default_value, "X'" ) ) { 578 | // Convert the hex string to ASCII bytes. 579 | return "'" . pack( 'H*', substr( $default_value, 2, -1 ) ) . "'"; 580 | } 581 | 582 | // Unquoted string literal. E.g.: abc 583 | return $this->quote_mysql_utf8_string_literal( $default_value ); 584 | } 585 | 586 | /** 587 | * Get a MySQL column or index data type from legacy data types cache table. 588 | * 589 | * This method retrieves MySQL column or index data types from a special table 590 | * that was used by an old version of the SQLite driver and that is otherwise 591 | * no longer needed. This is more precise than direct inference from SQLite. 592 | * 593 | * For columns, it returns full column type, including prefix length, e.g.: 594 | * int(11), bigint(20) unsigned, varchar(255), longtext 595 | * 596 | * For indexes, it returns one of: 597 | * KEY, PRIMARY, UNIQUE, FULLTEXT, SPATIAL 598 | * 599 | * @param string $table_name The table name. 600 | * @param string $column_or_index_name The column or index name. 601 | * @return string|null The MySQL definition, or null when not found. 602 | */ 603 | private function get_cached_mysql_data_type( string $table_name, string $column_or_index_name ): ?string { 604 | try { 605 | $mysql_type = $this->driver->execute_sqlite_query( 606 | 'SELECT mysql_type FROM _mysql_data_types_cache 607 | WHERE `table` = ? COLLATE NOCASE 608 | AND ( 609 | -- The old SQLite driver stored the MySQL data types in multiple 610 | -- formats - lowercase, uppercase, and, sometimes, with backticks. 611 | column_or_index = ? COLLATE NOCASE 612 | OR column_or_index = ? COLLATE NOCASE 613 | )', 614 | array( $table_name, $column_or_index_name, "`$column_or_index_name`" ) 615 | )->fetchColumn(); 616 | } catch ( PDOException $e ) { 617 | if ( str_contains( $e->getMessage(), 'no such table' ) ) { 618 | return null; 619 | } 620 | throw $e; 621 | } 622 | if ( false === $mysql_type ) { 623 | return null; 624 | } 625 | 626 | // Normalize index type for backward compatibility. Some older versions 627 | // of the SQLite driver stored index types with a " KEY" suffix, e.g., 628 | // "PRIMARY KEY" or "UNIQUE KEY". More recent versions omit the suffix. 629 | if ( str_ends_with( $mysql_type, ' KEY' ) ) { 630 | $mysql_type = substr( $mysql_type, 0, strlen( $mysql_type ) - strlen( ' KEY' ) ); 631 | } 632 | return $mysql_type; 633 | } 634 | 635 | /** 636 | * Get a MySQL column type from an SQLite column type. 637 | * 638 | * This method converts an SQLite column type to a MySQL column type as per 639 | * the SQLite column type affinity rules: 640 | * https://sqlite.org/datatype3.html#determination_of_column_affinity 641 | * 642 | * @param string $column_type The SQLite column type. 643 | * @return string The MySQL column type. 644 | */ 645 | private function get_mysql_column_type( string $column_type ): string { 646 | $type = strtoupper( $column_type ); 647 | 648 | /* 649 | * Following the rules of column affinity: 650 | * https://sqlite.org/datatype3.html#determination_of_column_affinity 651 | */ 652 | 653 | // 1. If the declared type contains the string "INT" then it is assigned 654 | // INTEGER affinity. 655 | if ( str_contains( $type, 'INT' ) ) { 656 | return 'int'; 657 | } 658 | 659 | // 2. If the declared type of the column contains any of the strings 660 | // "CHAR", "CLOB", or "TEXT" then that column has TEXT affinity. 661 | if ( str_contains( $type, 'TEXT' ) || str_contains( $type, 'CHAR' ) || str_contains( $type, 'CLOB' ) ) { 662 | return 'text'; 663 | } 664 | 665 | // 3. If the declared type for a column contains the string "BLOB" or 666 | // if no type is specified then the column has affinity BLOB. 667 | if ( str_contains( $type, 'BLOB' ) || '' === $type ) { 668 | return 'blob'; 669 | } 670 | 671 | // 4. If the declared type for a column contains any of the strings 672 | // "REAL", "FLOA", or "DOUB" then the column has REAL affinity. 673 | if ( str_contains( $type, 'REAL' ) || str_contains( $type, 'FLOA' ) ) { 674 | return 'float'; 675 | } 676 | if ( str_contains( $type, 'DOUB' ) ) { 677 | return 'double'; 678 | } 679 | 680 | /** 681 | * 5. Otherwise, the affinity is NUMERIC. 682 | * 683 | * While SQLite defaults to a NUMERIC column affinity, it's better to use 684 | * TEXT in this case, because numeric SQLite columns in non-strict tables 685 | * can contain any text data as well, when it is not a well-formed number. 686 | * 687 | * See: https://sqlite.org/datatype3.html#type_affinity 688 | */ 689 | return 'text'; 690 | } 691 | 692 | /** 693 | * Format a MySQL UTF-8 string literal for output in a CREATE TABLE statement. 694 | * 695 | * See WP_SQLite_Driver::quote_mysql_utf8_string_literal(). 696 | * 697 | * TODO: This is a copy of WP_SQLite_Driver::quote_mysql_utf8_string_literal(). 698 | * We may consider extracing it to reusable MySQL helpers. 699 | * 700 | * @param string $utf8_literal The UTF-8 string literal to escape. 701 | * @return string The escaped string literal. 702 | */ 703 | private function quote_mysql_utf8_string_literal( string $utf8_literal ): string { 704 | $backslash = chr( 92 ); 705 | $replacements = array( 706 | "'" => "''", // A single quote character ('). 707 | $backslash => $backslash . $backslash, // A backslash character (\). 708 | chr( 0 ) => $backslash . '0', // An ASCII NULL character (\0). 709 | chr( 10 ) => $backslash . 'n', // A newline (linefeed) character (\n). 710 | chr( 13 ) => $backslash . 'r', // A carriage return character (\r). 711 | ); 712 | return "'" . strtr( $utf8_literal, $replacements ) . "'"; 713 | } 714 | 715 | /** 716 | * Unquote a quoted MySQL identifier. 717 | * 718 | * Remove bounding quotes and replace escaped quotes with their values. 719 | * 720 | * @param string $quoted_identifier The quoted identifier value. 721 | * @return string The unquoted identifier value. 722 | */ 723 | private function unquote_mysql_identifier( string $quoted_identifier ): string { 724 | $first_byte = $quoted_identifier[0] ?? null; 725 | if ( '"' === $first_byte || '`' === $first_byte ) { 726 | $unquoted = substr( $quoted_identifier, 1, -1 ); 727 | return str_replace( $first_byte . $first_byte, $first_byte, $unquoted ); 728 | } 729 | return $quoted_identifier; 730 | } 731 | } 732 | --------------------------------------------------------------------------------