PluginProbe
Filter Everything — WordPress & WooCommerce Filters / trunk
Filter Everything — WordPress & WooCommerce Filters vtrunk
1.9.6 1.9.5 1.9.4 1.9.3 1.9.2.2 1.9.2.1 trunk 1.2.1 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 1.4.1 1.4.4 1.4.5 1.4.8 1.4.9 1.5.0 1.5.1 1.6.0 1.6.1 1.6.2 1.6.3 All 51 releases
filter-everything / src / Entities / PostMetaEntity.php

PostMetaEntity.php in Filter Everything — WordPress & WooCommerce Filters trunk, at src/Entities/PostMetaEntity.php

551 lines 18.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FilterEverything\Filter;
4
5 if ( ! defined('ABSPATH') ) {
6 exit;
7 }
8
9 class PostMetaEntity implements Entity
10 {
11 use PostMetaTrait;
12
13 public $items = [];
14 /**
15 * Declared explicitly: assigned from the outside in
16 * EntityManager::prepareEntitiesToDisplay(); dynamic properties are
17 * deprecated since PHP 8.2.
18 */
19 public $items_sort = [];
20
21 public $filter = [];
22
23 public $entityName = '';
24
25 public $excludedTerms = [];
26
27 public $isInclude = false;
28
29 private $new_meta_query = [];
30
31 /**
32 * @todo We need to set parameter PosType to select only that post meta terms, that
33 * belongs to this post type. Otherwise we will have many extra terms. !!! IMPORTANT
34 */
35 private $postTypes = [];
36
37 public function __construct( $postMetaName, $postType ){
38 $this->entityName = $postMetaName;
39
40 if( $postType ){
41 $this->setPostTypes( array( $postType ) );
42 }
43
44 $this->getAllExistingTerms();
45 }
46
47 public function setPostTypes( $postTypes )
48 {
49 $this->postTypes = $postTypes;
50
51 if( flrt_is_woocommerce()
52 && in_array( 'product', $this->postTypes )
53 && ! in_array( 'product_variation', $this->postTypes ) ){
54 $this->postTypes[] = 'product_variation';
55 }
56
57 }
58
59 public function setExcludedTerms( $excludedTerms, $isInclude )
60 {
61 $this->excludedTerms = $excludedTerms;
62 $this->isInclude = $isInclude;
63 }
64
65 public function getName()
66 {
67 return wp_unslash( $this->entityName );
68 }
69
70 public function getPostTypes()
71 {
72 return $this->postTypes;
73 }
74
75 function excludeTerms( $terms )
76 {
77 $exclude = [];
78
79 if( ! empty( $this->excludedTerms ) ){
80 $exclude = $this->excludedTerms;
81 }
82
83 $exclude_flipped = array_flip( $exclude );
84
85 if( $this->isInclude ){
86 $included_terms = [];
87 foreach( $terms as $index => $term ){
88 if( isset( $exclude_flipped[$term->slug] ) ){
89 $included_terms[$index] = $term;
90 }
91 }
92 $terms = $included_terms;
93 }else{
94 foreach( $terms as $index => $term ){
95 if( isset( $exclude_flipped[$term->slug] ) ){
96 unset( $terms[$index] );
97 }
98 }
99 }
100
101 return $terms;
102 }
103
104 function getTerms()
105 {
106 return $this->excludeTerms( $this->getAllExistingTerms() );
107 }
108
109 /**
110 * @param int $id term id
111 * @return false|object term object of false
112 */
113 public function getTerm( $slug ){
114 // To allow 0 as meta value
115 if( $slug === '' || $slug === false ){
116 return false;
117 }
118
119 foreach ( $this->getTerms() as $term ){
120 if( $slug == $term->slug ){
121 return $term;
122 }
123 }
124
125 return false;
126 }
127
128 public function getTermId( $slug )
129 {
130 /**
131 * Post meta value has no ID, so slug will be instead
132 */
133 return $slug;
134 }
135
136 /**
137 * @return array list of term_id and names useful to create Select dropdown
138 */
139 public function getTermsForSelect()
140 {
141 $toSelect = [];
142 foreach ( $this->getTerms() as $term ) {
143 $toSelect[$term->slug] = $term->name;
144 }
145
146 return $toSelect;
147 }
148
149 public function getTermsForSelect2()
150 {
151 $toSelect = [];
152 foreach ( $this->getTerms() as $term ) {
153 $toSelect[] = array( 'id' => $term->slug, 'text' =>$term->name );
154 }
155
156 return $toSelect;
157 }
158
159 function getAllExistingTerms( $force = false )
160 {
161 if( empty( $this->items ) || $force ) {
162 $this->items = $this->selectTerms();
163 }
164
165 return $this->items;
166 }
167
168 function populateTermsWithPostIds( $setId, $post_type )
169 {
170 // Correct term posts if they were selected without specified postTypes
171 foreach( $this->items as $index => $term ){
172 foreach( $term->post_types as $post_id => $term_post_type ){
173 if( ! in_array($term_post_type, $this->postTypes ) ){
174 $position = array_search( $post_id, $term->posts );
175 // To avoid unset $this->items[$index]->posts[0] when $position === false
176 if( $position !== false ){
177 unset( $this->items[$index]->posts[$position] );
178 }
179 }
180 }
181 }
182
183 /**
184 * Keep only posts that belong to the set universe (all queried posts of
185 * the set), same as TaxonomyEntity does. Without this the raw meta lists
186 * (e.g. _stock_status of ALL published products, including those excluded
187 * from the catalog) leak into wpcFilterJsonData, and the client-side
188 * recount shows inflated counters compared to the server-rendered ones.
189 *
190 * Term posts of "Used for Variations" meta keys legitimately contain
191 * variation IDs which are absent from the parent-product universe, so a
192 * post survives when it is in the universe OR in the universe expanded
193 * to variation IDs.
194 */
195 $em = Container::instance()->getEntityManager();
196 $allWpQueriedPostIds = $em->getAllSetWpQueriedPostIds( $setId );
197
198 if ( ! empty( $allWpQueriedPostIds ) ) {
199 $universe = array_flip( $allWpQueriedPostIds );
200 $universe_variations = apply_filters( 'wpc_from_products_to_variations', $universe );
201
202 foreach ( $this->items as $index => $term ) {
203 $intersected_posts = [];
204 foreach ( $term->posts as $post_id ) {
205 if ( isset( $universe[ $post_id ] ) || isset( $universe_variations[ $post_id ] ) ) {
206 $intersected_posts[] = $post_id;
207 }
208 }
209 $this->items[ $index ]->posts = $intersected_posts;
210 }
211 }
212 }
213
214 private function selectTerms(){
215 global $wpdb;
216
217 $e_name = wp_unslash( $this->entityName );
218 $transient_key = flrt_get_terms_transient_key( 'post_meta_' . $e_name );
219 $translatable_post_type_exists = false;
220
221 if ( false === ( $result = flrt_get_transient( $transient_key ) ) ) {
222
223 $sql[] = "SELECT {$wpdb->postmeta}.post_id,{$wpdb->postmeta}.meta_value,{$wpdb->posts}.post_type";
224 $sql[] = "FROM {$wpdb->postmeta}";
225 $sql[] = "LEFT JOIN {$wpdb->posts} ON ({$wpdb->postmeta}.post_id = {$wpdb->posts}.ID)";
226
227 /**
228 * @todo make it through apply_filter();
229 */
230 if ( flrt_wpml_active() && defined( 'ICL_LANGUAGE_CODE' ) ) {
231 $wpml_settings = get_option( 'icl_sitepress_settings' );
232
233 foreach ( $this->postTypes as $type ) {
234 if( isset( $wpml_settings['custom_posts_sync_option'][$type] ) ){
235 if( $wpml_settings['custom_posts_sync_option'][$type] === '1' ){
236 $translatable_post_type_exists = true;
237 break;
238 }
239 }
240 }
241
242 if ( $translatable_post_type_exists ) {
243 $sql[] = "LEFT JOIN {$wpdb->prefix}icl_translations AS wpml_translations";
244 $sql[] = "ON {$wpdb->postmeta}.post_id = wpml_translations.element_id";
245
246 if ( ! empty( $this->postTypes ) ) {
247 $sql[] = "AND wpml_translations.element_type IN(";
248 foreach ( $this->postTypes as $type ) {
249
250 if( isset( $wpml_settings['custom_posts_sync_option'][$type] ) ) {
251 if( $wpml_settings['custom_posts_sync_option'][$type] === '1' ) {
252 $LANG_IN[] = $wpdb->prepare("CONCAT('post_', '%s')", $type);
253 }
254 }
255
256 }
257 $sql[] = implode(",", $LANG_IN);
258 $sql[] = ")";
259 }
260 }
261 }
262
263 $sql[] = "WHERE {$wpdb->postmeta}.meta_key = %s";
264 $sql[] = "AND {$wpdb->postmeta}.meta_value IS NOT NULL";
265
266 /**
267 * @todo make it through apply_filter();
268 */
269 if ( flrt_wpml_active() && defined('ICL_LANGUAGE_CODE') && $translatable_post_type_exists ) {
270 $sql[] = $wpdb->prepare("AND wpml_translations.language_code = '%s'", ICL_LANGUAGE_CODE);
271 }
272
273 /**
274 * @notice It would be great to make LEFT JOIN posts where post type is post type from the filter SET
275 * But it seems we can't know post type on this stage of WP loading (in RequestParser).
276 */
277
278 /**
279 * Filters terms SQL-query and allows to modify it
280 */
281 $sql = apply_filters( 'wpc_filter_get_post_meta_terms_sql', $sql, $e_name );
282
283 $sql = implode(' ', $sql );
284
285 $sql = $wpdb->prepare( $sql, $e_name );
286
287 $result = $wpdb->get_results( $sql, ARRAY_A );
288
289 $result = $this->convertSelectResult( $result );
290
291 flrt_set_transient( $transient_key, $result, FLRT_TRANSIENT_PERIOD_HOURS * HOUR_IN_SECONDS );
292 }
293
294 return $result;
295 }
296
297 private function hasRestrictedSymbols( $str )
298 {
299 if( $str !== esc_attr( $str ) ){
300 return true;
301 }
302
303 if( preg_match( '/[#]+/', $str ) ){
304 return true;
305 }
306
307 return false;
308 }
309
310 private function convertSelectResult( $result ){
311 $return = [];
312
313 if( ! is_array( $result ) ){
314 return $return;
315 }
316 $customIndex = 1;
317
318 // To make standard format for terms array;
319 foreach ( $result as $index => $post_meta_row ){
320
321 if( is_serialized( $post_meta_row['meta_value'] ) ){
322 $data = maybe_unserialize( $post_meta_row['meta_value'] );
323 foreach ( $data as $i => $meta_value ){
324 // For multidimensional arrays stored in post meta
325 if( is_array( $meta_value ) ){
326 continue;
327 }
328 $customIndex++;
329 $slug = sanitize_title( $meta_value );
330 if( $this->hasRestrictedSymbols( $slug ) ){
331 continue;
332 }
333 $this->addNewTerm( $return, $slug, $post_meta_row, $meta_value, $customIndex );
334 }
335 }else{
336
337 $customIndex++;
338 $slug = sanitize_title( $post_meta_row['meta_value'] );
339 if( $this->hasRestrictedSymbols( $slug ) ){
340 continue;
341 }
342 $this->addNewTerm( $return, $slug, $post_meta_row, $post_meta_row['meta_value'], $customIndex );
343 }
344
345 }
346
347 return $return;
348 }
349
350 private function addNewTerm( &$return, $slug, $post_meta_row, $meta_value, $index )
351 {
352 if( isset( $return[ $slug ] ) ){
353 $return[ $slug ]->posts[] = $post_meta_row['post_id'];
354 $return[ $slug ]->count++;
355 $return[ $slug ]->post_types[$post_meta_row['post_id']] = $post_meta_row['post_type'];
356 }else{
357 $termObject = new \stdClass();
358 $termObject->slug = $slug;
359 $termObject->meta_value = $meta_value;
360 $termObject->name = apply_filters( 'wpc_filter_post_meta_term_name', $meta_value, $this->getName() );
361 $termObject->term_id = ($index + 1); // To avoid term_id = 0
362 $termObject->posts = array( $post_meta_row['post_id'] );
363 $termObject->count = 1;
364 $termObject->cross_count = 0;
365 $termObject->post_types[ $post_meta_row['post_id'] ] = $post_meta_row['post_type'];
366 $termObject->wp_queried = false;
367 $return[ $slug ] = $termObject;
368 }
369 }
370
371 private function addMetaQueryArray( $meta_query_array, $relation = false )
372 {
373 if( ! isset( $meta_query_array['key'] ) ){
374 return false;
375 }
376
377 $existing_meta_query = $this->new_meta_query;
378 foreach( $existing_meta_query as $index => $present_query ){
379
380 if( $this->hasNestedQueries( $present_query ) ){
381 foreach ( $present_query as $k => $nested_present_query ){
382 if( ! isset( $nested_present_query['key'] ) ){
383 // relation arg
384 continue;
385 }
386 if( $this->isTheSameMetaQuery( $nested_present_query, $meta_query_array ) ){
387 return false;
388 }
389 }
390 }else{
391 if( $this->isTheSameMetaQuery( $present_query, $meta_query_array ) ){
392 return false;
393 }
394 }
395
396 }
397
398 if( $relation && in_array( $relation, array( 'AND', 'OR' ) ) ){
399 $index = $this->findNestedIndexForQuery($meta_query_array);
400 $this->new_meta_query[$index][] = $meta_query_array;
401 $this->new_meta_query[$index]['relation'] = $relation;
402 }else{
403 $this->new_meta_query[] = $meta_query_array;
404 }
405
406 }
407
408 private function addMetaKeyToQuery( $wp_query ){
409 $args = [];
410
411 $args['key'] = $wp_query->get( 'meta_key' );
412 $args['value'] = $wp_query->get( 'meta_value' );
413
414 // Modified since v 1.6.5 to avoid adding meta_value IN('') condition to SQL query
415 $args['compare'] = ( $compare = $wp_query->get( 'meta_compare' ) ) ? $compare : '';
416
417 if($args['compare'] ){
418 $wp_query->set( 'meta_key', '' );
419 $wp_query->set( 'meta_value', '' );
420
421 $this->addMetaQueryArray( $args );
422 }
423 }
424
425 private function isMetaValueSerialized( $metaKey )
426 {
427 global $wpdb;
428
429 $sql = "SELECT {$wpdb->postmeta}.meta_value FROM {$wpdb->postmeta} WHERE {$wpdb->postmeta}.meta_key = %s";
430 $sql .= " AND {$wpdb->postmeta}.meta_value != ''";
431 $sql .= " LIMIT 0,1";
432 $sql = $wpdb->prepare( $sql, $metaKey );
433
434 $result = $wpdb->get_results( $sql );
435
436 if( ! empty( $result ) && isset( $result[0]->meta_value ) ){
437 return is_serialized( $result[0]->meta_value );
438 }
439
440 return false;
441 }
442
443 public function importExistingMetaQuery( $wp_query )
444 {
445 // Try to check if there is meta_key, meta_value and meta_compare
446 if( $wp_query->get('meta_key') ){
447 $this->addMetaKeyToQuery( $wp_query );
448 }
449
450 $already_existing_meta_query = $wp_query->get('meta_query');
451
452 if( is_array( $already_existing_meta_query ) ){
453 foreach( $already_existing_meta_query as $value ){
454 if( $this->hasNestedQueries( $value ) ){
455 foreach( $value as $n => $nested_meta_query ){
456 $this->addMetaQueryArray( $nested_meta_query, $value['relation'] );
457 }
458 }else{
459 $this->addMetaQueryArray( $value );
460 }
461
462 }
463 }
464 }
465
466 /**
467 * @return object WP_Query
468 */
469 public function addTermsToWpQuery( $queried_value, $wp_query ){
470
471 // Add existing Meta Query if present
472 $this->importExistingMetaQuery($wp_query);
473
474 $serialized = $this->isMetaValueSerialized( $queried_value['e_name'] );
475
476 // Serialized data stored in meta_value should be matched by regexp
477 if( $serialized ){
478 $replace_from = array( "+", ":", ".", "*", ";", "-" );
479 $replace_to = array( "\+", "\:", "\.", "\*", "\;", "\-" );
480
481 // For multiple queries we have to set correct relation
482 if( count( $queried_value['values'] ) > 1 ){
483 $relation = ( $queried_value['logic'] === 'and' ) ? 'AND' : 'OR';
484
485 foreach ( $queried_value['values'] as $slug ) {
486 $term = $this->getTerm($slug);
487 $term_value = str_replace( $replace_from, $replace_to, $term->meta_value );
488 $this->addMetaQueryArray(
489 array(
490 'key' => $queried_value['e_name'],
491 'value' => '.*;s:[0-9]+:"'.$term_value.'".*',
492 'compare' => 'REGEXP'
493 ),
494 $relation
495 );
496 }
497
498 }else{
499 // Single term selected in filter
500 foreach ( $queried_value['values'] as $slug ) {
501 $term = $this->getTerm($slug);
502 $term_value = str_replace( $replace_from, $replace_to, $term->meta_value );
503 $this->addMetaQueryArray(
504 array(
505 'key' => $queried_value['e_name'],
506 'value' => '.*;s:[0-9]+:"'.$term_value.'".*',
507 'compare' => 'REGEXP'
508 )
509 );
510 }
511 }
512
513 }else{
514 // If data stored as stings
515 if( $queried_value['logic'] === 'and' ){
516 foreach ( $queried_value['values'] as $slug ) {
517 $term = $this->getTerm($slug);
518 $this->addMetaQueryArray(
519 array(
520 'key' => $queried_value['e_name'],
521 'value' => $term->meta_value,
522 'compare' => '='
523 )
524 );
525 }
526 } elseif ( $queried_value['logic'] === 'or' ){
527 $meta_values = [];
528 foreach ( $queried_value['values'] as $slug ){
529 $term = $this->getTerm($slug);
530 $meta_values[] = $term->meta_value;
531 }
532 $this->addMetaQueryArray(
533 array(
534 'key' => $queried_value['e_name'],
535 'value' => $meta_values,
536 'compare' => 'IN'
537 )
538 );
539 }
540 }
541
542 if( count($this->new_meta_query) > 1 ){
543 $this->new_meta_query['relation'] = 'AND';
544 }
545
546 $wp_query->set('meta_query', $this->new_meta_query );
547 $this->new_meta_query = [];
548
549 return $wp_query;
550 }
551 }