',', 0xFE13 => ':', 0xFE14 => ';', 0xFE15 => '!', 0xFE16 => '?', 0xFE33 => '_', 0xFE34 => '_', 0xFE35 => '(', 0xFE36 => ')', 0xFE37 => '{', 0xFE38 => '}', 0xFE47 => '[', 0xFE48 => ']', 0xFE4D => '_', 0xFE4E => '_', 0xFE4F => '_', 0xFE50 => ',', 0xFE52 => '.', 0xFE54 => ';', 0xFE55 => ':', 0xFE56 => '?', 0xFE57 => '!', 0xFE59 => '(', 0xFE5A => ')', 0xFE5B => '{', 0xFE5C => '}', 0xFE5F => '#', 0xFE60 => '&', 0xFE61 => '*', 0xFE62 => '+', 0xFE63 => '-', 0xFE64 => '<', 0xFE65 => '>', 0xFE66 => '=', 0xFE68 => '\\', 0xFE69 => '$', 0xFE6A => '%', 0xFE6B => '@', ); /** * Full-width forms (U+FF01–U+FF5E and the ideographic space) and the * SMALL_FORMS as their plain characters, for a server without the intl * extension, which would otherwise do this. Built on first use. * * @var array|null */ private static ?array $compat = null; /** * A name the way the database compares it, lower-cased: see plain(). * * @param string $name Option name or meta key. */ public static function folded( string $name ): string { return strtolower( self::plain( $name ) ); } /** * A name with what the database ignores taken out, letter case kept: * spaces around it, control and invisible characters (zero-width * spaces and joiners, variation selectors), accents, and compatibility * forms such as full-width or mathematical letters and ligatures, which * become their plain letters. This may take out a little more than the * database does (a soft hyphen, a tab), which only makes a protected * name's look-alike easier to recognise. * * @param string $name Option name or meta key. */ public static function plain( string $name ): string { $name = trim( $name ); // Printable ASCII, the usual case, is compared as it is. if ( 1 !== preg_match( '/[^\x20-\x7e]/', $name ) ) { return $name; } // Not UTF-8: the database refuses a query that holds it, so no row is // reached. Control characters still go, as the database ignores them. if ( 1 !== preg_match( '//u', $name ) ) { return trim( (string) preg_replace( '/[\x00-\x1f\x7f]/', '', $name ) ); } // "ß" is "ss" to the database. The locale is fixed, so that a German // site does not turn "ä" into "ae" where the database reads "a". $name = remove_accents( strtr( $name, array( "\u{00DF}" => 'ss', "\u{1E9E}" => 'SS', ) ), 'en_US' ); // Compatibility forms: full-width, mathematical, circled and // superscript letters are their plain letters, a ligature is its // letters ("fi"), and accents remove_accents() does not know come off // as marks. if ( class_exists( 'Normalizer' ) ) { $decomposed = \Normalizer::normalize( $name, \Normalizer::FORM_KD ); if ( is_string( $decomposed ) ) { $name = $decomposed; } } $name = strtr( $name, self::compat_forms() ); // Control and format characters, combining marks and variation // selectors. $name = (string) preg_replace( '/[\p{Cc}\p{Cf}\p{Mn}\p{Me}]/u', '', $name ); return trim( $name ); } /** * Whether PHP and the database agree on what a name matches: its plain * form is printable ASCII. A name with letters left that PHP cannot * reduce (another script, or a look-alike on a server without the intl * extension) might be read by the database as another name. * * @param string $name Option name or meta key. */ public static function checkable( string $name ): bool { $plain = self::plain( $name ); return '' !== $plain && 1 !== preg_match( '/[^\x20-\x7e]/', $plain ); } /** * The name of the stored option the database matches a name to, as it * is stored, or '' when no option matches. It differs from the name * given when the two are spelled differently ("Blogname", "défault_role"). * * @param string $name Option name. */ public static function stored_option( string $name ): string { global $wpdb; $name = trim( $name ); if ( '' === $name ) { return ''; } // An autoloaded option is listed under its stored name, and option // names are unique as the database compares them, so a name listed // exactly is the row itself. $autoloaded = wp_load_alloptions(); if ( is_array( $autoloaded ) && array_key_exists( $name, $autoloaded ) ) { return $name; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- the stored spelling of the row get_option() would read; no API returns it. $stored = $wpdb->get_var( $wpdb->prepare( 'SELECT option_name FROM %i WHERE option_name = %s LIMIT 1', $wpdb->options, $name ) ); return null === $stored ? '' : (string) $stored; } /** * The keys, as stored, of a post's custom fields that the database * matches a key to. update_post_meta() and delete_post_meta() change * every one of them, whichever spelling was given. * * @param int $post_id Post id (not a revision). * @param string $key Meta key. * @return array */ public static function stored_meta_keys( int $post_id, string $key ): array { global $wpdb; if ( $post_id <= 0 || '' === $key ) { return array(); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- which stored keys the meta functions' own queries reach; the meta cache is keyed by exact name and cannot say. $keys = $wpdb->get_col( $wpdb->prepare( 'SELECT DISTINCT meta_key FROM %i WHERE post_id = %d AND meta_key = %s', $wpdb->postmeta, $post_id, $key ) ); return array_map( 'strval', (array) $keys ); } /** * Full-width, vertical and small forms and their plain characters. * * @return array */ private static function compat_forms(): array { if ( null === self::$compat ) { $compat = array( "\u{3000}" => ' ' ); for ( $code = 0xFF01; $code <= 0xFF5E; $code++ ) { $compat[ self::utf8( $code ) ] = chr( $code - 0xFEE0 ); } foreach ( self::SMALL_FORMS as $code => $plain ) { $compat[ self::utf8( $code ) ] = $plain; } self::$compat = $compat; } return self::$compat; } /** * A character from U+0800 to U+FFFF, in UTF-8. * * @param int $code Code point. */ private static function utf8( int $code ): string { return chr( 0xE0 | ( $code >> 12 ) ) . chr( 0x80 | ( ( $code >> 6 ) & 0x3F ) ) . chr( 0x80 | ( $code & 0x3F ) ); } }