diff -r c7c34916027a -r 177826044cd9 wp/wp-includes/class-wp-meta-query.php --- a/wp/wp-includes/class-wp-meta-query.php Mon Oct 14 18:06:33 2019 +0200 +++ b/wp/wp-includes/class-wp-meta-query.php Mon Oct 14 18:28:13 2019 +0200 @@ -99,7 +99,7 @@ * * @since 3.2.0 * @since 4.2.0 Introduced support for naming query clauses by associative array keys. - * + * @since 5.1.0 Introduced $compare_key clause parameter, which enables LIKE key matches. * * @param array $meta_query { * Array of meta query clauses. When first-order clauses or sub-clauses use strings as @@ -110,23 +110,26 @@ * @type array { * Optional. An array of first-order clause parameters, or another fully-formed meta query. * - * @type string $key Meta key to filter by. - * @type string $value Meta value to filter by. - * @type string $compare MySQL operator used for comparing the $value. Accepts '=', - * '!=', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE', - * 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', 'REGEXP', - * 'NOT REGEXP', 'RLIKE', 'EXISTS' or 'NOT EXISTS'. - * Default is 'IN' when `$value` is an array, '=' otherwise. - * @type string $type MySQL data type that the meta_value column will be CAST to for - * comparisons. Accepts 'NUMERIC', 'BINARY', 'CHAR', 'DATE', - * 'DATETIME', 'DECIMAL', 'SIGNED', 'TIME', or 'UNSIGNED'. - * Default is 'CHAR'. + * @type string $key Meta key to filter by. + * @type string $compare_key MySQL operator used for comparing the $key. Accepts '=' and 'LIKE'. + * Default '='. + * @type string $value Meta value to filter by. + * @type string $compare MySQL operator used for comparing the $value. Accepts '=', + * '!=', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE', + * 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', 'REGEXP', + * 'NOT REGEXP', 'RLIKE', 'EXISTS' or 'NOT EXISTS'. + * Default is 'IN' when `$value` is an array, '=' otherwise. + * @type string $type MySQL data type that the meta_value column will be CAST to for + * comparisons. Accepts 'NUMERIC', 'BINARY', 'CHAR', 'DATE', + * 'DATETIME', 'DECIMAL', 'SIGNED', 'TIME', or 'UNSIGNED'. + * Default is 'CHAR'. * } * } */ public function __construct( $meta_query = false ) { - if ( !$meta_query ) + if ( ! $meta_query ) { return; + } if ( isset( $meta_query['relation'] ) && strtoupper( $meta_query['relation'] ) == 'OR' ) { $this->relation = 'OR'; @@ -161,7 +164,7 @@ } elseif ( ! is_array( $query ) ) { continue; - // First-order clause. + // First-order clause. } elseif ( $this->is_first_order_clause( $query ) ) { if ( isset( $query['value'] ) && array() === $query['value'] ) { unset( $query['value'] ); @@ -169,7 +172,7 @@ $clean_queries[ $key ] = $query; - // Otherwise, it's a nested query, so we recurse. + // Otherwise, it's a nested query, so we recurse. } else { $cleaned_query = $this->sanitize_query( $query ); @@ -186,17 +189,17 @@ // Sanitize the 'relation' key provided in the query. if ( isset( $relation ) && 'OR' === strtoupper( $relation ) ) { $clean_queries['relation'] = 'OR'; - $this->has_or_relation = true; + $this->has_or_relation = true; - /* - * If there is only a single clause, call the relation 'OR'. - * This value will not actually be used to join clauses, but it - * simplifies the logic around combining key-only queries. - */ + /* + * If there is only a single clause, call the relation 'OR'. + * This value will not actually be used to join clauses, but it + * simplifies the logic around combining key-only queries. + */ } elseif ( 1 === count( $clean_queries ) ) { $clean_queries['relation'] = 'OR'; - // Default to AND. + // Default to AND. } else { $clean_queries['relation'] = 'AND'; } @@ -236,7 +239,7 @@ * the rest of the meta_query). */ $primary_meta_query = array(); - foreach ( array( 'key', 'compare', 'type' ) as $key ) { + foreach ( array( 'key', 'compare', 'type', 'compare_key' ) as $key ) { if ( ! empty( $qv[ "meta_$key" ] ) ) { $primary_meta_query[ $key ] = $qv[ "meta_$key" ]; } @@ -275,16 +278,19 @@ * @return string MySQL type. */ public function get_cast_for_type( $type = '' ) { - if ( empty( $type ) ) + if ( empty( $type ) ) { return 'CHAR'; + } $meta_type = strtoupper( $type ); - if ( ! preg_match( '/^(?:BINARY|CHAR|DATE|DATETIME|SIGNED|UNSIGNED|TIME|NUMERIC(?:\(\d+(?:,\s?\d+)?\))?|DECIMAL(?:\(\d+(?:,\s?\d+)?\))?)$/', $meta_type ) ) + if ( ! preg_match( '/^(?:BINARY|CHAR|DATE|DATETIME|SIGNED|UNSIGNED|TIME|NUMERIC(?:\(\d+(?:,\s?\d+)?\))?|DECIMAL(?:\(\d+(?:,\s?\d+)?\))?)$/', $meta_type ) ) { return 'CHAR'; + } - if ( 'NUMERIC' == $meta_type ) + if ( 'NUMERIC' == $meta_type ) { $meta_type = 'SIGNED'; + } return $meta_type; } @@ -333,7 +339,7 @@ * * @since 3.1.0 * - * @param array $clauses Array containing the query's JOIN and WHERE clauses. + * @param array $sql Array containing the query's JOIN and WHERE clauses. * @param array $queries Array of meta queries. * @param string $type Type of meta. * @param string $primary_table Primary table. @@ -364,7 +370,7 @@ * To keep $this->queries unaltered, pass a copy. */ $queries = $this->queries; - $sql = $this->get_sql_for_query( $queries ); + $sql = $this->get_sql_for_query( $queries ); if ( ! empty( $sql['where'] ) ) { $sql['where'] = ' AND ' . $sql['where']; @@ -404,7 +410,7 @@ $indent = ''; for ( $i = 0; $i < $depth; $i++ ) { - $indent .= " "; + $indent .= ' '; } foreach ( $query as $key => &$clause ) { @@ -426,7 +432,7 @@ } $sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] ); - // This is a subquery, so we recurse. + // This is a subquery, so we recurse. } else { $clause_sql = $this->get_sql_for_query( $clause, $depth + 1 ); @@ -482,7 +488,7 @@ $sql_chunks = array( 'where' => array(), - 'join' => array(), + 'join' => array(), ); if ( isset( $clause['compare'] ) ) { @@ -491,18 +497,39 @@ $clause['compare'] = isset( $clause['value'] ) && is_array( $clause['value'] ) ? 'IN' : '='; } - if ( ! in_array( $clause['compare'], array( - '=', '!=', '>', '>=', '<', '<=', - 'LIKE', 'NOT LIKE', - 'IN', 'NOT IN', - 'BETWEEN', 'NOT BETWEEN', - 'EXISTS', 'NOT EXISTS', - 'REGEXP', 'NOT REGEXP', 'RLIKE' - ) ) ) { + if ( ! in_array( + $clause['compare'], + array( + '=', + '!=', + '>', + '>=', + '<', + '<=', + 'LIKE', + 'NOT LIKE', + 'IN', + 'NOT IN', + 'BETWEEN', + 'NOT BETWEEN', + 'EXISTS', + 'NOT EXISTS', + 'REGEXP', + 'NOT REGEXP', + 'RLIKE', + ) + ) ) { $clause['compare'] = '='; } - $meta_compare = $clause['compare']; + if ( isset( $clause['compare_key'] ) && 'LIKE' === strtoupper( $clause['compare_key'] ) ) { + $clause['compare_key'] = strtoupper( $clause['compare_key'] ); + } else { + $clause['compare_key'] = '='; + } + + $meta_compare = $clause['compare']; + $meta_compare_key = $clause['compare_key']; // First build the JOIN clause, if one is required. $join = ''; @@ -510,16 +537,21 @@ // We prefer to avoid joins if possible. Look for an existing join compatible with this clause. $alias = $this->find_compatible_table_alias( $clause, $parent_query ); if ( false === $alias ) { - $i = count( $this->table_aliases ); + $i = count( $this->table_aliases ); $alias = $i ? 'mt' . $i : $this->meta_table; // JOIN clauses for NOT EXISTS have their own syntax. if ( 'NOT EXISTS' === $meta_compare ) { $join .= " LEFT JOIN $this->meta_table"; $join .= $i ? " AS $alias" : ''; - $join .= $wpdb->prepare( " ON ($this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column AND $alias.meta_key = %s )", $clause['key'] ); - // All other JOIN clauses. + if ( 'LIKE' === $meta_compare_key ) { + $join .= $wpdb->prepare( " ON ($this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column AND $alias.meta_key LIKE %s )", '%' . $wpdb->esc_like( $clause['key'] ) . '%' ); + } else { + $join .= $wpdb->prepare( " ON ($this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column AND $alias.meta_key = %s )", $clause['key'] ); + } + + // All other JOIN clauses. } else { $join .= " INNER JOIN $this->meta_table"; $join .= $i ? " AS $alias" : ''; @@ -527,15 +559,15 @@ } $this->table_aliases[] = $alias; - $sql_chunks['join'][] = $join; + $sql_chunks['join'][] = $join; } // Save the alias to this clause, for future siblings to find. $clause['alias'] = $alias; // Determine the data type. - $_meta_type = isset( $clause['type'] ) ? $clause['type'] : ''; - $meta_type = $this->get_cast_for_type( $_meta_type ); + $_meta_type = isset( $clause['type'] ) ? $clause['type'] : ''; + $meta_type = $this->get_cast_for_type( $_meta_type ); $clause['cast'] = $meta_type; // Fallback for clause keys is the table alias. Key must be a string. @@ -544,7 +576,7 @@ } // Ensure unique clause keys, so none are overwritten. - $iterator = 1; + $iterator = 1; $clause_key_base = $clause_key; while ( isset( $this->clauses[ $clause_key ] ) ) { $clause_key = $clause_key_base . '-' . $iterator; @@ -561,7 +593,11 @@ if ( 'NOT EXISTS' === $meta_compare ) { $sql_chunks['where'][] = $alias . '.' . $this->meta_id_column . ' IS NULL'; } else { - $sql_chunks['where'][] = $wpdb->prepare( "$alias.meta_key = %s", trim( $clause['key'] ) ); + if ( 'LIKE' === $meta_compare_key ) { + $sql_chunks['where'][] = $wpdb->prepare( "$alias.meta_key LIKE %s", '%' . $wpdb->esc_like( trim( $clause['key'] ) ) . '%' ); + } else { + $sql_chunks['where'][] = $wpdb->prepare( "$alias.meta_key = %s", trim( $clause['key'] ) ); + } } } @@ -578,36 +614,36 @@ } switch ( $meta_compare ) { - case 'IN' : - case 'NOT IN' : + case 'IN': + case 'NOT IN': $meta_compare_string = '(' . substr( str_repeat( ',%s', count( $meta_value ) ), 1 ) . ')'; - $where = $wpdb->prepare( $meta_compare_string, $meta_value ); + $where = $wpdb->prepare( $meta_compare_string, $meta_value ); break; - case 'BETWEEN' : - case 'NOT BETWEEN' : + case 'BETWEEN': + case 'NOT BETWEEN': $meta_value = array_slice( $meta_value, 0, 2 ); - $where = $wpdb->prepare( '%s AND %s', $meta_value ); + $where = $wpdb->prepare( '%s AND %s', $meta_value ); break; - case 'LIKE' : - case 'NOT LIKE' : + case 'LIKE': + case 'NOT LIKE': $meta_value = '%' . $wpdb->esc_like( $meta_value ) . '%'; - $where = $wpdb->prepare( '%s', $meta_value ); + $where = $wpdb->prepare( '%s', $meta_value ); break; // EXISTS with a value is interpreted as '='. - case 'EXISTS' : + case 'EXISTS': $meta_compare = '='; - $where = $wpdb->prepare( '%s', $meta_value ); + $where = $wpdb->prepare( '%s', $meta_value ); break; // 'value' is ignored for NOT EXISTS. - case 'NOT EXISTS' : + case 'NOT EXISTS': $where = ''; break; - default : + default: $where = $wpdb->prepare( '%s', $meta_value ); break; @@ -687,7 +723,7 @@ if ( 'OR' === $parent_query['relation'] ) { $compatible_compares = array( '=', 'IN', 'BETWEEN', 'LIKE', 'REGEXP', 'RLIKE', '>', '>=', '<', '<=' ); - // Clauses joined by AND with "negative" operators share a join only if they also share a key. + // Clauses joined by AND with "negative" operators share a join only if they also share a key. } elseif ( isset( $sibling['key'] ) && isset( $clause['key'] ) && $sibling['key'] === $clause['key'] ) { $compatible_compares = array( '!=', 'NOT IN', 'NOT LIKE' ); } @@ -710,7 +746,7 @@ * @param array $parent_query Parent of $clause. * @param object $this WP_Meta_Query object. */ - return apply_filters( 'meta_query_find_compatible_table_alias', $alias, $clause, $parent_query, $this ) ; + return apply_filters( 'meta_query_find_compatible_table_alias', $alias, $clause, $parent_query, $this ); } /**