<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[52176] trunk: WPDB: Capture error in `wpdb::$last_error` when insert fails instead of silently failing for invalid data or value too long.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/52176">52176</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/52176","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>hellofromTonya</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2021-11-16 02:57:53 +0000 (Tue, 16 Nov 2021)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>WPDB: Capture error in `wpdb::$last_error` when insert fails instead of silently failing for invalid data or value too long.

Instead of silently failing when attempting to insert a value into a field, this commit saves the error in the `wpdb::$last_error` property.

Sets `last_error` with an error message if:
* `wpdb::query()` fails for invalid data
* `wpdb::process_fields()` fails to process the value(s) for the field(s) where the value could be too long or contain invalid data

Sets `last_query` if `wpdb::query()` fails for invalid data.

If `__()` is not available, uses non-translated error message to ensure the error is captured.

There is no change to wpdb aborting when an error occurs.

Adds tests.

Props dlt101, mnelson4, dd32, pento, hellofromTonya, davidbaumwald, sergeybiryukov, johnbillion, swissspidy, datainterlock, anandau14, anthonyeden, asif2bd, audrasjb, chaion07, dpegasusm, fpcsjames, galbaras, jdgrimes, justindocanto, kwisatz, liammitchell, lucasw89, lukecarbis, nettsite, nlpro, procodewp, psufan, richardfoley, skunkbad, travisnorthcutt, woodyhayday, zoiec.
Fixes <a href="https://core.trac.wordpress.org/ticket/37267">#37267</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludeswpdbphp">trunk/src/wp-includes/wp-db.php</a></li>
<li><a href="#trunktestsphpunittestsdbphp">trunk/tests/phpunit/tests/db.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludeswpdbphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/wp-db.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/wp-db.php   2021-11-16 02:13:25 UTC (rev 52175)
+++ trunk/src/wp-includes/wp-db.php     2021-11-16 02:57:53 UTC (rev 52176)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2015,7 +2015,15 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        // to flush again, just to make sure everything is clear.
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->flush();
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( $stripped_query !== $query ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                $this->insert_id = 0;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         $this->insert_id  = 0;
+                               $this->last_query = $query;
+
+                               if ( function_exists( '__' ) ) {
+                                       $this->last_error = __( 'WordPress database error: Could not perform query because it contains invalid data.' );
+                               } else {
+                                       $this->last_error = 'WordPress database error: Could not perform query because it contains invalid data.';
+                               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                                 return false;
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2535,6 +2543,32 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $converted_data = $this->strip_invalid_text( $data );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                if ( $data !== $converted_data ) {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                       $problem_fields = array();
+                       foreach ( $data as $field => $value ) {
+                               if ( $value !== $converted_data[ $field ] ) {
+                                       $problem_fields[] = $field;
+                               }
+                       }
+
+                       if ( 1 === count( $problem_fields ) ) {
+                               if ( function_exists( '__' ) ) {
+                                       /* translators: %s Database field where the error occurred. */
+                                       $message = __( 'WordPress database error: Processing the value for the following field failed: %s. The supplied value may be too long or contains invalid data.' );
+                               } else {
+                                       $message = 'WordPress database error: Processing the value for the following field failed: %s. The supplied value may be too long or contains invalid data.';
+                               }
+                       } else {
+                               if ( function_exists( '__' ) ) {
+                                       /* translators: %s Database fields where the error occurred. */
+                                       $message = __( 'WordPress database error: Processing the value for the following fields failed: %s. The supplied value may be too long or contains invalid data.' );
+                               } else {
+                                       $message = 'WordPress database error: Processing the value for the following fields failed: %s. The supplied value may be too long or contains invalid data.';
+                               }
+                       }
+
+                       $this->last_error = sprintf( $message, implode( ', ', $problem_fields ) );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                         return false;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre></div>
<a id="trunktestsphpunittestsdbphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/db.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/db.php  2021-11-16 02:13:25 UTC (rev 52175)
+++ trunk/tests/phpunit/tests/db.php    2021-11-16 02:57:53 UTC (rev 52176)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -33,6 +33,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                parent::set_up();
</span><span class="cx" style="display: block; padding: 0 10px">                $this->_queries = array();
</span><span class="cx" style="display: block; padding: 0 10px">                add_filter( 'query', array( $this, 'query_filter' ) );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                self::$_wpdb->last_error     = null;
+               $GLOBALS['wpdb']->last_error = null;
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1096,6 +1098,164 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @dataProvider data_process_single_field_invalid_data
+        * @dataProvider data_process_multiple_fields_invalid_data
+        *
+        * @ticket 32315
+        *
+        * @covers wpdb::process_fields
+        *
+        * @param array  $data           Data to process.
+        * @param string $errored_fields Expected fields in the error message.
+        */
+       public function test_process_fields_value_too_long_for_field( array $data, $errored_fields ) {
+               global $wpdb;
+
+               $this->assertFalse( self::$_wpdb->process_fields( $wpdb->posts, $data, null ) );
+               $this->assertSame( $this->get_db_error_value_too_long( $errored_fields ), self::$_wpdb->last_error );
+       }
+
+       /**
+        * @dataProvider data_process_single_field_invalid_data
+        *
+        * @ticket 32315
+        *
+        * @covers wpdb::insert
+        *
+        * @param array  $data           Data to process.
+        * @param string $errored_fields Expected fields in the error message.
+        */
+       public function test_insert_value_too_long_for_field( array $data, $errored_fields ) {
+               global $wpdb;
+
+               $this->assertFalse( $wpdb->insert( $wpdb->posts, $data ) );
+               $this->assertSame( $this->get_db_error_value_too_long( $errored_fields ), $wpdb->last_error );
+       }
+
+       /**
+        * @dataProvider data_process_single_field_invalid_data
+        *
+        * @ticket 32315
+        *
+        * @covers wpdb::replace
+        *
+        * @param array  $data           Data to process.
+        * @param string $errored_fields Expected fields in the error message.
+        */
+       public function test_replace_value_too_long_for_field( array $data, $errored_fields ) {
+               global $wpdb;
+
+               $this->assertFalse( $wpdb->replace( $wpdb->posts, $data ) );
+               $this->assertSame( $this->get_db_error_value_too_long( $errored_fields ), $wpdb->last_error );
+       }
+
+       /**
+        * @dataProvider data_process_single_field_invalid_data
+        *
+        * @ticket 32315
+        *
+        * @covers wpdb::update
+        *
+        * @param array  $data           Data to process.
+        * @param string $errored_fields Expected fields in the error message.
+        */
+       public function test_update_value_too_long_for_field( array $data, $errored_fields ) {
+               global $wpdb;
+
+               $this->assertFalse( $wpdb->update( $wpdb->posts, $data, array() ) );
+               $this->assertSame( $this->get_db_error_value_too_long( $errored_fields ), $wpdb->last_error );
+       }
+
+       /**
+        * @dataProvider data_process_single_field_invalid_data
+        *
+        * @ticket 32315
+        *
+        * @covers wpdb::delete
+        *
+        * @param array  $data           Data to process.
+        * @param string $errored_fields Expected fields in the error message.
+        */
+       public function test_delete_value_too_long_for_field( array $data, $errored_fields ) {
+               global $wpdb;
+
+               $this->assertFalse( $wpdb->delete( $wpdb->posts, $data, array() ) );
+               $this->assertSame( $this->get_db_error_value_too_long( $errored_fields ), $wpdb->last_error );
+       }
+
+       /**
+        * Assert the error message matches the fields.
+        *
+        * @param string $errored_fields Expected fields in the error message.
+        */
+       private function get_db_error_value_too_long( $errored_fields ) {
+               return sprintf(
+                       'WordPress database error: Processing the value for the following field%s failed: %s. ' .
+                       'The supplied value may be too long or contains invalid data.',
+                       str_contains( $errored_fields, ', ' ) ? 's' : '',
+                       $errored_fields
+               );
+       }
+
+       /**
+        * Data provider.
+        *
+        * @return array
+        */
+       public function data_process_single_field_invalid_data() {
+               return array(
+                       'too long'      => array(
+                               'data'           => array( 'post_status' => str_repeat( 'a', 21 ) ),
+                               'errored_fields' => 'post_status',
+                       ),
+                       'invalid chars' => array(
+                               'data'           => array( 'post_status' => "\xF5" ),
+                               'errored_fields' => 'post_status',
+                       ),
+               );
+       }
+
+       /**
+        * Data provider.
+        *
+        * @return array
+        */
+       public function data_process_multiple_fields_invalid_data() {
+               return array(
+                       'too long'      => array(
+                               'data'           => array(
+                                       'post_status'  => str_repeat( 'a', 21 ),
+                                       'post_content' => "\xF5",
+                               ),
+                               'errored_fields' => 'post_status, post_content',
+                       ),
+                       'invalid chars' => array(
+                               'data'           => array(
+                                       'post_status' => "\xF5",
+                                       'post_name'   => str_repeat( "\xF5", 21 ),
+                               ),
+                               'errored_fields' => 'post_status, post_name',
+                       ),
+               );
+       }
+
+       /**
+        * @ticket 32315
+        */
+       public function test_query_value_contains_invalid_chars() {
+               global $wpdb;
+
+               $this->assertFalse(
+                       $wpdb->query( "INSERT INTO {$wpdb->posts} (post_status) VALUES ('\xF5')" )
+               );
+
+               $this->assertSame(
+                       'WordPress database error: Could not perform query because it contains invalid data.',
+                       $wpdb->last_error
+               );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * @ticket 15158
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function test_null_insert() {
</span></span></pre>
</div>
</div>

</body>
</html>