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 / PostMetaDateEntity.php

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

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