Make WordPress Core


Ignore:
Timestamp:
10/31/2017 01:01:54 PM (8 years ago)
Author:
pento
Message:

Database: Restore numbered placeholders in wpdb::prepare().

[41496] removed support for numbered placeholders in queries send through wpdb::prepare(), which, despite being undocumented, were quite commonly used.

This change restores support for numbered placeholders (as well as a subset of placeholder formatting), while also adding extra checks to ensure the correct number of arguments are being passed to wpdb::prepare(), given the number of placeholders.

Merges [41662], [42056] to the 3.7 branch.
See #41925.

Location:
branches/3.7
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • branches/3.7

  • branches/3.7/tests/phpunit/tests/db.php

    r41508 r42068  
    150150        global $wpdb;
    151151        $sql = $wpdb->prepare( "UPDATE test_table SET string_column = '%%f is a float, %%d is an int %d, %%s is a string', field = %s", 3, '4' );
     152        $this->assertContains( $wpdb->placeholder_escape(), $sql );
     153
     154        $sql = $wpdb->remove_placeholder_escape( $sql );
    152155        $this->assertEquals( "UPDATE test_table SET string_column = '%f is a float, %d is an int 3, %s is a string', field = '4'", $sql );
    153156    }
     
    173176    }
    174177
    175         function test_prepare_vsprintf() {
    176                 global $wpdb;
     178    function test_prepare_vsprintf() {
     179        global $wpdb;
    177180
    178181        $prepared = $wpdb->prepare( "SELECT * FROM $wpdb->users WHERE id = %d AND user_login = %s", array( 1, "admin" ) );
     
    191194        $prepared = @$wpdb->prepare( "SELECT * FROM $wpdb->users WHERE id = %d AND user_login = %s", array( array( 1 ), "admin" ) );
    192195        $this->assertEquals( "SELECT * FROM $wpdb->users WHERE id = 0 AND user_login = 'admin'", $prepared );
    193         }
     196    }
     197
     198    /**
     199     * @ticket 42040
     200     * @dataProvider data_prepare_incorrect_arg_count
     201     * @expectedIncorrectUsage wpdb::prepare
     202     */
     203    public function test_prepare_incorrect_arg_count( $query, $args, $expected ) {
     204        global $wpdb;
     205
     206        // $query is the first argument to be passed to wpdb::prepare()
     207        array_unshift( $args, $query );
     208
     209        $prepared = @call_user_func_array( array( $wpdb, 'prepare' ), $args );
     210        $this->assertEquals( $expected, $prepared );
     211    }
     212
     213    public function data_prepare_incorrect_arg_count() {
     214        global $wpdb;
     215
     216        return array(
     217            array(
     218                "SELECT * FROM $wpdb->users WHERE id = %d AND user_login = %s",     // Query
     219                array( 1, "admin", "extra-arg" ),                                   // ::prepare() args, to be passed via call_user_func_array
     220                "SELECT * FROM $wpdb->users WHERE id = 1 AND user_login = 'admin'", // Expected output
     221            ),
     222            array(
     223                "SELECT * FROM $wpdb->users WHERE id = %%%d AND user_login = %s",
     224                array( 1 ),
     225                false,
     226            ),
     227            array(
     228                "SELECT * FROM $wpdb->users WHERE id = %d AND user_login = %s",
     229                array( array( 1, "admin", "extra-arg" ) ),
     230                "SELECT * FROM $wpdb->users WHERE id = 1 AND user_login = 'admin'",
     231            ),
     232            array(
     233                "SELECT * FROM $wpdb->users WHERE id = %d AND %% AND user_login = %s",
     234                array( 1, "admin", "extra-arg" ),
     235                "SELECT * FROM $wpdb->users WHERE id = 1 AND {$wpdb->placeholder_escape()} AND user_login = 'admin'",
     236            ),
     237            array(
     238                "SELECT * FROM $wpdb->users WHERE id = %%%d AND %F AND %f AND user_login = %s",
     239                array( 1, 2.3, "4.5", "admin", "extra-arg" ),
     240                "SELECT * FROM $wpdb->users WHERE id = {$wpdb->placeholder_escape()}1 AND 2.300000 AND 4.500000 AND user_login = 'admin'",
     241            ),
     242            array(
     243                "SELECT * FROM $wpdb->users WHERE id = %d AND user_login = %s",
     244                array( array( 1 ), "admin", "extra-arg" ),
     245                "SELECT * FROM $wpdb->users WHERE id = 0 AND user_login = 'admin'",
     246            ),
     247            array(
     248                "SELECT * FROM $wpdb->users WHERE id = %d and user_nicename = %s and user_status = %d and user_login = %s",
     249                array( 1, "admin", 0 ),
     250                '',
     251            ),
     252            array(
     253                "SELECT * FROM $wpdb->users WHERE id = %d and user_nicename = %s and user_status = %d and user_login = %s",
     254                array( array( 1, "admin", 0 ) ),
     255                '',
     256            ),
     257            array(
     258                "SELECT * FROM $wpdb->users WHERE id = %d and %% and user_login = %s and user_status = %d and user_login = %s",
     259                array( 1, "admin", "extra-arg" ),
     260                '',
     261            ),
     262        );
     263    }
    194264
    195265    function test_db_version() {
     
    609679
    610680    /**
    611      *
    612      */
    613     function test_prepare_with_unescaped_percents() {
    614         global $wpdb;
    615 
    616         $sql = $wpdb->prepare( '%d %1$d %%% %', 1 );
    617         $this->assertEquals( '1 %1$d %% %', $sql );
     681     * @dataProvider data_prepare_with_placeholders
     682     */
     683    function test_prepare_with_placeholders_and_individual_args( $sql, $values, $incorrect_usage, $expected) {
     684        global $wpdb;
     685
     686        if ( $incorrect_usage ) {
     687            $this->setExpectedIncorrectUsage( 'wpdb::prepare' );
     688        }
     689
     690        if ( ! is_array( $values ) ) {
     691            $values = array( $values );
     692        }
     693
     694        array_unshift( $values, $sql );
     695
     696        $sql = call_user_func_array( array( $wpdb, 'prepare' ), $values );
     697        $this->assertEquals( $expected, $sql );
     698    }
     699
     700    /**
     701     * @dataProvider data_prepare_with_placeholders
     702     */
     703    function test_prepare_with_placeholders_and_array_args( $sql, $values, $incorrect_usage, $expected) {
     704        global $wpdb;
     705
     706        if ( $incorrect_usage ) {
     707            $this->setExpectedIncorrectUsage( 'wpdb::prepare' );
     708        }
     709
     710        if ( ! is_array( $values ) ) {
     711            $values = array( $values );
     712        }
     713
     714        $sql = call_user_func_array( array( $wpdb, 'prepare' ), array( $sql, $values ) );
     715        $this->assertEquals( $expected, $sql );
     716    }
     717
     718    function data_prepare_with_placeholders() {
     719        global $wpdb;
     720
     721        return array(
     722            array(
     723                '%5s',   // SQL to prepare
     724                'foo',   // Value to insert in the SQL
     725                false,   // Whether to expect an incorrect usage error or not
     726                '  foo', // Expected output
     727            ),
     728            array(
     729                '%1$d %%% % %%1$d%% %%%1$d%%',
     730                1,
     731                true,
     732                "1 {$wpdb->placeholder_escape()}{$wpdb->placeholder_escape()} {$wpdb->placeholder_escape()} {$wpdb->placeholder_escape()}1\$d{$wpdb->placeholder_escape()} {$wpdb->placeholder_escape()}1{$wpdb->placeholder_escape()}",
     733            ),
     734            array(
     735                '%-5s',
     736                'foo',
     737                false,
     738                'foo  ',
     739            ),
     740            array(
     741                '%05s',
     742                'foo',
     743                false,
     744                '00foo',
     745            ),
     746            array(
     747                "%'#5s",
     748                'foo',
     749                false,
     750                '##foo',
     751            ),
     752            array(
     753                '%.3s',
     754                'foobar',
     755                false,
     756                'foo',
     757            ),
     758            array(
     759                '%.3f',
     760                5.123456,
     761                false,
     762                '5.123',
     763            ),
     764            array(
     765                '%.3f',
     766                5.12,
     767                false,
     768                '5.120',
     769            ),
     770            array(
     771                '%s',
     772                ' %s ',
     773                false,
     774                "' {$wpdb->placeholder_escape()}s '",
     775            ),
     776            array(
     777                '%1$s',
     778                ' %s ',
     779                false,
     780                " {$wpdb->placeholder_escape()}s ",
     781            ),
     782            array(
     783                '%1$s',
     784                ' %1$s ',
     785                false,
     786                " {$wpdb->placeholder_escape()}1\$s ",
     787            ),
     788            array(
     789                '%d %1$d %%% %',
     790                1,
     791                true,
     792                "1 1 {$wpdb->placeholder_escape()}{$wpdb->placeholder_escape()} {$wpdb->placeholder_escape()}",
     793            ),
     794            array(
     795                '%d %2$s',
     796                array( 1, 'hello' ),
     797                false,
     798                "1 hello",
     799            ),
     800            array(
     801                "'%s'",
     802                'hello',
     803                false,
     804                "'hello'",
     805            ),
     806            array(
     807                '"%s"',
     808                'hello',
     809                false,
     810                "'hello'",
     811            ),
     812            array(
     813                "%s '%1\$s'",
     814                'hello',
     815                true,
     816                "'hello' 'hello'",
     817            ),
     818            array(
     819                "%s '%1\$s'",
     820                'hello',
     821                true,
     822                "'hello' 'hello'",
     823            ),
     824            array(
     825                '%s "%1$s"',
     826                'hello',
     827                true,
     828                "'hello' \"hello\"",
     829            ),
     830            array(
     831                "%%s %%'%1\$s'",
     832                'hello',
     833                false,
     834                "{$wpdb->placeholder_escape()}s {$wpdb->placeholder_escape()}'hello'",
     835            ),
     836            array(
     837                '%%s %%"%1$s"',
     838                'hello',
     839                false,
     840                "{$wpdb->placeholder_escape()}s {$wpdb->placeholder_escape()}\"hello\"",
     841            ),
     842            array(
     843                '%s',
     844                ' %  s ',
     845                false,
     846                "' {$wpdb->placeholder_escape()}  s '",
     847            ),
     848            array(
     849                '%%f %%"%1$f"',
     850                3,
     851                false,
     852                "{$wpdb->placeholder_escape()}f {$wpdb->placeholder_escape()}\"3.000000\"",
     853            ),
     854            array(
     855                'WHERE second=\'%2$s\' AND first=\'%1$s\'',
     856                array( 'first arg', 'second arg' ),
     857                false,
     858                "WHERE second='second arg' AND first='first arg'",
     859            ),
     860            array(
     861                'WHERE second=%2$d AND first=%1$d',
     862                array( 1, 2 ),
     863                false,
     864                "WHERE second=2 AND first=1",
     865            ),
     866            array(
     867                "'%'%%s",
     868                'hello',
     869                true,
     870                "'{$wpdb->placeholder_escape()}'{$wpdb->placeholder_escape()}s",
     871            ),
     872            array(
     873                "'%'%%s%s",
     874                'hello',
     875                false,
     876                "'{$wpdb->placeholder_escape()}'{$wpdb->placeholder_escape()}s'hello'",
     877            ),
     878            array(
     879                "'%'%%s %s",
     880                'hello',
     881                false,
     882                "'{$wpdb->placeholder_escape()}'{$wpdb->placeholder_escape()}s 'hello'",
     883            ),
     884            array(
     885                "'%-'#5s' '%'#-+-5s'",
     886                array( 'hello', 'foo' ),
     887                false,
     888                "'hello' 'foo##'",
     889            ),
     890        );
     891    }
     892
     893    /**
     894     * @dataProvider data_escape_and_prepare
     895     */
     896    function test_escape_and_prepare( $escape, $sql, $values, $incorrect_usage, $expected ) {
     897        global $wpdb;
     898
     899        if ( $incorrect_usage ) {
     900            $this->setExpectedIncorrectUsage( 'wpdb::prepare' );
     901        }
     902
     903        $escape = esc_sql( $escape );
     904
     905        $sql = str_replace( '{ESCAPE}', $escape, $sql );
     906
     907        $actual = $wpdb->prepare( $sql, $values );
     908
     909        $this->assertEquals( $expected, $actual );
     910    }
     911
     912    function data_escape_and_prepare() {
     913        global $wpdb;
     914        return array(
     915            array(
     916                '%s',                                  // String to pass through esc_url()
     917                ' {ESCAPE} ',                          // Query to insert the output of esc_url() into, replacing "{ESCAPE}"
     918                'foo',                                 // Data to send to prepare()
     919                true,                                  // Whether to expect an incorrect usage error or not
     920                " {$wpdb->placeholder_escape()}s ",    // Expected output
     921            ),
     922            array(
     923                'foo%sbar',
     924                "SELECT * FROM bar WHERE foo='{ESCAPE}' OR baz=%s",
     925                array( ' SQLi -- -', 'pewpewpew' ),
     926                true,
     927                null,
     928            ),
     929            array(
     930                '%s',
     931                ' %s {ESCAPE} ',
     932                'foo',
     933                false,
     934                " 'foo' {$wpdb->placeholder_escape()}s ",
     935            ),
     936        );
     937    }
     938
     939    /**
     940     * @expectedIncorrectUsage wpdb::prepare
     941     */
     942    function test_double_prepare() {
     943        global $wpdb;
     944
     945        $part = $wpdb->prepare( ' AND meta_value = %s', ' %s ' );
     946        $this->assertNotContains( '%s', $part );
     947        $query = $wpdb->prepare( 'SELECT * FROM {$wpdb->postmeta} WHERE meta_key = %s $part', array( 'foo', 'bar' ) );
     948        $this->assertNull( $query );
     949    }
     950
     951    function test_prepare_numeric_placeholders_float_args() {
     952        global $wpdb;
     953
     954        $actual = $wpdb->prepare(
     955            'WHERE second=%2$f AND first=%1$f',
     956            1.1,
     957            2.2
     958        );
     959
     960        /* Floats can be right padded, need to assert differently */
     961        $this->assertContains( ' first=1.1', $actual );
     962        $this->assertContains( ' second=2.2', $actual );
     963    }
     964
     965    function test_prepare_numeric_placeholders_float_array() {
     966        global $wpdb;
     967
     968        $actual = $wpdb->prepare(
     969            'WHERE second=%2$f AND first=%1$f',
     970            array( 1.1, 2.2 )
     971        );
     972
     973        /* Floats can be right padded, need to assert differently */
     974        $this->assertContains( ' first=1.1', $actual );
     975        $this->assertContains( ' second=2.2', $actual );
     976    }
     977
     978    function test_query_unescapes_placeholders() {
     979        global $wpdb;
     980
     981        $value = ' %s ';
     982
     983        $wpdb->query( "CREATE TABLE {$wpdb->prefix}test_placeholder( a VARCHAR(100) );" );
     984        $sql = $wpdb->prepare( "INSERT INTO {$wpdb->prefix}test_placeholder VALUES(%s)", $value );
     985        $wpdb->query( $sql );
     986
     987        $actual = $wpdb->get_var( "SELECT a FROM {$wpdb->prefix}test_placeholder" );
     988
     989        $wpdb->query( "DROP TABLE {$wpdb->prefix}test_placeholder" );
     990
     991        $this->assertNotContains( '%s', $sql );
     992        $this->assertEquals( $value, $actual );
     993    }
     994
     995    function test_esc_sql_with_unsupported_placeholder_type() {
     996        global $wpdb;
     997
     998        $sql = $wpdb->prepare( ' %s %1$c ', 'foo' );
     999        $sql = $wpdb->prepare( " $sql %s ", 'foo' );
     1000
     1001        $this->assertEquals( "  'foo' {$wpdb->placeholder_escape()}1\$c  'foo' ", $sql );
    6181002    }
    6191003}
Note: See TracChangeset for help on using the changeset viewer.