PluginProbe
Filter Everything — WordPress & WooCommerce Filters / 1.9.2.1
Filter Everything — WordPress & WooCommerce Filters v1.9.2.1
1.9.7 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 All 52 releases
filter-everything / src / Entities / PostMetaDateEntity.php

PostMetaDateEntity.php in Filter Everything — WordPress & WooCommerce Filters 1.9.2.1, at src/Entities/PostMetaDateEntity.php

541 lines 18.5 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 use DateTime;
10 use DateTimeImmutable;
11 use DateTimeZone;
12
13 class PostMetaDateEntity extends PostDateEntity
14 {
15 use PostMetaTrait;
16
17 private $new_meta_query = [];
18
19 private $date_format;
20
21 private $meta_date_format;
22
23 private $meta_date_type;
24
25 private $doRecalculate = '';
26
27 private $post_and_types = [];
28
29 public function __construct($name, $postType)
30 {
31
32 /**
33 * @feature clean code from unused methods
34 */
35 // This object is being created two times
36 // One time without post type when RequestParser select all filter terms
37 // Second time with post type when Filters widget requires filter terms to display them
38 $this->entityName = $name;
39 $this->setPostTypes(array($postType));
40 $this->setFromAndTo();
41 if (empty($this->meta_date_type)) {
42 $this->meta_date_type = $this->detectMetaTypeForKey($this->entityName, $postType);
43 }
44 $this->getAllExistingTerms();
45 }
46
47 protected function queryTerms($alreadyFilteredPosts = [])
48 {
49 global $wpdb;
50 $IN = false;
51 $translatable_post_type_exists = false;
52 $lang = '';
53 $key_in = '';
54
55 /**
56 * Check if any post type is translatable
57 */
58
59 if (flrt_wpml_active() && defined('ICL_LANGUAGE_CODE')) {
60 $lang = ICL_LANGUAGE_CODE;
61 $wpml_settings = get_option('icl_sitepress_settings');
62
63 foreach ($this->postTypes as $type) {
64 if (isset($wpml_settings['custom_posts_sync_option'][$type])) {
65 if ($wpml_settings['custom_posts_sync_option'][$type] === '1') {
66 $translatable_post_type_exists = true;
67 break;
68 }
69 }
70 }
71 }
72
73 /**
74 * Set Post types
75 */
76 if (!empty($this->postTypes) && isset($this->postTypes[0]) && $this->postTypes[0]) {
77 foreach ($this->postTypes as $postType) {
78 $key_in .= '_' . $postType;
79 $pieces[] = $wpdb->prepare("%s", $postType);
80 }
81
82 $IN = implode(", ", $pieces);
83 }
84
85 /**
86 * Set transient key
87 */
88 $e_name = wp_unslash($this->entityName);
89 $transient_key = flrt_get_terms_transient_key( 'post_meta_date_'. $this->getName() . $key_in );
90
91 if (false === ($result = flrt_get_transient($transient_key))) {
92 // Get all post meta values
93
94 $sql[] = "SELECT {$wpdb->posts}.ID,{$wpdb->postmeta}.meta_value as post_date, {$wpdb->posts}.post_type";
95 $sql[] = "FROM {$wpdb->posts}";
96 $sql[] = "LEFT JOIN {$wpdb->postmeta} ON ({$wpdb->postmeta}.post_id = {$wpdb->posts}.ID)";
97
98 /**
99 * If post type is translatable with WPML, get post meta values only with current language
100 */
101 if (flrt_wpml_active() && $lang && $translatable_post_type_exists) {
102 $sql[] = "LEFT JOIN {$wpdb->prefix}icl_translations AS wpml_translations";
103 $sql[] = "ON {$wpdb->posts}.ID = wpml_translations.element_id";
104
105 if (!empty($this->postTypes)) {
106
107 $sql[] = "AND wpml_translations.element_type IN(";
108
109 foreach ($this->postTypes as $type) {
110 $LANG_IN[] = $wpdb->prepare("CONCAT('post_', '%s')", $type);
111 }
112 $sql[] = implode(",", $LANG_IN);
113
114 $sql[] = ")";
115 }
116 }
117
118 $e_name = wp_unslash( $this->entityName );
119 $sql[] = $wpdb->prepare("WHERE {$wpdb->postmeta}.meta_key = %s", $e_name);
120 $sql[] = "AND {$wpdb->postmeta}.meta_value IS NOT NULL";
121 $sql[] = "AND {$wpdb->postmeta}.meta_value <> ''";
122
123 if (!empty($alreadyFilteredPosts)) {
124 //$sql[] = "AND {$wpdb->postmeta}.post_id IN( ".implode(",",$alreadyFilteredPosts).")";
125 }
126
127 if ($IN) {
128 $sql[] = "AND {$wpdb->posts}.post_type IN( {$IN} )";
129 }
130
131 if (flrt_wpml_active() && $lang && $translatable_post_type_exists) {
132 $sql[] = $wpdb->prepare("AND wpml_translations.language_code = %s", $lang);
133 }
134
135 /**
136 * Filters terms SQL-query and allows to modify it
137 */
138 $sql = apply_filters('wpc_filter_get_meta_date_terms_sql', $sql, $this->postTypes);
139
140 $sql = implode(' ', $sql);
141
142 $result = $wpdb->get_results($sql, ARRAY_A);
143 if ($this->date_type !== $this->meta_date_type) {
144 if (!in_array($this->meta_date_type, ['TIME', 'DATETIME', 'DATE'])) {
145 foreach ($result as $key => $value) {
146 if (in_array($this->meta_date_format, ['U', 'Ums'])) {
147 $val = $value['post_date'];
148 if ($this->meta_date_format === 'Ums') {
149 $val = (int)floor((int)$value['post_date'] / 1000);
150 }
151 $temp_date = (new \DateTime())->setTimestamp($val);
152 $temp_date = $temp_date->format('Y-m-d H:i:s');
153 } else {
154 $temp_date = \DateTime::createFromFormat($this->meta_date_format, $value['post_date']);
155 if ($temp_date !== false) {
156 $temp_date = $temp_date->format('Y-m-d H:i:s');
157 }
158 }
159
160 if ($temp_date != false) {
161 $result[$key]['post_date'] = $temp_date;
162 } else {
163 unset($result[$key]);
164 }
165
166 }
167 }
168 if ($this->meta_date_type === 'TIME') {
169 foreach ($result as $key => $value) {
170 $result[$key]['post_date'] = $this->timeToTodayDateTime($value['post_date']);
171 }
172 }
173 }
174
175 flrt_set_transient($transient_key, $result, FLRT_TRANSIENT_PERIOD_HOURS * HOUR_IN_SECONDS);
176 }
177
178 return $result;
179 }
180
181 protected function setFromAndTo()
182 {
183 $wpManager = Container::instance()->getWpManager();
184 $queried_values = $wpManager->getQueryVar('queried_values', []);
185 $filter_slug = false;
186
187 /**
188 * Check if this filter was queried
189 */
190 foreach ($queried_values as $slug => $filter) {
191 /**
192 * At this point we do not know what exactly filter was queired
193 * because filters with the same slug can be in multiple Filter Sets with
194 * different date_type values.
195 */
196 if ($filter['e_name'] === $this->getName()) {
197 $filter_slug = $slug;
198 break;
199 }
200 }
201
202 /**
203 * If this filter was queried we have to receive its $from and $to values
204 */
205 if ($filter_slug) {
206 if (isset($queried_values[$filter_slug]['values']['to']) && $this->to === false) {
207 $to = str_replace('.', ':', $queried_values[$filter_slug]['values']['to']);
208
209 $this->date_type = $this->guessMetaTypeFromValue($to, false, true);
210
211 $this->to = $this->time_to = apply_filters('wpc_unset_date_shift', $to, $this->getName(), $this->date_type);
212 if ($this->date_type === 'TIME') {
213 $this->time_to = $this->to;
214 }
215 }
216
217 if (isset($queried_values[$filter_slug]['values']['from']) && $this->from === false) {
218 $from = str_replace('.', ':', $queried_values[$filter_slug]['values']['from']);
219 $this->date_type = $this->guessMetaTypeFromValue($from, false, true);
220 $this->from = apply_filters('wpc_unset_date_shift', $from, $this->getName(), $this->date_type);
221 if ($this->date_type === 'TIME') {
222 $this->time_from = $this->from;
223 }
224 }
225 }
226 }
227
228 public function addTermsToWpQuery($queried_value, $wp_query)
229 {
230 $meta_query = [];
231 $key = $queried_value['e_name'];
232 $possible_date_query = $wp_query->get( 'date_query' );
233
234 if( ! empty( $possible_date_query ) ) {
235 $this->new_date_query = $possible_date_query;
236 }
237
238 // Add existing Meta Query if present
239 $this->importExistingMetaQuery( $wp_query );
240
241 if (strtolower((string)$this->date_type) !== 'time' && $this->date_type !== $this->meta_date_type) {
242 $tz = wp_timezone();
243 if ($this->meta_date_format === 'U' || $this->meta_date_format === 'Ums') {
244 if ($this->from !== false) {
245 $dt = \DateTimeImmutable::createFromFormat($this->date_format, $this->from, $tz);
246 $this->from = $dt ? $dt->getTimestamp() : false;
247 }
248 if ($this->to !== false) {
249 $dt = \DateTimeImmutable::createFromFormat($this->date_format, $this->to, $tz);
250 $this->to = $dt ? $dt->getTimestamp() : false;
251 }
252 if ($this->from !== false || $this->to !== false) {
253 $this->date_type = $this->meta_date_type;
254 }
255 }
256 }
257
258 if (strtolower((string)$this->date_type) === 'time' && in_array($this->meta_date_format, ['U', 'Ums'], true)) {
259
260 $normalizeTime = function ($t) {
261 if ($t === false || $t === null || $t === '') return false;
262 $t = str_replace('.', ':', $t);
263 if (preg_match('/^\d{1,2}$/', $t)) $t .= ':00:00';
264 elseif (preg_match('/^\d{1,2}:\d{2}$/', $t)) $t .= ':00';
265 return $t;
266 };
267 $tFrom = $normalizeTime($this->from);
268 $tTo = $normalizeTime($this->to);
269
270 if ($tFrom !== false || $tTo !== false) {
271 if ($tFrom === false) {
272 $tFrom = '00:00:00';
273 }
274 if ($tTo === false) {
275 $tTo = '24:00:00';
276 }
277
278 $wp_query->set('time_meta_key', $key);
279 $wp_query->set('time_between', [$tFrom, $tTo]);
280 $wp_query->set('time_is_ms', $this->meta_date_format === 'Ums');
281
282 static $wpc_time_where_added = false;
283 if (!$wpc_time_where_added) {
284 add_filter('posts_where', function ($where, $q) {
285 global $wpdb;
286 $timeBetween = $q->get('time_between');
287 $metaKey = $q->get('time_meta_key');
288
289 if (!$timeBetween || !$metaKey) {
290 return $where;
291 }
292
293 [$tFromL, $tToL] = $timeBetween;
294 $tzString = wp_timezone_string() ?: 'UTC';
295 $isMs = (bool)$q->get('time_is_ms');
296
297 $fieldExpr = $isMs
298 ? 'CAST(pm.meta_value AS UNSIGNED) / 1000'
299 : 'CAST(pm.meta_value AS UNSIGNED)';
300
301 $where .= $wpdb->prepare(
302 " AND EXISTS (
303 SELECT 1
304 FROM {$wpdb->postmeta} pm
305 WHERE pm.post_id = {$wpdb->posts}.ID
306 AND pm.meta_key = %s
307 AND TIME(CONVERT_TZ(FROM_UNIXTIME($fieldExpr), 'UTC', %s)) >= %s
308 AND TIME(CONVERT_TZ(FROM_UNIXTIME($fieldExpr), 'UTC', %s)) < %s
309 )",
310 $metaKey, $tzString, $tFromL, $tzString, $tToL
311 );
312
313 return $where;
314 }, 10, 2);
315 $wpc_time_where_added = true;
316 }
317
318 $wp_query->set('meta_query', $this->new_meta_query);
319 $this->new_meta_query = [];
320 return $wp_query;
321 }
322 }
323
324 if ($this->from !== false && $this->to === false) {
325
326 $meta_query = array(
327 'key' => $key,
328 'value' => $this->from,
329 'compare' => '>=',
330 'type' => $this->date_type
331 );
332 $this->addMetaQueryArray($meta_query);
333 }
334
335 if ($this->to !== false && $this->from === false) {
336
337 $meta_query = array(
338 'key' => $key,
339 'value' => $this->to,
340 'compare' => '<=',
341 'type' => $this->date_type
342 );
343 $this->addMetaQueryArray($meta_query);
344 }
345
346 if ($this->to !== false && $this->from !== false) {
347
348 $meta_query = array(
349 'key' => $key,
350 'value' => [$this->from, $this->to],
351 'compare' => 'BETWEEN',
352 'type' => $this->date_type
353 );
354 $this->addMetaQueryArray($meta_query);
355 }
356
357 $this->addMetaQueryArray($meta_query);
358
359 if (count($this->new_meta_query) > 1) {
360 $this->new_meta_query['relation'] = 'AND';
361 }
362
363 $wp_query->set('meta_query', $this->new_meta_query);
364 $this->new_meta_query = [];
365
366 return $wp_query;
367 }
368
369 private function detectMetaTypeForKey($meta_key, $post_type = 'any', $sampleSize = 10)
370 {
371 global $wpdb;
372
373 $post_type_sql = '';
374 $params = [$meta_key];
375
376 if ($post_type !== 'any') {
377 if (is_array($post_type)) {
378 $placeholders = implode(',', array_fill(0, count($post_type), '%s'));
379 $post_type_sql = " AND p.post_type IN ($placeholders) ";
380 $params = array_merge($params, $post_type);
381 } else {
382 $post_type_sql = " AND p.post_type = %s ";
383 $params[] = $post_type;
384 }
385 }
386
387 $sql = "
388 SELECT pm.meta_value
389 FROM {$wpdb->postmeta} pm
390 INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
391 WHERE pm.meta_key = %s
392 AND pm.meta_value IS NOT NULL
393 AND pm.meta_value <> ''
394 {$post_type_sql}
395 LIMIT %d";
396 $params[] = $sampleSize;
397
398 $values = $wpdb->get_col($wpdb->prepare($sql, $params));
399
400 if (empty($values)) {
401 return 'CHAR';
402 }
403
404 $counts = [
405 'DATETIME' => 0,
406 'DATE' => 0,
407 'NUMERIC' => 0,
408 'CHAR' => 0,
409 ];
410
411 foreach ($values as $v) {
412 $type = $this->guessMetaTypeFromValue($v, true);
413 if (!isset($counts[$type])) {
414 $counts[$type] = 0;
415 }
416 $counts[$type]++;
417 }
418
419 arsort($counts);
420 $best = key($counts);
421
422 return $best ?: 'CHAR';
423 }
424
425 private function guessMetaTypeFromValue($value, $change_meta_date_format = false, $change_date_format = false)
426 {
427 // Normalize input
428 $trimmed = trim((string)$value);
429 $apply_meta_date_format = $change_meta_date_format;
430 $apply_date_format = $change_date_format;
431
432 // Local setter to avoid repeating the same condition
433 $setFormat = function ($fmt) use ($apply_meta_date_format, $apply_date_format) {
434 if ($apply_meta_date_format) {
435 $this->meta_date_format = $fmt;
436 }
437 if ($apply_date_format) {
438 $this->date_format = $fmt;
439 }
440 };
441
442 // Empty value -> treat as CHAR
443 if ($trimmed === '') {
444 $setFormat(null);
445 return 'CHAR';
446 }
447
448 // Predefined patterns for readability
449 $TIME_PATTERN = '/^([01]\d|2[0-3]):[0-5]\d(?:[:][0-5]\d)?$/';
450 $DATETIME_PATTERN = '/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}$/';
451 $DATE_PATTERN = '/^\d{4}-\d{2}-\d{2}$/';
452 $ISO8601_PATTERN = '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:Z|[+\-]\d{2}:\d{2})?$/';
453
454 // TIME: HH:MM or HH:MM:SS
455 if (preg_match($TIME_PATTERN, $trimmed)) {
456 $setFormat('H:i:s');
457 return 'TIME';
458 }
459
460 // Digits only
461 if (ctype_digit($trimmed)) {
462 $len = strlen($trimmed);
463
464 switch ($len) {
465 // 13 digits — likely UNIX timestamp in milliseconds
466 case 13:
467 if ((int)$trimmed > 999999999999) {
468 $setFormat('Ums');
469 return 'NUMERIC';
470 }
471 break;
472
473 // 10 digits — UNIX timestamp in seconds
474 case 10:
475 $setFormat('U');
476 return 'NUMERIC';
477
478 // 8 digits — Ymd format (e.g., 20250831)
479 case 8:
480 $setFormat('Ymd');
481 return 'NUMERIC';
482
483 default:
484 // Any other all-digit string is still NUMERIC
485 $setFormat(null);
486 return 'NUMERIC';
487 }
488 }
489
490 // MySQL DATETIME: Y-m-d H:i:s
491 if (preg_match($DATETIME_PATTERN, $trimmed)) {
492 $setFormat('Y-m-d H:i:s');
493 return 'DATETIME';
494 }
495
496 // MySQL DATE: Y-m-d
497 if (preg_match($DATE_PATTERN, $trimmed)) {
498 $setFormat('Y-m-d');
499 return 'DATE';
500 }
501
502 // ISO 8601: 2025-08-31T15:30:45Z or with an offset
503 if (preg_match($ISO8601_PATTERN, $trimmed)) {
504 $setFormat('Y-m-d\TH:i:sP');
505 return 'CHAR';
506 }
507
508 // Fallback
509 $setFormat(null);
510 return 'CHAR';
511 }
512
513 private function timeToTodayDateTime( $value, ?\DateTimeZone $tz = null): ?string
514 {
515 $value = trim($value);
516
517 if (!preg_match('/^([01]\d|2[0-3]):[0-5]\d(?:[:][0-5]\d)?$/', $value)) {
518 return null;
519 }
520
521 if (strlen($value) === 5) { // HH:MM
522 $value .= ':00';
523 }
524
525 if (!$tz) {
526 if (function_exists('wp_timezone')) {
527 $tz = wp_timezone(); // \DateTimeZone
528 } else {
529 $tzString = function_exists('wp_timezone_string') ? wp_timezone_string() : '';
530 $tz = $tzString ? new \DateTimeZone($tzString) : new \DateTimeZone(date_default_timezone_get());
531 }
532 }
533
534 $now = new DateTime('now', $tz);
535 $now->modify('-1 day');
536 $datePart = $now->format('Y-m-d');
537
538 return $datePart . ' ' . $value; // Y-m-d H:i:s
539 }
540
541 }