WordPress.org

Make WordPress Core

Ticket #29642: 29642.unified.patch

File 29642.unified.patch, 32.4 KB (added by boonebgorges, 5 years ago)
  • new file src/wp-includes/class-wp-recursive-query.php

    diff --git src/wp-includes/class-wp-recursive-query.php src/wp-includes/class-wp-recursive-query.php
    new file mode 100644
    index 0000000..5fec3f5
    - +  
     1<?php
     2
     3/**
     4 * Base class for creating query classes that generate SQL fragments for filtering results based on recursive query params.
     5 *
     6 * @since 4.1.0
     7 */
     8abstract class WP_Recursive_Query {
     9
     10        /**
     11         * Query arguments passed to the constructor.
     12         *
     13         * @since 4.1.0
     14         * @access public
     15         * @var array
     16         */
     17        public $queries = array();
     18
     19        /**
     20         * A flat list of table aliases used in JOIN clauses.
     21         *
     22         * @since 4.1.0
     23         * @access protected
     24         * @var array
     25         */
     26        protected $table_aliases = array();
     27
     28        /**
     29         * Generate SQL clauses to be appended to a main query.
     30         *
     31         * Extending classes should call this method from within a publicly
     32         * accessible get_sql() method, and manipulate the SQL as necessary.
     33         * For example, {@link WP_Tax_Query::get_sql()} is merely a wrapper for
     34         * get_sql_clauses(), while {@link WP_Date_Query::get_sql()} discards
     35         * the empty 'join' clauses are discarded, and passes the 'where'
     36         * clause through apply_filters().
     37         *
     38         * @since 4.1.0
     39         * @access protected
     40         *
     41         * @param  string $primary_table
     42         * @param  string $primary_id_column
     43         * @return array
     44         */
     45        protected function get_sql_clauses() {
     46                $sql = $this->get_sql_for_query( $this->queries );
     47
     48                if ( ! empty( $sql['where'] ) ) {
     49                        $sql['where'] = ' AND ' . "\n" . $sql['where'] . "\n";
     50                }
     51
     52                return $sql;
     53        }
     54
     55        /**
     56         * Generate SQL clauses for a single query array.
     57         *
     58         * If nested subqueries are found, this method recurses the tree to
     59         * produce the properly nested SQL.
     60         *
     61         * Subclasses generally do not need to call this method. It is invoked
     62         * automatically from get_sql_clauses().
     63         *
     64         * @since 4.1.0
     65         * @access protected
     66         *
     67         * @param  array $query Query to parse.
     68         * @param  int   $depth Optional. Number of tree levels deep we
     69         *                      currently are. Used to calculate indentation.
     70         * @return array
     71         */
     72        protected function get_sql_for_query( $query, $depth = 0 ) {
     73                $sql_chunks = array(
     74                        'join'  => array(),
     75                        'where' => array(),
     76                );
     77
     78                $sql = array(
     79                        'join'  => '',
     80                        'where' => '',
     81                );
     82
     83                $indent = '';
     84                for ( $i = 0; $i < $depth; $i++ ) {
     85                        $indent .= "\t";
     86                }
     87
     88                foreach ( $query as $key => $clause ) {
     89                        if ( 'relation' === $key ) {
     90                                $relation = $query['relation'];
     91                        } else if ( is_array( $clause ) ) {
     92                                // This is a first-order clause
     93                                if ( $this->is_first_order_clause( $clause ) ) {
     94                                        $clause_sql = $this->get_sql_for_clause( $clause, $query );
     95
     96                                        $where_count = count( $clause_sql['where'] );
     97                                        if ( ! $where_count ) {
     98                                                $sql_chunks['where'][] = '';
     99                                        } else if ( 1 === $where_count ) {
     100                                                $sql_chunks['where'][] = $clause_sql['where'][0];
     101                                        } else {
     102                                                $sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )';
     103                                        }
     104
     105                                        $sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] );
     106                                // This is a subquery
     107                                } else {
     108                                        $clause_sql = $this->get_sql_for_query( $clause, $depth + 1 );
     109
     110                                        $sql_chunks['where'][] = $clause_sql['where'];
     111                                        $sql_chunks['join'][]  = $clause_sql['join'];
     112
     113                                }
     114                        }
     115                }
     116
     117                // Filter empties
     118                $sql_chunks['join']  = array_filter( $sql_chunks['join'] );
     119                $sql_chunks['where'] = array_filter( $sql_chunks['where'] );
     120
     121                if ( empty( $relation ) ) {
     122                        $relation = 'AND';
     123                }
     124
     125                if ( ! empty( $sql_chunks['join'] ) ) {
     126                        $sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) );
     127                }
     128
     129                if ( ! empty( $sql_chunks['where'] ) ) {
     130                        $sql['where'] = '( ' . "\n\t" . $indent . implode( ' ' . "\n\t" . $indent . $relation . ' ' . "\n\t" . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')' . "\n";
     131                }
     132
     133                return $sql;
     134        }
     135
     136        /**
     137         * Generate JOIN and WHERE clauses for a first-order clause.
     138         *
     139         * Must be overridden in a subclass.
     140         *
     141         * @since 4.1.0
     142         * @access protected
     143         *
     144         * @param  array $clause       Array of arguments belonging to the clause.
     145         * @param  array $parent_query Parent query to which the clause belongs.
     146         * @return array {
     147         *     @type array $join  Array of subclauses for the JOIN statement.
     148         *     @type array $where Array of subclauses for the WHERE statement.
     149         * }
     150         */
     151        abstract protected function get_sql_for_clause( $clause, $parent_query );
     152
     153        /**
     154         * Determine whether a clause is first-order.
     155         *
     156         * Must be overridden in a subclass.
     157         *
     158         * @since 4.1.0
     159         * @access protected
     160         *
     161         * @param  array $q Clause to check.
     162         * @return bool
     163         */
     164        abstract protected function is_first_order_clause( $query );
     165}
  • src/wp-includes/date.php

    diff --git src/wp-includes/date.php src/wp-includes/date.php
    index 1cf4c6d..dbad09e 100644
     
    88 *
    99 * @since 3.7.0
    1010 */
    11 class WP_Date_Query {
    12         /**
    13          * List of date queries.
    14          *
    15          * @since 3.7.0
    16          * @access public
    17          * @var array
    18          */
    19         public $queries = array();
     11class WP_Date_Query extends WP_Recursive_Query {
    2012
    2113        /**
    22          * The relation between the queries. Can be either 'AND' or 'OR' and can be changed via the query arguments.
     14         * The column to query against. Can be changed via the query arguments.
    2315         *
    2416         * @since 3.7.0
    2517         * @access public
    2618         * @var string
    2719         */
    28         public $relation = 'AND';
     20        public $column = 'post_date';
    2921
    3022        /**
    31          * The column to query against. Can be changed via the query arguments.
     23         * The value comparison operator. Can be changed via the query arguments.
    3224         *
    3325         * @since 3.7.0
    3426         * @access public
    35          * @var string
     27         * @var array
    3628         */
    37         public $column = 'post_date';
     29        public $compare = '=';
    3830
    3931        /**
    40          * The value comparison operator. Can be changed via the query arguments.
     32         * Supported time-related parameter keys.
    4133         *
    42          * @since 3.7.0
     34         * @since 4.1.0
    4335         * @access public
    4436         * @var array
    4537         */
    46         public $compare = '=';
     38        public $time_keys = array( 'after', 'before', 'year', 'month', 'monthnum', 'week', 'w', 'dayofyear', 'day', 'dayofweek', 'hour', 'minute', 'second' );
     39
    4740
    4841        /**
    4942         * Constructor.
    class WP_Date_Query { 
    109102         *                              'comment_date', 'comment_date_gmt'.
    110103         */
    111104        public function __construct( $date_query, $default_column = 'post_date' ) {
    112                 if ( empty( $date_query ) || ! is_array( $date_query ) )
    113                         return;
    114105
    115                 if ( isset( $date_query['relation'] ) && strtoupper( $date_query['relation'] ) == 'OR' )
     106                if ( isset( $date_query['relation'] ) && 'OR' === strtoupper( $date_query['relation'] ) ) {
    116107                        $this->relation = 'OR';
    117                 else
     108                } else {
    118109                        $this->relation = 'AND';
     110                }
     111
     112                if ( ! is_array( $date_query ) ) {
     113                        return;
     114                }
    119115
    120                 if ( ! empty( $date_query['column'] ) )
    121                         $this->column = esc_sql( $date_query['column'] );
    122                 else
    123                         $this->column = esc_sql( $default_column );
     116                if ( ! isset( $date_query[0] ) && ! empty( $date_query ) ) {
     117                        $date_query = array( $date_query );
     118                }
     119
     120                if ( empty( $date_query ) ) {
     121                        return;
     122                }
     123
     124                if ( ! empty( $date_query['column'] ) ) {
     125                        $date_query['column'] = esc_sql( $date_query['column'] );
     126                } else {
     127                        $date_query['column'] = esc_sql( $default_column );
     128                }
    124129
    125130                $this->column = $this->validate_column( $this->column );
    126131
    127132                $this->compare = $this->get_compare( $date_query );
    128133
    129                 // If an array of arrays wasn't passed, fix it
    130                 if ( ! isset( $date_query[0] ) )
    131                         $date_query = array( $date_query );
     134                $this->queries = $this->sanitize_query( $date_query );
     135
     136                return;
     137        }
     138
     139        /**
     140         * Recursive-friendly query sanitizer.
     141         *
     142         * Ensures that each query-level clause has a 'relation' key, and that
     143         * each first-order clause contains all the necessary keys from
     144         * $defaults.
     145         *
     146         * @since 4.1.0
     147         *
     148         * @param  array $query A tax_query query clause.
     149         * @return array
     150         */
     151        protected function sanitize_query( $query, $parent_query = null ) {
     152                $sanitized_query = array();
     153
     154                $defaults = array(
     155                        'column' => 'post_date',
     156                        'compare' => '=',
     157                        'relation' => 'AND',
     158                );
    132159
    133                 $this->queries = array();
    134                 foreach ( $date_query as $key => $query ) {
    135                         if ( ! is_array( $query ) )
     160                // Numeric keys should always have array values.
     161                foreach ( $query as $qkey => $qvalue ) {
     162                        if ( is_numeric( $qkey ) && ! is_array( $qvalue ) ) {
     163                                unset( $query[ $qkey ] );
     164                        }
     165                }
     166
     167                // Each layer of query should have a value for each default key.
     168                foreach ( $defaults as $dkey => $dvalue ) {
     169                        if ( isset( $query[ $dkey ] ) ) {
    136170                                continue;
     171                        }
     172
     173                        if ( isset( $parent_query[ $dkey ] ) ) {
     174                                $query[ $dkey ] = $parent_query[ $dkey ];
     175                        } else {
     176                                $query[ $dkey ] = $dvalue;
     177                        }
     178                }
    137179
    138                         $this->queries[$key] = $query;
     180                foreach ( $query as $key => $q ) {
     181                        if ( ! is_array( $q ) || in_array( $key, $this->time_keys, true ) ) {
     182                                $sanitized_query[ $key ] = $q;
     183                        } else {
     184                                $sanitized_query[] = $this->sanitize_query( $q, $query );
     185                        }
    139186                }
     187
     188                return $sanitized_query;
    140189        }
    141190
    142191        /**
    class WP_Date_Query { 
    192241         * @return string MySQL WHERE parameters
    193242         */
    194243        public function get_sql() {
    195                 // The parts of the final query
    196                 $where = array();
    197 
    198                 foreach ( $this->queries as $key => $query ) {
    199                         $where_parts = $this->get_sql_for_subquery( $query );
    200                         if ( $where_parts ) {
    201                                 // Combine the parts of this subquery into a single string
    202                                 $where[ $key ] = '( ' . implode( ' AND ', $where_parts ) . ' )';
    203                         }
    204                 }
     244                $sql = $this->get_sql_clauses();
    205245
    206                 // Combine the subquery strings into a single string
    207                 if ( $where )
    208                         $where = ' AND ( ' . implode( " {$this->relation} ", $where ) . ' )';
    209                 else
    210                         $where = '';
     246                $where = $sql['where'];
    211247
    212248                /**
    213249                 * Filter the date query WHERE clause.
    class WP_Date_Query { 
    221257        }
    222258
    223259        /**
    224          * Turns a single date subquery into pieces for a WHERE clause.
     260         * Turns a single date clause into pieces for a WHERE clause.
    225261         *
    226262         * @since 3.7.0
    227263         * return array
    228264         */
    229         protected function get_sql_for_subquery( $query ) {
     265        protected function get_sql_for_clause( $query, $parent_query ) {
    230266                global $wpdb;
    231267
    232268                // The sub-parts of a $where part
    class WP_Date_Query { 
    293329                        }
    294330                }
    295331
    296                 return $where_parts;
     332                // Return an array of 'join' and 'where' for compatibility
     333                // with other query classes.
     334                return array(
     335                        'where' => $where_parts,
     336                        'join'  => array(),
     337                );
     338        }
     339
     340        protected function is_first_order_clause( $query ) {
     341                return isset( $query['after'] ) || isset( $query['before'] ) || isset( $query['year'] ) || isset( $query['month'] ) || isset( $query['monthnum'] ) || isset( $query['week'] ) || isset( $query['w'] ) || isset( $query['dayofyear'] ) || isset( $query['day'] ) || isset( $query['dayofweek'] ) || isset( $query['hour'] ) || isset( $query['minute'] ) || isset( $query['second'] );
    297342        }
    298343
    299344        /**
  • src/wp-includes/meta.php

    diff --git src/wp-includes/meta.php src/wp-includes/meta.php
    index e3c43cf..3b452fc 100644
    function get_meta_sql( $meta_query, $type, $primary_table, $primary_id_column, $ 
    853853 *
    854854 * @since 3.2.0
    855855 */
    856 class WP_Meta_Query {
     856class WP_Meta_Query extends WP_Recursive_Query {
    857857        /**
    858858        * List of metadata queries. A single query is an associative array:
    859859        * - 'key' string The meta key
    class WP_Meta_Query { 
    882882        public $relation;
    883883
    884884        /**
    885          * Constructor
     885         * Database table to query for the metadata.
    886886         *
    887          * @param array $meta_query (optional) A meta query
     887         * @access public
     888         * @var string
     889         */
     890        public $meta_table;
     891
     892        /**
     893         * Column in meta_table that represents the ID of the object the metadata belongs to.
     894         *
     895         * @access public
     896         * @var string
     897         */
     898        public $meta_id_column;
     899
     900        /**
     901         * Database table that where the metadata's objects are stored (eg $wpdb->users).
     902         *
     903         * @access public
     904         * @var string
     905         */
     906        public $primary_table;
     907
     908        /**
     909         * Column in primary_table that represents the ID of the object.
     910         *
     911         * @access public
     912         * @var string
     913         */
     914        public $primary_id_column;
     915
     916        /**
     917         * Constructor.
     918         *
     919         * @param array $meta_query (optional) A meta query.
    888920         */
    889921        public function __construct( $meta_query = false ) {
    890922                if ( !$meta_query )
    class WP_Meta_Query { 
    896928                        $this->relation = 'AND';
    897929                }
    898930
    899                 $this->queries = array();
     931                $this->queries = $this->sanitize_query( $meta_query );
     932        }
     933
     934        /**
     935         * Ensure that the meta_query argument passed to the class constructor is well-formed.
     936         *
     937         * Eliminates empty items and ensures that a 'relation' is set.
     938         *
     939         * @param  array $queries
     940         * @return array
     941         */
     942        public function sanitize_query( $queries ) {
     943                $clean_queries = array();
     944
     945                if ( ! is_array( $queries ) ) {
     946                        return $clean_queries;
     947                }
     948
     949                foreach ( $queries as $key => $query ) {
     950                        if ( 'relation' === $key ) {
     951                                $relation = $query;
     952
     953                        // First-order clause.
     954                        } else if ( $this->is_first_order_clause( $query ) ) {
     955                                $clean_queries[] = $query;
     956
     957                        // Otherwise, it's a nested query.
     958                        } else {
     959                                $cleaned_query = $this->sanitize_query( $query );
     960
     961                                if ( ! empty( $cleaned_query ) ) {
     962                                        $clean_queries[] = $cleaned_query;
     963                                }
     964                        }
     965                }
     966
     967                if ( empty( $clean_queries ) ) {
     968                        return $clean_queries;
     969                }
     970
     971                // Sanitize the 'relation' key provided in the query.
     972                if ( isset( $relation ) && 'OR' === strtoupper( $relation ) ) {
     973                        $clean_queries['relation'] = 'OR';
    900974
    901                 foreach ( $meta_query as $key => $query ) {
    902                         if ( ! is_array( $query ) )
    903                                 continue;
     975                // If there is only a single clause, call the relation 'OR'.
     976                // This value will not actually be used to join clauses, but it
     977                // simplifies the logic around combining key-only queries.
     978                } else if ( 1 === count( $clean_queries ) ) {
     979                        $clean_queries['relation'] = 'OR';
    904980
    905                         $this->queries[] = $query;
     981                // Default to AND.
     982                } else {
     983                        $clean_queries['relation'] = 'AND';
    906984                }
     985
     986                return $clean_queries;
     987        }
     988
     989        protected function is_first_order_clause( $query ) {
     990                return isset( $query['key'] ) || isset( $query['value'] );
    907991        }
    908992
    909993        /**
    class WP_Meta_Query { 
    9581042        }
    9591043
    9601044        /**
    961          * Generates SQL clauses to be appended to a main query.
     1045         * Generate SQL JOIN and WHERE clauses for a first-order query clause.
    9621046         *
    963          * @since 3.2.0
    964          * @access public
     1047         * "First-order" means that it's an array with a 'key' or 'value'.
    9651048         *
    966          * @param string $type Type of meta
    967          * @param string $primary_table
    968          * @param string $primary_id_column
    969          * @param object $context (optional) The main query object
    970          * @return array( 'join' => $join_sql, 'where' => $where_sql )
     1049         * @param  array $clause       Query clause.
     1050         * @param  array $parent_query Parent query array.
     1051         * @return array
    9711052         */
    972         public function get_sql( $type, $primary_table, $primary_id_column, $context = null ) {
     1053        public function get_sql_for_clause( $clause, $parent_query ) {
    9731054                global $wpdb;
    9741055
    975                 if ( ! $meta_table = _get_meta_table( $type ) )
    976                         return false;
    977 
    978                 $meta_id_column = sanitize_key( $type . '_id' );
    979 
    980                 $join = array();
    981                 $where = array();
     1056                $sql_chunks = array(
     1057                        'where' => array(),
     1058                        'join' => array(),
     1059                );
    9821060
    983                 $key_only_queries = array();
    984                 $queries = array();
     1061                $i = count( $this->table_aliases );
     1062                $alias = $i ? 'mt' . $i : $this->meta_table;
    9851063
    986                 // Split out the queries with empty arrays as value
    987                 foreach ( $this->queries as $k => $q ) {
    988                         if ( isset( $q['value'] ) && is_array( $q['value'] ) && empty( $q['value'] ) ) {
    989                                 $key_only_queries[$k] = $q;
    990                                 unset( $this->queries[$k] );
    991                         }
    992                 }
    993 
    994                 // Split out the meta_key only queries (we can only do this for OR)
    995                 if ( 'OR' == $this->relation ) {
    996                         foreach ( $this->queries as $k => $q ) {
    997                                 if ( ( empty( $q['compare'] ) || 'NOT EXISTS' != $q['compare'] ) && ! array_key_exists( 'value', $q ) && ! empty( $q['key'] ) )
    998                                         $key_only_queries[$k] = $q;
    999                                 else
    1000                                         $queries[$k] = $q;
    1001                         }
     1064                if ( isset( $clause['compare'] ) ) {
     1065                        $meta_compare = strtoupper( $clause['compare'] );
    10021066                } else {
    1003                         $queries = $this->queries;
     1067                        $meta_compare = isset( $clause['value'] ) && is_array( $clause['value'] ) ? 'IN' : '=';
    10041068                }
    10051069
    1006                 // Specify all the meta_key only queries in one go
    1007                 if ( $key_only_queries ) {
    1008                         $join[]  = "INNER JOIN $meta_table ON $primary_table.$primary_id_column = $meta_table.$meta_id_column";
    1009 
    1010                         foreach ( $key_only_queries as $key => $q )
    1011                                 $where["key-only-$key"] = $wpdb->prepare( "$meta_table.meta_key = %s", trim( $q['key'] ) );
     1070                if ( ! in_array( $meta_compare, array(
     1071                        '=', '!=', '>', '>=', '<', '<=',
     1072                        'LIKE', 'NOT LIKE',
     1073                        'IN', 'NOT IN',
     1074                        'BETWEEN', 'NOT BETWEEN',
     1075                        'EXISTS', 'NOT EXISTS',
     1076                        'REGEXP', 'NOT REGEXP', 'RLIKE'
     1077                ) ) ) {
     1078                        $meta_compare = '=';
    10121079                }
    10131080
    1014                 foreach ( $queries as $k => $q ) {
    1015                         $meta_key = isset( $q['key'] ) ? trim( $q['key'] ) : '';
    1016                         $meta_type = $this->get_cast_for_type( isset( $q['type'] ) ? $q['type'] : '' );
    1017 
    1018                         if ( array_key_exists( 'value', $q ) && is_null( $q['value'] ) )
    1019                                 $q['value'] = '';
    1020 
    1021                         $meta_value = isset( $q['value'] ) ? $q['value'] : null;
    1022 
    1023                         if ( isset( $q['compare'] ) )
    1024                                 $meta_compare = strtoupper( $q['compare'] );
    1025                         else
    1026                                 $meta_compare = is_array( $meta_value ) ? 'IN' : '=';
    1027 
    1028                         if ( ! in_array( $meta_compare, array(
    1029                                 '=', '!=', '>', '>=', '<', '<=',
    1030                                 'LIKE', 'NOT LIKE',
    1031                                 'IN', 'NOT IN',
    1032                                 'BETWEEN', 'NOT BETWEEN',
    1033                                 'NOT EXISTS',
    1034                                 'REGEXP', 'NOT REGEXP', 'RLIKE'
    1035                         ) ) )
    1036                                 $meta_compare = '=';
    1037 
    1038                         $i = count( $join );
    1039                         $alias = $i ? 'mt' . $i : $meta_table;
    1040 
    1041                         if ( 'NOT EXISTS' == $meta_compare ) {
    1042                                 $join[$i]  = "LEFT JOIN $meta_table";
    1043                                 $join[$i] .= $i ? " AS $alias" : '';
    1044                                 $join[$i] .= " ON ($primary_table.$primary_id_column = $alias.$meta_id_column AND $alias.meta_key = '$meta_key')";
    1045 
    1046                                 $where[$k] = ' ' . $alias . '.' . $meta_id_column . ' IS NULL';
    1047 
    1048                                 continue;
     1081                // There are a number of different query structures that get
     1082                // built in different ways.
     1083                // 1. Key-only clauses - (a) clauses without a 'value' key that
     1084                //    appear in the context of an OR relation and do not use
     1085                //    'NOT EXISTS' as the 'compare', or (b) clauses with an
     1086                //    empty array for 'value'.
     1087                if ( ! empty( $clause['key'] ) && (
     1088                        ( ! array_key_exists( 'value', $clause ) && 'NOT EXISTS' !== $meta_compare && 'OR' === $parent_query['relation'] ) ||
     1089                        ( isset( $clause['value'] ) && is_array( $clause['value'] ) && empty( $clause['value'] ) )
     1090                ) ) {
     1091
     1092                        $alias = $this->meta_table;
     1093                        $sql_chunks['join'][] = " INNER JOIN $this->meta_table ON ($this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column)";
     1094                        $sql_chunks['where'][] = $wpdb->prepare( "$this->meta_table.meta_key = %s", trim( $clause['key'] ) );
     1095
     1096                // 2. NOT EXISTS.
     1097                } else if ( 'NOT EXISTS' === $meta_compare ) {
     1098                        $join  = " LEFT JOIN $this->meta_table";
     1099                        $join .= $i ? " AS $alias" : '';
     1100                        $join .= $wpdb->prepare( " ON ($this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column AND $alias.meta_key = %s )", $clause['key'] );
     1101                        $sql_chunks['join'][] = $join;
     1102
     1103                        $sql_chunks['where'][] = $alias . '.' . $this->meta_id_column . ' IS NULL';
     1104
     1105                // 3. EXISTS and other key-only queries.
     1106                } else if ( 'EXISTS' === $meta_compare || ( ! empty( $clause['key'] ) && ! array_key_exists( 'value', $clause ) ) ) {
     1107                        $join  = " INNER JOIN $this->meta_table";
     1108                        $join .= $i ? " AS $alias" : '';
     1109                        $join .= " ON ( $this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column )";
     1110                        $sql_chunks['join'][] = $join;
     1111
     1112                        $sql_chunks['where'][] = $wpdb->prepare( $alias . '.meta_key = %s', trim( $clause['key'] ) );
     1113
     1114                // 4. Clauses that have a value.
     1115                } else if ( array_key_exists( 'value', $clause ) ) {
     1116                        $join  = " INNER JOIN $this->meta_table";
     1117                        $join .= $i ? " AS $alias" : '';
     1118                        $join .= " ON ($this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column)";
     1119                        $sql_chunks['join'][] = $join;
     1120
     1121                        if ( ! empty( $clause['key'] ) ) {
     1122                                $sql_chunks['where'][] = $wpdb->prepare( "$alias.meta_key = %s", trim( $clause['key'] ) );
    10491123                        }
    10501124
    1051                         $join[$i]  = "INNER JOIN $meta_table";
    1052                         $join[$i] .= $i ? " AS $alias" : '';
    1053                         $join[$i] .= " ON ($primary_table.$primary_id_column = $alias.$meta_id_column)";
    1054 
    1055                         $where[$k] = '';
    1056                         if ( !empty( $meta_key ) )
    1057                                 $where[$k] = $wpdb->prepare( "$alias.meta_key = %s", $meta_key );
     1125                        $meta_type = $this->get_cast_for_type( isset( $clause['type'] ) ? $clause['type'] : '' );
    10581126
    1059                         if ( is_null( $meta_value ) ) {
    1060                                 if ( empty( $where[$k] ) )
    1061                                         unset( $join[$i] );
    1062                                 continue;
    1063                         }
     1127                        $meta_value = isset( $clause['value'] ) ? $clause['value'] : '';
    10641128
    10651129                        if ( in_array( $meta_compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ) ) ) {
    1066                                 if ( ! is_array( $meta_value ) )
     1130                                if ( ! is_array( $meta_value ) ) {
    10671131                                        $meta_value = preg_split( '/[,\s]+/', $meta_value );
    1068 
    1069                                 if ( empty( $meta_value ) ) {
    1070                                         unset( $join[$i] );
    1071                                         continue;
    10721132                                }
    10731133                        } else {
    10741134                                $meta_value = trim( $meta_value );
    10751135                        }
    10761136
    1077                         if ( 'IN' == substr( $meta_compare, -2) ) {
     1137                        if ( 'IN' == substr( $meta_compare, -2 ) ) {
    10781138                                $meta_compare_string = '(' . substr( str_repeat( ',%s', count( $meta_value ) ), 1 ) . ')';
    1079                         } elseif ( 'BETWEEN' == substr( $meta_compare, -7) ) {
     1139                        } elseif ( 'BETWEEN' == substr( $meta_compare, -7 ) ) {
    10801140                                $meta_value = array_slice( $meta_value, 0, 2 );
    10811141                                $meta_compare_string = '%s AND %s';
    10821142                        } elseif ( 'LIKE' == $meta_compare || 'NOT LIKE' == $meta_compare ) {
    class WP_Meta_Query { 
    10861146                                $meta_compare_string = '%s';
    10871147                        }
    10881148
    1089                         if ( ! empty( $where[$k] ) )
    1090                                 $where[$k] .= ' AND ';
     1149                        $sql_chunks['where'][] = $wpdb->prepare( "CAST($alias.meta_value AS {$meta_type}) {$meta_compare} {$meta_compare_string}", $meta_value );
     1150                }
    10911151
    1092                         $where[$k] = ' (' . $where[$k] . $wpdb->prepare( "CAST($alias.meta_value AS {$meta_type}) {$meta_compare} {$meta_compare_string})", $meta_value );
     1152                // Multiple WHERE clauses (for meta_key and meta_value) should
     1153                // be joined in parentheses.
     1154                if ( 1 < count( $sql_chunks['where'] ) ) {
     1155                        $sql_chunks['where'] = array( '( ' . implode( ' AND ', $sql_chunks['where'] ) . ' )' );
    10931156                }
    10941157
    1095                 $where = array_filter( $where );
     1158                $this->table_aliases[] = $alias;
    10961159
    1097                 if ( empty( $where ) )
    1098                         $where = '';
    1099                 else
    1100                         $where = ' AND (' . implode( "\n{$this->relation} ", $where ) . ' )';
     1160                return $sql_chunks;
     1161        }
     1162
     1163        /**
     1164         * Generates SQL clauses to be appended to a main query.
     1165         *
     1166         * @since 3.2.0
     1167         * @access public
     1168         *
     1169         * @param  string $type              Type of meta, eg 'user', 'post'.
     1170         * @param  string $primary_table     Database table where the object being
     1171         *                                   filtered is stored (eg wp_users).
     1172         * @param  string $primary_id_column ID column for the filtered object in $primary_table
     1173         * @param  object $context           Optional. The main query object.
     1174         * @return array {
     1175         *      @type string $join SQL fragment to append to the main JOIN clause.
     1176         *      @type string $where SQL fragment to append to the main WHERE clause.
     1177         * }
     1178         */
     1179        public function get_sql( $type, $primary_table, $primary_id_column, $context = null ) {
     1180                global $wpdb;
     1181
     1182                if ( ! $meta_table = _get_meta_table( $type ) ) {
     1183                        return false;
     1184                }
     1185
     1186                $this->meta_table     = $meta_table;
     1187                $this->meta_id_column = sanitize_key( $type . '_id' );
     1188
     1189                $this->primary_table     = $primary_table;
     1190                $this->primary_id_column = $primary_id_column;
    11011191
    1102                 $join = implode( "\n", $join );
    1103                 if ( ! empty( $join ) )
    1104                         $join = ' ' . $join;
     1192                $sql = $this->get_sql_clauses();
    11051193
    11061194                /**
    11071195                 * Filter the meta query's generated SQL.
    class WP_Meta_Query { 
    11191207                 *     @type object $context           The main query object.
    11201208                 * }
    11211209                 */
    1122                 return apply_filters_ref_array( 'get_meta_sql', array( compact( 'join', 'where' ), $this->queries, $type, $primary_table, $primary_id_column, $context ) );
     1210                return apply_filters_ref_array( 'get_meta_sql', array( $sql, $this->queries, $type, $primary_table, $primary_id_column, $context ) );
    11231211        }
    11241212}
    11251213
  • src/wp-includes/taxonomy.php

    diff --git src/wp-includes/taxonomy.php src/wp-includes/taxonomy.php
    index 1b4902d..52224cb 100644
    function get_tax_sql( $tax_query, $primary_table, $primary_id_column ) { 
    630630 *
    631631 * @since 3.1.0
    632632 */
    633 class WP_Tax_Query {
     633class WP_Tax_Query extends WP_Recursive_Query {
    634634
    635635        /**
    636636         * List of taxonomy queries. A single taxonomy query is an associative array:
    class WP_Tax_Query { 
    667667         * @access private
    668668         * @var string
    669669         */
    670         private static $no_results = array( 'join' => '', 'where' => ' AND 0 = 1' );
     670        private static $no_results = array( 'join' => array( '' ), 'where' => array( '0 = 1' ) );
     671
     672        /**
     673         * Terms and taxonomies fetched by this query.
     674         *
     675         * We store this data in a flat array because they are referenced in a
     676         * number of places by WP_Query.
     677         *
     678         * @since 4.1.0
     679         * @access public
     680         * @var array
     681         */
     682        public $queried_terms = array();
    671683
    672684        /**
    673685         * Constructor.
    class WP_Tax_Query { 
    699711                        $this->relation = 'AND';
    700712                }
    701713
    702                 $defaults = array(
    703                         'taxonomy' => '',
    704                         'terms' => array(),
    705                         'include_children' => true,
    706                         'field' => 'term_id',
    707                         'operator' => 'IN',
    708                 );
    709 
    710                 foreach ( $tax_query as $query ) {
    711                         if ( ! is_array( $query ) )
    712                                 continue;
    713 
    714                         $query = array_merge( $defaults, $query );
    715 
    716                         $query['terms'] = (array) $query['terms'];
     714                $this->queries = $this->sanitize_query( $tax_query );
     715        }
    717716
    718                         $this->queries[] = $query;
    719                 }
     717        public function get_sql( $primary_table, $primary_id_column ) {
     718                $this->primary_table = $primary_table;
     719                $this->primary_id_column = $primary_id_column;
     720                return $this->get_sql_clauses();
    720721        }
    721722
    722723        /**
    723          * Generates SQL clauses to be appended to a main query.
     724         * Generate SQL JOIN and WHERE clauses for a first-order query clause.
    724725         *
    725          * @since 3.1.0
    726          * @access public
    727          *
    728          * @param string $primary_table
    729          * @param string $primary_id_column
     726         * @param  array         $clause       Query clause.
     727         * @param  WP_Meta_Query $parent_query Query object.
    730728         * @return array
    731729         */
    732         public function get_sql( $primary_table, $primary_id_column ) {
     730        protected function get_sql_for_clause( $clause, $parent_query ) {
    733731                global $wpdb;
    734732
     733                $sql = array(
     734                        'where' => array(),
     735                        'join'  => array(),
     736                );
     737
    735738                $join = '';
    736                 $where = array();
    737                 $i = 0;
    738                 $count = count( $this->queries );
    739739
    740                 foreach ( $this->queries as $index => $query ) {
    741                         $this->clean_query( $query );
     740                $this->clean_query( $clause );
     741
     742                if ( is_wp_error( $clause ) ) {
     743                        return self::$no_results;
     744                }
     745
     746                $terms = $clause['terms'];
     747                $operator = strtoupper( $clause['operator'] );
    742748
    743                         if ( is_wp_error( $query ) ) {
     749                if ( 'IN' == $operator ) {
     750
     751                        if ( empty( $terms ) ) {
    744752                                return self::$no_results;
    745753                        }
    746754
    747                         $terms = $query['terms'];
    748                         $operator = strtoupper( $query['operator'] );
     755                        $terms = implode( ',', $terms );
    749756
    750                         if ( 'IN' == $operator ) {
     757                        $i = count( $this->table_aliases );
     758                        $alias = $i ? 'tt' . $i : $wpdb->term_relationships;
     759                        $this->table_aliases[] = $alias;
    751760
    752                                 if ( empty( $terms ) ) {
    753                                         if ( 'OR' == $this->relation ) {
    754                                                 if ( ( $index + 1 === $count ) && empty( $where ) ) {
    755                                                         return self::$no_results;
    756                                                 }
    757                                                 continue;
    758                                         } else {
    759                                                 return self::$no_results;
    760                                         }
    761                                 }
     761                        $join .= " INNER JOIN $wpdb->term_relationships";
     762                        $join .= $i ? " AS $alias" : '';
     763                        $join .= " ON ($this->primary_table.$this->primary_id_column = $alias.object_id)";
    762764
    763                                 $terms = implode( ',', $terms );
     765                        $where = "$alias.term_taxonomy_id $operator ($terms)";
    764766
    765                                 $alias = $i ? 'tt' . $i : $wpdb->term_relationships;
     767                } elseif ( 'NOT IN' == $operator ) {
    766768
    767                                 $join .= " INNER JOIN $wpdb->term_relationships";
    768                                 $join .= $i ? " AS $alias" : '';
    769                                 $join .= " ON ($primary_table.$primary_id_column = $alias.object_id)";
     769                        if ( empty( $terms ) ) {
     770                                continue;
     771                        }
    770772
    771                                 $where[] = "$alias.term_taxonomy_id $operator ($terms)";
    772                         } elseif ( 'NOT IN' == $operator ) {
     773                        $terms = implode( ',', $terms );
    773774
    774                                 if ( empty( $terms ) ) {
    775                                         continue;
    776                                 }
     775                        $where = "$this->primary_table.$this->primary_id_column NOT IN (
     776                                SELECT object_id
     777                                FROM $wpdb->term_relationships
     778                                WHERE term_taxonomy_id IN ($terms)
     779                        )";
    777780
    778                                 $terms = implode( ',', $terms );
     781                } elseif ( 'AND' == $operator ) {
    779782
    780                                 $where[] = "$primary_table.$primary_id_column NOT IN (
    781                                         SELECT object_id
    782                                         FROM $wpdb->term_relationships
    783                                         WHERE term_taxonomy_id IN ($terms)
    784                                 )";
    785                         } elseif ( 'AND' == $operator ) {
     783                        if ( empty( $terms ) ) {
     784                                continue;
     785                        }
    786786
    787                                 if ( empty( $terms ) ) {
    788                                         continue;
    789                                 }
     787                        $num_terms = count( $terms );
    790788
    791                                 $num_terms = count( $terms );
     789                        $terms = implode( ',', $terms );
    792790
    793                                 $terms = implode( ',', $terms );
     791                        $where = "(
     792                                SELECT COUNT(1)
     793                                FROM $wpdb->term_relationships
     794                                WHERE term_taxonomy_id IN ($terms)
     795                                AND object_id = $this->primary_table.$this->primary_id_column
     796                        ) = $num_terms";
     797                }
    794798
    795                                 $where[] = "(
    796                                         SELECT COUNT(1)
    797                                         FROM $wpdb->term_relationships
    798                                         WHERE term_taxonomy_id IN ($terms)
    799                                         AND object_id = $primary_table.$primary_id_column
    800                                 ) = $num_terms";
    801                         }
     799                $sql['join'][]  = $join;
     800                $sql['where'][] = $where;
     801                return $sql;
     802        }
    802803
    803                         $i++;
    804                 }
     804        /**
     805         * Determine whether a clause is first-order.
     806         *
     807         * A "first-order" clause is one that contains any of the first-order
     808         * clause keys ('terms', 'taxonomy', 'include_children', 'field',
     809         * 'operator'). An empty clause also counts as a first-order clause,
     810         * for backward compatibility. Any clause that doesn't meet this is
     811         * determined, by process of elimination, to be a higher-order query.
     812         *
     813         * @since 4.1.0
     814         *
     815         * @param  array $q Clause to check.
     816         * @return bool
     817         */
     818        protected function is_first_order_clause( $q ) {
     819                return empty( $q ) || array_key_exists( 'terms', $q ) || array_key_exists( 'taxonomy', $q ) || array_key_exists( 'include_children', $q ) || array_key_exists( 'field', $q ) || array_key_exists( 'operator', $q );
     820        }
    805821
    806                 if ( ! empty( $where ) ) {
    807                         $where = ' AND ( ' . implode( " $this->relation ", $where ) . ' )';
    808                 } else {
    809                         $where = '';
     822        /**
     823         * Recursive-friendly query sanitizer.
     824         *
     825         * Ensures that each query-level clause has a 'relation' key, and that
     826         * each first-order clause contains all the necessary keys from
     827         * $defaults.
     828         *
     829         * @since 4.1.0
     830         *
     831         * @param  array $query A tax_query query clause.
     832         * @return array
     833         */
     834        protected function sanitize_query( $query ) {
     835                $sanitized_query = array();
     836
     837                $defaults = array(
     838                        'taxonomy' => '',
     839                        'terms' => array(),
     840                        'include_children' => true,
     841                        'field' => 'term_id',
     842                        'operator' => 'IN',
     843                );
     844
     845                foreach ( $query as $key => $q ) {
     846                        if ( 'relation' === $key ) {
     847                                $sanitized_query['relation'] = $q;
     848                        } else if ( is_array( $q ) ) {
     849                                if ( ! $this->is_first_order_clause( $q ) ) {
     850                                        $sanitized_query[] = $this->sanitize_query( $q );
     851                                } else {
     852                                        $sanitized_clause = array_merge( $defaults, $q );
     853                                        $sanitized_clause['terms'] = (array) $sanitized_clause['terms'];
     854                                        $sanitized_query[] = $sanitized_clause;
     855
     856                                        // Keep a copy of the clause in the
     857                                        // flat $queried_terms array, for use
     858                                        // in WP_Query
     859                                        if ( ! empty( $sanitized_clause['taxonomy'] ) && 'NOT IN' !== $sanitized_clause['operator'] ) {
     860                                                $taxonomy = $sanitized_clause['taxonomy'];
     861                                                if ( ! isset( $this->queried_terms[ $taxonomy ] ) ) {
     862                                                        $this->queried_terms[ $taxonomy ] = array();
     863                                                }
     864
     865                                                // Backward compatibility: Only store the first
     866                                                // 'terms' and 'field' found for a given taxonomy
     867                                                if ( isset( $sanitized_clause['terms'] ) && ! isset( $this->queried_terms[ $taxonomy ]['terms'] ) ) {
     868                                                        $this->queried_terms[ $taxonomy ]['terms'] = $sanitized_clause['terms'];
     869                                                }
     870
     871                                                if ( isset( $sanitized_clause['field'] ) && ! isset( $this->queried_terms[ $taxonomy ]['field'] ) ) {
     872                                                        $this->queried_terms[ $taxonomy ]['field'] = $sanitized_clause['field'];
     873                                                }
     874                                        }
     875                                }
     876                        }
    810877                }
    811                 return compact( 'join', 'where' );
     878
     879                return $sanitized_query;
    812880        }
    813881
    814882        /**
  • src/wp-settings.php

    diff --git src/wp-settings.php src/wp-settings.php
    index 9795971..5dca996 100644
    wp_not_installed(); 
    111111// Load most of WordPress.
    112112require( ABSPATH . WPINC . '/class-wp-walker.php' );
    113113require( ABSPATH . WPINC . '/class-wp-ajax-response.php' );
     114require( ABSPATH . WPINC . '/class-wp-recursive-query.php' );
    114115require( ABSPATH . WPINC . '/formatting.php' );
    115116require( ABSPATH . WPINC . '/capabilities.php' );
    116117require( ABSPATH . WPINC . '/query.php' );