Sin descripción

class-wp-meta-query.php 29KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884
  1. <?php
  2. /**
  3. * Meta API: WP_Meta_Query class
  4. *
  5. * @package WordPress
  6. * @subpackage Meta
  7. * @since 4.4.0
  8. */
  9. /**
  10. * Core class used to implement meta queries for the Meta API.
  11. *
  12. * Used for generating SQL clauses that filter a primary query according to metadata keys and values.
  13. *
  14. * WP_Meta_Query is a helper that allows primary query classes, such as WP_Query and WP_User_Query,
  15. *
  16. * to filter their results by object metadata, by generating `JOIN` and `WHERE` subclauses to be attached
  17. * to the primary SQL query string.
  18. *
  19. * @since 3.2.0
  20. */
  21. class WP_Meta_Query {
  22. /**
  23. * Array of metadata queries.
  24. *
  25. * See WP_Meta_Query::__construct() for information on meta query arguments.
  26. *
  27. * @since 3.2.0
  28. * @var array
  29. */
  30. public $queries = array();
  31. /**
  32. * The relation between the queries. Can be one of 'AND' or 'OR'.
  33. *
  34. * @since 3.2.0
  35. * @var string
  36. */
  37. public $relation;
  38. /**
  39. * Database table to query for the metadata.
  40. *
  41. * @since 4.1.0
  42. * @var string
  43. */
  44. public $meta_table;
  45. /**
  46. * Column in meta_table that represents the ID of the object the metadata belongs to.
  47. *
  48. * @since 4.1.0
  49. * @var string
  50. */
  51. public $meta_id_column;
  52. /**
  53. * Database table that where the metadata's objects are stored (eg $wpdb->users).
  54. *
  55. * @since 4.1.0
  56. * @var string
  57. */
  58. public $primary_table;
  59. /**
  60. * Column in primary_table that represents the ID of the object.
  61. *
  62. * @since 4.1.0
  63. * @var string
  64. */
  65. public $primary_id_column;
  66. /**
  67. * A flat list of table aliases used in JOIN clauses.
  68. *
  69. * @since 4.1.0
  70. * @var array
  71. */
  72. protected $table_aliases = array();
  73. /**
  74. * A flat list of clauses, keyed by clause 'name'.
  75. *
  76. * @since 4.2.0
  77. * @var array
  78. */
  79. protected $clauses = array();
  80. /**
  81. * Whether the query contains any OR relations.
  82. *
  83. * @since 4.3.0
  84. * @var bool
  85. */
  86. protected $has_or_relation = false;
  87. /**
  88. * Constructor.
  89. *
  90. * @since 3.2.0
  91. * @since 4.2.0 Introduced support for naming query clauses by associative array keys.
  92. * @since 5.1.0 Introduced `$compare_key` clause parameter, which enables LIKE key matches.
  93. * @since 5.3.0 Increased the number of operators available to `$compare_key`. Introduced `$type_key`,
  94. * which enables the `$key` to be cast to a new data type for comparisons.
  95. *
  96. * @param array $meta_query {
  97. * Array of meta query clauses. When first-order clauses or sub-clauses use strings as
  98. * their array keys, they may be referenced in the 'orderby' parameter of the parent query.
  99. *
  100. * @type string $relation Optional. The MySQL keyword used to join the clauses of the query.
  101. * Accepts 'AND' or 'OR'. Default 'AND'.
  102. * @type array ...$0 {
  103. * Optional. An array of first-order clause parameters, or another fully-formed meta query.
  104. *
  105. * @type string|string[] $key Meta key or keys to filter by.
  106. * @type string $compare_key MySQL operator used for comparing the $key. Accepts:
  107. * - '='
  108. * - '!='
  109. * - 'LIKE'
  110. * - 'NOT LIKE'
  111. * - 'IN'
  112. * - 'NOT IN'
  113. * - 'REGEXP'
  114. * - 'NOT REGEXP'
  115. * - 'RLIKE',
  116. * - 'EXISTS' (alias of '=')
  117. * - 'NOT EXISTS' (alias of '!=')
  118. * Default is 'IN' when `$key` is an array, '=' otherwise.
  119. * @type string $type_key MySQL data type that the meta_key column will be CAST to for
  120. * comparisons. Accepts 'BINARY' for case-sensitive regular expression
  121. * comparisons. Default is ''.
  122. * @type string|string[] $value Meta value or values to filter by.
  123. * @type string $compare MySQL operator used for comparing the $value. Accepts:
  124. * - '=',
  125. * - '!='
  126. * - '>'
  127. * - '>='
  128. * - '<'
  129. * - '<='
  130. * - 'LIKE'
  131. * - 'NOT LIKE'
  132. * - 'IN'
  133. * - 'NOT IN'
  134. * - 'BETWEEN'
  135. * - 'NOT BETWEEN'
  136. * - 'REGEXP'
  137. * - 'NOT REGEXP'
  138. * - 'RLIKE'
  139. * - 'EXISTS'
  140. * - 'NOT EXISTS'
  141. * Default is 'IN' when `$value` is an array, '=' otherwise.
  142. * @type string $type MySQL data type that the meta_value column will be CAST to for
  143. * comparisons. Accepts:
  144. * - 'NUMERIC'
  145. * - 'BINARY'
  146. * - 'CHAR'
  147. * - 'DATE'
  148. * - 'DATETIME'
  149. * - 'DECIMAL'
  150. * - 'SIGNED'
  151. * - 'TIME'
  152. * - 'UNSIGNED'
  153. * Default is 'CHAR'.
  154. * }
  155. * }
  156. */
  157. public function __construct( $meta_query = false ) {
  158. if ( ! $meta_query ) {
  159. return;
  160. }
  161. if ( isset( $meta_query['relation'] ) && 'OR' === strtoupper( $meta_query['relation'] ) ) {
  162. $this->relation = 'OR';
  163. } else {
  164. $this->relation = 'AND';
  165. }
  166. $this->queries = $this->sanitize_query( $meta_query );
  167. }
  168. /**
  169. * Ensure the 'meta_query' argument passed to the class constructor is well-formed.
  170. *
  171. * Eliminates empty items and ensures that a 'relation' is set.
  172. *
  173. * @since 4.1.0
  174. *
  175. * @param array $queries Array of query clauses.
  176. * @return array Sanitized array of query clauses.
  177. */
  178. public function sanitize_query( $queries ) {
  179. $clean_queries = array();
  180. if ( ! is_array( $queries ) ) {
  181. return $clean_queries;
  182. }
  183. foreach ( $queries as $key => $query ) {
  184. if ( 'relation' === $key ) {
  185. $relation = $query;
  186. } elseif ( ! is_array( $query ) ) {
  187. continue;
  188. // First-order clause.
  189. } elseif ( $this->is_first_order_clause( $query ) ) {
  190. if ( isset( $query['value'] ) && array() === $query['value'] ) {
  191. unset( $query['value'] );
  192. }
  193. $clean_queries[ $key ] = $query;
  194. // Otherwise, it's a nested query, so we recurse.
  195. } else {
  196. $cleaned_query = $this->sanitize_query( $query );
  197. if ( ! empty( $cleaned_query ) ) {
  198. $clean_queries[ $key ] = $cleaned_query;
  199. }
  200. }
  201. }
  202. if ( empty( $clean_queries ) ) {
  203. return $clean_queries;
  204. }
  205. // Sanitize the 'relation' key provided in the query.
  206. if ( isset( $relation ) && 'OR' === strtoupper( $relation ) ) {
  207. $clean_queries['relation'] = 'OR';
  208. $this->has_or_relation = true;
  209. /*
  210. * If there is only a single clause, call the relation 'OR'.
  211. * This value will not actually be used to join clauses, but it
  212. * simplifies the logic around combining key-only queries.
  213. */
  214. } elseif ( 1 === count( $clean_queries ) ) {
  215. $clean_queries['relation'] = 'OR';
  216. // Default to AND.
  217. } else {
  218. $clean_queries['relation'] = 'AND';
  219. }
  220. return $clean_queries;
  221. }
  222. /**
  223. * Determine whether a query clause is first-order.
  224. *
  225. * A first-order meta query clause is one that has either a 'key' or
  226. * a 'value' array key.
  227. *
  228. * @since 4.1.0
  229. *
  230. * @param array $query Meta query arguments.
  231. * @return bool Whether the query clause is a first-order clause.
  232. */
  233. protected function is_first_order_clause( $query ) {
  234. return isset( $query['key'] ) || isset( $query['value'] );
  235. }
  236. /**
  237. * Constructs a meta query based on 'meta_*' query vars
  238. *
  239. * @since 3.2.0
  240. *
  241. * @param array $qv The query variables
  242. */
  243. public function parse_query_vars( $qv ) {
  244. $meta_query = array();
  245. /*
  246. * For orderby=meta_value to work correctly, simple query needs to be
  247. * first (so that its table join is against an unaliased meta table) and
  248. * needs to be its own clause (so it doesn't interfere with the logic of
  249. * the rest of the meta_query).
  250. */
  251. $primary_meta_query = array();
  252. foreach ( array( 'key', 'compare', 'type', 'compare_key', 'type_key' ) as $key ) {
  253. if ( ! empty( $qv[ "meta_$key" ] ) ) {
  254. $primary_meta_query[ $key ] = $qv[ "meta_$key" ];
  255. }
  256. }
  257. // WP_Query sets 'meta_value' = '' by default.
  258. if ( isset( $qv['meta_value'] ) && '' !== $qv['meta_value'] && ( ! is_array( $qv['meta_value'] ) || $qv['meta_value'] ) ) {
  259. $primary_meta_query['value'] = $qv['meta_value'];
  260. }
  261. $existing_meta_query = isset( $qv['meta_query'] ) && is_array( $qv['meta_query'] ) ? $qv['meta_query'] : array();
  262. if ( ! empty( $primary_meta_query ) && ! empty( $existing_meta_query ) ) {
  263. $meta_query = array(
  264. 'relation' => 'AND',
  265. $primary_meta_query,
  266. $existing_meta_query,
  267. );
  268. } elseif ( ! empty( $primary_meta_query ) ) {
  269. $meta_query = array(
  270. $primary_meta_query,
  271. );
  272. } elseif ( ! empty( $existing_meta_query ) ) {
  273. $meta_query = $existing_meta_query;
  274. }
  275. $this->__construct( $meta_query );
  276. }
  277. /**
  278. * Return the appropriate alias for the given meta type if applicable.
  279. *
  280. * @since 3.7.0
  281. *
  282. * @param string $type MySQL type to cast meta_value.
  283. * @return string MySQL type.
  284. */
  285. public function get_cast_for_type( $type = '' ) {
  286. if ( empty( $type ) ) {
  287. return 'CHAR';
  288. }
  289. $meta_type = strtoupper( $type );
  290. if ( ! preg_match( '/^(?:BINARY|CHAR|DATE|DATETIME|SIGNED|UNSIGNED|TIME|NUMERIC(?:\(\d+(?:,\s?\d+)?\))?|DECIMAL(?:\(\d+(?:,\s?\d+)?\))?)$/', $meta_type ) ) {
  291. return 'CHAR';
  292. }
  293. if ( 'NUMERIC' === $meta_type ) {
  294. $meta_type = 'SIGNED';
  295. }
  296. return $meta_type;
  297. }
  298. /**
  299. * Generates SQL clauses to be appended to a main query.
  300. *
  301. * @since 3.2.0
  302. *
  303. * @param string $type Type of meta. Possible values include but are not limited
  304. * to 'post', 'comment', 'blog', 'term', and 'user'.
  305. * @param string $primary_table Database table where the object being filtered is stored (eg wp_users).
  306. * @param string $primary_id_column ID column for the filtered object in $primary_table.
  307. * @param object $context Optional. The main query object that corresponds to the type, for
  308. * example a `WP_Query`, `WP_User_Query`, or `WP_Site_Query`.
  309. * @return string[]|false {
  310. * Array containing JOIN and WHERE SQL clauses to append to the main query,
  311. * or false if no table exists for the requested meta type.
  312. *
  313. * @type string $join SQL fragment to append to the main JOIN clause.
  314. * @type string $where SQL fragment to append to the main WHERE clause.
  315. * }
  316. */
  317. public function get_sql( $type, $primary_table, $primary_id_column, $context = null ) {
  318. $meta_table = _get_meta_table( $type );
  319. if ( ! $meta_table ) {
  320. return false;
  321. }
  322. $this->table_aliases = array();
  323. $this->meta_table = $meta_table;
  324. $this->meta_id_column = sanitize_key( $type . '_id' );
  325. $this->primary_table = $primary_table;
  326. $this->primary_id_column = $primary_id_column;
  327. $sql = $this->get_sql_clauses();
  328. /*
  329. * If any JOINs are LEFT JOINs (as in the case of NOT EXISTS), then all JOINs should
  330. * be LEFT. Otherwise posts with no metadata will be excluded from results.
  331. */
  332. if ( false !== strpos( $sql['join'], 'LEFT JOIN' ) ) {
  333. $sql['join'] = str_replace( 'INNER JOIN', 'LEFT JOIN', $sql['join'] );
  334. }
  335. /**
  336. * Filters the meta query's generated SQL.
  337. *
  338. * @since 3.1.0
  339. *
  340. * @param string[] $sql Array containing the query's JOIN and WHERE clauses.
  341. * @param array $queries Array of meta queries.
  342. * @param string $type Type of meta. Possible values include but are not limited
  343. * to 'post', 'comment', 'blog', 'term', and 'user'.
  344. * @param string $primary_table Primary table.
  345. * @param string $primary_id_column Primary column ID.
  346. * @param object $context The main query object that corresponds to the type, for
  347. * example a `WP_Query`, `WP_User_Query`, or `WP_Site_Query`.
  348. */
  349. return apply_filters_ref_array( 'get_meta_sql', array( $sql, $this->queries, $type, $primary_table, $primary_id_column, $context ) );
  350. }
  351. /**
  352. * Generate SQL clauses to be appended to a main query.
  353. *
  354. * Called by the public WP_Meta_Query::get_sql(), this method is abstracted
  355. * out to maintain parity with the other Query classes.
  356. *
  357. * @since 4.1.0
  358. *
  359. * @return string[] {
  360. * Array containing JOIN and WHERE SQL clauses to append to the main query.
  361. *
  362. * @type string $join SQL fragment to append to the main JOIN clause.
  363. * @type string $where SQL fragment to append to the main WHERE clause.
  364. * }
  365. */
  366. protected function get_sql_clauses() {
  367. /*
  368. * $queries are passed by reference to get_sql_for_query() for recursion.
  369. * To keep $this->queries unaltered, pass a copy.
  370. */
  371. $queries = $this->queries;
  372. $sql = $this->get_sql_for_query( $queries );
  373. if ( ! empty( $sql['where'] ) ) {
  374. $sql['where'] = ' AND ' . $sql['where'];
  375. }
  376. return $sql;
  377. }
  378. /**
  379. * Generate SQL clauses for a single query array.
  380. *
  381. * If nested subqueries are found, this method recurses the tree to
  382. * produce the properly nested SQL.
  383. *
  384. * @since 4.1.0
  385. *
  386. * @param array $query Query to parse (passed by reference).
  387. * @param int $depth Optional. Number of tree levels deep we currently are.
  388. * Used to calculate indentation. Default 0.
  389. * @return string[] {
  390. * Array containing JOIN and WHERE SQL clauses to append to a single query array.
  391. *
  392. * @type string $join SQL fragment to append to the main JOIN clause.
  393. * @type string $where SQL fragment to append to the main WHERE clause.
  394. * }
  395. */
  396. protected function get_sql_for_query( &$query, $depth = 0 ) {
  397. $sql_chunks = array(
  398. 'join' => array(),
  399. 'where' => array(),
  400. );
  401. $sql = array(
  402. 'join' => '',
  403. 'where' => '',
  404. );
  405. $indent = '';
  406. for ( $i = 0; $i < $depth; $i++ ) {
  407. $indent .= ' ';
  408. }
  409. foreach ( $query as $key => &$clause ) {
  410. if ( 'relation' === $key ) {
  411. $relation = $query['relation'];
  412. } elseif ( is_array( $clause ) ) {
  413. // This is a first-order clause.
  414. if ( $this->is_first_order_clause( $clause ) ) {
  415. $clause_sql = $this->get_sql_for_clause( $clause, $query, $key );
  416. $where_count = count( $clause_sql['where'] );
  417. if ( ! $where_count ) {
  418. $sql_chunks['where'][] = '';
  419. } elseif ( 1 === $where_count ) {
  420. $sql_chunks['where'][] = $clause_sql['where'][0];
  421. } else {
  422. $sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )';
  423. }
  424. $sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] );
  425. // This is a subquery, so we recurse.
  426. } else {
  427. $clause_sql = $this->get_sql_for_query( $clause, $depth + 1 );
  428. $sql_chunks['where'][] = $clause_sql['where'];
  429. $sql_chunks['join'][] = $clause_sql['join'];
  430. }
  431. }
  432. }
  433. // Filter to remove empties.
  434. $sql_chunks['join'] = array_filter( $sql_chunks['join'] );
  435. $sql_chunks['where'] = array_filter( $sql_chunks['where'] );
  436. if ( empty( $relation ) ) {
  437. $relation = 'AND';
  438. }
  439. // Filter duplicate JOIN clauses and combine into a single string.
  440. if ( ! empty( $sql_chunks['join'] ) ) {
  441. $sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) );
  442. }
  443. // Generate a single WHERE clause with proper brackets and indentation.
  444. if ( ! empty( $sql_chunks['where'] ) ) {
  445. $sql['where'] = '( ' . "\n " . $indent . implode( ' ' . "\n " . $indent . $relation . ' ' . "\n " . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')';
  446. }
  447. return $sql;
  448. }
  449. /**
  450. * Generate SQL JOIN and WHERE clauses for a first-order query clause.
  451. *
  452. * "First-order" means that it's an array with a 'key' or 'value'.
  453. *
  454. * @since 4.1.0
  455. *
  456. * @global wpdb $wpdb WordPress database abstraction object.
  457. *
  458. * @param array $clause Query clause (passed by reference).
  459. * @param array $parent_query Parent query array.
  460. * @param string $clause_key Optional. The array key used to name the clause in the original `$meta_query`
  461. * parameters. If not provided, a key will be generated automatically.
  462. * @return string[] {
  463. * Array containing JOIN and WHERE SQL clauses to append to a first-order query.
  464. *
  465. * @type string $join SQL fragment to append to the main JOIN clause.
  466. * @type string $where SQL fragment to append to the main WHERE clause.
  467. * }
  468. */
  469. public function get_sql_for_clause( &$clause, $parent_query, $clause_key = '' ) {
  470. global $wpdb;
  471. $sql_chunks = array(
  472. 'where' => array(),
  473. 'join' => array(),
  474. );
  475. if ( isset( $clause['compare'] ) ) {
  476. $clause['compare'] = strtoupper( $clause['compare'] );
  477. } else {
  478. $clause['compare'] = isset( $clause['value'] ) && is_array( $clause['value'] ) ? 'IN' : '=';
  479. }
  480. $non_numeric_operators = array(
  481. '=',
  482. '!=',
  483. 'LIKE',
  484. 'NOT LIKE',
  485. 'IN',
  486. 'NOT IN',
  487. 'EXISTS',
  488. 'NOT EXISTS',
  489. 'RLIKE',
  490. 'REGEXP',
  491. 'NOT REGEXP',
  492. );
  493. $numeric_operators = array(
  494. '>',
  495. '>=',
  496. '<',
  497. '<=',
  498. 'BETWEEN',
  499. 'NOT BETWEEN',
  500. );
  501. if ( ! in_array( $clause['compare'], $non_numeric_operators, true ) && ! in_array( $clause['compare'], $numeric_operators, true ) ) {
  502. $clause['compare'] = '=';
  503. }
  504. if ( isset( $clause['compare_key'] ) ) {
  505. $clause['compare_key'] = strtoupper( $clause['compare_key'] );
  506. } else {
  507. $clause['compare_key'] = isset( $clause['key'] ) && is_array( $clause['key'] ) ? 'IN' : '=';
  508. }
  509. if ( ! in_array( $clause['compare_key'], $non_numeric_operators, true ) ) {
  510. $clause['compare_key'] = '=';
  511. }
  512. $meta_compare = $clause['compare'];
  513. $meta_compare_key = $clause['compare_key'];
  514. // First build the JOIN clause, if one is required.
  515. $join = '';
  516. // We prefer to avoid joins if possible. Look for an existing join compatible with this clause.
  517. $alias = $this->find_compatible_table_alias( $clause, $parent_query );
  518. if ( false === $alias ) {
  519. $i = count( $this->table_aliases );
  520. $alias = $i ? 'mt' . $i : $this->meta_table;
  521. // JOIN clauses for NOT EXISTS have their own syntax.
  522. if ( 'NOT EXISTS' === $meta_compare ) {
  523. $join .= " LEFT JOIN $this->meta_table";
  524. $join .= $i ? " AS $alias" : '';
  525. if ( 'LIKE' === $meta_compare_key ) {
  526. $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'] ) . '%' );
  527. } else {
  528. $join .= $wpdb->prepare( " ON ( $this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column AND $alias.meta_key = %s )", $clause['key'] );
  529. }
  530. // All other JOIN clauses.
  531. } else {
  532. $join .= " INNER JOIN $this->meta_table";
  533. $join .= $i ? " AS $alias" : '';
  534. $join .= " ON ( $this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column )";
  535. }
  536. $this->table_aliases[] = $alias;
  537. $sql_chunks['join'][] = $join;
  538. }
  539. // Save the alias to this clause, for future siblings to find.
  540. $clause['alias'] = $alias;
  541. // Determine the data type.
  542. $_meta_type = isset( $clause['type'] ) ? $clause['type'] : '';
  543. $meta_type = $this->get_cast_for_type( $_meta_type );
  544. $clause['cast'] = $meta_type;
  545. // Fallback for clause keys is the table alias. Key must be a string.
  546. if ( is_int( $clause_key ) || ! $clause_key ) {
  547. $clause_key = $clause['alias'];
  548. }
  549. // Ensure unique clause keys, so none are overwritten.
  550. $iterator = 1;
  551. $clause_key_base = $clause_key;
  552. while ( isset( $this->clauses[ $clause_key ] ) ) {
  553. $clause_key = $clause_key_base . '-' . $iterator;
  554. $iterator++;
  555. }
  556. // Store the clause in our flat array.
  557. $this->clauses[ $clause_key ] =& $clause;
  558. // Next, build the WHERE clause.
  559. // meta_key.
  560. if ( array_key_exists( 'key', $clause ) ) {
  561. if ( 'NOT EXISTS' === $meta_compare ) {
  562. $sql_chunks['where'][] = $alias . '.' . $this->meta_id_column . ' IS NULL';
  563. } else {
  564. /**
  565. * In joined clauses negative operators have to be nested into a
  566. * NOT EXISTS clause and flipped, to avoid returning records with
  567. * matching post IDs but different meta keys. Here we prepare the
  568. * nested clause.
  569. */
  570. if ( in_array( $meta_compare_key, array( '!=', 'NOT IN', 'NOT LIKE', 'NOT EXISTS', 'NOT REGEXP' ), true ) ) {
  571. // Negative clauses may be reused.
  572. $i = count( $this->table_aliases );
  573. $subquery_alias = $i ? 'mt' . $i : $this->meta_table;
  574. $this->table_aliases[] = $subquery_alias;
  575. $meta_compare_string_start = 'NOT EXISTS (';
  576. $meta_compare_string_start .= "SELECT 1 FROM $wpdb->postmeta $subquery_alias ";
  577. $meta_compare_string_start .= "WHERE $subquery_alias.post_ID = $alias.post_ID ";
  578. $meta_compare_string_end = 'LIMIT 1';
  579. $meta_compare_string_end .= ')';
  580. }
  581. switch ( $meta_compare_key ) {
  582. case '=':
  583. case 'EXISTS':
  584. $where = $wpdb->prepare( "$alias.meta_key = %s", trim( $clause['key'] ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
  585. break;
  586. case 'LIKE':
  587. $meta_compare_value = '%' . $wpdb->esc_like( trim( $clause['key'] ) ) . '%';
  588. $where = $wpdb->prepare( "$alias.meta_key LIKE %s", $meta_compare_value ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
  589. break;
  590. case 'IN':
  591. $meta_compare_string = "$alias.meta_key IN (" . substr( str_repeat( ',%s', count( $clause['key'] ) ), 1 ) . ')';
  592. $where = $wpdb->prepare( $meta_compare_string, $clause['key'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
  593. break;
  594. case 'RLIKE':
  595. case 'REGEXP':
  596. $operator = $meta_compare_key;
  597. if ( isset( $clause['type_key'] ) && 'BINARY' === strtoupper( $clause['type_key'] ) ) {
  598. $cast = 'BINARY';
  599. } else {
  600. $cast = '';
  601. }
  602. $where = $wpdb->prepare( "$alias.meta_key $operator $cast %s", trim( $clause['key'] ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
  603. break;
  604. case '!=':
  605. case 'NOT EXISTS':
  606. $meta_compare_string = $meta_compare_string_start . "AND $subquery_alias.meta_key = %s " . $meta_compare_string_end;
  607. $where = $wpdb->prepare( $meta_compare_string, $clause['key'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
  608. break;
  609. case 'NOT LIKE':
  610. $meta_compare_string = $meta_compare_string_start . "AND $subquery_alias.meta_key LIKE %s " . $meta_compare_string_end;
  611. $meta_compare_value = '%' . $wpdb->esc_like( trim( $clause['key'] ) ) . '%';
  612. $where = $wpdb->prepare( $meta_compare_string, $meta_compare_value ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
  613. break;
  614. case 'NOT IN':
  615. $array_subclause = '(' . substr( str_repeat( ',%s', count( $clause['key'] ) ), 1 ) . ') ';
  616. $meta_compare_string = $meta_compare_string_start . "AND $subquery_alias.meta_key IN " . $array_subclause . $meta_compare_string_end;
  617. $where = $wpdb->prepare( $meta_compare_string, $clause['key'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
  618. break;
  619. case 'NOT REGEXP':
  620. $operator = $meta_compare_key;
  621. if ( isset( $clause['type_key'] ) && 'BINARY' === strtoupper( $clause['type_key'] ) ) {
  622. $cast = 'BINARY';
  623. } else {
  624. $cast = '';
  625. }
  626. $meta_compare_string = $meta_compare_string_start . "AND $subquery_alias.meta_key REGEXP $cast %s " . $meta_compare_string_end;
  627. $where = $wpdb->prepare( $meta_compare_string, $clause['key'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
  628. break;
  629. }
  630. $sql_chunks['where'][] = $where;
  631. }
  632. }
  633. // meta_value.
  634. if ( array_key_exists( 'value', $clause ) ) {
  635. $meta_value = $clause['value'];
  636. if ( in_array( $meta_compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ), true ) ) {
  637. if ( ! is_array( $meta_value ) ) {
  638. $meta_value = preg_split( '/[,\s]+/', $meta_value );
  639. }
  640. } elseif ( is_string( $meta_value ) ) {
  641. $meta_value = trim( $meta_value );
  642. }
  643. switch ( $meta_compare ) {
  644. case 'IN':
  645. case 'NOT IN':
  646. $meta_compare_string = '(' . substr( str_repeat( ',%s', count( $meta_value ) ), 1 ) . ')';
  647. $where = $wpdb->prepare( $meta_compare_string, $meta_value );
  648. break;
  649. case 'BETWEEN':
  650. case 'NOT BETWEEN':
  651. $where = $wpdb->prepare( '%s AND %s', $meta_value[0], $meta_value[1] );
  652. break;
  653. case 'LIKE':
  654. case 'NOT LIKE':
  655. $meta_value = '%' . $wpdb->esc_like( $meta_value ) . '%';
  656. $where = $wpdb->prepare( '%s', $meta_value );
  657. break;
  658. // EXISTS with a value is interpreted as '='.
  659. case 'EXISTS':
  660. $meta_compare = '=';
  661. $where = $wpdb->prepare( '%s', $meta_value );
  662. break;
  663. // 'value' is ignored for NOT EXISTS.
  664. case 'NOT EXISTS':
  665. $where = '';
  666. break;
  667. default:
  668. $where = $wpdb->prepare( '%s', $meta_value );
  669. break;
  670. }
  671. if ( $where ) {
  672. if ( 'CHAR' === $meta_type ) {
  673. $sql_chunks['where'][] = "$alias.meta_value {$meta_compare} {$where}";
  674. } else {
  675. $sql_chunks['where'][] = "CAST($alias.meta_value AS {$meta_type}) {$meta_compare} {$where}";
  676. }
  677. }
  678. }
  679. /*
  680. * Multiple WHERE clauses (for meta_key and meta_value) should
  681. * be joined in parentheses.
  682. */
  683. if ( 1 < count( $sql_chunks['where'] ) ) {
  684. $sql_chunks['where'] = array( '( ' . implode( ' AND ', $sql_chunks['where'] ) . ' )' );
  685. }
  686. return $sql_chunks;
  687. }
  688. /**
  689. * Get a flattened list of sanitized meta clauses.
  690. *
  691. * This array should be used for clause lookup, as when the table alias and CAST type must be determined for
  692. * a value of 'orderby' corresponding to a meta clause.
  693. *
  694. * @since 4.2.0
  695. *
  696. * @return array Meta clauses.
  697. */
  698. public function get_clauses() {
  699. return $this->clauses;
  700. }
  701. /**
  702. * Identify an existing table alias that is compatible with the current
  703. * query clause.
  704. *
  705. * We avoid unnecessary table joins by allowing each clause to look for
  706. * an existing table alias that is compatible with the query that it
  707. * needs to perform.
  708. *
  709. * An existing alias is compatible if (a) it is a sibling of `$clause`
  710. * (ie, it's under the scope of the same relation), and (b) the combination
  711. * of operator and relation between the clauses allows for a shared table join.
  712. * In the case of WP_Meta_Query, this only applies to 'IN' clauses that are
  713. * connected by the relation 'OR'.
  714. *
  715. * @since 4.1.0
  716. *
  717. * @param array $clause Query clause.
  718. * @param array $parent_query Parent query of $clause.
  719. * @return string|false Table alias if found, otherwise false.
  720. */
  721. protected function find_compatible_table_alias( $clause, $parent_query ) {
  722. $alias = false;
  723. foreach ( $parent_query as $sibling ) {
  724. // If the sibling has no alias yet, there's nothing to check.
  725. if ( empty( $sibling['alias'] ) ) {
  726. continue;
  727. }
  728. // We're only interested in siblings that are first-order clauses.
  729. if ( ! is_array( $sibling ) || ! $this->is_first_order_clause( $sibling ) ) {
  730. continue;
  731. }
  732. $compatible_compares = array();
  733. // Clauses connected by OR can share joins as long as they have "positive" operators.
  734. if ( 'OR' === $parent_query['relation'] ) {
  735. $compatible_compares = array( '=', 'IN', 'BETWEEN', 'LIKE', 'REGEXP', 'RLIKE', '>', '>=', '<', '<=' );
  736. // Clauses joined by AND with "negative" operators share a join only if they also share a key.
  737. } elseif ( isset( $sibling['key'] ) && isset( $clause['key'] ) && $sibling['key'] === $clause['key'] ) {
  738. $compatible_compares = array( '!=', 'NOT IN', 'NOT LIKE' );
  739. }
  740. $clause_compare = strtoupper( $clause['compare'] );
  741. $sibling_compare = strtoupper( $sibling['compare'] );
  742. if ( in_array( $clause_compare, $compatible_compares, true ) && in_array( $sibling_compare, $compatible_compares, true ) ) {
  743. $alias = preg_replace( '/\W/', '_', $sibling['alias'] );
  744. break;
  745. }
  746. }
  747. /**
  748. * Filters the table alias identified as compatible with the current clause.
  749. *
  750. * @since 4.1.0
  751. *
  752. * @param string|false $alias Table alias, or false if none was found.
  753. * @param array $clause First-order query clause.
  754. * @param array $parent_query Parent of $clause.
  755. * @param WP_Meta_Query $query WP_Meta_Query object.
  756. */
  757. return apply_filters( 'meta_query_find_compatible_table_alias', $alias, $clause, $parent_query, $this );
  758. }
  759. /**
  760. * Checks whether the current query has any OR relations.
  761. *
  762. * In some cases, the presence of an OR relation somewhere in the query will require
  763. * the use of a `DISTINCT` or `GROUP BY` keyword in the `SELECT` clause. The current
  764. * method can be used in these cases to determine whether such a clause is necessary.
  765. *
  766. * @since 4.3.0
  767. *
  768. * @return bool True if the query contains any `OR` relations, otherwise false.
  769. */
  770. public function has_or_relation() {
  771. return $this->has_or_relation;
  772. }
  773. }