@@ -1371,12 +1371,13 @@ public function escape_by_ref( &$data ) {
1371
1371
* Quotes an identifier such as a table or field name.
1372
1372
*
1373
1373
* @since 6.2.0
1374
+ * @deprecated {WP_VERSION} Use {@see self::schema_object_name_for_query()}.
1374
1375
*
1375
1376
* @param string $identifier Identifier to escape.
1376
- * @return string Escaped identifier.
1377
+ * @return string|null Escaped and quoted identifier, if allowable, else `null` .
1377
1378
*/
1378
1379
public function quote_identifier ( $ identifier ) {
1379
- return ' ` ' . $ this -> _escape_identifier_value ( $ identifier ) . ' ` ' ;
1380
+ return self :: schema_object_name_for_query ( $ identifier, ' quote-always ' ) ;
1380
1381
}
1381
1382
1382
1383
/**
@@ -1388,13 +1389,168 @@ public function quote_identifier( $identifier ) {
1388
1389
*
1389
1390
* @since 6.2.0
1390
1391
*
1392
+ * @deprecated {WP_VERSION} Use {@see self::schema_object_name_for_query()}.
1393
+ *
1391
1394
* @link https://dev.mysql.com/doc/refman/8.0/en/identifiers.html
1392
1395
*
1393
1396
* @param string $identifier Identifier to escape.
1394
- * @return string Escaped identifier.
1397
+ * @return string|null Escaped and unquoted identifier, if allowable, else `null` .
1395
1398
*/
1396
1399
private function _escape_identifier_value ( $ identifier ) {
1397
- return str_replace ( '` ' , '`` ' , $ identifier );
1400
+ $ escaped = self ::schema_object_name_for_query ( $ identifier , 'quote-always ' );
1401
+
1402
+ return isset ( $ escaped ) ? substr ( $ escaped , 1 , -1 ) : null ;
1403
+ }
1404
+
1405
+ /**
1406
+ * Returns a version of a schema object name safe for directly including in a
1407
+ * query (with no additional escaping), or `null` if given an invalid name.
1408
+ *
1409
+ * Not all schema object names require quoting with a backtick: this function
1410
+ * only provides them when the name itself demands it. To always return the
1411
+ * quoted form, pass the appropriate option.
1412
+ *
1413
+ * Example:
1414
+ *
1415
+ * 'test' === schema_object_name_for_query( 'test' );
1416
+ * 'te$st' === schema_object_name_for_query( 'te$st' );
1417
+ * '9dogs' === schema_object_name_for_query( '9dogs' );
1418
+ * '☂' === schema_object_name_for_query( "\u{2602}" );
1419
+ *
1420
+ * '`%`' === schema_object_name_for_query( '%' );
1421
+ * '````' === schema_object_name_for_query( '`' );
1422
+ * '`$test`' === schema_object_name_for_query( '$test' );
1423
+ * '`1337`' === schema_object_name_for_query( '1337' );
1424
+ * '`a.b`' === schema_object_name_for_query( 'a.b' );
1425
+ *
1426
+ * // NUL bytes are not allowed.
1427
+ * null === schema_object_name_for_query( "wp_posts\x00wp_users" );
1428
+ *
1429
+ * // Supplementary characters are not allowed.
1430
+ * null === schema_object_name_for_query( "be\u{1F170}" );
1431
+ *
1432
+ * // Non-UTF8 encodings are not supported.
1433
+ * null === schema_object_name_for_query( "t\xE9st" );
1434
+ *
1435
+ * @see https://dev.mysql.com/doc/refman/8.4/en/identifiers.html
1436
+ *
1437
+ * @todo Need to fall back to valid UTF-8 detection. See #6883.
1438
+ *
1439
+ * @since {WP_VERSION}
1440
+ *
1441
+ * @param string $name Schema object name to include in a query.
1442
+ * @param ?('quote-always'|'quote-when-necessary') $quoting Optional. When set to `quote-always`, will return the
1443
+ * quoted form of the given schema object name. Default
1444
+ * is to only quote when necessary.
1445
+ * @return string|null Properly escaped form of the name, if allowable, else `null`.
1446
+ */
1447
+ public static function schema_object_name_for_query ( string $ name , string $ quoting = 'quote-when-necessary ' ): ?string {
1448
+ /**
1449
+ * Since object names are limited to 64 characters (except for
1450
+ * aliases and compound statement labels, which this code
1451
+ * overlooks), and since Supplementary Characters are not
1452
+ * permitted, the maximum length is 64 * 3 bytes, because
1453
+ * all Basic Multilingual Plane characters encode within
1454
+ * three bytes.
1455
+ *
1456
+ * @see https://dev.mysql.com/doc/refman/8.4/en/identifiers.html
1457
+ */
1458
+ static $ max_object_name_bytes = 192 ;
1459
+
1460
+ /**
1461
+ * MySQL interprets “characters” in an identifier as a UTF-8 code point.
1462
+ * The number of bytes may be more than the number of characters.
1463
+ *
1464
+ * Example:
1465
+ *
1466
+ * "aaa" -> 3 bytes, 3 characters
1467
+ * "アアア" -> 9 bytes, 3 characters
1468
+ * "🏴🏴🏴" -> 76 bytes, 19 character
1469
+ *
1470
+ * @see https://dev.mysql.com/doc/refman/8.4/en/identifiers.html
1471
+ */
1472
+ static $ max_object_name_chars = 64 ;
1473
+
1474
+ /*
1475
+ * > Internally, identifiers are converted to and are stored as Unicode (UTF-8).
1476
+ *
1477
+ * It’s not worth trying to convert encodings. If the given name isn’t
1478
+ * already UTF-8 it isn’t supported by this function, even if it might
1479
+ * be theoretically possible to convert it into UTF-8.
1480
+ */
1481
+ if ( ! mb_check_encoding ( $ name , 'UTF-8 ' ) ) {
1482
+ return null ;
1483
+ }
1484
+
1485
+ $ name_length = strlen ( $ name );
1486
+ if (
1487
+ 0 === $ name_length ||
1488
+ $ name_length > $ max_object_name_bytes ||
1489
+ mb_strlen ( $ name , 'UTF-8 ' ) > $ max_object_name_chars
1490
+ ) {
1491
+ return null ;
1492
+ }
1493
+
1494
+ /*
1495
+ * > Database, table, and column names cannot
1496
+ * > end with space characters.
1497
+ */
1498
+ if ( ' ' === $ name [ $ name_length - 1 ] ) {
1499
+ return null ;
1500
+ }
1501
+
1502
+ $ has_forbidden_characters = false ;
1503
+ $ needs_quoting = 'quote-always ' === $ quoting ;
1504
+ $ has_non_digits = false ;
1505
+
1506
+ /*
1507
+ * > Use of the dollar sign as the first character in the unquoted
1508
+ * > name of a database, table, view, column, stored program, or
1509
+ * > alias is deprecated, including such names used with qualifiers
1510
+ */
1511
+ $ needs_quoting |= '$ ' === $ name [0 ];
1512
+
1513
+ /*
1514
+ * > ASCII NUL (U+0000) and supplementary characters
1515
+ * > (U+10000 and higher) are not permitted in
1516
+ * > quoted or unquoted identifiers.
1517
+ */
1518
+ for ( $ i = 0 ; $ i < $ name_length ; $ i ++ ) {
1519
+ $ c = $ name [ $ i ];
1520
+ $ o = ord ( $ c );
1521
+
1522
+ /*
1523
+ * > Identifiers may begin with a digit but unless
1524
+ * > quoted may not consist solely of digits.
1525
+ */
1526
+ $ is_digit = $ c >= '0 ' && $ c <= '9 ' ;
1527
+ $ has_non_digits |= ! $ is_digit ;
1528
+
1529
+ /*
1530
+ * > Permitted characters in unquoted identifiers:
1531
+ * > - ASCII: [0-9,a-z,A-Z$_]
1532
+ * > - Extended: U+0080 .. U+FFFF
1533
+ */
1534
+ $ needs_quoting |= ! (
1535
+ ( $ c >= 'A ' && $ c <= 'Z ' ) ||
1536
+ ( $ c >= 'a ' && $ c <= 'z ' ) ||
1537
+ $ is_digit || '$ ' === $ c || '_ ' === $ c ||
1538
+ $ o >= 0x80
1539
+ );
1540
+
1541
+ $ has_forbidden_characters |= ( 0 === $ o ) || ( ( $ o & 0xF8 ) === 0xF0 );
1542
+ }
1543
+
1544
+ if ( $ has_forbidden_characters ) {
1545
+ return null ;
1546
+ }
1547
+
1548
+ if ( ! $ needs_quoting && $ has_non_digits ) {
1549
+ return $ name ;
1550
+ }
1551
+
1552
+ $ quoted_name = str_replace ( '` ' , '`` ' , $ name );
1553
+ return "` {$ quoted_name }` " ;
1398
1554
}
1399
1555
1400
1556
/**
@@ -1736,7 +1892,22 @@ public function prepare( $query, ...$args ) {
1736
1892
1737
1893
foreach ( $ args as $ i => $ value ) {
1738
1894
if ( in_array ( $ i , $ arg_identifiers , true ) ) {
1739
- $ args_escaped [] = $ this ->_escape_identifier_value ( $ value );
1895
+ $ quoted_identifier = self ::schema_object_name_for_query ( $ value , 'quote-always ' );
1896
+ if ( ! isset ( $ quoted_identifier ) ) {
1897
+ _doing_it_wrong (
1898
+ 'wpdb::prepare ' ,
1899
+ sprintf (
1900
+ /* translators: 1: Name of database Identifier. */
1901
+ __ ( 'The given identifier name is not allowed by MySQL: "%1$s". ' ),
1902
+ $ value
1903
+ ),
1904
+ '{WP_VERSION} '
1905
+ );
1906
+
1907
+ return null ;
1908
+ }
1909
+
1910
+ $ args_escaped [] = substr ( $ quoted_identifier , 1 , -1 );
1740
1911
} elseif ( is_int ( $ value ) || is_float ( $ value ) ) {
1741
1912
$ args_escaped [] = $ value ;
1742
1913
} else {
0 commit comments