class-wp-meta-query.php 30 KB

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