Changeset 53575
- Timestamp:
- 06/24/2022 08:33:56 PM (2 years ago)
- Location:
- trunk
- Files:
-
- 2 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-includes/wp-db.php
r53505 r53575 646 646 647 647 /** 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 648 676 * Whether to use mysqli over mysql. Default false. 649 677 * … … 1349 1377 1350 1378 /** 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1351 1410 * Prepares a SQL query for safe execution. 1352 1411 * … … 1356 1415 * - %f (float) 1357 1416 * - %s (string) 1417 1358 1418 * 1359 1419 * All placeholders MUST be left unquoted in the query string. A corresponding argument … … 1381 1441 * by updating the function signature. The second parameter was changed 1382 1442 * from `$args` to `...$args`. 1443 1444 1445 1446 1383 1447 * 1384 1448 * @link https://www.php.net/sprintf Description of syntax. … … 1412 1476 } 1413 1477 1414 // If args were passed as an array (as in vsprintf), move them up.1415 $passed_as_array = false;1416 if ( isset( $args[0] ) && is_array( $args[0] ) && 1 === count( $args ) ) {1417 $passed_as_array = true;1418 $args = $args[0];1419 }1420 1421 foreach ( $args as $arg ) {1422 if ( ! is_scalar( $arg ) && ! is_null( $arg ) ) {1423 wp_load_translations_early();1424 _doing_it_wrong(1425 'wpdb::prepare',1426 sprintf(1427 /* translators: %s: Value type. */1428 __( 'Unsupported value type (%s).' ),1429 gettype( $arg )1430 ),1431 '4.8.2'1432 );1433 }1434 }1435 1436 1478 /* 1437 1479 * Specify the formatting allowed in a placeholder. The following are allowed: … … 1454 1496 $query = str_replace( "'%s'", '%s', $query ); // Strip any existing single quotes. 1455 1497 $query = str_replace( '"%s"', '%s', $query ); // Strip any existing double quotes. 1456 $query = preg_replace( '/(?<!%)%s/', "'%s'", $query ); // Quote the strings, avoiding escaped strings like %%s. 1457 1458 $query = preg_replace( "/(?<!%)(%($allowed_format)?f)/", '%\\2F', $query ); // Force floats to be locale-unaware. 1459 1460 $query = preg_replace( "/%(?:%|$|(?!($allowed_format)?[sdF]))/", '%%\\1', $query ); // Escape any unescaped percents. 1461 1462 // Count the number of valid placeholders in the query. 1463 $placeholders = preg_match_all( "/(^|[^%]|(%%)+)%($allowed_format)?[sdF]/", $query, $matches ); 1498 1499 $query = preg_replace( "/%(?:%|$|(?!($allowed_format)?[sdfFi]))/", '%%\\1', $query ); // Escape any unescaped percents (i.e. anything unrecognised). 1500 1501 // Extract placeholders from the query. 1502 $split_query = preg_split( "/(^|[^%]|(?:%%)+)(%(?:$allowed_format)?[sdfFi])/", $query, -1, PREG_SPLIT_DELIM_CAPTURE ); 1503 1504 $split_query_count = count( $split_query ); 1505 $placeholder_count = ( ( $split_query_count - 1 ) / 3 ); // Split always returns with 1 value before the first placeholder (even with $query = "%s"), then 3 additional values per placeholder. 1506 1507 // If args were passed as an array (as in vsprintf), move them up. 1508 $passed_as_array = ( isset( $args[0] ) && is_array( $args[0] ) && 1 === count( $args ) ); 1509 if ( $passed_as_array ) { 1510 $args = $args[0]; 1511 } 1512 1513 $new_query = ''; 1514 $key = 2; // keys 0 and 1 in $split_query contain values before the first placeholder. 1515 $arg_id = 0; 1516 $arg_identifiers = array(); 1517 $arg_strings = array(); 1518 while ( $key < $split_query_count ) { 1519 $placeholder = $split_query[ $key ]; 1520 1521 $format = substr( $placeholder, 1, -1 ); 1522 $type = substr( $placeholder, -1 ); 1523 1524 if ( 'f' === $type ) { // Force floats to be locale-unaware. 1525 $type = 'F'; 1526 $placeholder = '%' . $format . $type; 1527 } 1528 1529 if ( 'i' === $type ) { 1530 $placeholder = '`%' . $format . 's`'; 1531 $argnum_pos = strpos( $format, '$' ); // Using a simple strpos() due to previous checking (e.g. $allowed_format). 1532 if ( false !== $argnum_pos ) { 1533 $arg_identifiers[] = ( intval( substr( $format, 0, $argnum_pos ) ) - 1 ); // sprintf argnum starts at 1, $arg_id from 0. 1534 } else { 1535 $arg_identifiers[] = $arg_id; 1536 } 1537 } elseif ( 'd' !== $type && 'F' !== $type ) { // i.e. ('s' === $type), where 'd' and 'F' keeps $placeholder unchanged, and we ensure string escaping is used as a safe default (e.g. even if 'x'). 1538 $argnum_pos = strpos( $format, '$' ); 1539 if ( false !== $argnum_pos ) { 1540 $arg_strings[] = ( intval( substr( $format, 0, $argnum_pos ) ) - 1 ); 1541 } 1542 if ( true !== $this->allow_unsafe_unquoted_parameters || '' === $format ) { // Unquoted strings for backwards compatibility (dangerous). 1543 $placeholder = "'%" . $format . "s'"; 1544 } 1545 } 1546 1547 $new_query .= $split_query[ $key - 2 ] . $split_query[ $key - 1 ] . $placeholder; // Glue (-2), any leading characters (-1), then the new $placeholder. 1548 1549 $key += 3; 1550 $arg_id++; 1551 } 1552 $query = $new_query . $split_query[ $key - 2 ]; // Replace $query; and add remaining $query characters, or index 0 if there were no placeholders. 1553 1554 $dual_use = array_intersect( $arg_identifiers, $arg_strings ); 1555 if ( count( $dual_use ) ) { 1556 wp_load_translations_early(); 1557 _doing_it_wrong( 1558 'wpdb::prepare', 1559 sprintf( 1560 /* translators: %s: A comma-separated list of arguments found to be a problem. */ 1561 __( 'Arguments (%s) cannot be used for both String and Identifier escaping.' ), 1562 implode( ', ', $dual_use ) 1563 ), 1564 '6.1.0' 1565 ); 1566 1567 return; 1568 } 1464 1569 1465 1570 $args_count = count( $args ); 1466 1571 1467 if ( $args_count !== $placeholder s) {1468 if ( 1 === $placeholder s&& $passed_as_array ) {1572 if ( $args_count !== $placeholder ) { 1573 if ( 1 === $placeholder && $passed_as_array ) { 1469 1574 // If the passed query only expected one argument, but the wrong number of arguments were sent as an array, bail. 1470 1575 wp_load_translations_early(); … … 1487 1592 /* translators: 1: Number of placeholders, 2: Number of arguments passed. */ 1488 1593 __( 'The query does not contain the correct number of placeholders (%1$d) for the number of arguments passed (%2$d).' ), 1489 $placeholder s,1594 $placeholder, 1490 1595 $args_count 1491 1596 ), … … 1497 1602 * return an empty string to avoid a fatal error on PHP 8. 1498 1603 */ 1499 if ( $args_count < $placeholders ) { 1500 $max_numbered_placeholder = ! empty( $matches[3] ) ? max( array_map( 'intval', $matches[3] ) ) : 0; 1501 1604 if ( $args_count < $placeholder_count ) { 1605 $max_numbered_placeholder = 0; 1606 for ( $i = 2, $l = $split_query_count; $i < $l; $i += 3 ) { 1607 $argnum = intval( substr( $split_query[ $i ], 1 ) ); // Assume a leading number is for a numbered placeholder, e.g. '%3$s'. 1608 if ( $max_numbered_placeholder < $argnum ) { 1609 $max_numbered_placeholder = $argnum; 1610 } 1611 } 1502 1612 if ( ! $max_numbered_placeholder || $args_count < $max_numbered_placeholder ) { 1503 1613 return ''; … … 1507 1617 } 1508 1618 1509 array_walk( $args, array( $this, 'escape_by_ref' ) ); 1510 $query = vsprintf( $query, $args ); 1619 $args_escaped = array(); 1620 1621 foreach ( $args as $i => $value ) { 1622 if ( in_array( $i, $arg_identifiers, true ) ) { 1623 $args_escaped[] = $this->_escape_identifier_value( $value ); 1624 } elseif ( is_int( $value ) || is_float( $value ) ) { 1625 $args_escaped[] = $value; 1626 } else { 1627 if ( ! is_scalar( $value ) && ! is_null( $value ) ) { 1628 wp_load_translations_early(); 1629 _doing_it_wrong( 1630 'wpdb::prepare', 1631 sprintf( 1632 /* translators: %s: Value type. */ 1633 __( 'Unsupported value type (%s).' ), 1634 gettype( $value ) 1635 ), 1636 '4.8.2' 1637 ); 1638 $value = ''; // Preserving old behaviour, where values are escaped as strings. 1639 } 1640 $args_escaped[] = $this->_real_escape( $value ); 1641 } 1642 } 1643 1644 $query = vsprintf( $query, $args_escaped ); 1511 1645 1512 1646 return $this->add_placeholder_escape( $query ); … … 3739 3873 3740 3874 /** 3741 * Determines if a database supports a particular feature. 3875 * Determine DB or WPDB support for a particular feature. 3876 * 3877 * Capability sniffs for the database server and current version of wpdb. 3878 * 3879 * Database sniffs test based on the version of MySQL the site is using. 3880 * 3881 * WPDB sniffs are added as new features are introduced to allow theme and plugin 3882 * developers to determine feature support. This is to account for drop-ins which may 3883 * introduce feature support at a different time to WordPress. 3742 3884 * 3743 3885 * @since 2.7.0 3744 3886 * @since 4.1.0 Added support for the 'utf8mb4' feature. 3745 3887 * @since 4.6.0 Added support for the 'utf8mb4_520' feature. 3888 3746 3889 * 3747 3890 * @see wpdb::db_version() 3748 3891 * 3749 3892 * @param string $db_cap The feature to check for. Accepts 'collation', 'group_concat', 3750 * 'subqueries', 'set_charset', 'utf8mb4', or 'utf8mb4_520'. 3751 * @return int|false Whether the database feature is supported, false otherwise. 3893 * 'subqueries', 'set_charset', 'utf8mb4', 'utf8mb4_520', 3894 * or 'identifier_placeholders'. 3895 * @return bool True when the database feature is supported, false otherwise. 3752 3896 */ 3753 3897 public function has_cap( $db_cap ) { … … 3783 3927 case 'utf8mb4_520': // @since 4.6.0 3784 3928 return version_compare( $version, '5.6', '>=' ); 3929 3930 3785 3931 } 3786 3932 -
trunk/tests/phpunit/tests/db.php
r52218 r53575 493 493 $this->assertTrue( $wpdb->has_cap( 'group_concat' ) ); 494 494 $this->assertTrue( $wpdb->has_cap( 'subqueries' ) ); 495 495 496 $this->assertTrue( $wpdb->has_cap( 'COLLATION' ) ); 496 497 $this->assertTrue( $wpdb->has_cap( 'GROUP_CONCAT' ) ); 497 498 $this->assertTrue( $wpdb->has_cap( 'SUBQUERIES' ) ); 499 498 500 $this->assertSame( 499 501 version_compare( $wpdb->db_version(), '5.0.7', '>=' ), … … 1718 1720 "'hello' 'foo##'", 1719 1721 ), 1720 ); 1722 array( 1723 'SELECT * FROM %i WHERE %i = %d;', 1724 array( 'my_table', 'my_field', 321 ), 1725 false, 1726 'SELECT * FROM `my_table` WHERE `my_field` = 321;', 1727 ), 1728 array( 1729 'WHERE %i = %d;', 1730 array( 'evil_`_field', 321 ), 1731 false, 1732 'WHERE `evil_``_field` = 321;', // To quote the identifier itself, then you need to double the character, e.g. `a``b`. 1733 ), 1734 array( 1735 'WHERE %i = %d;', 1736 array( 'evil_````````_field', 321 ), 1737 false, 1738 'WHERE `evil_````````````````_field` = 321;', 1739 ), 1740 array( 1741 'WHERE %i = %d;', 1742 array( '``evil_field``', 321 ), 1743 false, 1744 'WHERE `````evil_field````` = 321;', 1745 ), 1746 array( 1747 'WHERE %i = %d;', 1748 array( 'evil\'field', 321 ), 1749 false, 1750 'WHERE `evil\'field` = 321;', 1751 ), 1752 array( 1753 'WHERE %i = %d;', 1754 array( 'evil_\``_field', 321 ), 1755 false, 1756 'WHERE `evil_\````_field` = 321;', 1757 ), 1758 array( 1759 'WHERE %i = %d;', 1760 array( 'evil_%s_field', 321 ), 1761 false, 1762 "WHERE `evil_{$wpdb->placeholder_escape()}s_field` = 321;", 1763 ), 1764 array( 1765 'WHERE %i = %d;', 1766 array( 'value`', 321 ), 1767 false, 1768 'WHERE `value``` = 321;', 1769 ), 1770 array( 1771 'WHERE `%i = %d;', 1772 array( ' AND evil_value', 321 ), 1773 false, 1774 'WHERE `` AND evil_value` = 321;', // Won't run (SQL parse error: "Unclosed quote"). 1775 ), 1776 array( 1777 'WHERE %i` = %d;', 1778 array( 'evil_value -- ', 321 ), 1779 false, 1780 'WHERE `evil_value -- `` = 321;', // Won't run (SQL parse error: "Unclosed quote"). 1781 ), 1782 array( 1783 'WHERE `%i`` = %d;', 1784 array( ' AND true -- ', 321 ), 1785 false, 1786 'WHERE `` AND true -- ``` = 321;', // Won't run (Unknown column ''). 1787 ), 1788 array( 1789 'WHERE ``%i` = %d;', 1790 array( ' AND true -- ', 321 ), 1791 false, 1792 'WHERE ``` AND true -- `` = 321;', // Won't run (SQL parse error: "Unclosed quote"). 1793 ), 1794 array( 1795 'WHERE %2$i = %1$d;', 1796 array( '1', 'two' ), 1797 false, 1798 'WHERE `two` = 1;', 1799 ), 1800 array( 1801 'WHERE \'%i\' = 1 AND "%i" = 2 AND `%i` = 3 AND ``%i`` = 4 AND %15i = 5', 1802 array( 'my_field1', 'my_field2', 'my_field3', 'my_field4', 'my_field5' ), 1803 false, 1804 'WHERE \'`my_field1`\' = 1 AND "`my_field2`" = 2 AND ``my_field3`` = 3 AND ```my_field4``` = 4 AND ` my_field5` = 5', // Does not remove any existing quotes, always adds it's own (safer). 1805 ), 1806 array( 1807 'WHERE id = %d AND %i LIKE %2$s LIMIT 1', 1808 array( 123, 'field -- ', false ), 1809 true, // Incorrect usage. 1810 null, // Should be rejected, otherwise the `%1$s` could use Identifier escaping, e.g. 'WHERE `field -- ` LIKE field -- LIMIT 1' (thanks @vortfu). 1811 ), 1812 array( 1813 'WHERE %i LIKE %s LIMIT 1', 1814 array( "field' -- ", "field' -- " ), 1815 false, 1816 "WHERE `field' -- ` LIKE 'field\' -- ' LIMIT 1", // In contrast to the above, Identifier vs String escaping is used. 1817 ), 1818 ); 1819 } 1820 1821 public function test_allow_unsafe_unquoted_parameters() { 1822 global $wpdb; 1823 1824 $sql = 'WHERE (%i = %s) OR (%10i = %10s) OR (%5$i = %6$s)'; 1825 $values = array( 'field_a', 'string_a', 'field_b', 'string_b', 'field_c', 'string_c' ); 1826 1827 $default = $wpdb->allow_unsafe_unquoted_parameters; 1828 1829 $wpdb->allow_unsafe_unquoted_parameters = true; 1830 1831 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 1832 $part = $wpdb->prepare( $sql, $values ); 1833 $this->assertSame( 'WHERE (`field_a` = \'string_a\') OR (` field_b` = string_b) OR (`field_c` = string_c)', $part ); // Unsafe, unquoted parameters. 1834 1835 $wpdb->allow_unsafe_unquoted_parameters = false; 1836 1837 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared 1838 $part = $wpdb->prepare( $sql, $values ); 1839 $this->assertSame( 'WHERE (`field_a` = \'string_a\') OR (` field_b` = \' string_b\') OR (`field_c` = \'string_c\')', $part ); 1840 1841 $wpdb->allow_unsafe_unquoted_parameters = $default; 1842 1721 1843 } 1722 1844
Note: See TracChangeset
for help on using the changeset viewer.