diff --git a/wp/wp-admin/about.php b/wp/wp-admin/about.php index 041a459dd..6fbd0c3c9 100644 --- a/wp/wp-admin/about.php +++ b/wp/wp-admin/about.php @@ -63,6 +63,26 @@

+

+ Version %s addressed some security issues.' ), + '7.0.3' + ); + ?> + the release notes.' ), + sprintf( + /* translators: %s: WordPress version. */ + esc_url( __( 'https://wordpress.org/documentation/wordpress-version/version-%s/' ) ), + sanitize_title( '7.0.3' ) + ) + ); + ?> +

user_login = sanitize_user( wp_unslash( $_POST['user_login'] ), true ); } + $errors = new WP_Error(); + $pass1 = ''; $pass2 = ''; if ( isset( $_POST['pass1'] ) ) { @@ -78,7 +80,12 @@ function edit_user( $user_id = 0 ) { } if ( isset( $_POST['email'] ) ) { - $user->user_email = sanitize_text_field( wp_unslash( $_POST['email'] ) ); + $maybe_email = wp_unslash( $_POST['email'] ); + if ( is_string( $maybe_email ) && is_email( $maybe_email ) ) { + $user->user_email = $maybe_email; + } else { + $errors->add( 'invalid_email', __( 'Error: The email address is not correct.' ), array( 'form-field' => 'email' ) ); + } } if ( isset( $_POST['url'] ) ) { if ( empty( $_POST['url'] ) || 'http://' === $_POST['url'] ) { @@ -145,8 +152,6 @@ function edit_user( $user_id = 0 ) { $user->use_ssl = 1; } - $errors = new WP_Error(); - /* checking that username has been typed */ if ( '' === $user->user_login ) { $errors->add( 'user_login', __( 'Error: Please enter a username.' ) ); diff --git a/wp/wp-admin/js/inline-edit-post.js b/wp/wp-admin/js/inline-edit-post.js index 6e9f4e9f2..38557e233 100644 --- a/wp/wp-admin/js/inline-edit-post.js +++ b/wp/wp-admin/js/inline-edit-post.js @@ -359,7 +359,12 @@ window.wp = window.wp || {}; if ( !$(':input[name="post_author"] option[value="' + $('.post_author', rowData).text() + '"]', editRow).val() ) { // The post author no longer has edit capabilities, so we need to add them to the list of authors. - $(':input[name="post_author"]', editRow).prepend(''); + $(':input[name="post_author"]', editRow).prepend( + new Option( + $('#post-' + id + ' .author').text(), + $('.post_author', rowData).text() + ) + ); } if ( $( ':input[name="post_author"] option', editRow ).length === 1 ) { $('label.inline-edit-author', editRow).hide(); diff --git a/wp/wp-admin/js/inline-edit-post.min.js b/wp/wp-admin/js/inline-edit-post.min.js index cd0d3d0da..ff6d0e373 100644 --- a/wp/wp-admin/js/inline-edit-post.min.js +++ b/wp/wp-admin/js/inline-edit-post.min.js @@ -1,2 +1,2 @@ /*! This file is auto-generated */ -window.wp=window.wp||{},function(u,h){window.inlineEditPost={init:function(){var i=this,t=u("#inline-edit"),e=u("#bulk-edit"),t=(i.type=u("table.widefat").hasClass("pages")?"page":"post",i.what="#post-",t.on("keyup",function(t){if(27===t.which)return inlineEditPost.revert()}),e.on("keyup",function(t){if(27===t.which)return inlineEditPost.revert()}),u(".cancel",t).on("click",function(){return inlineEditPost.revert()}),u(".save",t).on("click",function(){return inlineEditPost.save(this)}),u("td",t).on("keydown",function(t){if(13===t.which&&!u(t.target).hasClass("cancel"))return inlineEditPost.save(this)}),u(".cancel",e).on("click",function(){return inlineEditPost.revert()}),u('#inline-edit .inline-edit-private input[value="private"]').on("click",function(){var t=u("input.inline-edit-password-input");u(this).prop("checked")?t.val("").prop("disabled",!0):t.prop("disabled",!1)}),u("#the-list").on("click",".editinline",function(){u(this).attr("aria-expanded","true"),inlineEditPost.edit(this)}),u("#inline-edit fieldset.inline-edit-categories").clone());t.find("*[id]").each(function(){this.id="bulk-edit-"+this.id}),u("#bulk-edit").find("fieldset:first").after(t).siblings("fieldset:last").prepend(u("#inline-edit .inline-edit-tags-wrap").clone()),u('select[name="_status"] option[value="future"]',e).remove(),u("#doaction").on("click",function(t){var e;u('#posts-filter .check-column input[type="checkbox"]:checked').length<1||(i.whichBulkButtonId=u(this).attr("id"),e=i.whichBulkButtonId.substr(2),"edit"===u('select[name="'+e+'"]').val()?(t.preventDefault(),i.setBulk()):0

  • ")}),a)return this.revert();u("#bulk-titles").html('"),e.each(function(){var t=u(this).val();u("#category_"+t).text().split(",").map(function(t){i[t]||(i[t]=0),i[t]++})}),u('.inline-edit-categories input[name="post_category[]"]').each(function(){var t;i[u(this).val()]==e.length?u(this).prop("checked",!0):0').attr("aria-label",t.trim()+": "+h.i18n.__("Some selected posts have this category"))))}),u('.inline-edit-categories input[name="post_category[]"]:indeterminate').on("change",function(){u(this).removeAttr("aria-label").parent().find('input[name="indeterminate_post_category[]"]').remove()}),u(".inline-edit-save button").on("click",function(){u('.inline-edit-categories input[name="post_category[]"]').prop("indeterminate",!1)}),u("#bulk-titles .ntdelbutton").click(function(){var t=u(this),e=t.attr("id").substr(1),i=t.parent().prev().children(".ntdelbutton"),t=t.parent().next().children(".ntdelbutton");u("input#cb-select-all-1, input#cb-select-all-2").prop("checked",!1),u('table.widefat input[value="'+e+'"]').prop("checked",!1),u("#_"+e).parent().remove(),h.a11y.speak(h.i18n.__("Item removed."),"assertive"),t.length?t.focus():i.length?i.focus():(u("#bulk-titles-list").remove(),inlineEditPost.revert(),h.a11y.speak(h.i18n.__("All selected items have been removed. Select new items to use Bulk Actions.")))}),"post"===t&&u("tr.inline-editor textarea[data-wp-taxonomy]").each(function(t,e){u(e).autocomplete("instance")||u(e).wpTagsSuggest()}),u("#bulk-edit .inline-edit-wrapper").attr("tabindex","-1").focus(),u("html, body").animate({scrollTop:0},"fast")},edit:function(n){var t,a,e,i,s,r,o,l,d=this,c=!0;for(d.revert(),"object"==typeof n&&(n=d.getId(n)),t=["post_title","post_name","post_author","_status","jj","mm","aa","hh","mn","ss","post_password","post_format","menu_order","page_template"],"page"===d.type&&t.push("post_parent"),a=u("#inline-edit").clone(!0),u("td",a).attr("colspan",u("th:visible, td:visible",".widefat:first thead").length),u("td",a).find("#quick-edit-legend").removeAttr("id"),u("td",a).find('p[id^="quick-edit-"]').removeAttr("id"),u(d.what+n).removeClass("is-expanded").hide().after(a).after(''),e=u("#inline_"+n),u(':input[name="post_author"] option[value="'+u(".post_author",e).text()+'"]',a).val()||u(':input[name="post_author"]',a).prepend('"),1===u(':input[name="post_author"] option',a).length&&u("label.inline-edit-author",a).hide(),o=0;od?u('select[name="_status"] option[value="future"]',a):u('select[name="_status"] option[value="publish"]',a)).remove(),d=u(".inline-edit-password-input").prop("disabled",!1),"private"===p&&(u('input[name="keep_private"]',a).prop("checked",!0),d.val("").prop("disabled",!0)),0<(i=u('select[name="post_parent"] option[value="'+n+'"]',a)).length){for(s=i[0].className.split("-")[1],r=i;c&&0!==(r=r.next("option")).length;)r[0].className.split("-")[1]<=s?c=!1:(r.remove(),r=i);i.remove()}return u(a).attr("id","edit-"+n).addClass("inline-editor").show(),u(".ptitle",a).trigger("focus"),!1},save:function(n){var t=u(".post_status_page").val()||"";return"object"==typeof n&&(n=this.getId(n)),u("table.widefat .spinner").addClass("is-active"),t={action:"inline-save",post_type:typenow,post_ID:n,edit_date:"true",post_status:t},t=u("#edit-"+n).find(":input").serialize()+"&"+u.param(t),u.post(ajaxurl,t,function(t){var e=u("#edit-"+n+" .inline-edit-save .notice-error"),i=e.find(".error");u("table.widefat .spinner").removeClass("is-active"),t?-1!==t.indexOf("]*?>/g,""),e.removeClass("hidden"),i.html(t),h.a11y.speak(i.text())):(e.removeClass("hidden"),i.text(h.i18n.__("Error while saving the changes.")),h.a11y.speak(h.i18n.__("Error while saving the changes.")))},"html"),!1},revert:function(){var t=u(".widefat"),e=u(".inline-editor",t).attr("id");return e&&(u(".spinner",t).removeClass("is-active"),("bulk-edit"===e?(u("#bulk-edit",t).removeClass("inline-editor").hide().siblings(".hidden").remove(),u("#bulk-titles").empty(),u("#inlineedit").append(u("#bulk-edit")),u("#"+inlineEditPost.whichBulkButtonId)):(u("#"+e).siblings("tr.hidden").addBack().remove(),e=e.substr(e.lastIndexOf("-")+1),u(this.what+e).show().find(".editinline").attr("aria-expanded","false"))).trigger("focus")),!1},getId:function(t){t=u(t).closest("tr").attr("id").split("-");return t[t.length-1]}},u(function(){inlineEditPost.init()}),u(function(){void 0!==h&&h.heartbeat&&h.heartbeat.interval(10)}).on("heartbeat-tick.wp-check-locked-posts",function(t,e){var n=e["wp-check-locked-posts"]||{},a="wp-locked";u("#the-list tr").each(function(t,e){var i=e.id,e=u(e);n.hasOwnProperty(i)?e.hasClass(a)||(i=n[i],e.find(".column-title .locked-text").text(i.text),e.find(".check-column checkbox").prop("checked",!1),i.avatar_src&&(i=u("",{class:"avatar avatar-18 photo",width:18,height:18,alt:"",src:i.avatar_src,srcset:i.avatar_src_2x?i.avatar_src_2x+" 2x":void 0}),e.find(".column-title .locked-avatar").empty().append(i)),e.addClass(a)):e.hasClass(a)&&e.removeClass(a).find(".locked-info span").empty()})}).on("heartbeat-send.wp-check-locked-posts",function(t,e){var i=[];u("#the-list tr").each(function(t,e){e.id&&i.push(e.id)}),i.length&&(e["wp-check-locked-posts"]=i)})}(jQuery,window.wp); \ No newline at end of file +window.wp=window.wp||{},function(u,h){window.inlineEditPost={init:function(){var i=this,t=u("#inline-edit"),e=u("#bulk-edit"),t=(i.type=u("table.widefat").hasClass("pages")?"page":"post",i.what="#post-",t.on("keyup",function(t){if(27===t.which)return inlineEditPost.revert()}),e.on("keyup",function(t){if(27===t.which)return inlineEditPost.revert()}),u(".cancel",t).on("click",function(){return inlineEditPost.revert()}),u(".save",t).on("click",function(){return inlineEditPost.save(this)}),u("td",t).on("keydown",function(t){if(13===t.which&&!u(t.target).hasClass("cancel"))return inlineEditPost.save(this)}),u(".cancel",e).on("click",function(){return inlineEditPost.revert()}),u('#inline-edit .inline-edit-private input[value="private"]').on("click",function(){var t=u("input.inline-edit-password-input");u(this).prop("checked")?t.val("").prop("disabled",!0):t.prop("disabled",!1)}),u("#the-list").on("click",".editinline",function(){u(this).attr("aria-expanded","true"),inlineEditPost.edit(this)}),u("#inline-edit fieldset.inline-edit-categories").clone());t.find("*[id]").each(function(){this.id="bulk-edit-"+this.id}),u("#bulk-edit").find("fieldset:first").after(t).siblings("fieldset:last").prepend(u("#inline-edit .inline-edit-tags-wrap").clone()),u('select[name="_status"] option[value="future"]',e).remove(),u("#doaction").on("click",function(t){var e;u('#posts-filter .check-column input[type="checkbox"]:checked').length<1||(i.whichBulkButtonId=u(this).attr("id"),e=i.whichBulkButtonId.substr(2),"edit"===u('select[name="'+e+'"]').val()?(t.preventDefault(),i.setBulk()):0
    - -
    -

    - ' . esc_html( basename( WP_CONTENT_DIR ) ) . '/db.php' - ); - ?> -

    -
    - +
    +

    ' . esc_html( basename( WP_CONTENT_DIR ) ) . '/db.php' ); ?> - - -

    -

    - ' . esc_html( basename( WP_CONTENT_DIR ) ) . '/db.php' - ); - ?> -

    -
    - +

    +

    @@ -136,7 +123,7 @@ function sqlite_integration_admin_screen() {

    - +
    'sqlite-db-integration', 'parent' => 'top-secondary', 'title' => $title, - 'href' => esc_url( admin_url( 'options-general.php?page=sqlite-integration' ) ), + 'href' => esc_url( sqlite_plugin_get_admin_page_url() ), 'meta' => false, ); $admin_bar->add_node( $args ); } add_action( 'admin_bar_menu', 'sqlite_plugin_adminbar_item', 999 ); + +/** + * Get the SQLite integration admin page URL. + * + * @access private + * + * @return string Admin page URL. + */ +function sqlite_plugin_get_admin_page_url() { + if ( is_multisite() ) { + return network_admin_url( 'settings.php?page=sqlite-integration' ); + } + return admin_url( 'options-general.php?page=sqlite-integration' ); +} diff --git a/wp/wp-content/plugins/sqlite-database-integration/capabilities.php b/wp/wp-content/plugins/sqlite-database-integration/capabilities.php new file mode 100644 index 000000000..deab5108c --- /dev/null +++ b/wp/wp-content/plugins/sqlite-database-integration/capabilities.php @@ -0,0 +1,80 @@ +set_prefix( $table_prefix ); - // Get the perflab options, remove the database/sqlite module and update the option. - $row = $wpdb_mysql->get_row( $wpdb_mysql->prepare( "SELECT option_value FROM $wpdb_mysql->options WHERE option_name = %s LIMIT 1", 'active_plugins' ) ); - if ( is_object( $row ) ) { - $value = maybe_unserialize( $row->option_value ); - if ( is_array( $value ) ) { - $value_flipped = array_flip( $value ); - $items = array_reverse( explode( DIRECTORY_SEPARATOR, SQLITE_MAIN_FILE ) ); - $item = $items[1] . DIRECTORY_SEPARATOR . $items[0]; - unset( $value_flipped[ $item ] ); - $value = array_flip( $value_flipped ); - $wpdb_mysql->update( $wpdb_mysql->options, array( 'option_value' => maybe_serialize( $value ) ), array( 'option_name' => 'active_plugins' ) ); - } - } + sqlite_plugin_deactivate_in_mysql( $wpdb_mysql, $network_deactivating ); }, PHP_INT_MAX ); @@ -66,3 +62,52 @@ function () { wp_cache_flush(); } register_deactivation_hook( SQLITE_MAIN_FILE, 'sqlite_plugin_remove_db_file' ); // Remove db.php file on plugin deactivation. + +/** + * Deactivate the plugin in the original MySQL database. + * + * @access private + * + * @param wpdb $wpdb_mysql MySQL database connection. + * @param bool $network_deactivating Whether the plugin is being deactivated network-wide. + */ +function sqlite_plugin_deactivate_in_mysql( $wpdb_mysql, $network_deactivating ) { + if ( $network_deactivating ) { + $network_id = get_current_network_id(); + $row = $wpdb_mysql->get_row( $wpdb_mysql->prepare( "SELECT meta_value AS active_plugins FROM $wpdb_mysql->sitemeta WHERE site_id = %d AND meta_key = %s LIMIT 1", $network_id, 'active_sitewide_plugins' ) ); + $table = $wpdb_mysql->sitemeta; + $value_column = 'meta_value'; + $where = array( + 'site_id' => $network_id, + 'meta_key' => 'active_sitewide_plugins', + ); + } else { + $row = $wpdb_mysql->get_row( $wpdb_mysql->prepare( "SELECT option_value AS active_plugins FROM $wpdb_mysql->options WHERE option_name = %s LIMIT 1", 'active_plugins' ) ); + $table = $wpdb_mysql->options; + $value_column = 'option_value'; + $where = array( 'option_name' => 'active_plugins' ); + } + + if ( ! is_object( $row ) ) { + return; + } + + $active_plugins = maybe_unserialize( $row->active_plugins ); + if ( ! is_array( $active_plugins ) ) { + return; + } + + $item = plugin_basename( SQLITE_MAIN_FILE ); + if ( $network_deactivating ) { + $key = $item; + } else { + $key = array_search( $item, $active_plugins, true ); + } + + if ( false === $key || ! array_key_exists( $key, $active_plugins ) ) { + return; + } + + unset( $active_plugins[ $key ] ); + $wpdb_mysql->update( $table, array( $value_column => maybe_serialize( $active_plugins ) ), $where ); +} diff --git a/wp/wp-content/plugins/sqlite-database-integration/load.php b/wp/wp-content/plugins/sqlite-database-integration/load.php index fe8fbd5d5..ba8e6f5d8 100644 --- a/wp/wp-content/plugins/sqlite-database-integration/load.php +++ b/wp/wp-content/plugins/sqlite-database-integration/load.php @@ -3,8 +3,9 @@ * Plugin Name: SQLite Database Integration * Description: SQLite database driver drop-in. * Author: The WordPress Team - * Version: 3.0.0-rc.7 + * Version: 3.0.0-rc.8 * Requires PHP: 7.2 + * Network: true * Textdomain: sqlite-database-integration * * This feature plugin allows WordPress to use SQLite instead of MySQL as its database. @@ -20,6 +21,7 @@ define( 'SQLITE_MAIN_FILE', __FILE__ ); +require_once __DIR__ . '/capabilities.php'; require_once __DIR__ . '/admin-page.php'; require_once __DIR__ . '/activate.php'; require_once __DIR__ . '/deactivate.php'; diff --git a/wp/wp-content/plugins/sqlite-database-integration/readme.txt b/wp/wp-content/plugins/sqlite-database-integration/readme.txt index d19a6521c..ae7a6fe95 100644 --- a/wp/wp-content/plugins/sqlite-database-integration/readme.txt +++ b/wp/wp-content/plugins/sqlite-database-integration/readme.txt @@ -4,7 +4,7 @@ Contributors: wordpressdotorg, aristath, janjakes, zieladam, berislav.grgic Requires at least: 6.4 Tested up to: 7.0 Requires PHP: 7.2 -Stable tag: 3.0.0-rc.7 +Stable tag: 3.0.0-rc.8 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html Tags: performance, database @@ -44,6 +44,17 @@ with SQLite syntax and behavior. == Changelog == += 3.0.0-rc.8 = + +* Preserve aliases in `UPDATE JOIN` translation ([#462](https://github.com/WordPress/sqlite-database-integration/pull/462)) +* Restrict SQLite installation permissions ([#464](https://github.com/WordPress/sqlite-database-integration/pull/464)) +* Improve string escaping ([#466](https://github.com/WordPress/sqlite-database-integration/pull/466)) +* Stabilize WordPress E2E tests ([#459](https://github.com/WordPress/sqlite-database-integration/pull/459)) +* Address WordPress PHPUnit test fails: charset detection, length validation etc ([#331](https://github.com/WordPress/sqlite-database-integration/pull/331)) +* Add Unicode support to the user-defined `REVERSE()` function ([#453](https://github.com/WordPress/sqlite-database-integration/pull/453)) +* Simplify the MySQL-on-SQLite driver API ([#449](https://github.com/WordPress/sqlite-database-integration/pull/449)) +* Add WordPress 7.0 compatibility ([#450](https://github.com/WordPress/sqlite-database-integration/pull/450)) + = 3.0.0-rc.7 = * Preserve configured SQLite journal mode in driver wrapper ([#447](https://github.com/WordPress/sqlite-database-integration/pull/447)) diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/load.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/load.php index 165e27e97..8ef704366 100644 --- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/load.php +++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/load.php @@ -36,10 +36,10 @@ require_once __DIR__ . '/sqlite/class-wp-sqlite-connection.php'; require_once __DIR__ . '/sqlite/class-wp-sqlite-configurator.php'; require_once __DIR__ . '/sqlite/class-wp-sqlite-driver.php'; -require_once __DIR__ . '/sqlite/class-wp-sqlite-driver-exception.php'; +require_once __DIR__ . '/sqlite/class-wp-mysql-on-sqlite-exception.php'; require_once __DIR__ . '/sqlite/class-wp-sqlite-information-schema-builder.php'; require_once __DIR__ . '/sqlite/class-wp-sqlite-information-schema-exception.php'; require_once __DIR__ . '/sqlite/class-wp-sqlite-information-schema-reconstructor.php'; require_once __DIR__ . '/sqlite/class-wp-sqlite-pdo-user-defined-functions.php'; require_once __DIR__ . '/sqlite/class-wp-mysql-on-sqlite.php'; -require_once __DIR__ . '/sqlite/class-wp-pdo-proxy-statement.php'; +require_once __DIR__ . '/sqlite/class-wp-mysql-on-sqlite-statement.php'; diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/mysql/class-wp-mysql-lexer.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/mysql/class-wp-mysql-lexer.php index d6ee9970e..bd9a617cd 100644 --- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/mysql/class-wp-mysql-lexer.php +++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/mysql/class-wp-mysql-lexer.php @@ -16,6 +16,8 @@ * https://github.com/mysql/mysql-workbench/blob/8.0.38/library/parsers/grammars/MySQLLexer.g4 * https://github.com/mysql/mysql-workbench/blob/8.0.38/library/parsers/grammars/predefined.tokens * https://github.com/mysql/mysql-workbench/blob/8.0.38/library/parsers/mysql/MySQLBaseLexer.cpp + * + * @access private */ class WP_MySQL_Lexer { /** @@ -32,6 +34,7 @@ class WP_MySQL_Lexer { const SQL_MODE_PIPES_AS_CONCAT = 2; const SQL_MODE_IGNORE_SPACE = 4; const SQL_MODE_NO_BACKSLASH_ESCAPES = 8; + const SQL_MODE_ANSI_QUOTES = 16; /** * Character masks for frequently used character classes. @@ -2209,6 +2212,19 @@ public function __construct( $this->sql_modes |= self::SQL_MODE_IGNORE_SPACE; } elseif ( 'NO_BACKSLASH_ESCAPES' === $sql_mode ) { $this->sql_modes |= self::SQL_MODE_NO_BACKSLASH_ESCAPES; + } elseif ( 'ANSI_QUOTES' === $sql_mode ) { + $this->sql_modes |= self::SQL_MODE_ANSI_QUOTES; + } elseif ( 'ANSI' === $sql_mode ) { + /* + * Expand the composite ANSI mode into its lexer-relevant components. + * The ANSI mode also implies REAL_AS_FLOAT and ONLY_FULL_GROUP_BY, + * which do not affect the lexer. + * + * See: https://dev.mysql.com/doc/refman/8.4/en/sql-mode.html#sqlmode_ansi + */ + $this->sql_modes |= self::SQL_MODE_PIPES_AS_CONCAT + | self::SQL_MODE_IGNORE_SPACE + | self::SQL_MODE_ANSI_QUOTES; } } } @@ -2891,15 +2907,30 @@ private function read_number(): ?int { * * Rules: * 1. Quotes can be escaped by doubling them ('', "", ``). - * 2. Backslashes escape the next character, unless NO_BACKSLASH_ESCAPES is set. + * 2. In string literals, backslashes escape the next character, + * unless the NO_BACKSLASH_ESCAPES SQL mode is set. + * 3. In identifiers, backslashes are always literal and never escape. */ private function read_quoted_text(): ?int { $quote = $this->sql[ $this->bytes_already_read ]; $this->bytes_already_read += 1; // Consume the quote. - $no_backslash_escapes = $this->is_sql_mode_active( - self::SQL_MODE_NO_BACKSLASH_ESCAPES - ); + /* + * Determine whether the quote opens an identifier or a string literal. + * An identifier is quoted with a backtick or a double quote when the + * ANSI_QUOTES SQL mode is active. Otherwise, it is a string literal. + * + * See: https://dev.mysql.com/doc/refman/8.4/en/sql-mode.html#sqlmode_ansi_quotes + */ + $is_identifier_quote = '`' === $quote + || ( '"' === $quote && $this->is_sql_mode_active( self::SQL_MODE_ANSI_QUOTES ) ); + + /* + * Backslash escapes apply only to string literals, and only when the + * NO_BACKSLASH_ESCAPES SQL mode is not set. + */ + $backslash_is_escape = ! $is_identifier_quote + && ! $this->is_sql_mode_active( self::SQL_MODE_NO_BACKSLASH_ESCAPES ); // We need to look for the closing quote in a loop, as it can be escaped, // in which case the escape sequence is consumed and the loop continues. @@ -2912,9 +2943,9 @@ private function read_quoted_text(): ?int { $at = $quote_at; /* - * By default, quotes can be escaped with a "\". - * When NO_BACKSLASH_ESCAPES SQL mode is active, the "\" treated as - * a regular character. + * In string literals, quotes can be escaped with a backslash. When + * NO_BACKSLASH_ESCAPES SQL mode is active, the backslash is treated + * as a regular character. Identifiers never use backslash escaping. * * The quote is escaped only when the number of preceding backslashes * is odd - "\" is an escape sequence, "\\" is an escaped backslash, @@ -2925,7 +2956,7 @@ private function read_quoted_text(): ?int { * sits at the very start of the input. The `?? null` covers * positive out-of-range indexes belt-and-suspenders. */ - if ( ! $no_backslash_escapes ) { + if ( $backslash_is_escape ) { $i = 0; while ( ( $at - $i - 1 ) >= 0 && '\\' === ( $this->sql[ $at - $i - 1 ] ?? null ) ) { $i += 1; @@ -2948,13 +2979,10 @@ private function read_quoted_text(): ?int { $this->bytes_already_read = $at; - if ( '`' === $quote ) { + if ( $is_identifier_quote ) { return self::BACK_TICK_QUOTED_ID; - } elseif ( '"' === $quote ) { - return self::DOUBLE_QUOTED_TEXT; - } else { - return self::SINGLE_QUOTED_TEXT; } + return '"' === $quote ? self::DOUBLE_QUOTED_TEXT : self::SINGLE_QUOTED_TEXT; } private function read_line_comment(): int { diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/mysql/class-wp-mysql-parser.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/mysql/class-wp-mysql-parser.php index 69282b9c4..0417085fe 100644 --- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/mysql/class-wp-mysql-parser.php +++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/mysql/class-wp-mysql-parser.php @@ -1,5 +1,10 @@ sql_mode_no_backslash_escapes_enabled ) { + if ( + WP_MySQL_Lexer::BACK_TICK_QUOTED_ID === $this->id + || $this->sql_mode_no_backslash_escapes_enabled + ) { return str_replace( $quote . $quote, $quote, $value ); } diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/mysql/native/class-wp-mysql-lexer.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/mysql/native/class-wp-mysql-lexer.php index def8ca3fa..1a645ab52 100644 --- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/mysql/native/class-wp-mysql-lexer.php +++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/mysql/native/class-wp-mysql-lexer.php @@ -1,3 +1,8 @@ */ diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/mysql/native/trait-wp-mysql-native-parser-impl.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/mysql/native/trait-wp-mysql-native-parser-impl.php index c53e96e53..52628831a 100644 --- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/mysql/native/trait-wp-mysql-native-parser-impl.php +++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/mysql/native/trait-wp-mysql-native-parser-impl.php @@ -13,6 +13,8 @@ * * Adding a public method here is enough to plumb a new public method * through to the native parser; the using class does not need touching. + * + * @access private */ trait WP_MySQL_Native_Parser_Impl { /** diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/parser/class-wp-parser-grammar.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/parser/class-wp-parser-grammar.php index 9bf30b973..5e9222112 100644 --- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/parser/class-wp-parser-grammar.php +++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/parser/class-wp-parser-grammar.php @@ -8,6 +8,8 @@ * representation, and precomputes a lookup table for quick branch selection. * * @TODO: Add more details about the grammar implementation. + * + * @access private */ class WP_Parser_Grammar { /** diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/parser/class-wp-parser-node.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/parser/class-wp-parser-node.php index b61f38d5e..c64c83db8 100644 --- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/parser/class-wp-parser-node.php +++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/parser/class-wp-parser-node.php @@ -8,6 +8,8 @@ * Each node can contain children, consisting of other nodes and grammar tokens. * In this way, a parser node constitutes a recursive structure that represents * a parse (sub)tree at each level of the full grammar tree. + * + * @access private */ class WP_Parser_Node { /** diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/parser/class-wp-parser-token.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/parser/class-wp-parser-token.php index 4132ba382..0f4ea54b2 100644 --- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/parser/class-wp-parser-token.php +++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/parser/class-wp-parser-token.php @@ -7,6 +7,8 @@ * In a parse tree, a token represent a leaf, that is, a node without children. * It is a simple generic container for a token ID and value, that can be used * as a base class and extended for specific use cases. + * + * @access private */ class WP_Parser_Token { /** diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/parser/class-wp-parser.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/parser/class-wp-parser.php index 4436892fa..9953f1a58 100644 --- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/parser/class-wp-parser.php +++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/parser/class-wp-parser.php @@ -7,6 +7,8 @@ * * @TODO: Add a detailed description and list the properties that a grammar must * satisfy in order to be supported by this parser (e.g., no left recursion). + * + * @access private */ class WP_Parser { protected $grammar; diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-mysql-on-sqlite-exception.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-mysql-on-sqlite-exception.php new file mode 100644 index 000000000..72112c16e --- /dev/null +++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-mysql-on-sqlite-exception.php @@ -0,0 +1,59 @@ +code = $code; + $this->driver = $driver; + $this->errorInfo = $error_info ?? $this->create_error_info( $message, $code, $previous ); + } + + public function get_driver(): WP_MySQL_On_SQLite { + return $this->driver; + } + + /** + * Create PDO-style error information from an originating exception or from + * the emulated driver error. + * + * @param string $message The exception message. + * @param int|string $code The exception code. + * @param Throwable|null $previous The previous throwable. + * @return array PDO-style error information. + */ + private function create_error_info( string $message, $code, ?Throwable $previous ): array { + if ( $previous instanceof PDOException && is_array( $previous->errorInfo ) ) { + return $previous->errorInfo; + } + + $sqlstate = is_string( $code ) && 5 === strlen( $code ) ? $code : 'HY000'; + return array( $sqlstate, 1105, $message ); + } +} diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-pdo-proxy-statement.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-mysql-on-sqlite-statement.php similarity index 77% rename from wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-pdo-proxy-statement.php rename to wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-mysql-on-sqlite-statement.php index f56340d70..7e01766be 100644 --- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-pdo-proxy-statement.php +++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-mysql-on-sqlite-statement.php @@ -8,6 +8,7 @@ * PDO uses camel case naming, enable non-snake case: * phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid * phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase + * phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase * * PDO uses $class as a variable name, enable it: * phpcs:disable Universal.NamingConventions.NoReservedKeywordParameterNames.classFound @@ -26,7 +27,12 @@ * we conditionally define traits with different APIs based on the PHP version. */ if ( PHP_VERSION_ID < 80000 ) { - trait WP_PDO_Proxy_Statement_PHP_Compat { + /** + * PHP compatibility methods for WP_MySQL_On_SQLite_Statement. + * + * @access private + */ + trait WP_MySQL_On_SQLite_Statement_PHP_Compat { /** * Set the default fetch mode for this statement. * @@ -59,9 +65,26 @@ public function fetchAll( $mode = null, $class_name = null, $constructor_args = } return $this->fetchAllRows( $mode, $class_name, $constructor_args ); } + + /** + * Get metadata for a column in a result set. + * + * @param int $column The index of the column (0-indexed). + * @return array|false The column metadata as an associative array, + * or false if the column does not exist. + */ + #[ReturnTypeWillChange] + public function getColumnMeta( $column ) { + return $this->getColumnMetadata( $column ); + } } } else { - trait WP_PDO_Proxy_Statement_PHP_Compat { + /** + * PHP compatibility methods for WP_MySQL_On_SQLite_Statement. + * + * @access private + */ + trait WP_MySQL_On_SQLite_Statement_PHP_Compat { /** * Set the default fetch mode for this statement. * @@ -84,14 +107,29 @@ public function setFetchMode( $mode, ...$args ): bool { public function fetchAll( $mode = PDO::FETCH_DEFAULT, ...$args ): array { return $this->fetchAllRows( $mode, ...$args ); } + + /** + * Get metadata for a column in a result set. + * + * @param int $column The index of the column (0-indexed). + * @return array|false The column metadata as an associative array, + * or false if the column does not exist. + */ + #[ReturnTypeWillChange] + public function getColumnMeta( int $column ) { + if ( $column < 0 ) { + throw new ValueError( 'PDOStatement::getColumnMeta(): Argument #1 ($column) must be greater than or equal to 0' ); + } + return $this->getColumnMetadata( $column ); + } } } /** - * PDOStatement implementation that operates on in-memory data. + * PDOStatement implementation for MySQL-on-SQLite query results. * - * This class implements a complete PDOStatement interface on top of PHP arrays. - * It is used for result sets that are composed or transformed in the PHP layer. + * Delegates operations to the underlying SQLite statement while adapting + * MySQL-specific behavior such as affected row counts. * * PDO supports the following fetch modes: * - PDO::FETCH_DEFAULT: current default fetch mode (available from PHP 8.0) @@ -108,8 +146,8 @@ public function fetchAll( $mode = PDO::FETCH_DEFAULT, ...$args ): array { * - PDO::FETCH_BOUND: bind values to PHP variables, can't be used with fetchAll() * - PDO::FETCH_FUNC: custom function, only works with fetchAll(), can't be default [1 extra arg] */ -class WP_PDO_Proxy_Statement extends PDOStatement { - use WP_PDO_Proxy_Statement_PHP_Compat; +class WP_MySQL_On_SQLite_Statement extends PDOStatement implements IteratorAggregate { + use WP_MySQL_On_SQLite_Statement_PHP_Compat; /** * The original PDO statement. @@ -118,6 +156,20 @@ class WP_PDO_Proxy_Statement extends PDOStatement { */ private $statement; + /** + * Resolve MySQL-compatible metadata by column index. + * + * @var callable + */ + private $column_meta_resolver; + + /** + * Resolved MySQL-compatible metadata, keyed by column index. + * + * @var array + */ + private $resolved_column_meta = array(); + /** * The number of affected rows. * @@ -128,15 +180,25 @@ class WP_PDO_Proxy_Statement extends PDOStatement { /** * Constructor. * - * @param PDOStatement $statement The original PDO statement. - * @param int $affected_rows The number of affected rows. + * @param PDOStatement $statement The original PDO statement. + * @param string $query The original MySQL query. + * @param callable $column_meta_resolver Resolves metadata by column index. + * @param int|null $affected_rows The number of affected rows. */ public function __construct( PDOStatement $statement, + string $query, + callable $column_meta_resolver, ?int $affected_rows = null ) { - $this->statement = $statement; - $this->affected_rows = $affected_rows; + $this->statement = $statement; + + // Userland can only initialize PDOStatement::$queryString on PHP 8.1+. + if ( PHP_VERSION_ID >= 80100 ) { + $this->queryString = $query; + } + $this->column_meta_resolver = $column_meta_resolver; + $this->affected_rows = $affected_rows; } /** @@ -211,17 +273,6 @@ public function fetchObject( $class = 'stdClass', $constructorArgs = array() ) { return $this->statement->fetchObject( $class, $constructorArgs ); } - /** - * Get metadata for a column in a result set. - * - * @param int $column The index of the column (0-indexed). - * @return array|false The column metadata as an associative array, - * or false if the column does not exist. - */ - public function getColumnMeta( $column ): array { - throw new RuntimeException( 'Not implemented' ); - } - /** * Fetch the SQLSTATE associated with the last statement operation. * @@ -229,7 +280,7 @@ public function getColumnMeta( $column ): array { * or null if there is no error. */ public function errorCode(): ?string { - throw new RuntimeException( 'Not implemented' ); + return $this->statement->errorCode(); } /** @@ -241,7 +292,11 @@ public function errorCode(): ?string { * 2: Driver-specific error message. */ public function errorInfo(): array { - throw new RuntimeException( 'Not implemented' ); + // Normalize successful results. PDO_SQLite may retain stale driver-specific fields on PHP < 8.0. + if ( '00000' === $this->statement->errorCode() ) { + return array( '00000', null, null ); + } + return $this->statement->errorInfo(); } /** @@ -272,7 +327,7 @@ public function setAttribute( $attribute, $value ): bool { * @return Iterator The iterator for the result set. */ public function getIterator(): Iterator { - throw new RuntimeException( 'Not implemented' ); + yield from $this->statement; } /** @@ -290,7 +345,7 @@ public function nextRowset(): bool { * @return bool True on success, false on failure. */ public function closeCursor(): bool { - throw new RuntimeException( 'Not implemented' ); + return $this->statement->closeCursor(); } /** @@ -303,8 +358,8 @@ public function closeCursor(): bool { * @param mixed $driverOptions Optional parameters for the driver. * @return bool True on success, false on failure. */ - public function bindColumn( $column, &$var, $type = null, $maxLength = null, $driverOptions = null ): bool { - throw new RuntimeException( 'Not implemented' ); + public function bindColumn( $column, &$var, $type = PDO::PARAM_STR, $maxLength = 0, $driverOptions = null ): bool { + return $this->statement->bindColumn( $column, $var, $type, $maxLength, $driverOptions ); } /** @@ -344,10 +399,27 @@ public function debugDumpParams(): ?bool { throw new RuntimeException( 'Not implemented' ); } + /** + * Get metadata for a column in a result set. + * + * This is used internally by the "WP_MySQL_On_SQLite_Statement_PHP_Compat" trait, + * that is defined conditionally based on the current PHP version. + * + * @param int $column The index of the column (0-indexed). + * @return array|false The column metadata as an associative array, + * or false if the column does not exist. + */ + private function getColumnMetadata( $column ) { + if ( ! array_key_exists( $column, $this->resolved_column_meta ) ) { + $this->resolved_column_meta[ $column ] = ( $this->column_meta_resolver )( $column ); + } + return $this->resolved_column_meta[ $column ]; + } + /** * Fetch all remaining rows from the result set. * - * This is used internally by the "WP_PDO_Proxy_Statement_PHP_Compat" trait, + * This is used internally by the "WP_MySQL_On_SQLite_Statement_PHP_Compat" trait, * that is defined conditionally based on the current PHP version. * * @param int $mode The fetch mode to use. @@ -361,7 +433,7 @@ private function fetchAllRows( $mode = null, ...$args ): array { /** * Set the default fetch mode for this statement. * - * This is used internally by the "WP_PDO_Proxy_Statement_PHP_Compat" trait, + * This is used internally by the "WP_MySQL_On_SQLite_Statement_PHP_Compat" trait, * that is defined conditionally based on the current PHP version. * * @param int $mode The fetch mode to set as the default. diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-mysql-on-sqlite.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-mysql-on-sqlite.php index c8f20e27a..3f769fe97 100644 --- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-mysql-on-sqlite.php +++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-mysql-on-sqlite.php @@ -3,7 +3,30 @@ /* * The SQLite driver uses PDO. Enable PDO function calls: * phpcs:disable WordPress.DB.RestrictedClasses.mysql__PDO + * + * PDO uses camel case naming, enable non-snake case: + * phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid + * phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + * + * PDO uses $string as a parameter name, enable it: + * phpcs:disable Universal.NamingConventions.NoReservedKeywordParameterNames.stringFound + * + * We conditionally define a trait for PHP-version-specific PDO methods: + * phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound + */ + +/* + * The "PDO::connect()" method in PHP 8.4 uses a "static" return type declaration, + * which PHP 7 cannot parse. Therefore, a conditional file import is needed. */ +if ( PHP_VERSION_ID >= 80400 ) { + require_once __DIR__ . '/trait-wp-mysql-on-sqlite-pdo-compat-php-84.php'; +} else { + /** + * @access private + */ + trait WP_MySQL_On_SQLite_PDO_Compat {} +} /** * SQLite driver for MySQL. @@ -15,10 +38,12 @@ * The driver requires PDO with the SQLite driver, and the PCRE engine. */ class WP_MySQL_On_SQLite extends PDO { + use WP_MySQL_On_SQLite_PDO_Compat; + /** * The path to the MySQL SQL grammar file. */ - const MYSQL_GRAMMAR_PATH = __DIR__ . '/../mysql/mysql-grammar.php'; + private const MYSQL_GRAMMAR_PATH = __DIR__ . '/../mysql/mysql-grammar.php'; /** * The minimum required version of SQLite. @@ -31,6 +56,8 @@ class WP_MySQL_On_SQLite extends PDO { /** * An identifier prefix for internal database objects. * + * @access private + * * @TODO: Do not allow accessing objects with this prefix. */ const RESERVED_PREFIX = '_wp_sqlite_'; @@ -40,6 +67,8 @@ class WP_MySQL_On_SQLite extends PDO { * * This special table is used to emulate MySQL global variables and to store * some internal configuration values. + * + * @access private */ const GLOBAL_VARIABLES_TABLE_NAME = self::RESERVED_PREFIX . 'global_variables'; @@ -47,22 +76,87 @@ class WP_MySQL_On_SQLite extends PDO { * Name of the connection-private TEMP table used to build empty result sets * without acquiring a write lock on the database. See create_result_statement_from_data(). */ - const EMPTY_RESULT_TABLE_NAME = self::RESERVED_PREFIX . 'empty_result'; + private const EMPTY_RESULT_TABLE_NAME = self::RESERVED_PREFIX . 'empty_result'; /** * The name of the SQLite driver version variable. * * This internal variable is used to store the latest version of the SQLite * driver that was used to initialize and configure the SQLite database. + * + * @access private */ const DRIVER_VERSION_VARIABLE_NAME = self::RESERVED_PREFIX . 'driver_version'; + /** + * MySQL SQL modes mapped to their bitmask values. + * + * The modes are ordered by their bit position, matching MySQL's canonical + * serialization order. + * + * See: + * https://github.com/mysql/mysql-server/blob/8.4/sql/system_variables.h + * https://github.com/mysql/mysql-server/blob/5.7/sql/sys_vars.cc + */ + private const SQL_MODES = array( + 'REAL_AS_FLOAT' => 1 << 0, + 'PIPES_AS_CONCAT' => 1 << 1, + 'ANSI_QUOTES' => 1 << 2, + 'IGNORE_SPACE' => 1 << 3, + 'NOT_USED' => 1 << 4, + 'ONLY_FULL_GROUP_BY' => 1 << 5, + 'NO_UNSIGNED_SUBTRACTION' => 1 << 6, + 'NO_DIR_IN_CREATE' => 1 << 7, + 'POSTGRESQL' => 1 << 8, + 'ORACLE' => 1 << 9, + 'MSSQL' => 1 << 10, + 'DB2' => 1 << 11, + 'MAXDB' => 1 << 12, + 'NO_KEY_OPTIONS' => 1 << 13, + 'NO_TABLE_OPTIONS' => 1 << 14, + 'NO_FIELD_OPTIONS' => 1 << 15, + 'MYSQL323' => 1 << 16, + 'MYSQL40' => 1 << 17, + 'ANSI' => 1 << 18, + 'NO_AUTO_VALUE_ON_ZERO' => 1 << 19, + 'NO_BACKSLASH_ESCAPES' => 1 << 20, + 'STRICT_TRANS_TABLES' => 1 << 21, + 'STRICT_ALL_TABLES' => 1 << 22, + 'NO_ZERO_IN_DATE' => 1 << 23, + 'NO_ZERO_DATE' => 1 << 24, + 'ALLOW_INVALID_DATES' => 1 << 25, + 'ERROR_FOR_DIVISION_BY_ZERO' => 1 << 26, + 'TRADITIONAL' => 1 << 27, + 'NO_AUTO_CREATE_USER' => 1 << 28, + 'HIGH_NOT_PRECEDENCE' => 1 << 29, + 'NO_ENGINE_SUBSTITUTION' => 1 << 30, + 'PAD_CHAR_TO_FULL_LENGTH' => 1 << 31, + + // Modes below require 64-bit PHP. + // TODO: Consider supporting these values on 32-bit PHP as well. + 'TIME_TRUNCATE_FRACTIONAL' => 1 << 32, + ); + + /** + * The default SQL modes shared by MySQL 5.7 and 8.0. + * + * MySQL 5.7 additionally enables NO_AUTO_CREATE_USER. + */ + private const DEFAULT_SQL_MODES = array( + 'ERROR_FOR_DIVISION_BY_ZERO', + 'NO_ENGINE_SUBSTITUTION', + 'NO_ZERO_DATE', + 'NO_ZERO_IN_DATE', + 'ONLY_FULL_GROUP_BY', + 'STRICT_TRANS_TABLES', + ); + /** * A map of MySQL tokens to SQLite data types. * * This is used to translate a MySQL data type to an SQLite data type. */ - const DATA_TYPE_MAP = array( + private const DATA_TYPE_MAP = array( // Numeric data types: WP_MySQL_Lexer::BIT_SYMBOL => 'INTEGER', WP_MySQL_Lexer::BOOL_SYMBOL => 'INTEGER', @@ -128,7 +222,7 @@ class WP_MySQL_On_SQLite extends PDO { * INFORMATION_SCHEMA tables. They keys are MySQL data types normalized * as they appear in the INFORMATION_SCHEMA. Values are SQLite data types. */ - const DATA_TYPE_STRING_MAP = array( + private const DATA_TYPE_STRING_MAP = array( // Numeric data types: 'bit' => 'INTEGER', 'bool' => 'INTEGER', @@ -199,7 +293,7 @@ class WP_MySQL_On_SQLite extends PDO { * https://www.sqlite.org/lang_datefunc.html * https://strftime.org/ */ - const MYSQL_DATE_FORMAT_TO_SQLITE_STRFTIME_MAP = array( + private const MYSQL_DATE_FORMAT_TO_SQLITE_STRFTIME_MAP = array( '%a' => '%D', '%b' => '%M', '%c' => '%n', @@ -241,7 +335,7 @@ class WP_MySQL_On_SQLite extends PDO { * See: * https://dev.mysql.com/doc/refman/8.4/en/data-type-defaults.html#data-type-defaults-implicit */ - const DATA_TYPE_IMPLICIT_DEFAULT_MAP = array( + private const DATA_TYPE_IMPLICIT_DEFAULT_MAP = array( // Numeric data types: 'bit' => '0', 'bool' => '0', @@ -311,7 +405,7 @@ class WP_MySQL_On_SQLite extends PDO { * * This is used to compute the column metadata from the information schema. */ - const COLUMN_INFO_MYSQL_TO_NATIVE_TYPES_MAP = array( + private const COLUMN_INFO_MYSQL_TO_NATIVE_TYPES_MAP = array( // Numeric data types: 'bit' => array( 'BIT', 16, 1, 0 ), 'tinyint' => array( 'TINY', 1, 4, 0 ), @@ -375,7 +469,7 @@ class WP_MySQL_On_SQLite extends PDO { * This is used to compute the MySQL column metadata for non-column fields * that have no records in the information schema (i.e., expressions). */ - const COLUMN_INFO_SQLITE_TO_NATIVE_TYPES_MAP = array( + private const COLUMN_INFO_SQLITE_TO_NATIVE_TYPES_MAP = array( 'NULL' => array( 'NULL', 6, 0, 0 ), 'INT' => array( 'LONGLONG', 8, 21, 0 ), 'INTEGER' => array( 'LONGLONG', 8, 21, 0 ), @@ -449,6 +543,47 @@ class WP_MySQL_On_SQLite extends PDO { */ private $connection; + /** + * Caller-visible PDO error mode. + * + * The underlying SQLite connection always uses exceptions so internal + * operations can handle errors reliably. + * + * @var int + */ + private $error_mode = PDO::ERRMODE_EXCEPTION; + + /** + * Whether fetched scalar values should be converted to strings. + * + * PDO SQLite cannot report PDO::ATTR_STRINGIFY_FETCHES on PHP 7.2–8.1, + * so the wrapper tracks its value on those versions. + * + * @var bool + */ + private $stringify_fetches = false; + + /** + * SQLSTATE associated with the last PDO operation. + * + * @var string|null + */ + private $error_code; + + /** + * Error information associated with the last PDO operation. + * + * @var array + */ + private $error_info = array( '', null, null ); + + /** + * ID generated by the last user-issued INSERT or REPLACE statement. + * + * @var string + */ + private $last_insert_id = '0'; + /** * User-defined functions registered on the SQLite connection. * @@ -591,16 +726,9 @@ class WP_MySQL_On_SQLite extends PDO { * TODO: This may be represented using a temporary table in the future, * together with GLOBAL SQL mode (a non-temporary table). * - * @var string[] + * @var int */ - private $active_sql_modes = array( - 'ERROR_FOR_DIVISION_BY_ZERO', - 'NO_ENGINE_SUBSTITUTION', - 'NO_ZERO_DATE', - 'NO_ZERO_IN_DATE', - 'ONLY_FULL_GROUP_BY', - 'STRICT_TRANS_TABLES', - ); + private $active_sql_modes; /** * A name-to-value map of MySQL system variables for the current session. @@ -635,23 +763,28 @@ class WP_MySQL_On_SQLite extends PDO { * @param string $dsn MySQL-on-SQLite DSN containing the SQLite path and database name. * @param string|null $username Optional. Ignored by this driver. * @param string|null $password Optional. Ignored by this driver. - * @param array $options { + * @param array|null $options { * Optional driver options. * + * Numeric keys are handled as standard PDO constructor options. + * Driver-specific PDO options are not supported. + * * @type int $mysql_version Optional. MySQL version to emulate. Default 80038. * @type PDO|null $pdo Optional. Existing SQLite PDO connection. * @type string|null $journal_mode Optional. SQLite journal mode. Default 'WAL'. * @type string|int|null $synchronous Optional. SQLite synchronous setting. * } * - * @throws WP_SQLite_Driver_Exception When the driver initialization fails. + * @throws WP_MySQL_On_SQLite_Exception When the driver initialization fails. */ public function __construct( string $dsn, ?string $username = null, ?string $password = null, - array $options = array() + ?array $options = null ) { + $options = $options ?? array(); + // PDO DSN can't include "\0" bytes; parsing stops at the first one. $first_null_byte_index = strpos( $dsn, "\0" ); if ( false !== $first_null_byte_index ) { @@ -692,9 +825,25 @@ public function __construct( $db_name = $args['dbname'] ?? 'sqlite_database'; // Create a new SQLite connection. + $pdo_options = array_filter( + $options, + function ( $key ) { + return is_int( $key ); + }, + ARRAY_FILTER_USE_KEY + ); + + $connection_pdo_options = array(); + if ( array_key_exists( PDO::ATTR_PERSISTENT, $pdo_options ) ) { + // Persistence is connection-time-only. Do not apply it again after initialization. + $connection_pdo_options[ PDO::ATTR_PERSISTENT ] = $pdo_options[ PDO::ATTR_PERSISTENT ]; + unset( $pdo_options[ PDO::ATTR_PERSISTENT ] ); + } + $connection_options = array( 'journal_mode' => $options['journal_mode'] ?? null, 'synchronous' => $options['synchronous'] ?? null, + 'pdo_options' => $connection_pdo_options, ); if ( isset( $options['pdo'] ) ) { $connection_options['pdo'] = $options['pdo']; @@ -706,6 +855,7 @@ public function __construct( $this->mysql_version = $options['mysql_version'] ?? 80038; $this->main_db_name = $db_name; $this->db_name = $db_name; + $this->set_sql_modes( $this->get_default_sql_modes() ); // Check the database name. if ( '' === $this->db_name ) { @@ -791,6 +941,35 @@ function ( string $sql, array $params ) { ); } ); + + foreach ( $pdo_options as $attribute => $value ) { + $this->setAttribute( $attribute, $value ); + } + } + + /** + * PDO API: Prepare a MySQL statement for execution. + * + * Prepared statements are not implemented yet. Report the standard PDO + * unsupported-function diagnostic instead of using uninitialized parent + * PDO state. + * + * @param string $query The MySQL statement to prepare. + * @param array|null $options Optional statement options. + * @return PDOStatement|false False when exceptions are disabled. + * + * @throws WP_MySQL_On_SQLite_Exception When exception mode is enabled. + */ + #[ReturnTypeWillChange] + public function prepare( $query, $options = null ) { + $driver_message = 'driver does not support prepared statements'; + $exception = $this->new_driver_exception( + 'SQLSTATE[IM001]: Driver does not support this function: ' . $driver_message, + 'IM001', + null, + array( 'IM001', 0, $driver_message ) + ); + return $this->handle_pdo_error( $exception ); } /** @@ -805,7 +984,7 @@ function ( string $sql, array $params ) { * * @return PDOStatement|false PDO statement, or false when the fetch mode is invalid on PHP < 8.1. * - * @throws WP_SQLite_Driver_Exception When the query execution fails. + * @throws WP_MySQL_On_SQLite_Exception When the query execution fails. */ #[ReturnTypeWillChange] public function query( string $query, ?int $fetch_mode = null, ...$fetch_mode_args ) { @@ -836,11 +1015,6 @@ public function query( string $query, ?int $fetch_mode = null, ...$fetch_mode_ar ); return false; } - - // When the default FETCH_BOTH is not set explicitly, additional - // arguments are ignored, and the argument count is not validated. - $fetch_mode = $this->connection->get_pdo()->getAttribute( PDO::ATTR_DEFAULT_FETCH_MODE ); - $fetch_mode_args = array(); } elseif ( PDO::FETCH_COLUMN === $fetch_mode ) { if ( 3 !== $arg_count ) { throw new ArgumentCountError( @@ -974,8 +1148,17 @@ public function query( string $query, ?int $fetch_mode = null, ...$fetch_mode_ar $this->last_result_statement = $this->create_result_statement_from_data( array(), array() ); } - $stmt = new WP_PDO_Proxy_Statement( $this->last_result_statement, $this->last_affected_rows ); - $stmt->setFetchMode( $fetch_mode, ...$fetch_mode_args ); + $stmt = new WP_MySQL_On_SQLite_Statement( + $this->last_result_statement, + $query, + $this->create_column_meta_resolver( $this->last_column_meta ), + $this->last_affected_rows + ); + if ( null !== $fetch_mode ) { + $stmt->setFetchMode( $fetch_mode, ...$fetch_mode_args ); + } + $this->error_code = '00000'; + $this->error_info = array( '00000', null, null ); return $stmt; } catch ( Throwable $e ) { try { @@ -984,12 +1167,14 @@ public function query( string $query, ?int $fetch_mode = null, ...$fetch_mode_ar } catch ( Throwable $rollback_exception ) { // Ignore rollback errors. } - if ( $e instanceof WP_SQLite_Driver_Exception ) { - throw $e; - } elseif ( $e instanceof WP_SQLite_Information_Schema_Exception ) { - throw $this->convert_information_schema_exception( $e ); + if ( $e instanceof WP_SQLite_Information_Schema_Exception ) { + $e = $this->convert_information_schema_exception( $e ); + } + if ( ! ( $e instanceof WP_MySQL_On_SQLite_Exception ) ) { + $e = $this->new_driver_exception( $e->getMessage(), $e->getCode(), $e ); } - throw $this->new_driver_exception( $e->getMessage(), $e->getCode(), $e ); + + return $this->handle_pdo_error( $e ); } finally { // A query that doesn't return any rows or fails sets found rows to 0. if ( ! $this->is_readonly || isset( $e ) ) { @@ -1006,19 +1191,139 @@ public function query( string $query, ?int $fetch_mode = null, ...$fetch_mode_ar #[ReturnTypeWillChange] public function exec( $query ) { $stmt = $this->query( $query ); + if ( false === $stmt ) { + return false; + } return $stmt->rowCount(); } + /** + * PDO API: Return the ID of the last inserted row. + * + * @param string|null $name Optional sequence name. Ignored by SQLite. + * @return string|false The last insert ID, or false on failure. + */ + #[ReturnTypeWillChange] + public function lastInsertId( $name = null ) { + if ( + is_array( $name ) + || is_resource( $name ) + || ( is_object( $name ) && ! method_exists( $name, '__toString' ) ) + ) { + if ( PHP_VERSION_ID >= 80000 ) { + throw new TypeError( + sprintf( + 'PDO::lastInsertId(): Argument #1 ($name) must be of type ?string, %s given', + get_debug_type( $name ) + ) + ); + } + trigger_error( + sprintf( 'PDO::lastInsertId() expects parameter 1 to be string, %s given', strtolower( gettype( $name ) ) ), + E_USER_WARNING + ); + return false; + } + + return $this->last_insert_id; + } + + /** + * PDO API: Fetch the SQLSTATE associated with the last operation. + * + * @return string|null The SQLSTATE error code, or null when unavailable. + */ + public function errorCode(): ?string { + return $this->error_code; + } + + /** + * PDO API: Fetch error information associated with the last operation. + * + * @return array Error information from the last PDO operation. + */ + public function errorInfo(): array { + return $this->error_info; + } + + /** + * PDO API: Quote a string for use in a MySQL query. + * + * @param string $string The string to quote. + * @param int $type The PDO parameter type. + * @return string The quoted string. + */ + #[ReturnTypeWillChange] + public function quote( $string, $type = PDO::PARAM_STR ) { + // Mirror PDO\MySQL::quote() value validation. + if ( + is_array( $string ) + || is_resource( $string ) + || ( is_object( $string ) && ! method_exists( $string, '__toString' ) ) + ) { + $given_type = is_object( $string ) ? get_class( $string ) : gettype( $string ); + throw new TypeError( + sprintf( + 'WP_MySQL_On_SQLite::quote(): Argument #1 ($string) must be of type string, %s given', + $given_type + ) + ); + } + $string = (string) $string; + + // Handle binary and national character prefixes. + $prefix = ''; + if ( PDO::PARAM_LOB === ( $type & PDO::PARAM_LOB ) ) { + $prefix = '_binary'; + } elseif ( + PDO::PARAM_STR_NATL === ( $type & PDO::PARAM_STR_NATL ) + && PDO::PARAM_STR_CHAR !== ( $type & PDO::PARAM_STR_CHAR ) + ) { + $prefix = 'N'; + } + + /* + * PDO uses mysqlnd by default and can alternatively use libmysqlclient. + * This escaped character mapping matches the escaping of both drivers. + * Their malformed multibyte sequence handling is not needed for UTF-8. + * + * @see https://github.com/php/php-src/blob/dd6e76cce27aaa0ed9f7520648ed1081dfb6af36/ext/mysqlnd/mysqlnd_charset.c#L905 + * @see https://github.com/mysql/mysql-server/blob/dc86e412f18b36ce271f791026714e8caa0ec919/mysys/charset.cc#L413 + * + * We can't use "addcslashes()" here, because it has an unusual handling + * of the ASCII NULL and Control+Z characters, escaping them to "\000" + * and "\032" instead of "\0" and "\Z", respectively. + * + * It is important to use "strtr()" and not "str_replace()", because + * "str_replace()" applies replacements one after another, modifying + * intermediate changes rather than just the original string: + * + * - str_replace( [ 'a', 'b' ], [ 'b', 'c' ], 'ab' ); // 'cc' (bad) + * - strtr( 'ab', [ 'a' => 'b', 'b' => 'c' ] ); // 'bc' (good) + */ + $backslash = chr( 92 ); + $replacements = array( + chr( 0 ) => $backslash . '0', // An ASCII NULL character (\0). + chr( 10 ) => $backslash . 'n', // A newline (linefeed) character (\n). + chr( 13 ) => $backslash . 'r', // A carriage return character (\r). + $backslash => $backslash . $backslash, // A backslash character (\). + "'" => $backslash . "'", // A single quote character ('). + '"' => $backslash . '"', // A double quote character ("). + chr( 26 ) => $backslash . 'Z', // An ASCII 26 (Control+Z) character. + ); + return $prefix . "'" . strtr( $string, $replacements ) . "'"; + } + /** * PDO API: Begin a transaction. * * @return bool True on success, false on failure. */ - // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid public function beginTransaction(): bool { if ( $this->inTransaction() ) { throw $this->new_driver_exception( 'There is already an active transaction' ); } + $this->flush(); $this->begin_user_transaction(); return true; } @@ -1032,6 +1337,7 @@ public function commit(): bool { if ( ! $this->inTransaction() ) { throw $this->new_driver_exception( 'There is no active transaction' ); } + $this->flush(); $this->commit_user_transaction(); return true; } @@ -1041,11 +1347,11 @@ public function commit(): bool { * * @return bool True on success, false on failure. */ - // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid public function rollBack(): bool { if ( ! $this->inTransaction() ) { throw $this->new_driver_exception( 'There is no active transaction' ); } + $this->flush(); $this->rollback_user_transaction(); return true; } @@ -1055,7 +1361,6 @@ public function rollBack(): bool { * * @return bool True if a transaction is active, false otherwise. */ - // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid public function inTransaction(): bool { if ( PHP_VERSION_ID < 80400 ) { /* @@ -1081,7 +1386,29 @@ public function inTransaction(): bool { * @return bool True on success, false on failure. */ public function setAttribute( $attribute, $value ): bool { - return $this->connection->get_pdo()->setAttribute( $attribute, $value ); + // PDO reserves IDs starting at 1000 for driver-specific attributes. + // These differ for MySQL and SQLite and are currently not supported. + if ( is_int( $attribute ) && 1000 <= $attribute ) { + return false; + } + + // Track the caller's error mode while keeping internal SQLite operations in exception mode. + if ( PDO::ATTR_ERRMODE === $attribute ) { + $pdo = $this->connection->get_pdo(); + $result = $pdo->setAttribute( $attribute, $value ); + if ( ! $result ) { + return false; + } + $this->error_mode = $pdo->getAttribute( PDO::ATTR_ERRMODE ); + $pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION ); + return true; + } + + $result = $this->connection->get_pdo()->setAttribute( $attribute, $value ); + if ( $result && PDO::ATTR_STRINGIFY_FETCHES === $attribute ) { + $this->stringify_fetches = (bool) $value; + } + return $result; } /** @@ -1096,12 +1423,36 @@ public function setAttribute( $attribute, $value ): bool { */ #[ReturnTypeWillChange] public function getAttribute( $attribute ) { + // Return the caller's error mode instead of the exception mode used internally. + if ( PDO::ATTR_ERRMODE === $attribute ) { + return $this->error_mode; + } + if ( PDO::ATTR_STRINGIFY_FETCHES === $attribute && PHP_VERSION_ID < 80200 ) { + // PDO SQLite cannot report this attribute before PHP 8.2. + return $this->stringify_fetches; + } return $this->connection->get_pdo()->getAttribute( $attribute ); } + /** + * Get the underlying SQLite PDO instance. + * + * Exposes the SQLite PDO for advanced use, bypassing MySQL emulation. + * Do not retain it across reconnections or modify driver-owned state. + * + * @since 3.0.0 + * + * @return PDO The underlying SQLite PDO instance. + */ + public function get_sqlite_pdo(): PDO { + return $this->connection->get_pdo(); + } + /** * Get the SQLite connection instance. * + * @access private + * * @return WP_SQLite_Connection */ public function get_connection(): WP_SQLite_Connection { @@ -1123,6 +1474,8 @@ public function get_sqlite_version(): string { * The saved driver version corresponds to the latest version of the SQLite * driver that was used to initialize and configure the SQLite database. * + * @access private + * * @return string SQLite driver version as a string. * @throws PDOException When the query execution fails. */ @@ -1148,16 +1501,25 @@ public function get_saved_driver_version(): string { /** * Check if a specific SQL mode is active. * + * @access private + * * @param string $mode The SQL mode to check. * @return bool True if the SQL mode is active, false otherwise. */ public function is_sql_mode_active( string $mode ): bool { - return in_array( strtoupper( $mode ), $this->active_sql_modes, true ); + $mode = strtoupper( $mode ); + if ( 'NOT_USED' === $mode && $this->mysql_version < 80000 ) { + return false; + } + return isset( self::SQL_MODES[ $mode ] ) + && ( $this->active_sql_modes & self::SQL_MODES[ $mode ] ) !== 0; } /** * Get the last executed MySQL query. * + * @access private + * * @return string|null */ public function get_last_mysql_query(): ?string { @@ -1167,28 +1529,19 @@ public function get_last_mysql_query(): ?string { /** * Get SQLite queries executed for the last MySQL query. * + * @access private + * * @return array{ sql: string, params: array }[] */ public function get_last_sqlite_queries(): array { return $this->last_sqlite_queries; } - /** - * Get the auto-increment value generated for the last query. - * - * @return int|string - */ - public function get_insert_id() { - $last_insert_id = $this->connection->get_last_insert_id(); - if ( is_numeric( $last_insert_id ) ) { - $last_insert_id = (int) $last_insert_id; - } - return $last_insert_id; - } - /** * Tokenize a MySQL query and initialize a parser. * + * @access private + * * @param string $query The MySQL query to parse. * @return WP_MySQL_Parser A parser initialized for the MySQL query. */ @@ -1196,7 +1549,7 @@ public function create_parser( string $query ): WP_MySQL_Parser { $lexer = new WP_MySQL_Lexer( $query, 80038, - $this->active_sql_modes + $this->get_active_sql_mode_names() ); $tokens = $lexer instanceof WP_MySQL_Native_Lexer ? $lexer->native_token_stream() @@ -1221,192 +1574,229 @@ private function reset_or_create_parser( $tokens ): WP_MySQL_Parser { } /** - * Get the number of columns returned by the last emulated query. + * Execute a query in SQLite. + * + * @access private * - * @return int + * @param string $sql The query to execute. + * @param array $params The query parameters. + * @throws PDOException When the query execution fails. + * @return PDOStatement The PDO statement object. */ - public function get_last_column_count(): int { - return count( $this->last_column_meta ); + public function execute_sqlite_query( string $sql, array $params = array() ): PDOStatement { + return $this->connection->query( $sql, $params ); } /** - * Get column metadata for results of the last emulated query. + * Create a lazy MySQL-compatible column metadata resolver. * - * @return array + * Only raw SQLite metadata and database context are snapshotted here. + * The INFORMATION_SCHEMA metadata is resolved lazily and may reflect + * schema changes made after statement execution. This is a trade-off + * that avoids schema queries when column metadata is not requested. + * + * @param array $raw_column_meta Raw SQLite result column metadata. + * @return callable The column metadata resolver. */ - public function get_last_column_meta(): array { - // Build the column metadata as per "PDOStatement::getColumnMeta()". - $column_meta = array(); - foreach ( $this->last_column_meta as $meta ) { - $table = $meta['table'] ?? null; - $name = $meta['name']; - $type = strtoupper( $meta['sqlite:decl_type'] ?? $meta['native_type'] ?? '' ); - - // When table is known, we can get data from the information schema. - $column_info = null; - if ( null !== $table ) { - $table_is_temporary = $this->information_schema_builder->temporary_table_exists( $table ); - $columns_table = $this->information_schema_builder->get_table_name( $table_is_temporary, 'columns' ); - $column_info = $this->execute_sqlite_query( - sprintf( - ' - SELECT - IS_NULLABLE, - DATA_TYPE, - COLUMN_TYPE, - COLUMN_KEY, - CHARACTER_MAXIMUM_LENGTH, - NUMERIC_PRECISION, - NUMERIC_SCALE - FROM %s - WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ? - ', - $this->quote_sqlite_identifier( $columns_table ) - ), - array( $this->get_saved_db_name(), $table, $name ) - )->fetch( PDO::FETCH_ASSOC ); + private function create_column_meta_resolver( array $raw_column_meta ): callable { + $db_name = $this->db_name; + return function ( $column ) use ( $raw_column_meta, $db_name ) { + if ( ! array_key_exists( $column, $raw_column_meta ) ) { + return false; + } - if ( false === $column_info ) { - $column_info = null; - } + $last_sqlite_queries = $this->last_sqlite_queries; + try { + return $this->resolve_column_meta( $raw_column_meta[ $column ], $db_name ); + } finally { + $this->last_sqlite_queries = $last_sqlite_queries; } + }; + } - // If we have information schema data, we can use it. - if ( null !== $column_info ) { - $type_info = self::COLUMN_INFO_MYSQL_TO_NATIVE_TYPES_MAP[ $column_info['DATA_TYPE'] ] ?? null; - if ( null === $type_info ) { - $type_info = self::COLUMN_INFO_SQLITE_TO_NATIVE_TYPES_MAP[ $type ] ?? null; - } - $native_type = $type_info[0]; - $mysqli_type = $type_info[1]; - $len = $type_info[2]; - $precision = $type_info[3]; + /** + * Resolve raw SQLite column metadata into MySQL-compatible metadata. + * + * @param array $meta Raw SQLite column metadata. + * @param string $db_name Database selected when the query was executed. + * @return array MySQL-compatible column metadata. + */ + private function resolve_column_meta( array $meta, string $db_name ): array { + $table = $meta['table'] ?? null; + $name = $meta['name']; + $type = strtoupper( $meta['sqlite:decl_type'] ?? $meta['native_type'] ?? '' ); - if ( 'tinyint(1)' === $column_info['COLUMN_TYPE'] ) { - $len = 1; - } + // When table is known, we can get data from the information schema. + $column_info = null; + if ( null !== $table ) { + $table_is_temporary = $this->information_schema_builder->temporary_table_exists( $table ); + $columns_table = $this->information_schema_builder->get_table_name( $table_is_temporary, 'columns' ); + $column_info = $this->execute_sqlite_query( + sprintf( + ' + SELECT + IS_NULLABLE, + DATA_TYPE, + COLUMN_TYPE, + COLUMN_KEY, + CHARACTER_MAXIMUM_LENGTH, + NUMERIC_PRECISION, + NUMERIC_SCALE + FROM %s + WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ? + ', + $this->quote_sqlite_identifier( $columns_table ) + ), + array( $this->get_saved_db_name( $db_name ), $table, $name ) + )->fetch( PDO::FETCH_ASSOC ); - if ( 'decimal' === $column_info['DATA_TYPE'] ) { - $len = (int) $column_info['NUMERIC_PRECISION'] + (int) $column_info['NUMERIC_SCALE']; - $precision = (int) $column_info['NUMERIC_SCALE']; - } + if ( false === $column_info ) { + $column_info = null; + } + } - if ( - str_contains( $column_info['COLUMN_TYPE'], 'unsigned' ) - && ! str_contains( $column_info['COLUMN_TYPE'], 'bigint' ) - ) { - $len -= 1; - } + // If we have information schema data, we can use it. + if ( null !== $column_info ) { + $type_info = self::COLUMN_INFO_MYSQL_TO_NATIVE_TYPES_MAP[ $column_info['DATA_TYPE'] ] ?? null; + if ( null === $type_info ) { + $type_info = self::COLUMN_INFO_SQLITE_TO_NATIVE_TYPES_MAP[ $type ] ?? null; + } + $native_type = $type_info[0]; + $mysqli_type = $type_info[1]; + $len = $type_info[2]; + $precision = $type_info[3]; - // If set, lenght can be taken from the information schema. - if ( isset( $column_info['CHARACTER_MAXIMUM_LENGTH'] ) ) { - $len = (int) $column_info['CHARACTER_MAXIMUM_LENGTH']; - } + if ( 'tinyint(1)' === $column_info['COLUMN_TYPE'] ) { + $len = 1; + } - // For string types, the length is multiplied by the maximum number - // of bytes per character for the used connection encoding. In our - // case, it's always "utf8mb4" and therefore 4 bytes per character. - if ( - str_contains( $column_info['DATA_TYPE'], 'text' ) - || str_contains( $column_info['DATA_TYPE'], 'char' ) - || 'enum' === $column_info['DATA_TYPE'] - || 'set' === $column_info['DATA_TYPE'] - ) { - // Except for "longtext" - this might be a MySQL bug. - if ( 'longtext' !== $column_info['DATA_TYPE'] ) { - $len = 4 * $len; - } - } + if ( 'decimal' === $column_info['DATA_TYPE'] ) { + $len = (int) $column_info['NUMERIC_PRECISION'] + (int) $column_info['NUMERIC_SCALE']; + $precision = (int) $column_info['NUMERIC_SCALE']; + } - // Flags. - $flags = array(); - if ( 'NO' === $column_info['IS_NULLABLE'] ) { - $flags[] = 'not_null'; - } - if ( 'PRI' === $column_info['COLUMN_KEY'] ) { - $flags[] = 'primary_key'; - } elseif ( 'UNI' === $column_info['COLUMN_KEY'] ) { - $flags[] = 'unique_key'; - } elseif ( 'MUL' === $column_info['COLUMN_KEY'] ) { - $flags[] = 'multiple_key'; - } - } else { - $type_info = self::COLUMN_INFO_SQLITE_TO_NATIVE_TYPES_MAP[ $type ]; - $native_type = $type_info[0]; - $mysqli_type = $type_info[1]; - $len = $type_info[2] ?? 0; - $precision = $type_info[3]; - - // Flags. - $flags = array(); - if ( 'NULL' !== $type ) { - $flags[] = 'not_null'; - } + if ( + str_contains( $column_info['COLUMN_TYPE'], 'unsigned' ) + && ! str_contains( $column_info['COLUMN_TYPE'], 'bigint' ) + ) { + $len -= 1; } - if ( 'BLOB' === $native_type || 'GEOMETRY' === $native_type ) { - $flags[] = 'blob'; + // If set, length can be taken from the information schema. + if ( isset( $column_info['CHARACTER_MAXIMUM_LENGTH'] ) ) { + $len = (int) $column_info['CHARACTER_MAXIMUM_LENGTH']; } - // PDO type. - if ( 'INT' === $type || 'INTEGER' === $type ) { - $pdo_type = PDO::PARAM_INT; - } else { - $pdo_type = PDO::PARAM_STR; + // For string types, the length is multiplied by the maximum number + // of bytes per character for the used connection encoding. In our + // case, it's always "utf8mb4" and therefore 4 bytes per character. + if ( + str_contains( $column_info['DATA_TYPE'], 'text' ) + || str_contains( $column_info['DATA_TYPE'], 'char' ) + || 'enum' === $column_info['DATA_TYPE'] + || 'set' === $column_info['DATA_TYPE'] + ) { + // Except for "longtext" - this might be a MySQL bug. + if ( 'longtext' !== $column_info['DATA_TYPE'] ) { + $len = 4 * $len; + } } - // MySQLi charset number. - $is_string = 'STRING' === $type || 'TEXT' === $type; - $is_binary = 'BLOB' === $type || 'GEOMETRY' === $native_type; - $is_datetime = str_contains( $native_type, 'DATE' ) || str_contains( $native_type, 'TIME' ) || 'YEAR' === $native_type; - if ( $is_string && ! $is_binary && ! $is_datetime ) { - $mysqli_charsetnr = 255; // utf8mb4_0900_ai_ci - } else { - $mysqli_charsetnr = 63; // binary + // Flags. + $flags = array(); + if ( 'NO' === $column_info['IS_NULLABLE'] ) { + $flags[] = 'not_null'; + } + if ( 'PRI' === $column_info['COLUMN_KEY'] ) { + $flags[] = 'primary_key'; + } elseif ( 'UNI' === $column_info['COLUMN_KEY'] ) { + $flags[] = 'unique_key'; + } elseif ( 'MUL' === $column_info['COLUMN_KEY'] ) { + $flags[] = 'multiple_key'; + } + } else { + $type_info = self::COLUMN_INFO_SQLITE_TO_NATIVE_TYPES_MAP[ $type ]; + $native_type = $type_info[0]; + $mysqli_type = $type_info[1]; + $len = $type_info[2] ?? 0; + $precision = $type_info[3]; + + // Flags. + $flags = array(); + if ( 'NULL' !== $type ) { + $flags[] = 'not_null'; } + } - $column_meta[] = array( - 'native_type' => $native_type, - 'pdo_type' => $pdo_type, - 'flags' => $flags, - 'table' => $meta['table'] ?? '', - 'name' => $meta['name'], - 'len' => $len, - 'precision' => $precision, - 'sqlite:decl_type' => $meta['sqlite:decl_type'] ?? '', + if ( 'BLOB' === $native_type || 'GEOMETRY' === $native_type ) { + $flags[] = 'blob'; + } - /* - * The MySQLi PHP extension exposes more MySQL column metadata than PDO. - * We'll add the data here for use cases such as "wpdb::get_col_info()". - */ - 'mysqli:orgname' => $meta['name'], // TODO: Use correct original name when alias is used. - 'mysqli:orgtable' => $meta['table'] ?? '', // TODO: Use correct original name when table alias is used. - 'mysqli:db' => $this->db_name, // TODO: Use correct DB for queries to information schema. - 'mysqli:charsetnr' => $mysqli_charsetnr, - 'mysqli:flags' => 0, // TODO: We can compute correct MySQL flags. - 'mysqli:type' => $mysqli_type, - ); + // PDO type. + if ( 'INT' === $type || 'INTEGER' === $type ) { + $pdo_type = PDO::PARAM_INT; + } else { + $pdo_type = PDO::PARAM_STR; } - return $column_meta; + + // MySQLi charset number. + $is_string = 'STRING' === $type || 'TEXT' === $type; + $is_binary = 'BLOB' === $type || 'GEOMETRY' === $native_type; + $is_datetime = str_contains( $native_type, 'DATE' ) || str_contains( $native_type, 'TIME' ) || 'YEAR' === $native_type; + if ( $is_string && ! $is_binary && ! $is_datetime ) { + $mysqli_charsetnr = 255; // utf8mb4_0900_ai_ci + } else { + $mysqli_charsetnr = 63; // binary + } + + return array( + 'native_type' => $native_type, + 'pdo_type' => $pdo_type, + 'flags' => $flags, + 'table' => $meta['table'] ?? '', + 'name' => $meta['name'], + 'len' => $len, + 'precision' => $precision, + 'sqlite:decl_type' => $meta['sqlite:decl_type'] ?? '', + + /* + * The MySQLi PHP extension exposes more MySQL column metadata than PDO. + * We'll add the data here for use cases such as "wpdb::get_col_info()". + */ + 'mysqli:orgname' => $meta['name'], // TODO: Use correct original name when alias is used. + 'mysqli:orgtable' => $meta['table'] ?? '', // TODO: Use correct original name when table alias is used. + 'mysqli:db' => $db_name, // TODO: Use correct DB for queries to information schema. + 'mysqli:charsetnr' => $mysqli_charsetnr, + 'mysqli:flags' => 0, // TODO: We can compute correct MySQL flags. + 'mysqli:type' => $mysqli_type, + ); } /** - * Execute a query in SQLite. + * Record and report a PDO operation error according to the configured mode. * - * @param string $sql The query to execute. - * @param array $params The query parameters. - * @throws PDOException When the query execution fails. - * @return PDOStatement The PDO statement object. + * @param WP_MySQL_On_SQLite_Exception $exception The operation error. + * @return false Always false when the error is not thrown. + * @throws WP_MySQL_On_SQLite_Exception When exception mode is enabled. */ - public function execute_sqlite_query( string $sql, array $params = array() ): PDOStatement { - return $this->connection->query( $sql, $params ); + private function handle_pdo_error( WP_MySQL_On_SQLite_Exception $exception ): bool { + $this->error_info = $exception->errorInfo; + $this->error_code = $this->error_info[0]; + + if ( PDO::ERRMODE_EXCEPTION === $this->error_mode ) { + throw $exception; + } + if ( PDO::ERRMODE_WARNING === $this->error_mode ) { + trigger_error( $exception->getMessage(), E_USER_WARNING ); + } + return false; } /** * Translate and execute a MySQL query in SQLite. * * @param WP_Parser_Node $node The "query" AST node with "simpleStatement" child. - * @throws WP_SQLite_Driver_Exception When the query is not supported. + * @throws WP_MySQL_On_SQLite_Exception When the query is not supported. */ private function execute_mysql_query( WP_Parser_Node $node ): void { if ( 'query' !== $node->rule_name ) { @@ -1705,7 +2095,7 @@ private function rollback_user_transaction(): void { * Execute a MySQL transaction or locking statement in SQLite. * * @param WP_Parser_Node $node The "transactionOrLockingStatement" AST node. - * @throws WP_SQLite_Driver_Exception When the query execution fails. + * @throws WP_MySQL_On_SQLite_Exception When the query execution fails. */ private function execute_transaction_or_locking_statement( WP_Parser_Node $node ): void { $subnode = $node->get_first_child_node(); @@ -1822,7 +2212,7 @@ private function execute_transaction_or_locking_statement( WP_Parser_Node $node * Translate and execute a MySQL SELECT statement in SQLite. * * @param WP_Parser_Node $node The "selectStatement" AST node. - * @throws WP_SQLite_Driver_Exception When the query execution fails. + * @throws WP_MySQL_On_SQLite_Exception When the query execution fails. */ private function execute_select_statement( WP_Parser_Node $node ): void { /* @@ -1902,7 +2292,7 @@ private function execute_select_statement( WP_Parser_Node $node ): void { * Translate and execute a MySQL INSERT or REPLACE statement in SQLite. * * @param WP_Parser_Node $node The "insertStatement" or "replaceStatement" AST node. - * @throws WP_SQLite_Driver_Exception When the query execution fails. + * @throws WP_MySQL_On_SQLite_Exception When the query execution fails. */ private function execute_insert_or_replace_statement( WP_Parser_Node $node ): void { $parts = array(); @@ -2019,17 +2409,19 @@ function ( $column ) { throw $e; } } + $this->last_insert_id = $this->connection->get_last_insert_id(); return; } $this->last_result_statement = $this->execute_sqlite_query( $query ); + $this->last_insert_id = $this->connection->get_last_insert_id(); } /** * Translate and execute a MySQL UPDATE statement in SQLite. * * @param WP_Parser_Node $node The "updateStatement" AST node. - * @throws WP_SQLite_Driver_Exception When the query execution fails. + * @throws WP_MySQL_On_SQLite_Exception When the query execution fails. */ private function execute_update_statement( WP_Parser_Node $node ): void { // @TODO: Add support for UPDATE with multiple tables and JOINs. @@ -2147,10 +2539,24 @@ private function execute_update_statement( WP_Parser_Node $node ): void { )->fetchAll( PDO::FETCH_COLUMN ); } - $matched_tables = array_merge( $matched_temporary_tables, $matched_persistent_tables ); - $updates_multiple_tables = count( $matched_tables ) > 1; - if ( 1 === count( $matched_tables ) ) { - $table_or_alias = $matched_tables[0]; + $matched_tables = array_merge( $matched_temporary_tables, $matched_persistent_tables ); + $matched_aliases = array(); + foreach ( $table_alias_map as $alias => $data ) { + // Derived tables do not have a table name. + if ( null === $data['table_name'] ) { + continue; + } + + foreach ( $matched_tables as $matched_table ) { + if ( 0 === strcasecmp( $data['table_name'], $matched_table ) ) { + $matched_aliases[] = $alias; + break; + } + } + } + $updates_multiple_tables = count( $matched_aliases ) > 1; + if ( 1 === count( $matched_aliases ) ) { + $table_or_alias = $matched_aliases[0]; } else { break; } @@ -2265,7 +2671,7 @@ private function execute_update_statement( WP_Parser_Node $node ): void { * Translate and execute a MySQL DELETE statement in SQLite. * * @param WP_Parser_Node $node The "deleteStatement" AST node. - * @throws WP_SQLite_Driver_Exception When the query execution fails. + * @throws WP_MySQL_On_SQLite_Exception When the query execution fails. */ private function execute_delete_statement( WP_Parser_Node $node ): void { /* @@ -2406,7 +2812,7 @@ private function execute_delete_statement( WP_Parser_Node $node ): void { * Translate and execute a MySQL CREATE TABLE statement in SQLite. * * @param WP_Parser_Node $node The "createStatement" AST node with "createTable" child. - * @throws WP_SQLite_Driver_Exception When the query execution fails. + * @throws WP_MySQL_On_SQLite_Exception When the query execution fails. */ private function execute_create_table_statement( WP_Parser_Node $node ): void { $subnode = $node->get_first_child_node(); @@ -2477,7 +2883,7 @@ private function execute_create_table_statement( WP_Parser_Node $node ): void { * Translate and execute a MySQL ALTER TABLE statement in SQLite. * * @param WP_Parser_Node $node The "alterStatement" AST node with "alterTable" child. - * @throws WP_SQLite_Driver_Exception When the query execution fails. + * @throws WP_MySQL_On_SQLite_Exception When the query execution fails. */ private function execute_alter_table_statement( WP_Parser_Node $node ): void { $table_ref = $node->get_first_descendant_node( 'tableRef' ); @@ -2565,7 +2971,7 @@ private function execute_alter_table_statement( WP_Parser_Node $node ): void { * Translate and execute a MySQL DROP TABLE statement in SQLite. * * @param WP_Parser_Node $node The "dropStatement" AST node with "dropTable" child. - * @throws WP_SQLite_Driver_Exception When the query execution fails. + * @throws WP_MySQL_On_SQLite_Exception When the query execution fails. */ private function execute_drop_table_statement( WP_Parser_Node $node ): void { // Record the changes in the information schema. @@ -2617,7 +3023,7 @@ private function execute_drop_table_statement( WP_Parser_Node $node ): void { * Translate and execute a MySQL TRUNCATE TABLE statement in SQLite. * * @param WP_Parser_Node $node The "truncateTableStatement" AST node. - * @throws WP_SQLite_Driver_Exception When the query execution fails. + * @throws WP_MySQL_On_SQLite_Exception When the query execution fails. */ private function execute_truncate_table_statement( WP_Parser_Node $node ): void { $table_ref = $node->get_first_child_node( 'tableRef' ); @@ -2648,7 +3054,7 @@ private function execute_truncate_table_statement( WP_Parser_Node $node ): void * Translate and execute a MySQL CREATE INDEX statement in SQLite. * * @param WP_Parser_Node $node The "createStatement" AST node with "createIndex" child. - * @throws WP_SQLite_Driver_Exception When the query execution fails. + * @throws WP_MySQL_On_SQLite_Exception When the query execution fails. */ private function execute_create_index_statement( WP_Parser_Node $node ): void { $create_index = $node->get_first_child_node( 'createIndex' ); @@ -2706,7 +3112,7 @@ private function execute_create_index_statement( WP_Parser_Node $node ): void { * Translate and execute a MySQL DROP INDEX statement in SQLite. * * @param WP_Parser_Node $node The "dropStatement" AST node with "dropIndex" child. - * @throws WP_SQLite_Driver_Exception When the query execution fails. + * @throws WP_MySQL_On_SQLite_Exception When the query execution fails. */ private function execute_drop_index_statement( WP_Parser_Node $node ): void { $drop_index = $node->get_first_child_node( 'dropIndex' ); @@ -2747,7 +3153,7 @@ private function execute_drop_index_statement( WP_Parser_Node $node ): void { * Translate and execute a MySQL SHOW statement in SQLite. * * @param WP_Parser_Node $node The "showStatement" AST node. - * @throws WP_SQLite_Driver_Exception When the query execution fails. + * @throws WP_MySQL_On_SQLite_Exception When the query execution fails. */ private function execute_show_statement( WP_Parser_Node $node ): void { $tokens = $node->get_child_tokens(); @@ -3045,7 +3451,7 @@ private function execute_show_index_statement( WP_Parser_Node $node ): void { * Translate and execute a MySQL SHOW TABLE STATUS statement in SQLite. * * @param WP_Parser_Node $node The "showStatement" AST node. - * @throws WP_SQLite_Driver_Exception When the query execution fails. + * @throws WP_MySQL_On_SQLite_Exception When the query execution fails. */ private function execute_show_table_status_statement( WP_Parser_Node $node ): void { // FROM/IN database. @@ -3132,7 +3538,7 @@ private function execute_show_table_status_statement( WP_Parser_Node $node ): vo * Translate and execute a MySQL SHOW TABLES statement in SQLite. * * @param WP_Parser_Node $node The "showStatement" AST node. - * @throws WP_SQLite_Driver_Exception When the query execution fails. + * @throws WP_MySQL_On_SQLite_Exception When the query execution fails. */ private function execute_show_tables_statement( WP_Parser_Node $node ): void { // FROM/IN database. @@ -3182,7 +3588,7 @@ private function execute_show_tables_statement( WP_Parser_Node $node ): void { * Translate and execute a MySQL SHOW COLUMNS statement in SQLite. * * @param WP_Parser_Node $node The "showStatement" AST node. - * @throws WP_SQLite_Driver_Exception When the query execution fails. + * @throws WP_MySQL_On_SQLite_Exception When the query execution fails. * @throws PDOException When given table doesn't exist. */ private function execute_show_columns_statement( WP_Parser_Node $node ): void { @@ -3277,7 +3683,7 @@ private function execute_show_columns_statement( WP_Parser_Node $node ): void { * Translate and execute a MySQL DESCRIBE statement in SQLite. * * @param WP_Parser_Node $node The "describeStatement" AST node. - * @throws WP_SQLite_Driver_Exception When the query execution fails. + * @throws WP_MySQL_On_SQLite_Exception When the query execution fails. */ private function execute_describe_statement( WP_Parser_Node $node ): void { $table_ref = $node->get_first_child_node( 'tableRef' ); @@ -3316,7 +3722,7 @@ private function execute_describe_statement( WP_Parser_Node $node ): void { * Translate and execute a MySQL USE statement in SQLite. * * @param WP_Parser_Node $node The "useStatement" AST node. - * @throws WP_SQLite_Driver_Exception When the query execution fails. + * @throws WP_MySQL_On_SQLite_Exception When the query execution fails. */ private function execute_use_statement( WP_Parser_Node $node ): void { $database_name = $this->unquote_sqlite_identifier( @@ -3341,7 +3747,7 @@ private function execute_use_statement( WP_Parser_Node $node ): void { * Translate and execute a MySQL SET statement in SQLite. * * @param WP_Parser_Node $node The "setStatement" AST node. - * @throws WP_SQLite_Driver_Exception When the query execution fails. + * @throws WP_MySQL_On_SQLite_Exception When the query execution fails. */ private function execute_set_statement( WP_Parser_Node $node ): void { /* @@ -3454,7 +3860,7 @@ private function execute_set_statement( WP_Parser_Node $node ): void { * @param WP_Parser_Node $value_node The "setExprOrDefault" AST node. * @param int $default_type The currently active default variable type. * One of the SESSION, GLOBAL, PERSIST, PERSIST_ONLY tokens. - * @throws WP_SQLite_Driver_Exception When the query execution fails. + * @throws WP_MySQL_On_SQLite_Exception When the query execution fails. */ private function execute_set_system_variable_statement( WP_Parser_Node $set_var_node, @@ -3518,14 +3924,26 @@ private function execute_set_system_variable_statement( * SET updatable_views_with_limit = false; SELECT @@updatable_views_with_limit; -> NO */ $lowercase_value = null === $value ? null : strtolower( $value ); - if ( 'on' === $lowercase_value || 'off' === $lowercase_value ) { + if ( 'sql_mode' !== $name && ( 'on' === $lowercase_value || 'off' === $lowercase_value ) ) { $value = 'on' === $lowercase_value ? 1 : 0; } if ( WP_MySQL_Lexer::SESSION_SYMBOL === $type ) { if ( 'sql_mode' === $name ) { - $modes = explode( ',', strtoupper( $value ) ); - $this->active_sql_modes = $modes; + if ( null !== $value_node->get_first_child_token( WP_MySQL_Lexer::DEFAULT_SYMBOL ) ) { + $sql_modes = $this->get_default_sql_modes(); + } elseif ( is_string( $value ) ) { + /* + * MySQL removes spaces only from the end of the complete value. + * Spaces within individual mode names remain invalid. + * + * See: https://github.com/mysql/mysql-server/blob/8.4/sql/strfunc.cc#L39-L64 + */ + $sql_modes = explode( ',', rtrim( $value, ' ' ) ); + } else { + $sql_modes = $value; + } + $this->set_sql_modes( $sql_modes ); } else { $this->session_system_variables[ $name ] = $value; } @@ -3545,7 +3963,7 @@ private function execute_set_system_variable_statement( * * @param WP_Parser_Node $user_variable The "userVariable" AST node. * @param WP_Parser_Node $expr The "expr" AST node. - * @throws WP_SQLite_Driver_Exception When the query execution fails. + * @throws WP_MySQL_On_SQLite_Exception When the query execution fails. */ private function execute_set_user_variable_statement( WP_Parser_Node $user_variable, @@ -3570,7 +3988,7 @@ private function execute_set_user_variable_statement( * - REPAIR TABLE * * @param WP_Parser_Node $node A "tableAdministrationStatement" AST node. - * @throws WP_SQLite_Driver_Exception When the query execution fails. + * @throws WP_MySQL_On_SQLite_Exception When the query execution fails. */ private function execute_administration_statement( WP_Parser_Node $node ): void { $first_token = $node->get_first_child_token(); @@ -3697,7 +4115,7 @@ private function execute_administration_statement( WP_Parser_Node $node ): void * @param WP_Parser_Node $node The "expr" AST node. * @return mixed The value of the expression. */ - public function evaluate_expression( WP_Parser_Node $node ) { + private function evaluate_expression( WP_Parser_Node $node ) { // To support expressions, we'll use a SQLite query. $stmt = $this->execute_sqlite_query( sprintf( 'SELECT %s', $this->translate( $node ) ) @@ -3721,7 +4139,7 @@ public function evaluate_expression( WP_Parser_Node $node ) { * * @param WP_Parser_Node|WP_MySQL_Token $node The AST node to translate. * @return string|null The translated query fragment. - * @throws WP_SQLite_Driver_Exception When the translation fails. + * @throws WP_MySQL_On_SQLite_Exception When the translation fails. */ private function translate( $node ): ?string { if ( null === $node ) { @@ -3876,7 +4294,7 @@ private function translate( $node ): ?string { $name = strtolower( $original_name ); $type = $type_token ? $type_token->id : WP_MySQL_Lexer::SESSION_SYMBOL; if ( 'sql_mode' === $name ) { - $value = implode( ',', $this->active_sql_modes ); + $value = implode( ',', $this->get_active_sql_mode_names() ); } elseif ( 'version' === $name ) { $version = (string) $this->mysql_version; $value = sprintf( @@ -4040,7 +4458,7 @@ private function translate_token( WP_MySQL_Token $token ): ?string { * @param array $nodes The MySQL token to translate. * @param string $separator The separator to use between fragments. * @return string|null The translated value. - * @throws WP_SQLite_Driver_Exception When the translation fails. + * @throws WP_MySQL_On_SQLite_Exception When the translation fails. */ private function translate_sequence( array $nodes, string $separator = ' ' ): ?string { $parts = array(); @@ -4146,7 +4564,7 @@ private function translate_pure_identifier( WP_Parser_Node $node ): string { * (table, view, procedure, trigger, etc.). * @param WP_Parser_Node|null $child_node An identifier node representing an object child name (column, index, etc.). * @return string The translated value. - * @throws WP_SQLite_Driver_Exception When the translation fails. + * @throws WP_MySQL_On_SQLite_Exception When the translation fails. */ private function translate_qualified_identifier( ?WP_Parser_Node $schema_node, @@ -4194,7 +4612,7 @@ private function translate_qualified_identifier( * * @param WP_Parser_Node $node The "queryExpression" AST node. * @return string The translated value. - * @throws WP_SQLite_Driver_Exception When the translation fails. + * @throws WP_MySQL_On_SQLite_Exception When the translation fails. */ private function translate_query_expression( WP_Parser_Node $node ): string { // Get the query expression subnode under which we need to look for the @@ -4269,7 +4687,7 @@ private function translate_query_expression( WP_Parser_Node $node ): string { * * @param WP_Parser_Node $node The "querySpecification" AST node. * @return string The translated value. - * @throws WP_SQLite_Driver_Exception When the translation fails. + * @throws WP_MySQL_On_SQLite_Exception When the translation fails. * @return string|null */ private function translate_query_specification( WP_Parser_Node $node ): string { @@ -4355,7 +4773,7 @@ private function translate_query_specification( WP_Parser_Node $node ): string { * * @param WP_Parser_Node $node The "simpleExprBody" AST node. * @return string The translated value. - * @throws WP_SQLite_Driver_Exception When the translation fails. + * @throws WP_MySQL_On_SQLite_Exception When the translation fails. */ private function translate_simple_expr_body( WP_Parser_Node $node ): string { $token = $node->get_first_child_token(); @@ -4437,7 +4855,7 @@ private function translate_cast_expr( WP_Parser_Node $expr, WP_Parser_Node $cast * * @param WP_Parser_Node $node The "predicateOperations" AST node. * @return string The translated value. - * @throws WP_SQLite_Driver_Exception When the translation fails. + * @throws WP_MySQL_On_SQLite_Exception When the translation fails. */ private function translate_like( WP_Parser_Node $node ): string { $tokens = $node->get_descendant_tokens(); @@ -4481,7 +4899,7 @@ private function translate_like( WP_Parser_Node $node ): string { * * @param WP_Parser_Node $node The "predicateOperations" AST node. * @return string The translated value. - * @throws WP_SQLite_Driver_Exception When the translation fails. + * @throws WP_MySQL_On_SQLite_Exception When the translation fails. */ private function translate_regexp_functions( WP_Parser_Node $node ): string { $tokens = $node->get_descendant_tokens(); @@ -4511,7 +4929,7 @@ private function translate_regexp_functions( WP_Parser_Node $node ): string { * * @param WP_Parser_Node $node The "runtimeFunctionCall" AST node. * @return string The translated value. - * @throws WP_SQLite_Driver_Exception When the translation fails. + * @throws WP_MySQL_On_SQLite_Exception When the translation fails. */ private function translate_runtime_function_call( WP_Parser_Node $node ): string { $child = $node->get_first_child(); @@ -4563,7 +4981,7 @@ private function translate_runtime_function_call( WP_Parser_Node $node ): string * * @param WP_Parser_Node $node The "functionCall" AST node. * @return string The translated value. - * @throws WP_SQLite_Driver_Exception When the translation fails. + * @throws WP_MySQL_On_SQLite_Exception When the translation fails. */ private function translate_function_call( WP_Parser_Node $node ): string { $nodes = $node->get_child_nodes(); @@ -4785,7 +5203,7 @@ private function translate_datetime_literal( string $value ): string { * @param WP_Parser_Node $node The "selectItem" AST node. * @return string The translated expression. */ - public function translate_select_item( WP_Parser_Node $node ): string { + private function translate_select_item( WP_Parser_Node $node ): string { /* * First, let's translate the select item subtree. * @@ -4899,9 +5317,9 @@ public function translate_select_item( WP_Parser_Node $node ): string { * * @param WP_Parser_Node $node The "tableRef" AST node. * @return string The translated value. - * @throws WP_SQLite_Driver_Exception When the translation fails. + * @throws WP_MySQL_On_SQLite_Exception When the translation fails. */ - public function translate_table_ref( WP_Parser_Node $node ): string { + private function translate_table_ref( WP_Parser_Node $node ): string { // The table reference is in "." or "
    " format. $parts = $node->get_descendant_nodes( 'identifier' ); $table = array_pop( $parts ); @@ -5022,7 +5440,7 @@ public function translate_table_ref( WP_Parser_Node $node ): string { * @param array $column_map Optional. A map of column names (old name -> new name) * to use when copying data from the original table. * When not provided, all columns are copied without renaming. - * @throws WP_SQLite_Driver_Exception + * @throws WP_MySQL_On_SQLite_Exception */ private function recreate_table_from_information_schema( bool $table_is_temporary, @@ -5190,7 +5608,7 @@ private function apply_auto_increment_table_option( * @param WP_Parser_Node $like_or_where The "likeOrWhere" AST node. * @param string $like_column The column name to use in the LIKE clause ("table_name", "column_name", etc.). * @return string The translated value. - * @throws WP_SQLite_Driver_Exception When the translation fails. + * @throws WP_MySQL_On_SQLite_Exception When the translation fails. */ private function translate_show_like_or_where_condition( WP_Parser_Node $like_or_where, string $like_column ): string { $like_clause = $like_or_where->get_first_child_node( 'likeClause' ); @@ -5682,6 +6100,171 @@ private function translate_update_list( string $table_name, WP_Parser_Node $pare return $fragment; } + /** + * Get the default SQL modes for the emulated MySQL version. + * + * @return string[] Default SQL mode names. + */ + private function get_default_sql_modes(): array { + $sql_modes = self::DEFAULT_SQL_MODES; + if ( $this->mysql_version < 80000 ) { + $sql_modes[] = 'NO_AUTO_CREATE_USER'; + } + + return $sql_modes; + } + + /** + * Set the active SQL modes from a name list or numeric bitmask. + * + * @param string[]|int $modes SQL mode names or a numeric bitmask. + */ + private function set_sql_modes( $modes ): void { + if ( null === $modes ) { + throw $this->new_invalid_sql_mode_exception( $modes ); + } + + $known_sql_modes_mask = array_sum( self::SQL_MODES ); + + // TIME_TRUNCATE_FRACTIONAL was introduced in MySQL 8.0.1. + if ( $this->mysql_version < 80001 ) { + $known_sql_modes_mask &= ~self::SQL_MODES['TIME_TRUNCATE_FRACTIONAL']; + } + + // Numeric SQL mode bitmap assignment (e.g., "SET sql_mode = 4" for ANSI_QUOTES). + if ( is_int( $modes ) ) { + // Ensure the bitmap contains only SQL mode bits known to the emulated MySQL version. + if ( $modes < 0 || ( $modes & ~$known_sql_modes_mask ) !== 0 ) { + throw $this->new_invalid_sql_mode_exception( $modes ); + } + + // Reject recognized but no longer supported SQL modes. + $unsupported_sql_modes = 0; + foreach ( self::SQL_MODES as $mode => $value ) { + if ( ( $modes & $value ) !== 0 && $this->is_sql_mode_removed( $mode ) ) { + $unsupported_sql_modes |= $value; + } + } + + if ( 0 !== $unsupported_sql_modes ) { + throw $this->new_driver_exception( + sprintf( + 'SQLSTATE[HY000]: General error: 3899 sql_mode=0x%08x is not supported.', + $unsupported_sql_modes + ), + 'HY000' + ); + } + + $sql_modes = $modes; + } elseif ( is_array( $modes ) ) { + // String SQL mode assignment (e.g., "SET sql_mode = 'ANSI_QUOTES,STRICT_TRANS_TABLES'"). + $sql_modes = 0; + foreach ( $modes as $mode ) { + if ( '' === $mode ) { + continue; + } + + $normalized_mode = strtoupper( $mode ); + $mode_value = self::SQL_MODES[ $normalized_mode ] ?? 0; + if ( + 0 === ( $mode_value & $known_sql_modes_mask ) + || ( 'NOT_USED' === $normalized_mode && $this->mysql_version < 80000 ) + || $this->is_sql_mode_removed( $normalized_mode ) + ) { + throw $this->new_invalid_sql_mode_exception( $mode ); + } + + $sql_modes |= $mode_value; + } + } else { + throw $this->new_driver_exception( + "SQLSTATE[42000]: Syntax error or access violation: 1232 Incorrect argument type to variable 'sql_mode'", + '42000' + ); + } + + if ( ( $sql_modes & self::SQL_MODES['NO_BACKSLASH_ESCAPES'] ) !== 0 ) { + throw $this->new_not_supported_exception( "SQL mode 'NO_BACKSLASH_ESCAPES'" ); + } + + /* + * MySQL retains composite SQL modes while enabling their component modes. + * Store both so "@@sql_mode" and individual mode checks match MySQL, even + * though not all resulting modes are respected by the emulation yet. + * + * See: + * https://dev.mysql.com/doc/refman/8.4/en/sql-mode.html#sql-mode-combo + * https://github.com/mysql/mysql-server/blob/8.4/sql/sys_vars.cc + */ + if ( ( $sql_modes & self::SQL_MODES['ANSI'] ) !== 0 ) { + $sql_modes |= self::SQL_MODES['REAL_AS_FLOAT'] + | self::SQL_MODES['PIPES_AS_CONCAT'] + | self::SQL_MODES['ANSI_QUOTES'] + | self::SQL_MODES['IGNORE_SPACE'] + | self::SQL_MODES['ONLY_FULL_GROUP_BY']; + } + + $this->active_sql_modes = $sql_modes; + } + + /** + * Check whether an SQL mode was removed from the emulated MySQL version. + * + * @param string $mode Normalized SQL mode name. + * @return bool Whether the SQL mode was removed. + */ + private function is_sql_mode_removed( string $mode ): bool { + /* + * MySQL still recognizes the legacy bits for modes removed in 8.0.11 + * so it can report them as unsupported, rather than unknown. + */ + return $this->mysql_version >= 80011 + && in_array( + $mode, + array( + 'POSTGRESQL', + 'ORACLE', + 'MSSQL', + 'DB2', + 'MAXDB', + 'NO_KEY_OPTIONS', + 'NO_TABLE_OPTIONS', + 'NO_FIELD_OPTIONS', + 'MYSQL323', + 'MYSQL40', + 'NO_AUTO_CREATE_USER', + ), + true + ); + } + + /** + * Get the active SQL mode names in canonical bitmask order. + * + * @return string[] Active SQL mode names. + */ + private function get_active_sql_mode_names(): array { + $active_modes = array(); + foreach ( self::SQL_MODES as $mode => $value ) { + if ( ( $this->active_sql_modes & $value ) !== 0 ) { + if ( 'NOT_USED' === $mode && $this->mysql_version < 80000 ) { + /* + * MySQL 5.7 represents reserved bit 4 with a comma in its mode + * name table. Two empty components preserve that serialization + * when the final list is joined with commas. + */ + $active_modes[] = ''; + $active_modes[] = ''; + } else { + $active_modes[] = $mode; + } + } + } + + return $active_modes; + } + /** * Store column metadata for the last SQLite statement. * @@ -6170,7 +6753,7 @@ private function get_database_name( WP_Parser_Node $node ): string { * @param string $table_name The name of the table to create. * @param string|null $new_table_name Override the original table name for ALTER TABLE emulation. * @return string[] Queries to create the table, indexes, and constraints. - * @throws WP_SQLite_Driver_Exception When the table information is missing. + * @throws WP_MySQL_On_SQLite_Exception When the table information is missing. */ private function get_sqlite_create_table_statement( bool $table_is_temporary, @@ -7019,6 +7602,7 @@ private function flush(): void { $this->last_sqlite_queries = array(); $this->last_result_statement = null; $this->last_affected_rows = null; + $this->last_insert_id = '0'; $this->last_column_meta = array(); $this->is_readonly = false; $this->wrapper_transaction_type = null; @@ -7124,19 +7708,21 @@ private function create_result_statement_from_data( array $columns, array $rows } /** - * Create a new SQLite driver exception. + * Create a new MySQL-on-SQLite driver exception. * - * @param string $message The exception message. - * @param int|string $code The exception code. For PDO errors, a string representing SQLSTATE. - * @param Throwable|null $previous The previous exception. - * @return WP_SQLite_Driver_Exception + * @param string $message The exception message. + * @param int|string $code The exception code. For PDO errors, a string representing SQLSTATE. + * @param Throwable|null $previous The previous exception. + * @param array|null $error_info PDO-style error information. + * @return WP_MySQL_On_SQLite_Exception */ private function new_driver_exception( string $message, $code = 0, - ?Throwable $previous = null - ): WP_SQLite_Driver_Exception { - return new WP_SQLite_Driver_Exception( $this, $message, $code, $previous ); + ?Throwable $previous = null, + ?array $error_info = null + ): WP_MySQL_On_SQLite_Exception { + return new WP_MySQL_On_SQLite_Exception( $this, $message, $code, $previous, $error_info ); } /** @@ -7145,10 +7731,10 @@ private function new_driver_exception( * This exception can be used to mark cases that should never occur according * to the MySQL grammar. It may serve as an assertion that should never fail. * - * @return WP_SQLite_Driver_Exception + * @return WP_MySQL_On_SQLite_Exception */ - private function new_invalid_input_exception(): WP_SQLite_Driver_Exception { - return new WP_SQLite_Driver_Exception( $this, 'MySQL query syntax error.' ); + private function new_invalid_input_exception(): WP_MySQL_On_SQLite_Exception { + return new WP_MySQL_On_SQLite_Exception( $this, 'MySQL query syntax error.' ); } /** @@ -7157,21 +7743,37 @@ private function new_invalid_input_exception(): WP_SQLite_Driver_Exception { * This exception can be used to mark MySQL constructs that are not supported. * * @param string $cause The cause, indicating which construct is not supported. - * @return WP_SQLite_Driver_Exception + * @return WP_MySQL_On_SQLite_Exception */ - private function new_not_supported_exception( string $cause ): WP_SQLite_Driver_Exception { - return new WP_SQLite_Driver_Exception( + private function new_not_supported_exception( string $cause ): WP_MySQL_On_SQLite_Exception { + return new WP_MySQL_On_SQLite_Exception( $this, sprintf( 'MySQL query not supported. Cause: %s', $cause ) ); } + /** + * Create a MySQL-compatible exception for an invalid SQL mode value. + * + * @param mixed $value The invalid SQL mode value. + * @return WP_MySQL_On_SQLite_Exception + */ + private function new_invalid_sql_mode_exception( $value ): WP_MySQL_On_SQLite_Exception { + return $this->new_driver_exception( + sprintf( + "SQLSTATE[42000]: Syntax error or access violation: 1231 Variable 'sql_mode' can't be set to the value of '%s'", + null === $value ? 'NULL' : (string) $value + ), + '42000' + ); + } + /** * Create a new access denied exception for the information schema database. * - * @return WP_SQLite_Driver_Exception + * @return WP_MySQL_On_SQLite_Exception */ - private function new_access_denied_to_information_schema_exception(): WP_SQLite_Driver_Exception { + private function new_access_denied_to_information_schema_exception(): WP_MySQL_On_SQLite_Exception { return $this->new_driver_exception( "Access denied for user 'root'@'%' to database 'information_schema'", '42000' @@ -7192,52 +7794,55 @@ private function new_access_denied_to_information_schema_exception(): WP_SQLite_ private function convert_information_schema_exception( WP_SQLite_Information_Schema_Exception $e ): Throwable { switch ( $e->get_type() ) { case WP_SQLite_Information_Schema_Exception::TYPE_DUPLICATE_TABLE_NAME: + $driver_message = sprintf( "Table '%s' already exists", $e->get_data()['table_name'] ); return $this->new_driver_exception( - sprintf( - "SQLSTATE[42S01]: Base table or view already exists: 1050 Table '%s' already exists", - $e->get_data()['table_name'] - ), - '42S01' + 'SQLSTATE[42S01]: Base table or view already exists: 1050 ' . $driver_message, + '42S01', + null, + array( '42S01', 1050, $driver_message ) ); case WP_SQLite_Information_Schema_Exception::TYPE_DUPLICATE_COLUMN_NAME: + $driver_message = sprintf( "Duplicate column name '%s'", $e->get_data()['column_name'] ); return $this->new_driver_exception( - sprintf( - "SQLSTATE[42S21]: Column already exists: 1060 Duplicate column name '%s'", - $e->get_data()['column_name'] - ), - '42S21' + 'SQLSTATE[42S21]: Column already exists: 1060 ' . $driver_message, + '42S21', + null, + array( '42S21', 1060, $driver_message ) ); case WP_SQLite_Information_Schema_Exception::TYPE_DUPLICATE_KEY_NAME: + $driver_message = sprintf( "Duplicate key name '%s'", $e->get_data()['key_name'] ); return $this->new_driver_exception( - sprintf( - "SQLSTATE[42000]: Syntax error or access violation: 1061 Duplicate key name '%s'", - $e->get_data()['key_name'] - ), - '42S21' + 'SQLSTATE[42000]: Syntax error or access violation: 1061 ' . $driver_message, + '42000', + null, + array( '42000', 1061, $driver_message ) ); case WP_SQLite_Information_Schema_Exception::TYPE_KEY_COLUMN_NOT_FOUND: + $driver_message = sprintf( "Key column '%s' doesn't exist in table", $e->get_data()['column_name'] ); return $this->new_driver_exception( - sprintf( - "SQLSTATE[42000]: Syntax error or access violation: 1072 Key column '%s' doesn't exist in table", - $e->get_data()['column_name'] - ), - '42000' + 'SQLSTATE[42000]: Syntax error or access violation: 1072 ' . $driver_message, + '42000', + null, + array( '42000', 1072, $driver_message ) ); case WP_SQLite_Information_Schema_Exception::TYPE_CONSTRAINT_DOES_NOT_EXIST: + $driver_message = sprintf( "Constraint '%s' does not exist.", $e->get_data()['name'] ); return $this->new_driver_exception( - sprintf( - "SQLSTATE[HY000]: General error: 3940 Constraint '%s' does not exist.", - $e->get_data()['name'] - ), - 'HY000' + 'SQLSTATE[HY000]: General error: 3940 ' . $driver_message, + 'HY000', + null, + array( 'HY000', 3940, $driver_message ) ); case WP_SQLite_Information_Schema_Exception::TYPE_MULTIPLE_CONSTRAINTS_WITH_NAME: + $driver_message = sprintf( + "Table has multiple constraints with the name '%s'. Please use constraint specific 'DROP' clause.", + $e->get_data()['name'] + ); return $this->new_driver_exception( - sprintf( - "SQLSTATE[HY000]: General error: 3939 Table has multiple constraints with the name '%s'. Please use constraint specific 'DROP' clause.", - $e->get_data()['name'] - ), - 'HY000' + 'SQLSTATE[HY000]: General error: 3939 ' . $driver_message, + 'HY000', + null, + array( 'HY000', 3939, $driver_message ) ); default: return $e; diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-configurator.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-configurator.php index 280cfc283..45da0863a 100644 --- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-configurator.php +++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-configurator.php @@ -9,6 +9,8 @@ * The configurator ensures that tables required for emulating MySQL behaviors * are created and populated with necessary data. It is also able to partially * repair and update these tables and metadata in case of database corruption. + * + * @access private */ class WP_SQLite_Configurator { /** diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-connection.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-connection.php index 170ba01c8..60792103b 100644 --- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-connection.php +++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-connection.php @@ -12,6 +12,8 @@ * It requires PDO with the SQLite driver, and currently, it is only a simple * wrapper that leaks some of the PDO APIs (returns PDOStatement values, etc.). * In the future, we may abstract it away from PDO and support SQLite3 as well. + * + * @access private */ class WP_SQLite_Connection { /** @@ -57,9 +59,9 @@ class WP_SQLite_Connection { /** * A query logger callback. * - * @var callable(string, array): void + * @var (callable(string, array): void)|null */ - private $query_logger; + private $query_logger = null; /** * Constructor. @@ -79,6 +81,7 @@ class WP_SQLite_Connection { * @type string|null $journal_mode Optional. SQLite journal mode. Defaults to WAL. * @type string|int|null $synchronous Optional. SQLite synchronous setting. Defaults to * NORMAL when the effective journal mode is WAL. + * @type array $pdo_options Optional. PDO constructor options. * } * * @throws InvalidArgumentException When some connection options are invalid. @@ -92,8 +95,13 @@ public function __construct( array $options ) { if ( ! isset( $options['path'] ) || ! is_string( $options['path'] ) ) { throw new InvalidArgumentException( 'Option "path" is required when "connection" is not provided.' ); } - $pdo_class = PHP_VERSION_ID >= 80400 ? PDO\SQLite::class : PDO::class; - $this->pdo = new $pdo_class( 'sqlite:' . $options['path'] ); + $pdo_class = PHP_VERSION_ID >= 80400 ? PDO\SQLite::class : PDO::class; + $pdo_options = $options['pdo_options'] ?? array(); + + // Internal driver operations require exceptions regardless of the + // caller-visible WP_MySQL_On_SQLite::ATTR_ERRMODE setting. + $pdo_options[ PDO::ATTR_ERRMODE ] = PDO::ERRMODE_EXCEPTION; + $this->pdo = new $pdo_class( 'sqlite:' . $options['path'], null, null, $pdo_options ); } // Throw exceptions on error. @@ -274,11 +282,11 @@ public function get_pdo(): PDO { } /** - * Set a logger for the queries. + * Set or clear a logger for SQLite queries. * - * @param callable(string, array): void $logger A query logger callback. + * @param (callable(string, array): void)|null $logger A query logger callback, or null to clear it. */ - public function set_query_logger( callable $logger ): void { + public function set_query_logger( ?callable $logger ): void { $this->query_logger = $logger; } } diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-driver-exception.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-driver-exception.php deleted file mode 100644 index 26a01ad2f..000000000 --- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-driver-exception.php +++ /dev/null @@ -1,33 +0,0 @@ -code = $code; - $this->driver = $driver; - } - - public function getDriver(): WP_MySQL_On_SQLite { - return $this->driver; - } -} diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-driver.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-driver.php index d7010ecf3..dacb0c759 100644 --- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-driver.php +++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-driver.php @@ -3,6 +3,9 @@ /* * The SQLite driver uses PDO. Enable PDO function calls: * phpcs:disable WordPress.DB.RestrictedClasses.mysql__PDO + * + * PDO uses camel case naming, enable non-snake case: + * phpcs:disable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid */ /** @@ -10,13 +13,15 @@ * (e.g., WP CLI SQLite Command), ensure the PDO-based classes are loaded. */ require_once __DIR__ . '/class-wp-mysql-on-sqlite.php'; -require_once __DIR__ . '/class-wp-pdo-proxy-statement.php'; +require_once __DIR__ . '/class-wp-mysql-on-sqlite-statement.php'; /** * Deprecated: A proxy of the WP_MySQL_On_SQLite class preserving the legacy API. * * This class temporarily preserves the legacy constructor and result API while * consumers transition to the PDO-based WP_MySQL_On_SQLite API. + * + * @deprecated 3.0.0 Use WP_MySQL_On_SQLite instead. */ class WP_SQLite_Driver { /** @@ -42,6 +47,13 @@ class WP_SQLite_Driver { */ private $mysql_on_sqlite_driver; + /** + * Statement returned for the last emulated query. + * + * @var WP_MySQL_On_SQLite_Statement|null + */ + private $last_statement; + /** * Results of the last emulated query. * @@ -54,10 +66,11 @@ class WP_SQLite_Driver { * * Set up an SQLite connection and the MySQL-on-SQLite driver. * - * @param WP_SQLite_Connection $connection A SQLite database connection. - * @param string $database The database name. + * @param WP_SQLite_Connection $connection A SQLite database connection. + * @param string $database The database name. + * @param int $mysql_version The emulated MySQL version as an integer. * - * @throws WP_SQLite_Driver_Exception When the driver initialization fails. + * @throws WP_MySQL_On_SQLite_Exception When the driver initialization fails. */ public function __construct( WP_SQLite_Connection $connection, @@ -65,7 +78,7 @@ public function __construct( int $mysql_version = 80038 ) { $this->mysql_on_sqlite_driver = new WP_MySQL_On_SQLite( - sprintf( 'mysql-on-sqlite:dbname=%s', $database ), + sprintf( 'mysql-on-sqlite:dbname=%s', str_replace( ';', ';;', $database ) ), null, null, array( @@ -74,10 +87,9 @@ public function __construct( 'journal_mode' => $connection->query( 'PRAGMA journal_mode' )->fetchColumn(), ) ); - $this->main_db_name = $database; $this->client_info = $this->mysql_on_sqlite_driver->client_info; - $connection->get_pdo()->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); + $this->mysql_on_sqlite_driver->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); } /** @@ -145,7 +157,11 @@ public function get_last_sqlite_queries(): array { * @return int|string */ public function get_insert_id() { - return $this->mysql_on_sqlite_driver->get_insert_id(); + $last_insert_id = $this->mysql_on_sqlite_driver->lastInsertId(); + if ( is_numeric( $last_insert_id ) ) { + $last_insert_id = (int) $last_insert_id; + } + return $last_insert_id; } /** @@ -155,10 +171,12 @@ public function get_insert_id() { * * @return mixed Return value, depending on the query type. * - * @throws WP_SQLite_Driver_Exception When the query execution fails. + * @throws WP_MySQL_On_SQLite_Exception When the query execution fails. */ public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mode_args ) { - $stmt = $this->mysql_on_sqlite_driver->query( $query, $fetch_mode, ...$fetch_mode_args ); + $this->last_statement = null; + $stmt = $this->mysql_on_sqlite_driver->query( $query, $fetch_mode, ...$fetch_mode_args ); + $this->last_statement = $stmt; if ( $stmt->columnCount() > 0 ) { $this->last_result = $stmt->fetchAll( $fetch_mode ); @@ -202,7 +220,7 @@ public function get_last_return_value() { * @return int */ public function get_last_column_count(): int { - return $this->mysql_on_sqlite_driver->get_last_column_count(); + return null === $this->last_statement ? 0 : $this->last_statement->columnCount(); } /** @@ -211,7 +229,16 @@ public function get_last_column_count(): int { * @return array */ public function get_last_column_meta(): array { - return $this->mysql_on_sqlite_driver->get_last_column_meta(); + if ( null === $this->last_statement ) { + return array(); + } + + $column_meta = array(); + $column_count = $this->last_statement->columnCount(); + for ( $i = 0; $i < $column_count; $i++ ) { + $column_meta[] = $this->last_statement->getColumnMeta( $i ); + } + return $column_meta; } /** @@ -229,7 +256,7 @@ public function execute_sqlite_query( string $sql, array $params = array() ): PD /** * Begin a new transaction or nested transaction. */ - public function beginTransaction(): void { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid + public function beginTransaction(): void { $this->mysql_on_sqlite_driver->beginTransaction(); } @@ -255,26 +282,4 @@ public function commit(): void { public function rollback(): void { $this->mysql_on_sqlite_driver->rollback(); } - - /** - * Proxy also the private property "$main_db_name", as it is used in tests. - */ - public function __set( string $name, $value ): void { - if ( 'main_db_name' === $name ) { - $closure = function ( string $value ) { - $this->main_db_name = $value; - }; - $closure->call( $this->mysql_on_sqlite_driver, $value ); - } - } - - /** - * Proxy also this private method, as it is used in tests. - */ - private function quote_mysql_utf8_string_literal( string $utf8_literal ): string { - $closure = function ( string $utf8_literal ) { - return $this->quote_mysql_utf8_string_literal( $utf8_literal ); - }; - return $closure->call( $this->mysql_on_sqlite_driver, $utf8_literal ); - } } diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-information-schema-builder.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-information-schema-builder.php index 7c28316df..b113f293b 100644 --- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-information-schema-builder.php +++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-information-schema-builder.php @@ -6,6 +6,8 @@ * This class builds and maintains MySQL INFORMATION_SCHEMA tables in SQLite. * It consumes the AST of MySQL DDL queries and records the schema information * in SQLite tables that emulate the MySQL INFORMATION_SCHEMA. + * + * @access private */ class WP_SQLite_Information_Schema_Builder { /** diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-information-schema-exception.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-information-schema-exception.php index 93a9ab88c..7b7e62920 100644 --- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-information-schema-exception.php +++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-information-schema-exception.php @@ -5,6 +5,8 @@ * * This class is used to represent errors that may occur when building * the MySQL information schema for emulation in SQLite. + * + * @access private */ class WP_SQLite_Information_Schema_Exception extends Exception { // Information schema exception types. diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-information-schema-reconstructor.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-information-schema-reconstructor.php index 3b60b1fc1..bc860c571 100644 --- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-information-schema-reconstructor.php +++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-information-schema-reconstructor.php @@ -14,6 +14,8 @@ * Currently, it reconstructs schema infromation for missing tables, and removes * stale data for tables that no longer exist. When used with WordPress, it uses * the "wp_get_db_schema()" function to reconstruct WordPress table information. + * + * @access private */ class WP_SQLite_Information_Schema_Reconstructor { /** @@ -76,7 +78,7 @@ public function ensure_correct_information_schema(): void { $sql = $this->generate_create_table_statement( $table ); $ast = $this->driver->create_parser( $sql )->parse(); if ( null === $ast ) { - throw new WP_SQLite_Driver_Exception( $this->driver, 'Failed to parse the MySQL query.' ); + throw new WP_MySQL_On_SQLite_Exception( $this->driver, 'Failed to parse the MySQL query.' ); } } @@ -111,7 +113,7 @@ private function record_drop_table( string $table_name ): void { $sql = sprintf( 'DROP TABLE %s', $this->connection->quote_identifier( $table_name ) ); // TODO: mysql quote $ast = $this->driver->create_parser( $sql )->parse(); if ( null === $ast ) { - throw new WP_SQLite_Driver_Exception( $this->driver, 'Failed to parse the MySQL query.' ); + throw new WP_MySQL_On_SQLite_Exception( $this->driver, 'Failed to parse the MySQL query.' ); } $this->schema_builder->record_drop_table( $ast->get_first_descendant_node( 'dropStatement' ) @@ -233,7 +235,7 @@ private function get_wp_create_table_statements(): array { while ( $parser->next_query() ) { $ast = $parser->get_query_ast(); if ( null === $ast ) { - throw new WP_SQLite_Driver_Exception( $this->driver, 'Failed to parse the MySQL query.' ); + throw new WP_MySQL_On_SQLite_Exception( $this->driver, 'Failed to parse the MySQL query.' ); } $create_node = $ast->get_first_descendant_node( 'createStatement' ); diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-pdo-user-defined-functions.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-pdo-user-defined-functions.php index 9e44bf17d..54dfae7fa 100644 --- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-pdo-user-defined-functions.php +++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/class-wp-sqlite-pdo-user-defined-functions.php @@ -19,6 +19,8 @@ * * This automatically enables ref_to_pdo_obj to replace the function in the SQL statement * to the ones defined here. + * + * @access private */ class WP_SQLite_PDO_User_Defined_Functions { diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/trait-wp-mysql-on-sqlite-pdo-compat-php-84.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/trait-wp-mysql-on-sqlite-pdo-compat-php-84.php new file mode 100644 index 000000000..f59d71322 --- /dev/null +++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/database/sqlite/trait-wp-mysql-on-sqlite-pdo-compat-php-84.php @@ -0,0 +1,31 @@ +resetDatabases(); - } - - private function resetDatabases() { - if ( file_exists( FQDB ) ) { - unlink( FQDB ); - } - $GLOBALS['mysql']->query( 'DROP DATABASE IF EXISTS ' . DB_NAME ); - $GLOBALS['mysql']->query( 'CREATE DATABASE ' . DB_NAME ); - $GLOBALS['mysql']->query( 'USE ' . DB_NAME ); - } - - public function query( $query ) { - /** - * In MySQL, AUTO_INCREMENT columns don't reuse IDs assigned in rollback transactions - * In SQLite, AUTOINCREMENT columns do reuse IDs assigned in rollback transactions - * - * Let's store the current AUTOINCREMENT value for each table, and restore it afterwards. - */ - if ( preg_match( '/^\s*rollback/i', $query ) ) { - $autoincrements = array(); - $tables = $GLOBALS['@pdo']->query( "SELECT name as `table` FROM sqlite_master WHERE type='table' ORDER BY name" )->fetchAll(); - foreach ( $tables as $table ) { - $table = $table['table']; - $autoincrement = $GLOBALS['@pdo']->query( "SELECT seq FROM sqlite_sequence WHERE name = '$table'" )->fetchColumn(); - $autoincrements[ $table ] = $autoincrement ?: 1; - } - } - $sqlite_retval = parent::query( $query ); - if ( preg_match( '/^\s*rollback/i', $query ) ) { - foreach ( $autoincrements as $table => $autoincrement ) { - $GLOBALS['@pdo']->query( "UPDATE sqlite_sequence SET seq = $autoincrement WHERE name = '$table'" ); - } - } - $this->crosscheck( $query, $sqlite_retval ); - return $sqlite_retval; - } - - private function crosscheck( $query, $sqlite_retval ) { - // echo $query."\n\n"; - // Be lenient on cross-checking some query types - if ( preg_match( '/^\s*SET storage_engine/i', $query ) ) { - return; - } - $this->show_errors = false; - $this->suppress_errors = true; - $GLOBALS['mysql']->show_errors = false; - $GLOBALS['mysql']->suppress_errors = true; - - ob_start(); - $mysql_retval = $GLOBALS['mysql']->query( $query ); - ob_end_clean(); - - $tests = array( - array( 'retval', $mysql_retval, $sqlite_retval ), - array( 'num_rows', $GLOBALS['mysql']->num_rows, $GLOBALS['sqlite']->num_rows ), - array( 'insert_id', $GLOBALS['mysql']->insert_id, $GLOBALS['sqlite']->insert_id ), - array( 'rows_affected', $GLOBALS['mysql']->rows_affected, $GLOBALS['sqlite']->rows_affected ), - ); - - foreach ( $tests as $test ) { - list($factor, $mysql, $sqlite) = $test; - if ( $mysql !== $sqlite ) { - if ( 'insert_id' === $factor ) { - // On multi-inserts MySQL returns the first inserted ID - // while SQLite returns the last one. The cached insert_id - // value stays the same for a number of subsequent queries. - // Let's forgive this for now. - continue; - } - if ( 'rows_affected' === $factor && $mysql_retval === $mysql ) { - // SQLite doesn't provide the rowcount() functionality - continue; - } - if ( 'retval' === $factor && $GLOBALS['mysql']->rows_affected === $mysql ) { - // SQLite doesn't provide the rowcount() functionality - continue; - } - echo "======================================================\n"; - echo "======== *** $factor *** differed for query ========= \n"; - echo "======================================================\n"; - echo "MySQL query: \n"; - echo "$query\n\n"; - - echo "SQLite queries: \n"; - foreach ( $this->dbh->last_translation->queries as $query ) { - echo $query->sql . "\n"; - } - echo "\n"; - - $this->report_factor( - 'error', - $GLOBALS['mysql']->last_error, - $GLOBALS['sqlite']->last_error - ); - foreach ( $tests as $test ) { - $this->report_factor( - $test[0], - $test[1], - $test[2] - ); - } - // throw new Exception(); - break; - } - } - } - - private function report_factor( $factor, $mysql, $sqlite ) { - echo "$factor: \n"; - echo ' MySQL: ' . var_export( $mysql, true ) . "\n"; - echo ' SQLite: ' . var_export( $sqlite, true ) . "\n\n"; - } -} diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-db.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-db.php index b94c66d20..ada0cf207 100644 --- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-db.php +++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-db.php @@ -56,6 +56,22 @@ public function __construct( $dbname ) { $this->charset = 'utf8mb4'; } + /** + * Returns the active MySQL-on-SQLite driver. + * + * @since 3.0.0 + * + * @return WP_MySQL_On_SQLite The active driver. + * @throws RuntimeException When there is no active database connection. + */ + public function get_driver(): WP_MySQL_On_SQLite { + if ( ! $this->dbh ) { + throw new RuntimeException( 'Cannot access the driver without an active database connection.' ); + } + + return $this->dbh; + } + /** * Method to set character set for the database. * @@ -71,17 +87,59 @@ public function set_charset( $dbh, $charset = null, $collate = null ) { } /** - * Method to get the character set for the database. - * Hardcoded to utf8mb4 for now. + * Retrieves the character set for the given column. + * + * This overrides wpdb::get_col_charset() to enable the parent's implementation + * for SQLite by temporarily setting the is_mysql flag. * - * @param string $table The table name. - * @param string $column The column name. + * @see wpdb::get_col_charset() * - * @return string The character set. + * @param string $table Table name. + * @param string $column Column name. + * @return string|false|WP_Error Column character set as a string. False if the column has + * no character set. WP_Error object on failure. */ public function get_col_charset( $table, $column ) { - // Hardcoded for now. - return 'utf8mb4'; + $original_is_mysql = $this->is_mysql ?? null; + + /* + * The parent method returns early when `$this->is_mysql` is falsy. + * Since SQLite doesn't set this flag, we enable it temporarily so + * the parent can run its full logic — querying column metadata via + * SHOW FULL COLUMNS (which the SQLite driver translates) and + * populating the `$this->col_meta` cache. + */ + try { + $this->is_mysql = true; + return parent::get_col_charset( $table, $column ); + } finally { + $this->is_mysql = $original_is_mysql; + } + } + + /** + * Retrieves the maximum string length allowed in a given column. + * + * This overrides wpdb::get_col_length() to enable the parent's implementation + * for SQLite by temporarily setting the is_mysql flag. + * + * @see wpdb::get_col_length() + * + * @param string $table Table name. + * @param string $column Column name. + * @return array|false|WP_Error Column length information, false if the column has + * no length. WP_Error object on failure. + */ + public function get_col_length( $table, $column ) { + $original_is_mysql = $this->is_mysql ?? null; + + // See get_col_charset() for an explanation of the is_mysql flag. + try { + $this->is_mysql = true; + return parent::get_col_length( $table, $column ); + } finally { + $this->is_mysql = $original_is_mysql; + } } /** @@ -130,14 +188,98 @@ public function set_sql_mode( $modes = array() ) { /** * Closes the current database connection. - * Noop in SQLite. * - * @return bool True to indicate the connection was successfully closed. + * This overrides wpdb::close() while closely mirroring its implementation. + * + * @see wpdb::close() + * + * @return bool True if the connection was successfully closed, + * false if it wasn't, or if the connection doesn't exist. */ public function close() { + if ( ! $this->dbh ) { + return false; + } + + $connection = $this->dbh->get_connection(); + $pdo = $connection->get_pdo(); + + try { + if ( $this->dbh->inTransaction() ) { + $this->dbh->rollBack(); + } elseif ( $pdo->inTransaction() ) { + $pdo->rollBack(); + } else { + /* + * On PHP < 8.4, PDO cannot detect transactions started via SQL. + * A savepoint ensures ROLLBACK succeeds with or without one. + */ + $pdo->exec( 'SAVEPOINT wp_sqlite_db_close' ); + $pdo->exec( 'ROLLBACK' ); + } + } catch ( Throwable $e ) { + return false; + } + + if ( + isset( $GLOBALS['@pdo'] ) + && $GLOBALS['@pdo'] === $pdo + ) { + unset( $GLOBALS['@pdo'] ); + } + + $connection->set_query_logger( null ); + $this->result = null; + $this->dbh = null; + $this->ready = false; + $this->has_connected = false; + return true; } + /** + * Determines the best charset and collation to use given a charset and collation. + * + * For example, when able, utf8mb4 should be used instead of utf8. + * + * This overrides wpdb::determine_charset() while closely mirroring its implementation. + * The override is needed because the parent checks for a mysqli connection object. + * + * @param string $charset The character set to check. + * @param string $collate The collation to check. + * @return array { + * The most appropriate character set and collation to use. + * + * @type string $charset Character set. + * @type string $collate Collation. + * } + */ + public function determine_charset( $charset, $collate ) { + if ( ! $this->dbh ) { + return compact( 'charset', 'collate' ); + } + + if ( 'utf8' === $charset ) { + $charset = 'utf8mb4'; + } + + if ( 'utf8mb4' === $charset ) { + // _general_ is outdated, so we can upgrade it to _unicode_, instead. + if ( ! $collate || 'utf8_general_ci' === $collate ) { + $collate = 'utf8mb4_unicode_ci'; + } else { + $collate = str_replace( 'utf8_', 'utf8mb4_', $collate ); + } + } + + // _unicode_520_ is a better collation, we should use that when it's available. + if ( $this->has_cap( 'utf8mb4_520' ) && 'utf8mb4_unicode_ci' === $collate ) { + $collate = 'utf8mb4_unicode_520_ci'; + } + + return compact( 'charset', 'collate' ); + } + /** * Method to select the database connection. * @@ -162,12 +304,20 @@ public function select( $db, $dbh = null ) { * @param string $data The string to escape. * * @return string escaped + * @throws RuntimeException When the database connection is not initialized. */ public function _real_escape( $data ) { if ( ! is_scalar( $data ) ) { return ''; } - $escaped = addslashes( $data ); + + if ( ! $this->dbh ) { + throw new RuntimeException( 'Cannot escape data without an active database connection.' ); + } + + // Escape the string without bounding quotes to mirror mysqli_real_escape_string(). + $quoted = $this->dbh->quote( (string) $data ); + $escaped = substr( $quoted, 1, -1 ); return $this->add_placeholder_escape( $escaped ); } @@ -268,17 +418,23 @@ public function flush() { * @see wpdb::db_connect() * * @param bool $allow_bail Not used. - * @return void + * @return bool True on a successful connection, false on failure. */ public function db_connect( $allow_bail = true ) { if ( $this->dbh ) { - return; + return $this->ready; } - $this->init_charset(); - $pdo = null; + $this->last_error = ''; if ( isset( $GLOBALS['@pdo'] ) ) { - $pdo = $GLOBALS['@pdo']; + trigger_error( + 'PDO injection via $GLOBALS[\'@pdo\'] is no longer supported. The existing PDO will be ignored and a new connection will be created.', + E_USER_WARNING + ); + } + + if ( ! isset( $this->charset ) ) { + $this->init_charset(); } // Migrate the database file from a legacy path, if it exists. @@ -314,10 +470,7 @@ public function db_connect( $allow_bail = true ) { $options = array( 'journal_mode' => defined( 'SQLITE_JOURNAL_MODE' ) ? SQLITE_JOURNAL_MODE : null, ); - if ( null !== $pdo ) { - $options['pdo'] = $pdo; - } - $this->dbh = new WP_MySQL_On_SQLite( + $dbh = new WP_MySQL_On_SQLite( sprintf( 'mysql-on-sqlite:path=%s;dbname=%s', str_replace( ';', ';;', FQDB ), @@ -327,27 +480,44 @@ public function db_connect( $allow_bail = true ) { null, $options ); - $this->dbh->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO - $GLOBALS['@pdo'] = $this->dbh->get_connection()->get_pdo(); + $dbh->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO + $this->dbh = $dbh; + + /** + * Exposes the SQLite PDO instance for backward compatibility. + * + * @deprecated 3.0.0 Use WP_SQLite_DB::get_driver() with + * WP_MySQL_On_SQLite::get_sqlite_pdo() instead. + */ + $GLOBALS['@pdo'] = $dbh->get_sqlite_pdo(); } catch ( Throwable $e ) { $this->last_error = $this->format_error_message( $e ); } if ( $this->last_error ) { return false; } + + $this->has_connected = true; + $this->set_charset( $this->dbh ); + $this->ready = true; $this->set_sql_mode(); + return true; } /** - * Method to dummy out wpdb::check_connection() + * Checks that the database connection is available. * * @param bool $allow_bail Not used. * - * @return bool + * @return bool True when the connection is available, false otherwise. */ public function check_connection( $allow_bail = true ) { - return true; + if ( $this->dbh ) { + return true; + } + + return $this->db_connect( $allow_bail ); } /** @@ -420,14 +590,13 @@ public function query( $query ) { $last_query_count = count( $this->queries ?? array() ); /* - * @TODO: WPDB uses "$this->check_current_query" to check table/column - * charset and strip all invalid characters from the query. - * This is an involved process that we can bypass for SQLite, - * if we simply strip all invalid UTF-8 characters from the query. + * @TODO: wpdb uses "$this->check_current_query" and table metadata to + * reject queries containing invalid text. Implement equivalent handling + * for SQLite without relying on the MySQL-specific conversion pipeline. * - * To do so, mb_convert_encoding can be used with an optional - * fallback to a htmlspecialchars method. E.g.: - * https://github.com/nette/utils/blob/be534713c227aeef57ce1883fc17bc9f9e29eca2/src/Utils/Strings.php#L42 + * PCRE's "u" modifier can validate UTF-8 without constructing a converted + * query copy: 1 === preg_match( '//u', $query ). The implementation must + * preserve wpdb's exemptions for prevalidated and binary data. */ $this->_do_query( $query ); @@ -448,7 +617,7 @@ public function query( $query ) { // Take note of the insert_id. if ( preg_match( '/^\s*(insert|replace)\s/i', $query ) ) { - $this->insert_id = $this->dbh->get_insert_id(); + $this->insert_id = (int) $this->dbh->lastInsertId(); } // Return number of rows affected. @@ -539,7 +708,11 @@ protected function load_col_info() { return; } $this->col_info = array(); - foreach ( $this->dbh->get_last_column_meta() as $column ) { + if ( null === $this->result ) { + return; + } + for ( $i = 0; $i < $this->result->columnCount(); $i++ ) { + $column = $this->result->getColumnMeta( $i ); $this->col_info[] = (object) array( 'name' => $column['name'], 'orgname' => $column['mysqli:orgname'], @@ -559,21 +732,22 @@ protected function load_col_info() { } /** - * Method to return what the database can do. + * Determines whether the database supports a given feature. * - * This overrides wpdb::has_cap() to avoid using MySQL functions. - * SQLite supports subqueries, but not support collation, group_concat and set_charset. + * The utf8mb4 check is handled here because older WordPress versions inspect + * the MySQL client library. All other capabilities use the parent logic. * * @see wpdb::has_cap() * - * @param string $db_cap The feature to check for. Accepts 'collation', - * 'group_concat', 'subqueries', 'set_charset', - * 'utf8mb4', or 'utf8mb4_520'. - * - * @return bool Whether the database feature is supported, false otherwise. + * @param string $db_cap The feature to check for. + * @return bool True when the database feature is supported, false otherwise. */ public function has_cap( $db_cap ) { - return 'subqueries' === strtolower( $db_cap ); + if ( 'utf8mb4' === strtolower( $db_cap ) ) { + return true; + } + + return parent::has_cap( $db_cap ); } /** @@ -592,9 +766,13 @@ public function db_version() { /** * Returns the version of the SQLite engine. * - * @return string SQLite engine version as a string. + * @return string SQLite engine version, or an empty string while disconnected. */ public function db_server_info() { + if ( ! $this->dbh ) { + return ''; + } + return $this->dbh->get_sqlite_version(); } @@ -647,7 +825,7 @@ private function ensure_database_directory( string $database_path ) { /** - * Format SQLite driver error message. + * Format MySQL-on-SQLite driver error message. * * @return string */ @@ -655,8 +833,8 @@ private function format_error_message( Throwable $e ) { $output = '
     
    ' . PHP_EOL; // Queries. - if ( $e instanceof WP_SQLite_Driver_Exception ) { - $driver = $e->getDriver(); + if ( $e instanceof WP_MySQL_On_SQLite_Exception ) { + $driver = $e->get_driver(); $output .= '
    ' . PHP_EOL; $output .= '

    MySQL query:

    ' . PHP_EOL; diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/sqlite/db.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/sqlite/db.php index 033135152..7ede214af 100644 --- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/sqlite/db.php +++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/sqlite/db.php @@ -51,21 +51,8 @@ require_once __DIR__ . '/class-wp-sqlite-db.php'; require_once __DIR__ . '/install-functions.php'; -$db_name = defined( 'DB_NAME' ) ? DB_NAME : ''; +$db_name = defined( 'DB_NAME' ) ? DB_NAME : ''; +$GLOBALS['wpdb'] = new WP_SQLite_DB( $db_name ); -/* - * Debug: Cross-check with MySQL. - * This is for debugging purpose only and requires files - * that are present in the GitHub repository - * but not the plugin published on WordPress.org. - */ -$crosscheck_tests_file_path = __DIR__ . '/class-wp-sqlite-crosscheck-db.php'; -if ( defined( 'SQLITE_DEBUG_CROSSCHECK' ) && SQLITE_DEBUG_CROSSCHECK && file_exists( $crosscheck_tests_file_path ) ) { - require_once $crosscheck_tests_file_path; - $GLOBALS['wpdb'] = new WP_SQLite_Crosscheck_DB( $db_name ); -} else { - $GLOBALS['wpdb'] = new WP_SQLite_DB( $db_name ); - - // Boot the Query Monitor plugin if it is active. - require_once __DIR__ . '/../../integrations/query-monitor/boot.php'; -} +// Boot the Query Monitor plugin if it is active. +require_once __DIR__ . '/../../integrations/query-monitor/boot.php'; diff --git a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/sqlite/install-functions.php b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/sqlite/install-functions.php index 89d1fc1ad..e849fe181 100644 --- a/wp/wp-content/plugins/sqlite-database-integration/wp-includes/sqlite/install-functions.php +++ b/wp/wp-content/plugins/sqlite-database-integration/wp-includes/sqlite/install-functions.php @@ -68,53 +68,6 @@ function sqlite_make_db_sqlite() { wp_die( $message, 'Database Error!' ); } - /* - * Debug: Cross-check with MySQL. - * This is for debugging purpose only and requires files - * that are present in the GitHub repository - * but not the plugin published on WordPress.org. - */ - if ( defined( 'SQLITE_DEBUG_CROSSCHECK' ) && SQLITE_DEBUG_CROSSCHECK ) { - $host = DB_HOST; - $port = 3306; - if ( str_contains( $host, ':' ) ) { - $host_parts = explode( ':', $host ); - $host = $host_parts[0]; - $port = $host_parts[1]; - } - $dsn = 'mysql:host=' . $host . '; port=' . $port . '; dbname=' . DB_NAME; - $pdo_class = PHP_VERSION_ID >= 80400 ? PDO\MySQL::class : PDO::class; // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO - $pdo_mysql = new $pdo_class( $dsn, DB_USER, DB_PASSWORD, array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ) ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO - $pdo_mysql->query( 'SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";' ); - $pdo_mysql->query( 'SET time_zone = "+00:00";' ); - foreach ( $queries as $query ) { - $query = trim( $query ); - if ( empty( $query ) ) { - continue; - } - try { - $pdo_mysql->beginTransaction(); - $pdo_mysql->query( $query ); - } catch ( PDOException $err ) { - $err_data = $err->errorInfo; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $err_code = $err_data[1]; - // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual - if ( 5 == $err_code || 6 == $err_code ) { - // If the database is locked, commit again. - $pdo_mysql->commit(); - } else { - $pdo_mysql->rollBack(); - $message = sprintf( - 'Error occurred while creating tables or indexes...
    Query was: %s
    ', - var_export( $query, true ) - ); - $message .= sprintf( 'Error message is: %s', $err_data[2] ); - wp_die( $message, 'Database Error!' ); - } - } - } - } - return true; } diff --git a/wp/wp-includes/blocks/latest-comments.php b/wp/wp-includes/blocks/latest-comments.php index eec7e1045..37ee11c4e 100644 --- a/wp/wp-includes/blocks/latest-comments.php +++ b/wp/wp-includes/blocks/latest-comments.php @@ -120,7 +120,10 @@ function render_block_core_latest_comments( $attributes ) { } $list_items_markup .= ''; if ( 'full' === $display_content ) { - $list_items_markup .= '
    ' . wpautop( get_comment_text( $comment ) ) . '
    '; + $comment_text = post_password_required( $comment->comment_post_ID ) + ? __( 'Password protected' ) + : get_comment_text( $comment ); + $list_items_markup .= '
    ' . wpautop( $comment_text ) . '
    '; } elseif ( 'excerpt' === $display_content ) { $list_items_markup .= '
    ' . wpautop( get_comment_excerpt( $comment ) ) . '
    '; } diff --git a/wp/wp-includes/blocks/post-content.php b/wp/wp-includes/blocks/post-content.php index 0be1a8730..b75d7144d 100644 --- a/wp/wp-includes/blocks/post-content.php +++ b/wp/wp-includes/blocks/post-content.php @@ -56,8 +56,18 @@ function render_block_core_post_content( $attributes, $content, $block ) { $tag_name = 'div'; - if ( ! empty( $attributes['tagName'] ) && tag_escape( $attributes['tagName'] ) === $attributes['tagName'] ) { - $tag_name = $attributes['tagName']; + if ( isset( $attributes['tagName'] ) && is_string( $attributes['tagName'] ) ) { + /** + * The allowed tag names match the options offered in the editor. + * + * @see packages/block-library/src/post-content/edit.js + */ + $allowed_tag_names = array( 'div', 'main', 'section', 'article' ); + $normalized_tag_name = strtolower( $attributes['tagName'] ); + + if ( in_array( $normalized_tag_name, $allowed_tag_names, true ) ) { + $tag_name = $normalized_tag_name; + } } $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => 'entry-content' ) ); diff --git a/wp/wp-includes/blocks/post-date.php b/wp/wp-includes/blocks/post-date.php index a8073dd84..4e6e9e52f 100644 --- a/wp/wp-includes/blocks/post-date.php +++ b/wp/wp-includes/blocks/post-date.php @@ -81,10 +81,10 @@ function render_block_core_post_date( $attributes, $content, $block ) { $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => implode( ' ', $classes ) ) ); - $time_tag = sprintf( '', $unformatted_date, $formatted_date ); + $time_tag = sprintf( '', esc_attr( $unformatted_date ), esc_html( $formatted_date ) ); if ( isset( $attributes['isLink'] ) && $attributes['isLink'] && isset( $block->context['postId'] ) ) { - $time_tag = sprintf( '%2s', get_the_permalink( $block->context['postId'] ), $time_tag ); + $time_tag = sprintf( '%2$s', esc_url( get_the_permalink( $block->context['postId'] ) ), $time_tag ); } return sprintf( '
    %2$s
    ', $wrapper_attributes, $time_tag ); diff --git a/wp/wp-includes/canonical.php b/wp/wp-includes/canonical.php index 9315ba7fb..ff0e773b0 100644 --- a/wp/wp-includes/canonical.php +++ b/wp/wp-includes/canonical.php @@ -985,7 +985,7 @@ function redirect_guess_404_permalink() { if ( empty( $post_types ) ) { return false; } - $where .= " AND post_type IN ('" . join( "', '", esc_sql( get_query_var( 'post_type' ) ) ) . "')"; + $where .= " AND post_type IN ('" . join( "', '", esc_sql( $post_types ) ) . "')"; } else { if ( ! in_array( get_query_var( 'post_type' ), $publicly_viewable_post_types, true ) ) { return false; diff --git a/wp/wp-includes/class-wp-query.php b/wp/wp-includes/class-wp-query.php index 854b5506f..b60ea9cf3 100644 --- a/wp/wp-includes/class-wp-query.php +++ b/wp/wp-includes/class-wp-query.php @@ -2815,11 +2815,11 @@ public function get_posts() { if ( $this->is_comment_feed && ! $this->is_singular ) { if ( $this->is_archive || $this->is_search ) { $cjoin = "JOIN {$wpdb->posts} ON ( {$wpdb->comments}.comment_post_ID = {$wpdb->posts}.ID ) $join "; - $cwhere = "WHERE comment_approved = '1' $where"; + $cwhere = "WHERE comment_approved = '1' AND {$wpdb->comments}.comment_type != 'note' $where"; $cgroupby = "{$wpdb->comments}.comment_id"; } else { // Other non-singular, e.g. front. $cjoin = "JOIN {$wpdb->posts} ON ( {$wpdb->comments}.comment_post_ID = {$wpdb->posts}.ID )"; - $cwhere = "WHERE ( post_status = 'publish' OR ( post_status = 'inherit' AND post_type = 'attachment' ) ) AND comment_approved = '1'"; + $cwhere = "WHERE ( post_status = 'publish' OR ( post_status = 'inherit' AND post_type = 'attachment' ) ) AND comment_approved = '1' AND {$wpdb->comments}.comment_type != 'note'"; $cgroupby = ''; } @@ -3477,7 +3477,7 @@ public function get_posts() { $cjoin = apply_filters_ref_array( 'comment_feed_join', array( '', &$this ) ); /** This filter is documented in wp-includes/class-wp-query.php */ - $cwhere = apply_filters_ref_array( 'comment_feed_where', array( "WHERE comment_post_ID = '{$this->posts[0]->ID}' AND comment_approved = '1'", &$this ) ); + $cwhere = apply_filters_ref_array( 'comment_feed_where', array( "WHERE comment_post_ID = '{$this->posts[0]->ID}' AND comment_approved = '1' AND {$wpdb->comments}.comment_type != 'note'", &$this ) ); /** This filter is documented in wp-includes/class-wp-query.php */ $cgroupby = apply_filters_ref_array( 'comment_feed_groupby', array( '', &$this ) ); diff --git a/wp/wp-includes/class-wp-script-modules.php b/wp/wp-includes/class-wp-script-modules.php index 154c95151..9c9f8a762 100644 --- a/wp/wp-includes/class-wp-script-modules.php +++ b/wp/wp-includes/class-wp-script-modules.php @@ -1058,15 +1058,16 @@ public function print_script_module_data(): void { * The data for a given Script Module, if provided, will be JSON serialized in a script * tag with an ID of the form `wp-script-module-data-{$module_id}`. * - * The data can be read on the client with a pattern like this: + * The data can be read on the client with a pattern like the following; you are encouraged + * to use this pattern _verbatim_ to avoid common pitfalls or vulnerabilities: * * Example: * - * const dataContainer = document.getElementById( 'wp-script-module-data-MyScriptModuleID' ); + * const dataContainer = document.querySelector( 'script[id="wp-script-module-data-MyScriptModuleID"]' ); * let data = {}; - * if ( dataContainer ) { + * if ( dataContainer instanceof HTMLScriptElement ) { * try { - * data = JSON.parse( dataContainer.textContent ); + * data = JSON.parse( dataContainer.text ); * } catch {} * } * // data.dataForClient === 'ok'; diff --git a/wp/wp-includes/http.php b/wp/wp-includes/http.php index 8280f4249..67da47cd4 100644 --- a/wp/wp-includes/http.php +++ b/wp/wp-includes/http.php @@ -595,9 +595,30 @@ function wp_http_validate_url( $url ) { } if ( $ip ) { $parts = array_map( 'intval', explode( '.', $ip ) ); - if ( 127 === $parts[0] || 10 === $parts[0] || 0 === $parts[0] - || ( 172 === $parts[0] && 16 <= $parts[1] && 31 >= $parts[1] ) - || ( 192 === $parts[0] && 168 === $parts[1] ) + + /* + * These IP address ranges are not considered valid external hosts for HTTP requests. + * + * If the host resolves to an IP address in these ranges, the request will be rejected unless the 'http_request_host_is_external' filter allows it. + * + * References: + * + * - IPv4 Special-Purpose Address Space: https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml + * - IPv4 Multicast Address Assignments: https://www.rfc-editor.org/rfc/rfc5771.html + */ + if ( 127 === $parts[0] || 10 === $parts[0] || 0 === $parts[0] // 127.0.0.0/8 (loopback), 10.0.0.0/8 (private), 0.0.0.0/8 (this network). + || ( 172 === $parts[0] && 16 <= $parts[1] && 31 >= $parts[1] ) // 172.16.0.0/12 (private). + || ( 192 === $parts[0] && 168 === $parts[1] ) // 192.168.0.0/16 (private). + || ( 192 === $parts[0] && 0 === $parts[1] && 0 === $parts[2] ) // 192.0.0.0/24 (IETF protocol assignments). + || ( 192 === $parts[0] && 0 === $parts[1] && 2 === $parts[2] ) // 192.0.2.0/24 (TEST-NET-1). + || ( 192 === $parts[0] && 88 === $parts[1] && 99 === $parts[2] ) // 192.88.99.0/24 (6to4 relay anycast). + || ( 198 === $parts[0] && 51 === $parts[1] && 100 === $parts[2] ) // 198.51.100.0/24 (TEST-NET-2). + || ( 203 === $parts[0] && 0 === $parts[1] && 113 === $parts[2] ) // 203.0.113.0/24 (TEST-NET-3). + || ( 169 === $parts[0] && 254 === $parts[1] ) // 169.254.0.0/16 (link-local and cloud metadata). + || ( 100 === $parts[0] && 64 <= $parts[1] && 127 >= $parts[1] ) // 100.64.0.0/10 (CGNAT). + || ( 198 === $parts[0] && 18 <= $parts[1] && 19 >= $parts[1] ) // 198.18.0.0/15 (benchmarking). + || ( 224 <= $parts[0] && 239 >= $parts[0] ) // 224.0.0.0/4 (multicast). + || 240 <= $parts[0] // 240.0.0.0/4 (reserved, includes 255.255.255.255 broadcast). ) { // If host appears local, reject unless specifically allowed. /** diff --git a/wp/wp-includes/js/wp-emoji-loader.js b/wp/wp-includes/js/wp-emoji-loader.js index 561d3656a..488210767 100644 --- a/wp/wp-includes/js/wp-emoji-loader.js +++ b/wp/wp-includes/js/wp-emoji-loader.js @@ -16,9 +16,12 @@ * @property {?string} source.wpemoji */ -const settings = /** @type {WPEmojiSettings} */ ( - JSON.parse( document.getElementById( 'wp-emoji-settings' ).textContent ) -); +const selector = 'script#wp-emoji-settings'; +const script = document.querySelector( selector ); +if ( ! ( script instanceof HTMLScriptElement ) ) { + throw new Error( `Element missing: ${ selector }`); +} +const settings = /** @type {WPEmojiSettings} */ ( JSON.parse( script.text ) ); // For compatibility with other scripts that read from this global, in particular wp-includes/js/wp-emoji.js (source file: js/_enqueues/wp/emoji.js). window._wpemojiSettings = settings; diff --git a/wp/wp-includes/js/wp-emoji-loader.min.js b/wp/wp-includes/js/wp-emoji-loader.min.js index b0cf072e3..e11c598b2 100644 --- a/wp/wp-includes/js/wp-emoji-loader.min.js +++ b/wp/wp-includes/js/wp-emoji-loader.min.js @@ -1,2 +1,2 @@ /*! This file is auto-generated */ -const a=JSON.parse(document.getElementById("wp-emoji-settings").textContent),o=(window._wpemojiSettings=a,"wpEmojiSettingsSupports"),s=["flag","emoji"];function i(e){try{var t={supportTests:e,timestamp:(new Date).valueOf()};sessionStorage.setItem(o,JSON.stringify(t))}catch(e){}}function c(e,t,n){e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(t,0,0);t=new Uint32Array(e.getImageData(0,0,e.canvas.width,e.canvas.height).data);e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(n,0,0);const a=new Uint32Array(e.getImageData(0,0,e.canvas.width,e.canvas.height).data);return t.every((e,t)=>e===a[t])}function p(e,t){e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(t,0,0);var n=e.getImageData(16,16,1,1);for(let e=0;e{s[e]=t(o,e,n,a)}),s}function r(e){var t=document.createElement("script");t.src=e,t.defer=!0,document.head.appendChild(t)}a.supports={everything:!0,everythingExceptFlag:!0},new Promise(t=>{let n=function(){try{var e=JSON.parse(sessionStorage.getItem(o));if("object"==typeof e&&"number"==typeof e.timestamp&&(new Date).valueOf(){i(n=e.data),r.terminate(),t(n)})}catch(e){}i(n=f(s,u,c,p))}t(n)}).then(e=>{for(const n in e)a.supports[n]=e[n],a.supports.everything=a.supports.everything&&a.supports[n],"flag"!==n&&(a.supports.everythingExceptFlag=a.supports.everythingExceptFlag&&a.supports[n]);var t;a.supports.everythingExceptFlag=a.supports.everythingExceptFlag&&!a.supports.flag,a.supports.everything||((t=a.source||{}).concatemoji?r(t.concatemoji):t.wpemoji&&t.twemoji&&(r(t.twemoji),r(t.wpemoji)))}); \ No newline at end of file +var e="script#wp-emoji-settings",t=document.querySelector(e);if(!(t instanceof HTMLScriptElement))throw new Error("Element missing: "+e);const r=JSON.parse(t.text),s=(window._wpemojiSettings=r,"wpEmojiSettingsSupports"),o=["flag","emoji"];function i(e){try{var t={supportTests:e,timestamp:(new Date).valueOf()};sessionStorage.setItem(s,JSON.stringify(t))}catch(e){}}function c(e,t,n){e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(t,0,0);t=new Uint32Array(e.getImageData(0,0,e.canvas.width,e.canvas.height).data);e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(n,0,0);const r=new Uint32Array(e.getImageData(0,0,e.canvas.width,e.canvas.height).data);return t.every((e,t)=>e===r[t])}function p(e,t){e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(t,0,0);var n=e.getImageData(16,16,1,1);for(let e=0;e{o[e]=t(s,e,n,r)}),o}function a(e){var t=document.createElement("script");t.src=e,t.defer=!0,document.head.appendChild(t)}r.supports={everything:!0,everythingExceptFlag:!0},new Promise(t=>{let n=function(){try{var e=JSON.parse(sessionStorage.getItem(s));if("object"==typeof e&&"number"==typeof e.timestamp&&(new Date).valueOf(){i(n=e.data),a.terminate(),t(n)})}catch(e){}i(n=f(o,u,c,p))}t(n)}).then(e=>{for(const n in e)r.supports[n]=e[n],r.supports.everything=r.supports.everything&&r.supports[n],"flag"!==n&&(r.supports.everythingExceptFlag=r.supports.everythingExceptFlag&&r.supports[n]);var t;r.supports.everythingExceptFlag=r.supports.everythingExceptFlag&&!r.supports.flag,r.supports.everything||((t=r.source||{}).concatemoji?a(t.concatemoji):t.wpemoji&&t.twemoji&&(a(t.twemoji),a(t.wpemoji)))}); \ No newline at end of file diff --git a/wp/wp-includes/kses.php b/wp/wp-includes/kses.php index a45d1697e..bf02737ec 100644 --- a/wp/wp-includes/kses.php +++ b/wp/wp-includes/kses.php @@ -2856,11 +2856,16 @@ function safecss_filter_attr( $css, $deprecated = '' ) { $css_test_string ); + // Bail if the recursive function stripping hit a PCRE error (e.g. stack/backtrack limit). + if ( null === $css_test_string ) { + continue; + } + /* * Disallow CSS containing \ ( & } = or comments, except for within url(), var(), calc(), etc. * which were removed from the test string above. */ - $allow_css = ! preg_match( '%[\\\(&=}]|/\*%', $css_test_string ); + $allow_css = 0 === preg_match( '%[\\\(&=}]|/\*%', $css_test_string ); /** * Filters the check for unsafe CSS in `safecss_filter_attr`. diff --git a/wp/wp-includes/user.php b/wp/wp-includes/user.php index 9c635f63d..360049a90 100644 --- a/wp/wp-includes/user.php +++ b/wp/wp-includes/user.php @@ -186,7 +186,7 @@ function wp_authenticate_username_password( sprintf( /* translators: %s: User name. */ __( 'Error: The username %s is not registered on this site. If you are unsure of your username, try your email address instead.' ), - $username + esc_html( $username ) ) ); } @@ -213,7 +213,7 @@ function wp_authenticate_username_password( sprintf( /* translators: %s: User name. */ __( 'Error: The password you entered for the username %s is incorrect.' ), - '' . $username . '' + '' . esc_html( $username ) . '' ) . ' ' . __( 'Lost your password?' ) . @@ -296,7 +296,7 @@ function wp_authenticate_email_password( sprintf( /* translators: %s: Email address. */ __( 'Error: The password you entered for the email address %s is incorrect.' ), - '' . $email . '' + '' . esc_html( $email ) . '' ) . ' ' . __( 'Lost your password?' ) . @@ -3568,7 +3568,7 @@ function register_new_user( $user_login, $user_email ) { sprintf( /* translators: %s: Link to the login page. */ __( 'Error: This email address is already registered. Log in with this address or choose another one.' ), - wp_login_url() + esc_url( wp_login_url() ) ) ); } @@ -3616,7 +3616,7 @@ function register_new_user( $user_login, $user_email ) { sprintf( /* translators: %s: Admin email address. */ __( 'Error: Could not register you… please contact the site admin!' ), - get_option( 'admin_email' ) + esc_attr( get_option( 'admin_email' ) ) ) ); return $errors; @@ -3839,18 +3839,26 @@ function _wp_get_current_user() { * * @since 3.0.0 * @since 4.9.0 This function was moved from wp-admin/includes/ms.php so it's no longer Multisite specific. + * @since 7.0.3 Added the `$user_id` parameter, which is sent with the `personal_options_update` action. + * + * @param int $user_id Optional. The ID of the user whose email is being changed. Defaults to `$_POST['user_id']` if set, otherwise 0. * * @global WP_Error $errors WP_Error object. */ -function send_confirmation_on_profile_email() { +function send_confirmation_on_profile_email( $user_id = 0 ) { global $errors; + // Maintain backward compatibility for those relying on a check based on $_POST['user_id']. + if ( ! $user_id && isset( $_POST['user_id'] ) ) { + $user_id = absint( $_POST['user_id'] ); + } + $current_user = wp_get_current_user(); if ( ! is_object( $errors ) ) { $errors = new WP_Error(); } - if ( $current_user->ID !== (int) $_POST['user_id'] ) { + if ( 0 === $current_user->ID || $current_user->ID !== (int) $user_id ) { return false; } @@ -3864,6 +3872,7 @@ function send_confirmation_on_profile_email() { ) ); + $_POST['email'] = addslashes( $current_user->user_email ); return; } @@ -3877,6 +3886,7 @@ function send_confirmation_on_profile_email() { ); delete_user_meta( $current_user->ID, '_new_email' ); + $_POST['email'] = addslashes( $current_user->user_email ); return; } diff --git a/wp/wp-includes/version.php b/wp/wp-includes/version.php index 7c6ce2a28..2c57bf92b 100644 --- a/wp/wp-includes/version.php +++ b/wp/wp-includes/version.php @@ -16,7 +16,7 @@ * * @global string $wp_version */ -$wp_version = '7.0.2'; +$wp_version = '7.0.3'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema. diff --git a/wp/wp-login.php b/wp/wp-login.php index abedea82c..f72d1c738 100644 --- a/wp/wp-login.php +++ b/wp/wp-login.php @@ -1220,7 +1220,7 @@ function wp_login_viewport_meta() { sprintf( /* translators: %s: Link to the login page. */ __( 'Check your email for the confirmation link, then visit the login page.' ), - wp_login_url() + esc_url( wp_login_url() ) ), 'message' ); @@ -1230,7 +1230,7 @@ function wp_login_viewport_meta() { sprintf( /* translators: %s: Link to the login page. */ __( 'Registration complete. Please check your email, then visit the login page.' ), - wp_login_url() + esc_url( wp_login_url() ) ), 'message' ); diff --git a/wp/wp-signup.php b/wp/wp-signup.php index 50ab624bb..05b7fa221 100644 --- a/wp/wp-signup.php +++ b/wp/wp-signup.php @@ -999,7 +999,11 @@ function signup_get_available_languages() { } break; case 'gimmeanotherblog': - validate_another_blog_signup(); + if ( 'all' === $active_signup || 'blog' === $active_signup ) { + validate_another_blog_signup(); + } else { + _e( 'Site registration has been disabled.' ); + } break; case 'default': default: