+/**
+ * Class for generating SQL clauses that filter a primary query according to metadata keys and values.
+ *
+ * `WP_Meta_Query` is a helper that allows primary query classes, such as {@see WP_Query} and {@see WP_User_Query},
+ * to filter their results by object metadata, by generating `JOIN` and `WHERE` subclauses to be attached
+ * to the primary SQL query string.
+ *
+ * @since 3.2.0
+ */
+class WP_Meta_Query {
+ /**
+ * Array of metadata queries.
+ *
+ * See {@see WP_Meta_Query::__construct()} for information on meta query arguments.
+ *
+ * @since 3.2.0
+ * @access public
+ * @var array
+ */
+ public $queries = array();
+
+ /**
+ * The relation between the queries. Can be one of 'AND' or 'OR'.
+ *
+ * @since 3.2.0
+ * @access public
+ * @var string
+ */
+ public $relation;
+
+ /**
+ * Database table to query for the metadata.
+ *
+ * @since 4.1.0
+ * @access public
+ * @var string
+ */
+ public $meta_table;
+
+ /**
+ * Column in meta_table that represents the ID of the object the metadata belongs to.
+ *
+ * @since 4.1.0
+ * @access public
+ * @var string
+ */
+ public $meta_id_column;
+
+ /**
+ * Database table that where the metadata's objects are stored (eg $wpdb->users).
+ *
+ * @since 4.1.0
+ * @access public
+ * @var string
+ */
+ public $primary_table;
+
+ /**
+ * Column in primary_table that represents the ID of the object.
+ *
+ * @since 4.1.0
+ * @access public
+ * @var string
+ */
+ public $primary_id_column;
+
+ /**
+ * A flat list of table aliases used in JOIN clauses.
+ *
+ * @since 4.1.0
+ * @access protected
+ * @var array
+ */
+ protected $table_aliases = array();
+
+ /**
+ * A flat list of clauses, keyed by clause 'name'.
+ *
+ * @since 4.2.0
+ * @access protected
+ * @var array
+ */
+ protected $clauses = array();
+
+ /**
+ * Whether the query contains any OR relations.
+ *
+ * @since 4.3.0
+ * @access protected
+ * @var bool
+ */
+ protected $has_or_relation = false;
+
+ /**
+ * Constructor.
+ *
+ * @since 3.2.0
+ * @since 4.2.0 Introduced support for naming query clauses by associative array keys.
+ *
+ * @access public
+ *
+ * @param array $meta_query {
+ * Array of meta query clauses. When first-order clauses use strings as their array keys, they may be
+ * referenced in the 'orderby' parameter of the parent query.
+ *
+ * @type string $relation Optional. The MySQL keyword used to join
+ * the clauses of the query. Accepts 'AND', or 'OR'. Default 'AND'.
+ * @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', or 'RLIKE'.
+ * 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 )
+ return;
+
+ if ( isset( $meta_query['relation'] ) && strtoupper( $meta_query['relation'] ) == 'OR' ) {
+ $this->relation = 'OR';
+ } else {
+ $this->relation = 'AND';
+ }
+
+ $this->queries = $this->sanitize_query( $meta_query );
+ }
+
+ /**
+ * Ensure the 'meta_query' argument passed to the class constructor is well-formed.
+ *
+ * Eliminates empty items and ensures that a 'relation' is set.
+ *
+ * @since 4.1.0
+ * @access public
+ *
+ * @param array $queries Array of query clauses.
+ * @return array Sanitized array of query clauses.
+ */
+ public function sanitize_query( $queries ) {
+ $clean_queries = array();
+
+ if ( ! is_array( $queries ) ) {
+ return $clean_queries;
+ }
+
+ foreach ( $queries as $key => $query ) {
+ if ( 'relation' === $key ) {
+ $relation = $query;
+
+ } elseif ( ! is_array( $query ) ) {
+ continue;
+
+ // First-order clause.
+ } elseif ( $this->is_first_order_clause( $query ) ) {
+ if ( isset( $query['value'] ) && array() === $query['value'] ) {
+ unset( $query['value'] );
+ }
+
+ $clean_queries[ $key ] = $query;
+
+ // Otherwise, it's a nested query, so we recurse.
+ } else {
+ $cleaned_query = $this->sanitize_query( $query );
+
+ if ( ! empty( $cleaned_query ) ) {
+ $clean_queries[ $key ] = $cleaned_query;
+ }
+ }
+ }
+
+ if ( empty( $clean_queries ) ) {
+ return $clean_queries;
+ }
+
+ // Sanitize the 'relation' key provided in the query.
+ if ( isset( $relation ) && 'OR' === strtoupper( $relation ) ) {
+ $clean_queries['relation'] = 'OR';
+ $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.
+ */
+ } elseif ( 1 === count( $clean_queries ) ) {
+ $clean_queries['relation'] = 'OR';
+
+ // Default to AND.
+ } else {
+ $clean_queries['relation'] = 'AND';
+ }
+
+ return $clean_queries;
+ }
+
+ /**
+ * Determine whether a query clause is first-order.
+ *
+ * A first-order meta query clause is one that has either a 'key' or
+ * a 'value' array key.
+ *
+ * @since 4.1.0
+ * @access protected
+ *
+ * @param array $query Meta query arguments.
+ * @return bool Whether the query clause is a first-order clause.
+ */
+ protected function is_first_order_clause( $query ) {
+ return isset( $query['key'] ) || isset( $query['value'] );
+ }
+
+ /**
+ * Constructs a meta query based on 'meta_*' query vars
+ *
+ * @since 3.2.0
+ * @access public
+ *
+ * @param array $qv The query variables
+ */
+ public function parse_query_vars( $qv ) {
+ $meta_query = array();
+
+ /*
+ * For orderby=meta_value to work correctly, simple query needs to be
+ * first (so that its table join is against an unaliased meta table) and
+ * needs to be its own clause (so it doesn't interfere with the logic of
+ * the rest of the meta_query).
+ */
+ $primary_meta_query = array();
+ foreach ( array( 'key', 'compare', 'type' ) as $key ) {
+ if ( ! empty( $qv[ "meta_$key" ] ) ) {
+ $primary_meta_query[ $key ] = $qv[ "meta_$key" ];
+ }
+ }
+
+ // WP_Query sets 'meta_value' = '' by default.
+ if ( isset( $qv['meta_value'] ) && '' !== $qv['meta_value'] && ( ! is_array( $qv['meta_value'] ) || $qv['meta_value'] ) ) {
+ $primary_meta_query['value'] = $qv['meta_value'];
+ }
+
+ $existing_meta_query = isset( $qv['meta_query'] ) && is_array( $qv['meta_query'] ) ? $qv['meta_query'] : array();
+
+ if ( ! empty( $primary_meta_query ) && ! empty( $existing_meta_query ) ) {
+ $meta_query = array(
+ 'relation' => 'AND',
+ $primary_meta_query,
+ $existing_meta_query,
+ );
+ } elseif ( ! empty( $primary_meta_query ) ) {
+ $meta_query = array(
+ $primary_meta_query,
+ );
+ } elseif ( ! empty( $existing_meta_query ) ) {
+ $meta_query = $existing_meta_query;
+ }
+
+ $this->__construct( $meta_query );
+ }