41 |
41 |
42 /** |
42 /** |
43 * Standard response when the query should not return any rows. |
43 * Standard response when the query should not return any rows. |
44 * |
44 * |
45 * @since 3.2.0 |
45 * @since 3.2.0 |
46 * |
|
47 * @static |
|
48 * @var string |
46 * @var string |
49 */ |
47 */ |
50 private static $no_results = array( 'join' => array( '' ), 'where' => array( '0 = 1' ) ); |
48 private static $no_results = array( |
|
49 'join' => array( '' ), |
|
50 'where' => array( '0 = 1' ), |
|
51 ); |
51 |
52 |
52 /** |
53 /** |
53 * A flat list of table aliases used in the JOIN clauses. |
54 * A flat list of table aliases used in the JOIN clauses. |
54 * |
55 * |
55 * @since 4.1.0 |
56 * @since 4.1.0 |
133 */ |
134 */ |
134 public function sanitize_query( $queries ) { |
135 public function sanitize_query( $queries ) { |
135 $cleaned_query = array(); |
136 $cleaned_query = array(); |
136 |
137 |
137 $defaults = array( |
138 $defaults = array( |
138 'taxonomy' => '', |
139 'taxonomy' => '', |
139 'terms' => array(), |
140 'terms' => array(), |
140 'field' => 'term_id', |
141 'field' => 'term_id', |
141 'operator' => 'IN', |
142 'operator' => 'IN', |
142 'include_children' => true, |
143 'include_children' => true, |
143 ); |
144 ); |
144 |
145 |
145 foreach ( $queries as $key => $query ) { |
146 foreach ( $queries as $key => $query ) { |
146 if ( 'relation' === $key ) { |
147 if ( 'relation' === $key ) { |
147 $cleaned_query['relation'] = $this->sanitize_relation( $query ); |
148 $cleaned_query['relation'] = $this->sanitize_relation( $query ); |
148 |
149 |
149 // First-order clause. |
150 // First-order clause. |
150 } elseif ( self::is_first_order_clause( $query ) ) { |
151 } elseif ( self::is_first_order_clause( $query ) ) { |
151 |
152 |
152 $cleaned_clause = array_merge( $defaults, $query ); |
153 $cleaned_clause = array_merge( $defaults, $query ); |
153 $cleaned_clause['terms'] = (array) $cleaned_clause['terms']; |
154 $cleaned_clause['terms'] = (array) $cleaned_clause['terms']; |
154 $cleaned_query[] = $cleaned_clause; |
155 $cleaned_query[] = $cleaned_clause; |
155 |
156 |
156 /* |
157 /* |
157 * Keep a copy of the clause in the flate |
158 * Keep a copy of the clause in the flate |
158 * $queried_terms array, for use in WP_Query. |
159 * $queried_terms array, for use in WP_Query. |
159 */ |
160 */ |
174 if ( ! empty( $cleaned_clause['field'] ) && ! isset( $this->queried_terms[ $taxonomy ]['field'] ) ) { |
175 if ( ! empty( $cleaned_clause['field'] ) && ! isset( $this->queried_terms[ $taxonomy ]['field'] ) ) { |
175 $this->queried_terms[ $taxonomy ]['field'] = $cleaned_clause['field']; |
176 $this->queried_terms[ $taxonomy ]['field'] = $cleaned_clause['field']; |
176 } |
177 } |
177 } |
178 } |
178 |
179 |
179 // Otherwise, it's a nested query, so we recurse. |
180 // Otherwise, it's a nested query, so we recurse. |
180 } elseif ( is_array( $query ) ) { |
181 } elseif ( is_array( $query ) ) { |
181 $cleaned_subquery = $this->sanitize_query( $query ); |
182 $cleaned_subquery = $this->sanitize_query( $query ); |
182 |
183 |
183 if ( ! empty( $cleaned_subquery ) ) { |
184 if ( ! empty( $cleaned_subquery ) ) { |
184 // All queries with children must have a relation. |
185 // All queries with children must have a relation. |
219 * for backward compatibility. Any clause that doesn't meet this is |
220 * for backward compatibility. Any clause that doesn't meet this is |
220 * determined, by process of elimination, to be a higher-order query. |
221 * determined, by process of elimination, to be a higher-order query. |
221 * |
222 * |
222 * @since 4.1.0 |
223 * @since 4.1.0 |
223 * |
224 * |
224 * @static |
|
225 * |
|
226 * @param array $query Tax query arguments. |
225 * @param array $query Tax query arguments. |
227 * @return bool Whether the query clause is a first-order clause. |
226 * @return bool Whether the query clause is a first-order clause. |
228 */ |
227 */ |
229 protected static function is_first_order_clause( $query ) { |
228 protected static function is_first_order_clause( $query ) { |
230 return is_array( $query ) && ( empty( $query ) || array_key_exists( 'terms', $query ) || array_key_exists( 'taxonomy', $query ) || array_key_exists( 'include_children', $query ) || array_key_exists( 'field', $query ) || array_key_exists( 'operator', $query ) ); |
229 return is_array( $query ) && ( empty( $query ) || array_key_exists( 'terms', $query ) || array_key_exists( 'taxonomy', $query ) || array_key_exists( 'include_children', $query ) || array_key_exists( 'field', $query ) || array_key_exists( 'operator', $query ) ); |
232 |
231 |
233 /** |
232 /** |
234 * Generates SQL clauses to be appended to a main query. |
233 * Generates SQL clauses to be appended to a main query. |
235 * |
234 * |
236 * @since 3.1.0 |
235 * @since 3.1.0 |
237 * |
|
238 * @static |
|
239 * |
236 * |
240 * @param string $primary_table Database table where the object being filtered is stored (eg wp_users). |
237 * @param string $primary_table Database table where the object being filtered is stored (eg wp_users). |
241 * @param string $primary_id_column ID column for the filtered object in $primary_table. |
238 * @param string $primary_id_column ID column for the filtered object in $primary_table. |
242 * @return array { |
239 * @return array { |
243 * Array containing JOIN and WHERE SQL clauses to append to the main query. |
240 * Array containing JOIN and WHERE SQL clauses to append to the main query. |
245 * @type string $join SQL fragment to append to the main JOIN clause. |
242 * @type string $join SQL fragment to append to the main JOIN clause. |
246 * @type string $where SQL fragment to append to the main WHERE clause. |
243 * @type string $where SQL fragment to append to the main WHERE clause. |
247 * } |
244 * } |
248 */ |
245 */ |
249 public function get_sql( $primary_table, $primary_id_column ) { |
246 public function get_sql( $primary_table, $primary_id_column ) { |
250 $this->primary_table = $primary_table; |
247 $this->primary_table = $primary_table; |
251 $this->primary_id_column = $primary_id_column; |
248 $this->primary_id_column = $primary_id_column; |
252 |
249 |
253 return $this->get_sql_clauses(); |
250 return $this->get_sql_clauses(); |
254 } |
251 } |
255 |
252 |
272 /* |
269 /* |
273 * $queries are passed by reference to get_sql_for_query() for recursion. |
270 * $queries are passed by reference to get_sql_for_query() for recursion. |
274 * To keep $this->queries unaltered, pass a copy. |
271 * To keep $this->queries unaltered, pass a copy. |
275 */ |
272 */ |
276 $queries = $this->queries; |
273 $queries = $this->queries; |
277 $sql = $this->get_sql_for_query( $queries ); |
274 $sql = $this->get_sql_for_query( $queries ); |
278 |
275 |
279 if ( ! empty( $sql['where'] ) ) { |
276 if ( ! empty( $sql['where'] ) ) { |
280 $sql['where'] = ' AND ' . $sql['where']; |
277 $sql['where'] = ' AND ' . $sql['where']; |
281 } |
278 } |
282 |
279 |
334 } else { |
331 } else { |
335 $sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )'; |
332 $sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )'; |
336 } |
333 } |
337 |
334 |
338 $sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] ); |
335 $sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] ); |
339 // This is a subquery, so we recurse. |
336 // This is a subquery, so we recurse. |
340 } else { |
337 } else { |
341 $clause_sql = $this->get_sql_for_query( $clause, $depth + 1 ); |
338 $clause_sql = $this->get_sql_for_query( $clause, $depth + 1 ); |
342 |
339 |
343 $sql_chunks['where'][] = $clause_sql['where']; |
340 $sql_chunks['where'][] = $clause_sql['where']; |
344 $sql_chunks['join'][] = $clause_sql['join']; |
341 $sql_chunks['join'][] = $clause_sql['join']; |
397 |
394 |
398 if ( is_wp_error( $clause ) ) { |
395 if ( is_wp_error( $clause ) ) { |
399 return self::$no_results; |
396 return self::$no_results; |
400 } |
397 } |
401 |
398 |
402 $terms = $clause['terms']; |
399 $terms = $clause['terms']; |
403 $operator = strtoupper( $clause['operator'] ); |
400 $operator = strtoupper( $clause['operator'] ); |
404 |
401 |
405 if ( 'IN' == $operator ) { |
402 if ( 'IN' == $operator ) { |
406 |
403 |
407 if ( empty( $terms ) ) { |
404 if ( empty( $terms ) ) { |
414 * Before creating another table join, see if this clause has a |
411 * Before creating another table join, see if this clause has a |
415 * sibling with an existing join that can be shared. |
412 * sibling with an existing join that can be shared. |
416 */ |
413 */ |
417 $alias = $this->find_compatible_table_alias( $clause, $parent_query ); |
414 $alias = $this->find_compatible_table_alias( $clause, $parent_query ); |
418 if ( false === $alias ) { |
415 if ( false === $alias ) { |
419 $i = count( $this->table_aliases ); |
416 $i = count( $this->table_aliases ); |
420 $alias = $i ? 'tt' . $i : $wpdb->term_relationships; |
417 $alias = $i ? 'tt' . $i : $wpdb->term_relationships; |
421 |
418 |
422 // Store the alias as part of a flat array to build future iterators. |
419 // Store the alias as part of a flat array to build future iterators. |
423 $this->table_aliases[] = $alias; |
420 $this->table_aliases[] = $alias; |
424 |
421 |
427 |
424 |
428 $join .= " LEFT JOIN $wpdb->term_relationships"; |
425 $join .= " LEFT JOIN $wpdb->term_relationships"; |
429 $join .= $i ? " AS $alias" : ''; |
426 $join .= $i ? " AS $alias" : ''; |
430 $join .= " ON ($this->primary_table.$this->primary_id_column = $alias.object_id)"; |
427 $join .= " ON ($this->primary_table.$this->primary_id_column = $alias.object_id)"; |
431 } |
428 } |
432 |
|
433 |
429 |
434 $where = "$alias.term_taxonomy_id $operator ($terms)"; |
430 $where = "$alias.term_taxonomy_id $operator ($terms)"; |
435 |
431 |
436 } elseif ( 'NOT IN' == $operator ) { |
432 } elseif ( 'NOT IN' == $operator ) { |
437 |
433 |
464 AND object_id = $this->primary_table.$this->primary_id_column |
460 AND object_id = $this->primary_table.$this->primary_id_column |
465 ) = $num_terms"; |
461 ) = $num_terms"; |
466 |
462 |
467 } elseif ( 'NOT EXISTS' === $operator || 'EXISTS' === $operator ) { |
463 } elseif ( 'NOT EXISTS' === $operator || 'EXISTS' === $operator ) { |
468 |
464 |
469 $where = $wpdb->prepare( "$operator ( |
465 $where = $wpdb->prepare( |
|
466 "$operator ( |
470 SELECT 1 |
467 SELECT 1 |
471 FROM $wpdb->term_relationships |
468 FROM $wpdb->term_relationships |
472 INNER JOIN $wpdb->term_taxonomy |
469 INNER JOIN $wpdb->term_taxonomy |
473 ON $wpdb->term_taxonomy.term_taxonomy_id = $wpdb->term_relationships.term_taxonomy_id |
470 ON $wpdb->term_taxonomy.term_taxonomy_id = $wpdb->term_relationships.term_taxonomy_id |
474 WHERE $wpdb->term_taxonomy.taxonomy = %s |
471 WHERE $wpdb->term_taxonomy.taxonomy = %s |
475 AND $wpdb->term_relationships.object_id = $this->primary_table.$this->primary_id_column |
472 AND $wpdb->term_relationships.object_id = $this->primary_table.$this->primary_id_column |
476 )", $clause['taxonomy'] ); |
473 )", |
|
474 $clause['taxonomy'] |
|
475 ); |
477 |
476 |
478 } |
477 } |
479 |
478 |
480 $sql['join'][] = $join; |
479 $sql['join'][] = $join; |
481 $sql['where'][] = $where; |
480 $sql['where'][] = $where; |
559 $query['terms'] = array_unique( (array) $query['terms'] ); |
558 $query['terms'] = array_unique( (array) $query['terms'] ); |
560 |
559 |
561 if ( is_taxonomy_hierarchical( $query['taxonomy'] ) && $query['include_children'] ) { |
560 if ( is_taxonomy_hierarchical( $query['taxonomy'] ) && $query['include_children'] ) { |
562 $this->transform_query( $query, 'term_id' ); |
561 $this->transform_query( $query, 'term_id' ); |
563 |
562 |
564 if ( is_wp_error( $query ) ) |
563 if ( is_wp_error( $query ) ) { |
565 return; |
564 return; |
|
565 } |
566 |
566 |
567 $children = array(); |
567 $children = array(); |
568 foreach ( $query['terms'] as $term ) { |
568 foreach ( $query['terms'] as $term ) { |
569 $children = array_merge( $children, get_term_children( $term, $query['taxonomy'] ) ); |
569 $children = array_merge( $children, get_term_children( $term, $query['taxonomy'] ) ); |
570 $children[] = $term; |
570 $children[] = $term; |
571 } |
571 } |
572 $query['terms'] = $children; |
572 $query['terms'] = $children; |
573 } |
573 } |
574 |
574 |
588 * @param array $query The single query. Passed by reference. |
588 * @param array $query The single query. Passed by reference. |
589 * @param string $resulting_field The resulting field. Accepts 'slug', 'name', 'term_taxonomy_id', |
589 * @param string $resulting_field The resulting field. Accepts 'slug', 'name', 'term_taxonomy_id', |
590 * or 'term_id'. Default 'term_id'. |
590 * or 'term_id'. Default 'term_id'. |
591 */ |
591 */ |
592 public function transform_query( &$query, $resulting_field ) { |
592 public function transform_query( &$query, $resulting_field ) { |
593 if ( empty( $query['terms'] ) ) |
593 if ( empty( $query['terms'] ) ) { |
594 return; |
594 return; |
595 |
595 } |
596 if ( $query['field'] == $resulting_field ) |
596 |
|
597 if ( $query['field'] == $resulting_field ) { |
597 return; |
598 return; |
|
599 } |
598 |
600 |
599 $resulting_field = sanitize_key( $resulting_field ); |
601 $resulting_field = sanitize_key( $resulting_field ); |
600 |
602 |
601 // Empty 'terms' always results in a null transformation. |
603 // Empty 'terms' always results in a null transformation. |
602 $terms = array_filter( $query['terms'] ); |
604 $terms = array_filter( $query['terms'] ); |