PluginProbe
HurryTimer – An Scarcity and Urgency Countdown Timer for WordPress & WooCommerce / 2.15.0
HurryTimer – An Scarcity and Urgency Countdown Timer for WordPress & WooCommerce v2.15.0
2.15.0 trunk 1.0.0 1.0.1 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.1.2 2.1.3 2.1.5 2.1.6 2.1.7 2.1.8 2.10.0 All 62 releases
hurrytimer / includes / Campaign.php

Campaign.php in HurryTimer – An Scarcity and Urgency Countdown Timer for WordPress & WooCommerce 2.15.0, at includes/Campaign.php

2,456 lines 61.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Hurrytimer;
4
5 use DateInterval;
6 use DatePeriod;
7 use DateTime;
8 use Hurrytimer\Dependencies\Carbon\Carbon;
9 use Hurrytimer\Dependencies\Carbon\CarbonPeriod;
10 use Hurrytimer\Utils\Helpers;
11
12 class Campaign
13 {
14
15 /**
16 * Campaign custom post ID
17 *
18 * @var int
19 */
20 private $id;
21
22 /**
23 * Campaign mode.
24 *
25 * @see C.php
26 *
27 * @var int
28 */
29 public $mode = C::MODE_REGULAR;
30
31 /**
32 * Evergreen duration array.
33 *
34 * @see getDuration()
35 *
36 * @var array
37 */
38 public $duration;
39
40 /**
41 * Recurrence duration
42 *
43 * @var int
44 */
45 public $recurringDuration;
46 public $recurringPauseDuration = ['days' => 0, 'hours' => 0, 'minutes' => 0, 'seconds' => 0];
47
48 public $recurringDurationOption = 'none';
49
50 /**
51 * Recurrence start date/time
52 *
53 * @var string
54 */
55 public $recurringStartTime;
56
57 /**
58 * Recurrence end option
59 *
60 * @var int
61 */
62 public $recurringEnd = C::RECURRING_END_NEVER;
63
64 /**
65 * Recurrence frequency
66 *
67 * @see C
68 * @var string
69 */
70 public $recurringFrequency = C::RECURRING_DAILY;
71
72 /**
73 * Recurrence interval
74 *
75 * @var int
76 */
77 public $recurringInterval = 1;
78
79 /**
80 * Recurrence count
81 *
82 * @var int
83 */
84 public $recurringCount = 2;
85
86 /**
87 * Recurrence end date/time
88 *
89 * @var
90 */
91 public $recurringUntil;
92
93 /**
94 * Recurrence allowed days
95 *
96 * @var array
97 */
98 public $recurringDays = [0, 1, 2, 3, 4, 5, 6];
99
100 /**
101 * Restart option after expiration.
102 *
103 * @see \Hurrytimer\CampaignRestart
104 *
105 * @var int
106 */
107 public $restart;
108 public $restartDuration = ['days' => 0, 'hours' => 0, 'minutes' => 0, 'seconds' => 0];
109
110 /**
111 * Headline text.
112 *
113 * @var string
114 */
115 public $headline = "Hurry Up!";
116
117 /**
118 * Headline color.
119 *
120 * @var string
121 */
122 public $headlineColor = "#000";
123
124 /**
125 * Headline size.
126 *
127 * @var int
128 */
129 public $headlineSize = 30;
130
131 /**
132 * Headline position.
133 *
134 * @var int
135 */
136 public $headlinePosition = C::HEADLINE_POSITION_ABOVE_TIMER;
137
138 /**
139 * Headline visibility.
140 *
141 * @var boolean
142 */
143 public $headlineVisibility = C::YES;
144
145 public $headlineSpacing = 5;
146
147 /**
148 * Control labels visibility.
149 *
150 * @var string
151 */
152 public $labelVisibility = C::YES;
153
154 /**
155 * Show/hide days block.
156 *
157 * @var string
158 */
159 public $daysVisibility = C::YES;
160
161 public $monthsVisibility = C::NO;
162
163 /**
164 * Show/hide hours block.
165 *
166 * @var string
167 */
168 public $hoursVisibility = C::YES;
169
170 /**
171 * Show/hide minutes block.
172 *
173 * @var string
174 */
175 public $minutesVisibility = C::YES;
176
177 /**
178 * Show/hide seconds block.
179 *
180 * @var string
181 */
182 public $secondsVisibility = C::YES;
183
184 /**
185 * Regular campaign end date/time.
186 *
187 * @var string
188 */
189 public $endDatetime;
190
191 /**
192 * Labels texts.
193 *
194 * @var array
195 */
196 public $labels
197 = [
198 'months' => 'months',
199 'days' => 'days',
200 'hours' => 'hrs',
201 'minutes' => 'mins',
202 'seconds' => 'secs',
203 ];
204
205 /**
206 * Digit color.
207 *
208 * @var string
209 */
210 public $digitColor = "#000";
211
212 /**
213 * Digit size.
214 *
215 * @var int
216 */
217 public $digitSize = 35;
218
219 /**
220 * Redirect action url.
221 *
222 * @var string
223 */
224 public $redirectUrl;
225
226 /**
227 * Label size.
228 *
229 * @var int
230 */
231 public $labelSize = 12;
232
233 /**
234 * Label color.
235 *
236 * @var string
237 */
238 public $labelColor = "#000";
239
240 /**
241 * End action.
242 *
243 * @var array
244 */
245 public $actions = [];
246
247 /**
248 * WooCommerce compaign position in product page.
249 *
250 * @var int
251 */
252 public $wcPosition = C::WC_POSITION_ABOVE_TITLE;
253
254 /**
255 * Enable/disable woocommerce integration.
256 *
257 * @var string
258 */
259 public $wcEnable = C::NO;
260
261 /**
262 * WooCommerce products selection.
263 *
264 * @var array
265 */
266 public $wcProductsSelection;
267
268 /**
269 * WooCommerce products selection type.
270 *
271 * @var int
272 */
273 public $wcProductsSelectionType;
274
275 /**
276 * WooCommerce conditions.
277 * @var array $wcConditions
278 */
279 public $wcConditions;
280
281 /**
282 * Timer block border color.
283 *
284 * @var string
285 */
286 public $blockBorderColor = "";
287
288 /**
289 * Timer Block border width.
290 *
291 * @var int
292 */
293
294 public $blockBorderWidth = 0;
295
296 /**
297 * Timer block radius.
298 *
299 * @var int
300 */
301 public $blockBorderRadius = 0;
302
303 /**
304 * Block size.
305 *
306 * @var int
307 */
308
309 public $blockSize = 'auto';
310
311 /**
312 * Block background color.
313 *
314 * @var string
315 */
316 public $blockBgColor = '';
317
318 /**
319 * Block spacing.
320 *
321 * @var int
322 */
323 public $blockSpacing = 5;
324
325 /**
326 * Block padding.
327 *
328 * @var int
329 */
330 public $blockPadding = 0;
331
332 /**
333 * Block spearator visibility.
334 *
335 * @var boolean
336 */
337 public $blockSeparatorVisibility = C::YES;
338
339 /**
340 * Label case
341 *
342 * @var string
343 */
344 public $labelCase = C::TRANSFORM_UPPERCASE;
345
346 /**
347 * Custom CSS
348 *
349 * @var string
350 */
351 public $customCss = '';
352
353 /**
354 * Block elements display
355 * Values: block, inline
356 *
357 * @var string
358 */
359 public $blockDisplay = 'block';
360
361 /**
362 * Enable sticky bar.
363 */
364 public $enableSticky = C::NO;
365
366 /**
367 * Sticky bar background color.
368 *
369 * @var string
370 */
371 public $stickyBarBgColor = '#eee';
372
373 /**
374 * Sticky bar close button color.
375 *
376 * @var string
377 */
378 public $stickyBarCloseBtnColor = '#fff';
379
380 /**
381 * Sticky bar position.
382 * Values: top, bottom
383 *
384 * @var string
385 */
386 public $stickyBarPosition = 'top';
387
388 /**
389 * Sticky bar padding.
390 *
391 * @var integer
392 */
393 public $stickyBarPadding = 5;
394
395 /**
396 * Sticky bar display pages.
397 *
398 * @var array
399 */
400 public $stickyBarPages = [];
401 public $stickyIncludeUrls = [];
402 public $stickyExcludeUrls = [];
403
404 /**
405 * Sticky exclude pages.
406 *
407 * @var array
408 */
409 public $stickyExcludePages = [];
410
411 /**
412 * Where to display the sticky bar option
413 *
414 * @var string
415 */
416 public $stickyBarDisplayOn = 'all_pages';
417
418 /**
419 * Show sticky bar close button?
420 *
421 * @var string
422 */
423 public $stickyBarDismissible = C::YES;
424
425
426 /**
427 * If dismissed, re-open sticky bar after x days.
428 * @var int
429 */
430 public $stickyBarDismissTimeout = 7;
431
432 /**
433 * CTA settings.
434 *
435 * @var array
436 */
437 public $callToAction
438 = [
439 'enabled' => C::NO,
440 'new_tab' => C::NO,
441 'url' => '',
442 'text' => 'Learn More',
443 'text_size' => 15,
444 'text_color' => '#fff',
445 'bg_color' => '#000',
446 'y_padding' => 10,
447 'x_padding' => 15,
448 'border_radius' => 3,
449 'spacing' => 5,
450
451 ];
452
453 /**
454 * Campaign elements display
455 * values: block,inline
456 *
457 * @var string
458 */
459 public $campaignDisplay = 'block';
460
461 /**
462 * Campaign aligments
463 * values: center,left,right
464 *
465 * @var string
466 */
467 public $campaignAlign = 'center';
468
469 /**
470 * Campaign spacing.
471 *
472 * @var integer
473 */
474 public $campaignSpacing = 10;
475
476 /**
477 * Campaign horizontal padding.
478 *
479 * @var integer
480 */
481 public $campaignXPadding = 10;
482
483 /**
484 * Campaign vertical padding.
485 *
486 * @var integer
487 */
488 public $campaignYPadding = 10;
489
490 /**
491 * Force evergreen timer to reset on reload.
492 */
493 public $reloadReset = false;
494
495
496 /**
497 *
498 */
499
500 public $detectionMethods = [
501 C::DETECTION_METHOD_COOKIE,
502 C::DETECTION_METHOD_IP,
503 ];
504
505 public $timezoneType = 'site';
506 public $timezone;
507
508
509 public $recurringMonthlyDayType = C::RECURRING_MONTHLY_DAY_OF_MONTH;
510
511
512 public $recurringUnselectedDaysAction = 'skip';
513
514 public $evergreenEndDay = 0;
515 public $evergreenEndTime = '';
516 public $evergreenEndType = 'duration';
517
518 /**
519 * Regular mode date ranges.
520 * Each entry: ['start' => 'Y-m-d h:i A', 'end' => 'Y-m-d h:i A']
521 * When only one range exists (or empty), falls back to the single endDatetime field.
522 *
523 * @var array
524 */
525 public $dateRanges = [];
526
527 public function __construct($id)
528 {
529 $this->recurringStartTime = Carbon::now(hurryt_tz($this->getTimezone()))->format('Y-m-d h:i A');
530 $this->id = $id;
531 }
532
533 /**
534 * Returns compaign post iD.
535 *
536 * @return int
537 */
538 public function get_id()
539 {
540 return $this->id;
541 }
542
543
544 /**
545 * Returns true if the current sticky bar can be displayed on the given page.
546 *
547 * @param $pageId
548 *
549 * @return bool
550 * @throws \Exception
551 */
552 public function show_sticky_on_page($pageId)
553 {
554 if ($this->enableSticky === C::NO) {
555 return true;
556 }
557
558 switch ($this->getStickyBarDisplayOn()):
559
560 case 'wc_products_pages':
561 if (!empty($pageId) && function_exists('is_product') && is_product()) {
562
563 $wc_campaign = new WCCampaign();
564 if (($wc_campaign->has_campaign($this, $pageId))
565 && $wc_campaign->met_conditions($this, $pageId)
566 ) {
567 return apply_filters('hurryt_show_sticky_bar', true, $this->get_id());
568 }
569 }
570
571 return apply_filters('hurryt_show_sticky_bar', false, $this->get_id());
572
573 case 'specific_pages':
574 $pageIds = $this->getStickyBarPages();
575
576 if (!empty($pageId) && !empty($pageIds) && in_array($pageId, $pageIds)) {
577 return apply_filters('hurryt_show_sticky_bar', true, $this->get_id());
578 }
579
580 foreach ($this->stickyIncludeUrls as $url) {
581 $current_url = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';
582
583 if (Helpers::are_urls_match($url, $current_url)) {
584 return apply_filters('hurryt_show_sticky_bar', true, $this->get_id());
585 break;
586 }
587 }
588 return apply_filters('hurryt_show_sticky_bar', false, $this->get_id());
589
590 case 'exclude_pages':
591 $pageIds = $this->getStickyExcludePages();
592 if (!empty($pageId) && !empty($pageIds) && in_array($pageId, $pageIds)) {
593 return apply_filters('hurryt_show_sticky_bar', false, $this->get_id());
594 }
595
596 foreach ($this->stickyExcludeUrls as $url) {
597 $current_url = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';
598 if (Helpers::are_urls_match($current_url, $url)) {
599 return apply_filters('hurryt_show_sticky_bar', false, $this->get_id());
600 break;
601 }
602 }
603
604 return apply_filters('hurryt_show_sticky_bar', true, $this->get_id());
605 case 'all_pages':
606 return apply_filters('hurryt_show_sticky_bar', true, $this->get_id());
607 default:
608 return apply_filters('hurryt_show_sticky_bar', false, $this->get_id());
609 endswitch;
610 }
611
612 /**
613 * Returns sticky bar pages.
614 *
615 * @return array
616 */
617 public function getStickyBarPages()
618 {
619 $pagesIds = $this->get_prop('sticky_bar_pages');
620
621 return array_filter(array_map('intval', $pagesIds));
622 }
623
624 public function getStickyExcludePages()
625 {
626 $page_ids = $this->get_prop('sticky_exclude_pages');
627
628 return array_filter(array_map('intval', $page_ids));
629 }
630
631 /**
632 * Set sticky bar pages.
633 *
634 * @param $value
635 */
636 public function setStickyBarPages($value)
637 {
638 $this->set_prop('sticky_bar_pages', $value);
639 }
640
641 public function setStickyExcludePages($value)
642 {
643 $this->set_prop('sticky_exclude_pages', $value);
644 }
645
646 public function setStickyBarDismissTimeout($value)
647 {
648 $this->set_prop('sticky_bar_dismiss_timeout', intval($value));
649 }
650
651 public function getStickyBarDismissTimeout()
652 {
653 return intval($this->get_prop('sticky_bar_dismiss_timeout'));
654 }
655
656 public function getStickyBarDisplayOn()
657 {
658
659 // backward compat.
660 $all_pages = $this->get_prop('sticky_bar_show_on_all_pages', false);
661
662 if ($all_pages === C::YES) {
663 $this->setStickyBarDisplayOn('all_pages');
664 }
665
666 $this->delete_prop('sticky_bar_show_on_all_pages');
667
668 $value = $this->get_prop('_hurryt_sticky_bar_display_on', false);
669
670 return !empty($value) ? $value : $this->stickyBarDisplayOn;
671 }
672
673 public function setStickyBarDisplayOn($value)
674 {
675 $this->set_prop('_hurryt_sticky_bar_display_on', $value);
676 }
677
678 /**
679 * Get raw setting.
680 *
681 * @param string $name
682 * @param boolean
683 *
684 * @return mixed
685 */
686 public function get_prop($name, $useDefault = true)
687 {
688
689 $value = get_post_meta($this->id, $name, true);
690 if (!empty($value)) {
691 return $value;
692 }
693 if ($useDefault) {
694 $name = str_replace('_hurryt_', '', $name);
695 return $this->{Helpers::snakeToCamelCase($name)};
696 }
697
698 return $value;
699 }
700
701 public function set_prop($name, $value)
702 {
703 $value = !empty($value) ? $value : '';
704 $_name = str_replace('_hurryt_', '', $name);
705 $getterMethod = Helpers::snakeToCamelCase($_name);
706 if (empty($value) && method_exists($this, $getterMethod)) {
707 $value = $this->{$getterMethod}();
708 }
709
710 update_post_meta($this->id, $name, $value);
711 }
712
713 /**
714 * Bulk save for compaign settings.
715 *
716 * @param $data
717 */
718 public function storeSettings($data)
719 {
720 if (!hurrytimer_allow_unfiltered_html()) {
721 // Exclude fields that have their own sanitization to avoid double-processing
722 // which can strip valid HTML (e.g. <a> tags in headline).
723 $html_fields = ['headline'];
724 $reserved = array_intersect_key($data, array_flip($html_fields));
725 $data = wp_kses_post_deep(array_diff_key($data, array_flip($html_fields)));
726 $data = array_merge($data, $reserved);
727 }
728
729 foreach ($data as $prop => $value) {
730 $method = Helpers::snakeToCamelCase("set_{$prop}");
731 if (method_exists($this, $method)) {
732 if($prop === 'timezone') {
733 $this->setTimezone($value, $data['timezone_type']);
734 }
735 else{
736 $this->$method($value ?: $this->$prop);
737 }
738 } elseif (property_exists(__CLASS__, Helpers::snakeToCamelCase($prop))) {
739 $this->set_prop($prop, $value);
740 }
741 }
742 if (!isset($data['wc_conditions'])) {
743 $this->setWcConditions([]);
744 }
745 if (!isset($data['sticky_bar_pages'])) {
746 $this->setStickyBarPages([]);
747 }
748 // Only use multiple deadlines if the toggle is active
749 if (isset($data['use_multiple_deadlines']) && $data['use_multiple_deadlines'] === '0') {
750 $this->setDateRanges([]);
751 }
752
753 CSS_Builder::get_instance()->generate_css();
754 }
755
756 //removeIf(pro)
757
758 /**
759 * @param int $mode
760 * ::PROP
761 */
762 public function setMode($mode)
763 {
764 if ($mode == C::MODE_RECURRING) {
765 return;
766 }
767 $this->set_prop('mode', $mode);
768 }
769 //endRemoveIf(pro)
770
771 /**
772 * TODO: Settings should load automatically upon instance creation.
773 */
774 public function loadSettings()
775 {
776
777 $reflection = new \ReflectionObject($this);
778 // TODO: Refactor prop loading to `$this->get_{prop}()`
779 foreach ($reflection->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) {
780 $name = $prop->getName();
781 $method = 'get' . ucfirst($name);
782 if (method_exists($this, $method)) {
783 $this->{$name} = $this->$method();
784 } else {
785 $this->{$name} = $this->get_prop(Helpers::camelToSnakeCase($name));
786 }
787 }
788
789 }
790
791 public function getHeadline()
792 {
793 $content = $this->get_prop('_hurryt_headline', false);
794
795 // User has set a custom headline
796 if( !empty( $content ) ){ return $content; }
797
798 // Backward compat.
799 if (empty($content) && version_compare(get_option('hurrytimer_version'), '2.3.0', '<=')) {
800 return get_the_title($this->id);
801 }
802
803 // Show default headline.
804 return $this->headline;
805 }
806
807 public function setHeadline($value)
808 {
809 $value = $this->sanitize_headline_html( $value );
810 $this->set_prop('_hurryt_headline', $value);
811 }
812
813 /**
814 * Custom HTML sanitizer for the headline field.
815 *
816 * Allowed tags: span, div, a, input, br, s, p, strong, em
817 * - style, class, id allowed on visual tags (CSS values are preserved as-is
818 * except javascript:/expression()/vbscript: patterns are removed)
819 * - <a>: href (blocked if javascript:), target, rel
820 * - <input>: type, value, style (no event handlers)
821 * - All on* event handler attributes stripped from every tag
822 * - Dangerous tags (script, iframe, object, embed, form, link, meta) stripped entirely
823 */
824 private function sanitize_headline_html( $value ) {
825 // Strip dangerous block-level tags and their contents.
826 $value = preg_replace(
827 '#<(script|iframe|object|embed|form|link|meta)[^>]*>.*?</\1>#is',
828 '',
829 $value
830 );
831 // Also strip self-closing variants.
832 $value = preg_replace(
833 '#<(script|iframe|object|embed|form|link|meta)[^>]*/?\s*>#is',
834 '',
835 $value
836 );
837
838 $allowed_tags = [ 'span', 'div', 'a', 'input', 'img', 'i', 'u', 'b', 'small', 'sup', 'sub', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'br', 's', 'p', 'strong', 'em' ];
839
840 // Use DOMDocument to walk and filter the fragment.
841 $doc = new \DOMDocument();
842 // Suppress warnings from malformed HTML; wrap in a root node.
843 @$doc->loadHTML(
844 '<?xml encoding="utf-8"?><div id="__hurrytimer_root__">' . $value . '</div>',
845 LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
846 );
847
848 $root = $doc->getElementById('__hurrytimer_root__');
849 if ( ! $root ) {
850 return '';
851 }
852
853 $this->_sanitize_headline_node( $root, $allowed_tags );
854
855 // Serialize only the inner HTML of our wrapper.
856 $html = '';
857 foreach ( $root->childNodes as $child ) {
858 $html .= $doc->saveHTML( $child );
859 }
860
861 return $html;
862 }
863
864 /**
865 * Recursively sanitize a DOM node for the headline field.
866 */
867 private function _sanitize_headline_node( \DOMNode $node, array $allowed_tags ) {
868 $remove = [];
869
870 foreach ( $node->childNodes as $child ) {
871 if ( $child->nodeType === XML_ELEMENT_NODE ) {
872 $tag = strtolower( $child->nodeName );
873
874 if ( ! in_array( $tag, $allowed_tags, true ) ) {
875 // Replace disallowed tag with its children (unwrap).
876 $remove[] = [ 'node' => $child, 'unwrap' => true ];
877 continue;
878 }
879
880 // Sanitize attributes.
881 $attrs_to_remove = [];
882 foreach ( $child->attributes as $attr ) {
883 $name = strtolower( $attr->name );
884 $val = $attr->value;
885
886 // Strip all event handlers.
887 if ( strncmp( $name, 'on', 2 ) === 0 ) {
888 $attrs_to_remove[] = $name;
889 continue;
890 }
891
892 // Per-tag attribute rules.
893 if ( $tag === 'a' ) {
894 if ( ! in_array( $name, [ 'href', 'target', 'rel', 'style', 'class', 'id' ], true ) ) {
895 $attrs_to_remove[] = $name;
896 continue;
897 }
898 if ( $name === 'href' && preg_match( '#^\s*javascript:#i', $val ) ) {
899 $attrs_to_remove[] = $name;
900 continue;
901 }
902 } elseif ( $tag === 'img' ) {
903 if ( ! in_array( $name, [ 'src', 'alt', 'width', 'height', 'style', 'class', 'id' ], true ) ) {
904 $attrs_to_remove[] = $name;
905 continue;
906 }
907 if ( $name === 'src' ) {
908 // Block javascript: protocol
909 if ( preg_match( '#^\s*javascript\s*:#i', $val ) ) {
910 $attrs_to_remove[] = $name;
911 continue;
912 }
913 // Block data: URIs except safe image types (no SVG — can contain scripts)
914 if ( preg_match( '#^\s*data\s*:#i', $val ) && ! preg_match( '#^\s*data\s*:\s*image/(png|jpe?g|gif|webp)\s*[;,]#i', $val ) ) {
915 $attrs_to_remove[] = $name;
916 continue;
917 }
918 }
919 } elseif ( $tag === 'input' ) {
920 if ( ! in_array( $name, [ 'type', 'value', 'style', 'class', 'id' ], true ) ) {
921 $attrs_to_remove[] = $name;
922 continue;
923 }
924 } else {
925 // Visual tags: allow style, class, id only.
926 if ( ! in_array( $name, [ 'style', 'class', 'id' ], true ) ) {
927 $attrs_to_remove[] = $name;
928 continue;
929 }
930 }
931
932 // Sanitize style values: remove JS injection patterns.
933 if ( $name === 'style' ) {
934 $val = preg_replace( '#javascript\s*:#i', '', $val );
935 $val = preg_replace( '#expression\s*\(#i', '', $val );
936 $val = preg_replace( '#vbscript\s*:#i', '', $val );
937 $child->setAttribute( 'style', $val );
938 }
939 }
940
941 foreach ( $attrs_to_remove as $a ) {
942 $child->removeAttribute( $a );
943 }
944
945 // Recurse.
946 $this->_sanitize_headline_node( $child, $allowed_tags );
947 }
948 }
949
950 // Unwrap or remove collected nodes.
951 foreach ( $remove as $item ) {
952 $n = $item['node'];
953 if ( $item['unwrap'] ) {
954 while ( $n->firstChild ) {
955 $node->insertBefore( $n->firstChild, $n );
956 }
957 }
958 if ( $n->parentNode ) {
959 $n->parentNode->removeChild( $n );
960 }
961 }
962 }
963
964 public function is_published()
965 {
966 return get_post_status($this->id) === "publish";
967 }
968
969 /**
970 * Check if WooCommerce is enabled for this campaign.
971 *
972 * @return bool
973 */
974 public function is_wc_enabled()
975 {
976 return $this->getWcEnable() === C::YES;
977 }
978
979 /**
980 * Check current countdown timer mode
981 * ::PROP
982 *
983 * @return bool
984 */
985 public function is_evergreen()
986 {
987 return $this->get_prop('mode') == C::MODE_EVERGREEN;
988 }
989
990 public function is_one_time()
991 {
992 return $this->get_prop('mode') == C::MODE_REGULAR;
993 }
994
995 public function is_recurring()
996 {
997 return $this->get_prop('mode') == C::MODE_RECURRING;
998 }
999
1000 public function getTimezone()
1001 {
1002
1003 if($this->timezoneType === 'site') {
1004 return '';
1005 }
1006 return '';
1007
1008 }
1009 public function setTimezone($timezone, $timezone_type)
1010 {
1011 if($timezone_type === 'site') {
1012 $timezone = '';
1013 }
1014
1015 $this->set_prop('timezone', $timezone);
1016 }
1017 /**
1018 * Save compaign endtime for regular mode.
1019 *
1020 * @param $date
1021 */
1022 public function setEndDatetime($date)
1023 {
1024 // Sanitize the date input to prevent XSS
1025 $date = sanitize_text_field($date);
1026 $this->set_prop('end_datetime', $date ?: $this->defaultEndDatetime());
1027 }
1028
1029 /**
1030 * Returns default end datetime for regular mode.
1031 * ::PROP
1032 *
1033 * @return false|string
1034 */
1035 private function defaultEndDatetime()
1036 {
1037 return date('Y-m-d h:i A', strtotime('+1 week'));
1038 }
1039
1040 /**
1041 * Save digit color.
1042 * ::PROP
1043 *
1044 * @param $color
1045 */
1046 public function setDigitColor($color)
1047 {
1048 $this->set_prop('digit_color', $color);
1049 }
1050
1051 /**
1052 * Returns headline visibility.
1053 * ::PROP
1054 *
1055 * @return mixed
1056 */
1057
1058 public function getHeadlineVisibility()
1059 {
1060 $meta = 'headline_visibility';
1061
1062 $value = $this->get_prop($meta, false);
1063 if ($value === C::NO || $value === C::YES) {
1064 return $value;
1065 }
1066
1067 $legacyMeta = 'display_headline';
1068 $legacyValue = filter_var($this->get_prop_legacy($legacyMeta), FILTER_VALIDATE_BOOLEAN);
1069 $legacyValue = $legacyValue ? C::YES : C::NO;
1070
1071 $this->set_prop($meta, $legacyValue);
1072
1073 $this->delete_prop($legacyMeta);
1074
1075 return $this->get_prop($meta);
1076 }
1077
1078 /**
1079 * Returns digit color.
1080 * Backward compat with older versions.
1081 * ::PROP
1082 *
1083 * @return mixed
1084 */
1085 public function getDigitColor()
1086 {
1087 $value = $this->get_prop('digit_color', false);
1088 if ($value) {
1089 return $value;
1090 }
1091
1092 $legacy = $this->get_prop_legacy('text_color');
1093 if (!$legacy) {
1094 return $this->digitColor;
1095 }
1096
1097 $this->setDigitColor($legacy);
1098 if (!$this->get_prop('label_color', false)) {
1099 $this->setLabelColor($legacy);
1100 }
1101
1102 $this->delete_prop('text_color');
1103
1104 return $this->get_prop('digit_color');
1105 }
1106
1107 /**
1108 * ::PROP
1109 *
1110 * @param $color
1111 */
1112 public function setLabelColor($color)
1113 {
1114 $this->set_prop('label_color', $color);
1115 }
1116
1117 /**
1118 * Get label color
1119 * ::PROP
1120 *
1121 * @return mixed
1122 */
1123 public function getLabelColor()
1124 {
1125 $value = $this->get_prop('label_color', false);
1126 if ($value) {
1127 return $value;
1128 }
1129
1130 $legacy = $this->get_prop_legacy('text_color');
1131 if (!$legacy) {
1132 return $this->labelColor;
1133 }
1134
1135 $this->setLabelColor($legacy);
1136 if (!$this->get_prop('digit_color', false)) {
1137 $this->setDigitColor($legacy);
1138 }
1139
1140 $this->delete_prop('text_color');
1141
1142 return $this->get_prop('label_color');
1143 }
1144
1145 /**
1146 *
1147 * Return timer digit size.
1148 * Backward comapt. with older versions.
1149 * ::PROP
1150 *
1151 * @return int
1152 * @since 1.2.4
1153 */
1154 public function getDigitSize()
1155 {
1156 $meta = 'digit_size';
1157 $value = $this->get_prop($meta, false);
1158 if (!empty($value)) {
1159 return $value;
1160 }
1161 $legacy = $this->get_prop_legacy('text_size');
1162
1163 if (empty($legacy)) {
1164 return $this->digitSize;
1165 }
1166
1167 $this->setDigitSize($legacy);
1168
1169 $this->delete_prop('text_size');
1170
1171 return $this->get_prop('digit_size');
1172 }
1173
1174 /**
1175 * Save timer digit size.
1176 * ::PROP
1177 *
1178 * @param $size
1179 */
1180 public function setDigitSize($size)
1181 {
1182 $this->set_prop('digit_size', $size);
1183 }
1184
1185 /**
1186 * Save timer label size.
1187 * ::PROP
1188 *
1189 * @param $size
1190 */
1191 public function setLabelSize($size)
1192 {
1193 $this->set_prop('label_size', $size);
1194 }
1195
1196
1197 /**
1198 * ::PROP
1199 *
1200 * @param $size
1201 */
1202 public function setHeadlineSize($size)
1203 {
1204 $this->set_prop('headline_size', $size);
1205 }
1206
1207 /**
1208 * Returns evergreen duration.
1209 * ::PROP
1210 *
1211 * @return array
1212 */
1213 public function getDuration()
1214 {
1215 $duration = (array)$this->get_prop('duration');
1216 return array_pad(array_map('intval', $duration), 4, 0);
1217 }
1218
1219
1220
1221 public function getRestartDuration($asSeconds = false)
1222 {
1223 $duration = (array)$this->get_prop('hurryt_restart_duration', false);
1224
1225 $duration = wp_parse_args($duration, $this->restartDuration);
1226 foreach ($duration as $unit => $value) {
1227 $duration[$unit] = (int)$value;
1228 }
1229
1230 if ($asSeconds) {
1231 $duration = $duration['days'] * DAY_IN_SECONDS +
1232 $duration['hours'] * HOUR_IN_SECONDS +
1233 $duration['minutes'] * MINUTE_IN_SECONDS +
1234 $duration['seconds'];
1235 }
1236 return $duration;
1237 }
1238
1239
1240 public function getRecurringPauseDuration($asSeconds = false)
1241 {
1242 $duration = (array)$this->get_prop('_hurryt_recurring_pause_duration', false);
1243
1244 $duration = wp_parse_args($duration, $this->recurringPauseDuration);
1245 foreach ($duration as $unit => $value) {
1246 $duration[$unit] = (int)$value;
1247 }
1248
1249 if ($asSeconds) {
1250 $duration = $duration['days'] * DAY_IN_SECONDS +
1251 $duration['hours'] * HOUR_IN_SECONDS +
1252 $duration['minutes'] * MINUTE_IN_SECONDS +
1253 $duration['seconds'];
1254 }
1255 return $duration;
1256 }
1257
1258 public function setRestartDuration($duration_array)
1259 {
1260 $this->set_prop('hurryt_restart_duration', $duration_array);
1261 }
1262 /**
1263 * ::PROP
1264 *
1265 * @param $value
1266 */
1267 public function setRecurringDuration($value)
1268 {
1269 $this->set_prop('_hurryt_recurring_duration', $value);
1270 }
1271 public function setRecurringPauseDuration($value)
1272 {
1273 $this->set_prop('_hurryt_recurring_pause_duration', $value);
1274 }
1275
1276 /**
1277 * Returns evergreen duration.
1278 *
1279 * @return array
1280 */
1281 public function getRecurringDuration()
1282 {
1283
1284 $duration = (array)$this->get_prop('_hurryt_recurring_duration', false);
1285 return array_pad(array_map('intval', $duration), 4, 0);
1286 }
1287
1288 /**
1289 * Returns actions array.
1290 *
1291 * @return array
1292 */
1293 public function getActions()
1294 {
1295 $legacy = $this->get_prop_legacy('end_action');
1296 if ($legacy) {
1297 $redirectUrl = $this->get_prop_legacy('redirect_url');
1298 $actions = [
1299 [
1300 'id' => intval($legacy),
1301 'redirectUrl' => $redirectUrl,
1302 ],
1303 ];
1304 $this->set_prop('actions', $actions);
1305 $this->delete_prop('end_action');
1306 $this->delete_prop('redirect_url');
1307
1308 return $this->mergeActions($actions);
1309 }
1310
1311 return $this->mergeActions($this->get_prop('actions'));
1312 }
1313
1314 private function mergeActions($actions)
1315 {
1316 $defaults = [
1317 [
1318 'id' => C::ACTION_NONE,
1319 'redirectUrl' => '',
1320 'message' => '',
1321 'coupon' => '',
1322 'wcStockStatus' => '',
1323 ],
1324 ];
1325
1326 if (empty($actions) || !is_array($actions)) {
1327 return $defaults;
1328 }
1329
1330 return array_map(function ($action) use ($defaults) {
1331 // Ensure we have an array
1332 $action = (array)$action;
1333
1334 // Parse with defaults to ensure all keys exist
1335 $action = wp_parse_args($action, $defaults[0]);
1336
1337 // Cast ID to integer
1338 $action['id'] = (int)$action['id'];
1339
1340 // Clean up message HTML if it exists
1341 if (!empty($action['message'])) {
1342 $action['message'] = preg_replace_callback(
1343 '#<style(\s=?[\S]*)?>([\s\S]*)?<\/style>#',
1344 function ($matches) {
1345 return isset($matches[1], $matches[2])
1346 ? '<style' . $matches[1] . '>' . preg_replace('/\<br(\s*)?\/?\>/i', '', $matches[2]) . '</style>'
1347 : '';
1348 },
1349 $action['message']
1350 );
1351 }
1352
1353 return $action;
1354 }, $actions);
1355 }
1356
1357
1358 public function get_action($needle)
1359 {
1360 $result = array_filter($this->actions, function ($action) use ($needle) {
1361 return $needle === (int)$action['id'];
1362 });
1363
1364 if (empty($result)) {
1365 return false;
1366 }
1367
1368 return array_shift($result);
1369 }
1370
1371 /**
1372 * Returns evergreen duration in seconds.
1373 *
1374 * @param array $duration
1375 *
1376 * @return int
1377 */
1378 public function durationInSeconds($duration = [])
1379 {
1380 list($d, $h, $m, $s) = empty($duration) ? $this->getDuration() : $duration;
1381
1382 return
1383 $d * DAY_IN_SECONDS +
1384 $h * HOUR_IN_SECONDS +
1385 $m * MINUTE_IN_SECONDS +
1386 $s;
1387 }
1388
1389 /**
1390 * Returns end datetime.
1391 *
1392 * @return string
1393 */
1394 public function getEndDatetime()
1395 {
1396 return $this->get_prop('end_datetime') ?: $this->defaultEndDatetime();
1397 }
1398
1399 /**
1400 * Return true if the recurrence is expired.
1401 *
1402 * @return bool
1403 */
1404 public function is_recurring_expired()
1405 {
1406 $endDate = $this->getRecurrenceEndDate();
1407
1408 if (!($endDate instanceof Carbon)) {
1409 return false;
1410 }
1411
1412 $now = Carbon::now(hurryt_tz($this->timezone));
1413
1414 return $now->greaterThan($endDate);
1415 }
1416
1417 /**
1418 * Calculate next recurrence.
1419 *
1420 * @return mixed
1421 * @throws \Exception
1422 */
1423 function timeToNextRecurrence()
1424 {
1425 $this->loadSettings();
1426
1427 if ($this->isMonthlyRecurring()) {
1428 $current = $this->getRecurrenceEndDate();
1429
1430 $next = $this->getRecurrenceEndDate(true);
1431
1432 $duration = $this->durationInSeconds($this->getRecurringDuration());
1433 $use_custom = $this->recurringDurationOption === 'custom' && $duration > 0;
1434
1435 if ($use_custom) {
1436 $time_remaining = $next->diffInSeconds($current);
1437 } else {
1438 $time_remaining = $this->getRecurringPauseDuration(true);
1439 }
1440 return $time_remaining;
1441 }
1442
1443 list($duration, $pause, $period) = $this->getRecurringDurationInSeconds();
1444
1445 $t = $pause ?: 0;
1446
1447 $waitBeforeRestart = $this->getRecurringPauseDuration(true);
1448
1449 $t += $waitBeforeRestart;
1450
1451 if (!$this->has_skipped_days()) {
1452 return $t;
1453 }
1454
1455 if ($this->isWeeklyRecurring()) {
1456 return $t;
1457 }
1458
1459 if ($this->recurringUnselectedDaysAction === 'hide') {
1460 return $t;
1461 }
1462
1463 return $waitBeforeRestart;
1464 }
1465
1466 /**
1467 * Return true if the current campaign can recur today.
1468 *
1469 * @return bool
1470 */
1471 public function can_recur_today()
1472 {
1473 $this->loadSettings();
1474
1475 $day = absint(Carbon::now(hurryt_tz($this->timezone))->format('w'));
1476
1477 $recur = in_array($day, array_map('absint', $this->recurringDays));
1478
1479 return apply_filters('hurryt_recur_today', $recur, $this->get_id());
1480 }
1481
1482 public function isMonthlyRecurring()
1483 {
1484 return $this->recurringFrequency === C::RECURRING_MONTHLY;
1485 }
1486
1487 public function isDayOfWeekRecurring()
1488 {
1489
1490 return $this->recurringMonthlyDayType === C::RECURRING_MONTHLY_DAY_OF_WEEK;
1491 }
1492
1493 public function isDayOfMonthRecurring()
1494 {
1495 return $this->recurringMonthlyDayType === C::RECURRING_MONTHLY_DAY_OF_MONTH;
1496 }
1497
1498 public function shouldEndRecurringByDate()
1499 {
1500 return absint($this->recurringEnd) === C::RECURRING_END_TIME;
1501 }
1502 public function shouldEndRecurringByRecurrences()
1503 {
1504 return (int)$this->recurringEnd === C::RECURRING_END_OCCURRENCES;
1505 }
1506
1507 public function shouldRecurForever()
1508 {
1509 return (int)$this->recurringEnd === C::RECURRING_END_NEVER;
1510 }
1511
1512 public function getRecurringRecurrences()
1513 {
1514 $num = (int)$this->recurringCount;
1515 return ++$num;
1516 }
1517
1518
1519 public function getRecurringPeriodEndDate()
1520 {
1521 list($d, $h, $m, $s) = $this->getRecurringDuration();
1522 $now = Carbon::now(hurryt_tz($this->timezone));
1523 $startDate = Carbon::parse($this->recurringStartTime, hurryt_tz($this->timezone));
1524 $interval = (int)$this->recurringInterval;
1525 $endDate = $now->copy();
1526
1527 switch ($this->recurringFrequency) {
1528 case C::RECURRING_DAILY:
1529 $endDate->minute = $startDate->minute;
1530 $endDate->addDays(max($interval, $d))->addHours($interval)->addMinutes($m)->addSeconds($s);
1531 break;
1532 case C::RECURRING_WEEKLY:
1533 $endDate->minute = $startDate->minute;
1534 $endDate->addWeeks($interval)->addHours($h)->addMinutes($m)->addSeconds($s);
1535 break;
1536 case C::RECURRING_HOURLY:
1537 $endDate->minute = $startDate->minute;
1538 $endDate->addHours(max($interval, $h))->addMinutes($m)->addSeconds($s);
1539 break;
1540 case C::RECURRING_MINUTELY:
1541 $minutes = max($interval, $m);
1542 $endDate->addMinutes($minutes)->addSeconds($s);
1543 break;
1544 }
1545
1546 return $endDate;
1547 }
1548
1549 public function getRecurringFrequencyInSeconds()
1550 {
1551 $freq = 0;
1552 switch ($this->recurringFrequency) {
1553 case C::RECURRING_WEEKLY:
1554 $freq = WEEK_IN_SECONDS;
1555 break;
1556 case C::RECURRING_DAILY:
1557 $freq = DAY_IN_SECONDS;
1558 break;
1559 case C::RECURRING_HOURLY:
1560 $freq = HOUR_IN_SECONDS;
1561 break;
1562 case C::RECURRING_MINUTELY:
1563 $freq = MINUTE_IN_SECONDS;
1564 break;
1565 }
1566
1567 return $freq * (int)$this->recurringInterval;
1568 }
1569 /**
1570 * @return array [duration, pause, period] in seconds
1571 */
1572 public function getRecurringDurationInSeconds()
1573 {
1574 $duration = $this->durationInSeconds($this->getRecurringDuration());
1575 $period = $this->getRecurringFrequencyInSeconds();
1576 $pause = $period - $duration;
1577 return [$duration, max(0, $pause), $period];
1578 }
1579
1580 /**
1581 * Returns the recurrence end date based on the current date/time.
1582 *
1583 * @param bool $next Return the next recurrence end date.
1584 *
1585 * @return Carbon
1586 */
1587
1588 public function getRecurrenceEndDate($next = false)
1589 {
1590
1591 $cached_key = $next ? 'recur_end_date__next' : 'recur_end_date';
1592 $cached = wp_cache_get($cached_key, 'hurrytimer');
1593 if ($cached instanceof Carbon) {
1594 return $cached;
1595 }
1596
1597 try {
1598
1599 $this->loadSettings();
1600 $now = Carbon::now(hurryt_tz($this->timezone));
1601 $startDate = Carbon::parse($this->recurringStartTime, hurryt_tz($this->timezone));
1602 $interval = (int)$this->recurringInterval;
1603
1604
1605 // Handle monthly recurring
1606 if ($this->isMonthlyRecurring()) {
1607 $start_date_modified = $startDate->copy();
1608
1609 if ($startDate->day >= 28) {
1610 $start_date_modified->startOfMonth();
1611 }
1612
1613 $endDate = $now->copy()->addMonths($interval);
1614
1615 if ($next) {
1616 $endDate->addMonths($interval);
1617 }
1618
1619 if ($this->shouldEndRecurringByDate()) {
1620 $endDate = Carbon::parse($this->recurringUntil, hurryt_tz($this->timezone));
1621 }
1622
1623 if ($this->isDayOfWeekRecurring()) {
1624 $endDate = $endDate->addMonths($interval);
1625 }
1626
1627 $period = CarbonPeriod::create($start_date_modified, 'P' . $interval . 'M', $endDate);
1628
1629 if ($this->shouldEndRecurringByRecurrences()) {
1630 $period->setRecurrences($this->getRecurringRecurrences());
1631 }
1632
1633 if ($period->count() === 0) {
1634 return $period->getEndDate();
1635 }
1636
1637 $dates = iterator_to_array($period);
1638
1639 $dates = array_map(function ($date) use ($startDate) {
1640 $is_overflow = $startDate->day >= $date->daysInMonth;
1641 if ($this->isDayOfMonthRecurring() && $is_overflow) {
1642 $date->endOfMonth();
1643 }
1644 if ($this->isDayOfWeekRecurring()) {
1645 $date->modifyWeekOfMonth($startDate);
1646 }
1647 $date->setTimeFrom($startDate);
1648
1649 return $date;
1650 }, $dates);
1651
1652
1653 $out_date = end($dates) ?: $period->getEndDate();
1654
1655 reset($dates);
1656
1657 foreach ($dates as $i => $date) {
1658 $duration = $this->durationInSeconds($this->getRecurringDuration());
1659 $custom = $this->recurringDurationOption === 'custom' && $duration > 0;
1660
1661 if ($custom && !$next) {
1662 $custom_end_date = $date->copy()->addSeconds($duration);
1663
1664 if ($custom_end_date->greaterThan($now)) {
1665 $out_date = $custom_end_date;
1666
1667 break;
1668 }
1669
1670 $next_date = isset($dates[$i + 1]) ? $dates[$i + 1] : $period->getEndDate();
1671
1672 if ($now->lessThan($next_date)) {
1673 $out_date = $custom_end_date;
1674 break;
1675 }
1676
1677 continue;
1678 }
1679
1680 if ($date->greaterThan($now)) {
1681 $pause_duration = $this->getRecurringPauseDuration(true);
1682
1683 if ($next && !$custom) {
1684 $out_date = isset($dates[$i + 1]) ? $dates[$i + 1] : $out_date;
1685
1686 if ($pause_duration > 0) {
1687 $out_date = $date;
1688 }
1689 } else {
1690 $out_date = $date;
1691 $prev_end_date = isset($dates[$i - 1]) ? $dates[$i - 1] : null;
1692 if ($prev_end_date && $prev_end_date->copy()->diffInSeconds($now) < $pause_duration) {
1693 $out_date = $prev_end_date;
1694 }
1695 }
1696
1697 break;
1698 }
1699 }
1700 if ($this->shouldEndRecurringByDate() && $out_date->lessThan($now)) {
1701 // TODO: Use the last recurrence instead of `getEndDate()`
1702 $out_date = $period->getEndDate();
1703 }
1704
1705 wp_cache_set($cached_key, $out_date, 'hurrytimer');
1706
1707 return $out_date;
1708 } // Monthly recurring
1709
1710
1711 // Prevent minutely recurring from affecting performance
1712 // by updating the start date year and month to current ones.
1713 if ($this->isMinutelyRecurring()) {
1714 $startDate->year = $now->year;
1715 $startDate->month = $now->month;
1716 $startDate->day = $now->day;
1717 }
1718
1719
1720 list($duration, $pause) = $this->getRecurringDurationInSeconds();
1721
1722 $period = CarbonPeriod::start($startDate, true);
1723
1724 $period->seconds($duration + $pause);
1725
1726 if ($this->shouldEndRecurringByRecurrences()) {
1727 $period->setRecurrences($this->getRecurringRecurrences());
1728 } else {
1729 $endDate = $this->getRecurringPeriodEndDate();
1730 $dateperiod_interval = new DateInterval('PT' .($duration + $pause) . 'S');
1731 $period = new DatePeriod($startDate->toDateTime(), $dateperiod_interval, $endDate->addDays(7 * $interval)->toDateTime());
1732 // $period->until($endDate->addDays(7 * $interval));
1733
1734 }
1735
1736 $date = null;
1737
1738 $i = 0;
1739
1740 $pause_duration = $this->getRecurringPauseDuration(true);
1741
1742 foreach ($period as $start) {
1743
1744 $start = Carbon::instance($start);
1745
1746 $end = $start->copy()->addSeconds($this->getRecurringFrequencyInSeconds() - 1);
1747
1748 if ($now->isBetween($start, $end->copy()->addSeconds($pause_duration))) {
1749 $date = $start->copy()->addSeconds($duration - 1);
1750 if (!in_array($date->format('w'), $this->recurringDays)) {
1751 continue;
1752 }
1753 break;
1754 }
1755
1756 if ($now->isBefore($end->copy()->addSeconds($pause_duration))) {
1757 $date = $start;
1758 if (!in_array($date->format('w'), $this->recurringDays)) {
1759 continue;
1760 }
1761 break;
1762 }
1763 }
1764
1765 if (empty($date)) {
1766 return $period->getEndDate();
1767 }
1768
1769 $stop_date = Carbon::parse($this->recurringUntil, hurryt_tz($this->timezone));
1770
1771 if ($this->shouldEndRecurringByDate() && $stop_date->lessThan($date)) {
1772 return $stop_date;
1773 }
1774
1775 return $date;
1776 } catch (\Exception $e) {
1777 return false;
1778 }
1779 }
1780
1781
1782 function should_hide_today($date, $now)
1783 {
1784 return ($date->format('w') === $now->format('w')) && ($this->isWeeklyRecurring() || $this->recurringUnselectedDaysAction === 'hide');
1785 }
1786 function has_skipped_days()
1787 {
1788 $recur_days = array_map('intval', $this->recurringDays);
1789 return count($recur_days) < 7;
1790 }
1791 function can_recur_on($date)
1792 {
1793 $recur_days = array_map('intval', $this->recurringDays);
1794
1795 if (in_array($date->format('w'), $recur_days)) {
1796 return true;
1797 }
1798
1799 if (($date->isStartOfDay() && in_array($this->get_previous_day($date->format('w')), $recur_days))) {
1800 return true;
1801 }
1802
1803 return false;
1804 }
1805 private function get_previous_day($current_day)
1806 {
1807 $current_day === 0 ? 6 : ($current_day - 1);
1808 }
1809
1810 /**
1811 * Returns compaign publish datetime.
1812 *
1813 * @return mixed
1814 */
1815 public function getStartTimestamp()
1816 {
1817 return get_the_date($this->id);
1818 }
1819
1820 /**
1821 * Returns position in WooCommerce product page.
1822 * ::PROP
1823 *
1824 * @return mixed
1825 */
1826 public function getWcPosition()
1827 {
1828 $legacy = $this->get_prop_legacy('position');
1829
1830 if (!$legacy) {
1831 return $this->get_prop('wc_position');
1832 }
1833 $this->set_prop('wc_position', $legacy);
1834 $this->delete_prop('position');
1835
1836 return $legacy;
1837 }
1838
1839 private function get_prop_legacy($name)
1840 {
1841 return get_post_meta($this->id, $name, true);
1842 }
1843
1844 /**
1845 * Returns true if WooCommerce integration is enabled.
1846 * ::PROP
1847 *
1848 * @return mixed
1849 */
1850 public function getWcEnable()
1851 {
1852 $value = $this->get_prop('wc_enable', false);
1853
1854 if ($value === C::YES || $value === C::NO) {
1855 return $value;
1856 }
1857
1858 $legacy = filter_var($value, FILTER_VALIDATE_BOOLEAN);
1859 if ($legacy) {
1860 $this->set_prop('wc_enable', C::YES);
1861 } else {
1862 $this->set_prop('wc_enable', C::NO);
1863 }
1864
1865 return $this->get_prop('wc_enable');
1866 }
1867
1868 /**
1869 * Returns true if label should be displayed.
1870 *
1871 * @return mixed
1872 */
1873 public function getLabelVisibility()
1874 {
1875 $meta = 'label_visibility';
1876 $value = $this->get_prop($meta, false);
1877 if ($value === C::NO || $value === C::YES) {
1878 return $value;
1879 }
1880
1881 $legacy = filter_var(
1882 $this->get_prop_legacy('display_labels'),
1883 FILTER_VALIDATE_BOOLEAN
1884 );
1885 $legacy = $legacy ? C::YES : C::NO;
1886
1887 $this->set_prop($meta, $legacy);
1888
1889 $this->delete_prop('display_labels');
1890
1891 return $this->get_prop($meta);
1892 }
1893
1894 /**
1895 *
1896 * Returns restart type.
1897 *
1898 * @return int
1899 */
1900 public function getRestart()
1901 {
1902 return $this->get_prop('restart') ?: C::RESTART_NONE;
1903 }
1904
1905 // removeIf(pro)
1906 public function setRestart($value)
1907 {
1908 if ($value == C::RESTART_AFTER_DURATION) {
1909 return;
1910 }
1911 $this->set_prop('restart', $value);
1912 }
1913 // endRemoveIf(pro)
1914 /**
1915 * Returns WooCommerce products selection type.
1916 *
1917 * @return string
1918 */
1919 public function getWcProductsSelectionType()
1920 {
1921 $legacy = $this->get_prop_legacy('products_type');
1922 if (!$legacy) {
1923 return $this->get_prop('wc_products_selection_type');
1924 }
1925 $this->set_prop('wc_products_selection_type', $legacy);
1926 $this->delete_prop('products_type');
1927
1928 return $legacy;
1929 }
1930
1931 /**
1932 * Delete setting item.
1933 *
1934 * @param $name
1935 */
1936 public function delete_prop($name)
1937 {
1938 delete_post_meta($this->id, $name);
1939 }
1940
1941 /**
1942 * Returns products selection IDs.
1943 * ::PROP
1944 *
1945 * @return array
1946 */
1947 public function getWcProductsSelection()
1948 {
1949 $result = $this->get_prop_legacy('products');
1950 if (!$result) {
1951 $result = $this->get_prop('wc_products_selection');
1952 } else {
1953 $this->set_prop('wc_products_selection', $result);
1954 $this->delete_prop('products');
1955 }
1956
1957 return is_array($result) ? $result : [];
1958 }
1959
1960 /**
1961 * Store custom labels.
1962 * ::PROP
1963 *
1964 * @param $labels
1965 */
1966 public function setLabels($labels)
1967 {
1968 // Sanitize each label to prevent XSS
1969 if (is_array($labels)) {
1970 $labels = array_map('sanitize_text_field', $labels);
1971 }
1972 $labels = wp_parse_args($labels, $this->labels);
1973 update_post_meta($this->id, 'labels', $labels);
1974 }
1975
1976 public function getLabels()
1977 {
1978 return wp_parse_args($this->get_prop('labels'), $this->labels);
1979 }
1980
1981 /**
1982 * Returns true if compaign can be published.
1983 *
1984 * @return bool
1985 */
1986 public function is_active()
1987 {
1988 return get_post_status($this->id) === "publish"
1989 || get_post_status($this->id) === "future";
1990 }
1991
1992 /**
1993 * Returns trus if the compaign is scheduled.
1994 *
1995 * @return bool
1996 */
1997 public function is_scheduled()
1998 {
1999 $scheduled = get_post_status($this->get_id()) === "future";
2000 if ($scheduled) {
2001 return true;
2002 }
2003
2004 if ($this->is_recurring()) {
2005 $start_date = Carbon::parse($this->recurringStartTime, hurryt_tz($this->timezone));
2006 $now = Carbon::now(hurryt_tz($this->timezone));
2007 $now->second = $start_date->second;
2008 if ($now->lessThan($start_date)) {
2009
2010 return true;
2011 }
2012 }
2013
2014 return $scheduled;
2015 }
2016
2017 /**
2018 * Returns true if fixed campaign datetime is expired.
2019 *
2020 * @param string|null $date
2021 *
2022 * @return bool
2023 */
2024 public function is_one_time_expired($date = null)
2025 {
2026 $endDate = $date === null ? date_create($this->endDatetime) : $date;
2027 $today = date_create(current_time("mysql"));
2028
2029 return $endDate < $today;
2030 }
2031
2032 /**
2033 * Check if a regular campaign with multiple deadlines has all deadlines expired.
2034 *
2035 * @return bool
2036 */
2037 public function is_all_date_ranges_expired()
2038 {
2039 $ranges = $this->getDateRanges();
2040 if (empty($ranges)) {
2041 return false; // No ranges means single-date mode, handled by is_one_time_expired
2042 }
2043
2044 $now = Carbon::now(hurryt_tz($this->timezone));
2045
2046 foreach ($ranges as $range) {
2047 if (empty($range['end'])) {
2048 continue;
2049 }
2050 $end = Carbon::parse($range['end'], hurryt_tz($this->timezone));
2051 if ($end->greaterThan($now)) {
2052 return false;
2053 }
2054 }
2055
2056 return true;
2057 }
2058
2059 /**
2060 * Get the next upcoming deadline from the date ranges list.
2061 *
2062 * @return Carbon|null
2063 */
2064 public function getActiveDateRange()
2065 {
2066 $this->loadSettings();
2067 $ranges = $this->getDateRanges();
2068
2069 if (empty($ranges)) {
2070 return null;
2071 }
2072
2073 $now = Carbon::now(hurryt_tz($this->timezone));
2074
2075 // Collect and sort end dates chronologically
2076 $endDates = [];
2077 foreach ($ranges as $range) {
2078 if (empty($range['end'])) {
2079 continue;
2080 }
2081 $endDates[] = Carbon::parse($range['end'], hurryt_tz($this->timezone));
2082 }
2083
2084 usort($endDates, function ($a, $b) {
2085 return $a->timestamp - $b->timestamp;
2086 });
2087
2088 // Return the first deadline that hasn't passed yet
2089 foreach ($endDates as $end) {
2090 if ($end->greaterThan($now)) {
2091 return $end;
2092 }
2093 }
2094
2095 return null;
2096 }
2097
2098 /**
2099 * Get all future deadlines (end dates that haven't expired yet), sorted chronologically.
2100 *
2101 * @return array Each entry: ['end' => Carbon, 'headline' => string]
2102 */
2103 public function getFutureDateRanges()
2104 {
2105 $this->loadSettings();
2106 $ranges = $this->getDateRanges();
2107
2108 if (empty($ranges)) {
2109 return [];
2110 }
2111
2112 $now = Carbon::now(hurryt_tz($this->timezone));
2113 $result = [];
2114
2115 foreach ($ranges as $range) {
2116 if (empty($range['end'])) {
2117 continue;
2118 }
2119 $end = Carbon::parse($range['end'], hurryt_tz($this->timezone));
2120
2121 if ($end->greaterThan($now)) {
2122 $result[] = [
2123 'end' => $end,
2124 'headline' => isset($range['headline']) ? $range['headline'] : '',
2125 ];
2126 }
2127 }
2128
2129 usort($result, function ($a, $b) {
2130 return $a['end']->timestamp - $b['end']->timestamp;
2131 });
2132
2133 return $result;
2134 }
2135
2136 /**
2137 * Get date ranges array.
2138 *
2139 * @return array
2140 */
2141 public function getDateRanges()
2142 {
2143 $ranges = $this->get_prop('date_ranges', false);
2144 if (!empty($ranges) && is_array($ranges)) {
2145 return $ranges;
2146 }
2147 return [];
2148 }
2149
2150 /**
2151 * Set date ranges array.
2152 *
2153 * @param array|string $value
2154 */
2155 public function setDateRanges($value)
2156 {
2157 if (is_string($value)) {
2158 $value = json_decode(stripslashes($value), true);
2159 }
2160 if (!is_array($value)) {
2161 $value = [];
2162 }
2163 // Filter out entries without an end date
2164 $value = array_filter($value, function ($entry) {
2165 return !empty($entry['end']);
2166 });
2167 // Re-index
2168 $value = array_values($value);
2169 // Use update_post_meta directly to allow saving an empty array
2170 update_post_meta($this->get_id(), 'date_ranges', $value);
2171 }
2172
2173 /**
2174 * Check if this regular campaign uses multiple deadlines.
2175 *
2176 * @return bool
2177 */
2178 public function hasMultipleDateRanges()
2179 {
2180 $ranges = $this->getDateRanges();
2181 return count($ranges) > 1;
2182 }
2183
2184 public function is_sticky_dismissed()
2185 {
2186 return isset($_COOKIE[Cookie_Detection::COOKIE_PREFIX . $this->get_id() . '_dismissed'])
2187 && $this->stickyBarDismissible === C::YES
2188 && $this->enableSticky === C::YES;
2189 }
2190
2191 /**
2192 * Returns compaign template wrapper.
2193 *
2194 * @param string
2195 *
2196 * @return string
2197 */
2198 public function wrap_template($content, $options = [])
2199 {
2200 $campaignBuilder = new CampaignBuilder($this);
2201
2202 return apply_filters('hurryt_campaign_content', $campaignBuilder->build($content, $options), $this->get_id());
2203 }
2204
2205 /*
2206 * Returns campaign template content.
2207 */
2208 public function build_template()
2209 {
2210 $campaignBuilder = new CampaignBuilder($this);
2211
2212 return $campaignBuilder->template();
2213 }
2214
2215 /**
2216 * ::PROP
2217 *
2218 * @param $value
2219 */
2220 public function setWcConditions($value)
2221 {
2222 $this->set_prop('_hurryt_wc_conditions', !empty($value) ? $value : []);
2223 }
2224
2225 /**
2226 * ::PROP
2227 *
2228 * @return mixed
2229 */
2230 public function getWcConditions()
2231 {
2232 return $this->get_prop('_hurryt_wc_conditions', false);
2233 }
2234
2235
2236 public function setRecurringUnselectedDaysAction($value)
2237 {
2238 $this->set_prop('_hurryt_recurring_unselected_days_action', $value);
2239 }
2240
2241 public function getRecurringUnselectedDaysAction()
2242 {
2243 return $this->get_prop('_hurryt_recurring_unselected_days_action');
2244 }
2245
2246 public function is_running()
2247 {
2248 // TODO: add support for weekly campaigns.
2249 if (
2250 $this->is_recurring()
2251 && !$this->can_recur_today()
2252 && ($this->recurringUnselectedDaysAction === 'hide' || $this->isWeeklyRecurring())
2253 ) {
2254 return false;
2255 }
2256
2257 $not_running = !$this->is_active() || $this->is_scheduled()
2258 || ($this->is_recurring() && empty($this->getRecurrenceEndDate()));
2259
2260
2261 return !$not_running;
2262 }
2263
2264 /**
2265 * Check if recurring or onetime campaign is expired.
2266 *
2267 * @return bool
2268 */
2269 public function is_expired()
2270 {
2271 if ($this->is_one_time() && $this->hasMultipleDateRanges()) {
2272 return $this->is_all_date_ranges_expired();
2273 }
2274 return ($this->is_one_time() && $this->is_one_time_expired())
2275 || ($this->is_recurring() && $this->is_recurring_expired());
2276 }
2277
2278 public function get_mode_slug()
2279 {
2280 switch ($this->mode) {
2281 case C::MODE_EVERGREEN:
2282 return 'evergreen';
2283 case C::MODE_RECURRING:
2284 return 'recurring';
2285 case C::MODE_REGULAR:
2286 return 'one_time';
2287 }
2288 }
2289
2290 public function getReloadReset()
2291 {
2292 return filter_var($this->get_prop('reload_reset'), FILTER_VALIDATE_BOOLEAN);
2293 }
2294
2295 public function getDetectionMethods()
2296 {
2297 return array_map('intval', $this->get_prop('_hurryt_detection_methods'));
2298 }
2299
2300 public function setDetectionMethods($methods)
2301 {
2302 // removeIf(pro)
2303 if (is_array($methods)) {
2304 $methods = array_filter($methods, function ($m) {
2305 return (int)$m !== C::DETECTION_METHOD_USER_SESSION;
2306 });
2307 }
2308 // endRemoveIf(pro)
2309 $this->set_prop('_hurryt_detection_methods', $methods);
2310 }
2311
2312
2313
2314 /**
2315 * @return bool
2316 */
2317 private function isDailyRecurring()
2318 {
2319 return $this->recurringFrequency === C::RECURRING_DAILY;
2320 }
2321
2322 /**
2323 * @return bool
2324 */
2325 private function isWeeklyRecurring()
2326 {
2327 return $this->recurringFrequency === C::RECURRING_WEEKLY;
2328 }
2329
2330 /**
2331 * @return bool
2332 */
2333 private function isHourlyRecurring()
2334 {
2335 return $this->recurringFrequency === C::RECURRING_HOURLY;
2336 }
2337
2338 /**
2339 * @return bool
2340 */
2341 private function isMinutelyRecurring()
2342 {
2343 return $this->recurringFrequency === C::RECURRING_MINUTELY;
2344 }
2345
2346 /**
2347 * Get the end time for evergreen timers
2348 *
2349 * @return string|null
2350 */
2351 public function getEndTime()
2352 {
2353 return $this->get_prop('end_time');
2354 }
2355
2356 /**
2357 * Set the end time for evergreen timers
2358 *
2359 * @param string $time
2360 */
2361 public function setEndTime($time)
2362 {
2363 $this->set_prop('end_time', $time);
2364 }
2365
2366 /**
2367 * Get the cookie name for this campaign
2368 *
2369 * @return string
2370 */
2371 public function getCookieName()
2372 {
2373 return Cookie_Detection::cookieName($this->id);
2374 }
2375
2376 /**
2377 * Get the reset token for this campaign
2378 *
2379 * @return string|null
2380 */
2381 public function getResetToken()
2382 {
2383 $evergreenCampaign = new EvergreenCampaign($this->id);
2384 $evergreenCampaign->loadSettings();
2385 return $evergreenCampaign->getInitiatedResetToken();
2386 }
2387
2388 /**
2389 * Check if the campaign should reset
2390 *
2391 * @return bool
2392 */
2393 public function shouldReset()
2394 {
2395 $evergreenCampaign = new EvergreenCampaign($this->id);
2396 $evergreenCampaign->loadSettings();
2397 return $evergreenCampaign->shouldResetTimer();
2398 }
2399
2400 /**
2401 * Get whether the campaign runs in background
2402 *
2403 * @return bool
2404 */
2405 public function getRunInBackground()
2406 {
2407 return $this->get_prop('run_in_background', false);
2408 }
2409
2410 /**
2411 * Get the end day for evergreen timers
2412 *
2413 * @return int
2414 */
2415 public function getEvergreenEndDay()
2416 {
2417 return (int)$this->get_prop('evergreen_end_day', false) ?: 0;
2418 }
2419
2420 /**
2421 * Set the end day for evergreen timers
2422 *
2423 * @param int $day
2424 */
2425 public function setEvergreenEndDay($day)
2426 {
2427 $this->set_prop('evergreen_end_day', (int)$day);
2428 }
2429
2430 public function getEvergreenEndTime()
2431 {
2432 return $this->get_prop('evergreen_end_time');
2433 }
2434
2435 public function setEvergreenEndTime($time)
2436 {
2437 $this->set_prop('evergreen_end_time', $time);
2438 }
2439
2440 public function getEvergreenEndType()
2441 {
2442 return $this->get_prop('evergreen_end_type');
2443 }
2444
2445 public function setEvergreenEndType($type){
2446
2447 // removeIf(pro)
2448 if(! hurrytimer_is_pro() ){
2449 $type = 'duration';
2450 }
2451 // endRemoveIf(pro)
2452 $this->set_prop('evergreen_end_type', $type);
2453 }
2454
2455 }
2456