PluginProbe
HurryTimer – An Scarcity and Urgency Countdown Timer for WordPress & WooCommerce / 2.6.2
HurryTimer – An Scarcity and Urgency Countdown Timer for WordPress & WooCommerce v2.6.2
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
← All changes | includes/Campaign.php +78 -706 trunk2.6.2 View file →
@@ -1,11 +1,8 @@
1 1 <?php
2 2
3 3 namespace Hurrytimer;
4 4
5 -use DateInterval;
6 -use DatePeriod;
7 -use DateTime;
8 5 use Hurrytimer\Dependencies\Carbon\Carbon;
9 6 use Hurrytimer\Dependencies\Carbon\CarbonPeriod;
10 7 use Hurrytimer\Utils\Helpers;
11 8
@@ -42,12 +39,9 @@
42 39 *
43 40 * @var int
44 41 */
45 42 public $recurringDuration;
46 - public $recurringPauseDuration = ['days' => 0, 'hours' => 0, 'minutes' => 0, 'seconds' => 0];
47 43
48 - public $recurringDurationOption = 'none';
49 -
50 44 /**
51 45 * Recurrence start date/time
52 46 *
53 47 * @var string
@@ -305,9 +299,9 @@
305 299 *
306 300 * @var int
307 301 */
308 302
309 - public $blockSize = 'auto';
303 + public $blockSize = 50;
310 304
311 305 /**
312 306 * Block background color.
313 307 *
@@ -501,33 +495,17 @@
501 495 C::DETECTION_METHOD_COOKIE,
502 496 C::DETECTION_METHOD_IP,
503 497 ];
504 498
505 - public $timezoneType = 'site';
506 - public $timezone;
507 499
508 -
509 500 public $recurringMonthlyDayType = C::RECURRING_MONTHLY_DAY_OF_MONTH;
510 501
511 502
512 - public $recurringUnselectedDaysAction = 'skip';
503 + public $recurringUnselectedDaysAction = 'hide';
513 504
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 505 public function __construct($id)
528 506 {
529 - $this->recurringStartTime = Carbon::now(hurryt_tz($this->getTimezone()))->format('Y-m-d h:i A');
507 + $this->recurringStartTime = Carbon::now(hurryt_tz())->format('Y-m-d h:i A');
530 508 $this->id = $id;
531 509 }
532 510
533 511 /**
@@ -572,9 +550,9 @@
572 550
573 551 case 'specific_pages':
574 552 $pageIds = $this->getStickyBarPages();
575 553
576 - if (!empty($pageId) && !empty($pageIds) && in_array($pageId, $pageIds)) {
554 + if (!empty($pageId) && !empty($pageIds) && in_array($pageId, $pageIds, true)) {
577 555 return apply_filters('hurryt_show_sticky_bar', true, $this->get_id());
578 556 }
579 557
580 558 foreach ($this->stickyIncludeUrls as $url) {
@@ -585,12 +563,13 @@
585 563 break;
586 564 }
587 565 }
588 566 return apply_filters('hurryt_show_sticky_bar', false, $this->get_id());
589 -
567 +
590 568 case 'exclude_pages':
591 569 $pageIds = $this->getStickyExcludePages();
592 - if (!empty($pageId) && !empty($pageIds) && in_array($pageId, $pageIds)) {
570 +
571 + if (!empty($pageId) && !empty($pageIds) && in_array($pageId, $pageIds, true)) {
593 572 return apply_filters('hurryt_show_sticky_bar', false, $this->get_id());
594 573 }
595 574
596 575 foreach ($this->stickyExcludeUrls as $url) {
@@ -716,26 +695,12 @@
716 695 * @param $data
717 696 */
718 697 public function storeSettings($data)
719 698 {
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 699 foreach ($data as $prop => $value) {
730 700 $method = Helpers::snakeToCamelCase("set_{$prop}");
731 701 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 - }
702 + $this->$method($value ?: $this->$prop);
738 703 } elseif (property_exists(__CLASS__, Helpers::snakeToCamelCase($prop))) {
739 704 $this->set_prop($prop, $value);
740 705 }
741 706 }
@@ -744,14 +709,10 @@
744 709 }
745 710 if (!isset($data['sticky_bar_pages'])) {
746 711 $this->setStickyBarPages([]);
747 712 }
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 713
753 - CSS_Builder::get_instance()->generate_css();
714 + CSS_Builder::get_instance()->build();
754 715 }
755 716
756 717 //removeIf(pro)
757 718
@@ -760,9 +721,9 @@
760 721 * ::PROP
761 722 */
762 723 public function setMode($mode)
763 724 {
764 - if ($mode == C::MODE_RECURRING) {
725 + if ( $mode == C::MODE_RECURRING ) {
765 726 return;
766 727 }
767 728 $this->set_prop('mode', $mode);
768 729 }
@@ -784,184 +745,28 @@
784 745 } else {
785 746 $this->{$name} = $this->get_prop(Helpers::camelToSnakeCase($name));
786 747 }
787 748 }
749 + }
788 750
789 - }
790 751
791 752 public function getHeadline()
792 753 {
793 - $content = $this->get_prop('_hurryt_headline', false);
754 + if (empty($this->id)) {
755 + return $this->headline;
756 + }
757 + if (metadata_exists('post', $this->id, '_hurryt_headline')) {
758 + return $this->get_prop('_hurryt_headline');
759 + }
794 760
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;
761 + return get_the_title($this->id);
805 762 }
806 763
807 764 public function setHeadline($value)
808 765 {
809 - $value = $this->sanitize_headline_html( $value );
810 766 $this->set_prop('_hurryt_headline', $value);
811 767 }
812 768
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 769 public function is_published()
965 770 {
966 771 return get_post_status($this->id) === "publish";
967 772 }
@@ -996,25 +801,8 @@
996 801 {
997 802 return $this->get_prop('mode') == C::MODE_RECURRING;
998 803 }
999 804
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 805 /**
1018 806 * Save compaign endtime for regular mode.
1019 807 *
1020 808 * @param $date
@@ -1020,10 +808,8 @@
1020 808 * @param $date
1021 809 */
1022 810 public function setEndDatetime($date)
1023 811 {
1024 - // Sanitize the date input to prevent XSS
1025 - $date = sanitize_text_field($date);
1026 812 $this->set_prop('end_datetime', $date ?: $this->defaultEndDatetime());
1027 813 }
1028 814
1029 815 /**
@@ -1216,9 +1002,8 @@
1216 1002 return array_pad(array_map('intval', $duration), 4, 0);
1217 1003 }
1218 1004
1219 1005
1220 -
1221 1006 public function getRestartDuration($asSeconds = false)
1222 1007 {
1223 1008 $duration = (array)$this->get_prop('hurryt_restart_duration', false);
1224 1009
@@ -1235,27 +1020,8 @@
1235 1020 }
1236 1021 return $duration;
1237 1022 }
1238 1023
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 1024 public function setRestartDuration($duration_array)
1259 1025 {
1260 1026 $this->set_prop('hurryt_restart_duration', $duration_array);
1261 1027 }
@@ -1267,13 +1033,10 @@
1267 1033 public function setRecurringDuration($value)
1268 1034 {
1269 1035 $this->set_prop('_hurryt_recurring_duration', $value);
1270 1036 }
1271 - public function setRecurringPauseDuration($value)
1272 - {
1273 - $this->set_prop('_hurryt_recurring_pause_duration', $value);
1274 - }
1275 1037
1038 +
1276 1039 /**
1277 1040 * Returns evergreen duration.
1278 1041 *
1279 1042 * @return array
@@ -1322,36 +1085,20 @@
1322 1085 'wcStockStatus' => '',
1323 1086 ],
1324 1087 ];
1325 1088
1326 - if (empty($actions) || !is_array($actions)) {
1089 + if (count($actions) === 0) {
1327 1090 return $defaults;
1328 1091 }
1329 1092
1330 1093 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 1094 $action['id'] = (int)$action['id'];
1339 1095
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 - }
1096 + $action['message'] = preg_replace_callback('#<style(\s=?[\S]*)?>([\s\S]*)?<\/style>#', function ($matches) {
1097 + return '<style' . $matches[1] . '>' . preg_replace('/\<br(\s*)?\/?\>/i', '', $matches[2]) . '</style>';
1098 + }, $action['message']);
1352 1099
1353 - return $action;
1100 + return array_merge($defaults[0], $action);
1354 1101 }, $actions);
1355 1102 }
1356 1103
1357 1104
@@ -1408,9 +1155,9 @@
1408 1155 if (!($endDate instanceof Carbon)) {
1409 1156 return false;
1410 1157 }
1411 1158
1412 - $now = Carbon::now(hurryt_tz($this->timezone));
1159 + $now = Carbon::now(hurryt_tz());
1413 1160
1414 1161 return $now->greaterThan($endDate);
1415 1162 }
1416 1163
@@ -1425,43 +1172,13 @@
1425 1172 $this->loadSettings();
1426 1173
1427 1174 if ($this->isMonthlyRecurring()) {
1428 1175 $current = $this->getRecurrenceEndDate();
1429 -
1430 1176 $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;
1177 + return absint($next->diffInSeconds($current));
1441 1178 }
1442 -
1443 1179 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;
1180 + return $pause ?: 0;
1464 1181 }
1465 1182
1466 1183 /**
1467 1184 * Return true if the current campaign can recur today.
@@ -1471,9 +1188,9 @@
1471 1188 public function can_recur_today()
1472 1189 {
1473 1190 $this->loadSettings();
1474 1191
1475 - $day = absint(Carbon::now(hurryt_tz($this->timezone))->format('w'));
1192 + $day = absint(Carbon::now(hurryt_tz())->format('w'));
1476 1193
1477 1194 $recur = in_array($day, array_map('absint', $this->recurringDays));
1478 1195
1479 1196 return apply_filters('hurryt_recur_today', $recur, $this->get_id());
@@ -1518,10 +1235,10 @@
1518 1235
1519 1236 public function getRecurringPeriodEndDate()
1520 1237 {
1521 1238 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));
1239 + $now = Carbon::now(hurryt_tz());
1240 + $startDate = Carbon::parse($this->recurringStartTime, hurryt_tz());
1524 1241 $interval = (int)$this->recurringInterval;
1525 1242 $endDate = $now->copy();
1526 1243
1527 1244 switch ($this->recurringFrequency) {
@@ -1579,32 +1296,22 @@
1579 1296
1580 1297 /**
1581 1298 * Returns the recurrence end date based on the current date/time.
1582 1299 *
1583 - * @param bool $next Return the next recurrence end date.
1584 - *
1585 1300 * @return Carbon
1586 1301 */
1587 -
1588 1302 public function getRecurrenceEndDate($next = false)
1589 1303 {
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 1304 try {
1598 -
1599 1305 $this->loadSettings();
1600 - $now = Carbon::now(hurryt_tz($this->timezone));
1601 - $startDate = Carbon::parse($this->recurringStartTime, hurryt_tz($this->timezone));
1306 + $now = Carbon::now(hurryt_tz());
1307 + $startDate = Carbon::parse($this->recurringStartTime, hurryt_tz());
1602 1308 $interval = (int)$this->recurringInterval;
1603 1309
1604 1310
1605 1311 // Handle monthly recurring
1606 1312 if ($this->isMonthlyRecurring()) {
1313 +
1607 1314 $start_date_modified = $startDate->copy();
1608 1315
1609 1316 if ($startDate->day >= 28) {
1610 1317 $start_date_modified->startOfMonth();
@@ -1616,15 +1323,11 @@
1616 1323 $endDate->addMonths($interval);
1617 1324 }
1618 1325
1619 1326 if ($this->shouldEndRecurringByDate()) {
1620 - $endDate = Carbon::parse($this->recurringUntil, hurryt_tz($this->timezone));
1327 + $endDate = Carbon::parse($this->recurringUntil, hurryt_tz());
1621 1328 }
1622 1329
1623 - if ($this->isDayOfWeekRecurring()) {
1624 - $endDate = $endDate->addMonths($interval);
1625 - }
1626 -
1627 1330 $period = CarbonPeriod::create($start_date_modified, 'P' . $interval . 'M', $endDate);
1628 1331
1629 1332 if ($this->shouldEndRecurringByRecurrences()) {
1630 1333 $period->setRecurrences($this->getRecurringRecurrences());
@@ -1633,79 +1336,44 @@
1633 1336 if ($period->count() === 0) {
1634 1337 return $period->getEndDate();
1635 1338 }
1636 1339
1637 - $dates = iterator_to_array($period);
1340 + $date = $period->first();
1341 + $i = 0;
1638 1342
1639 - $dates = array_map(function ($date) use ($startDate) {
1640 - $is_overflow = $startDate->day >= $date->daysInMonth;
1641 - if ($this->isDayOfMonthRecurring() && $is_overflow) {
1343 + while ($period->valid()) {
1344 +
1345 + $date = $period->current();
1346 +
1347 + $isOverflow = $startDate->day >= $date->daysInMonth;
1348 +
1349 + if ($this->isDayOfMonthRecurring() && $isOverflow) {
1642 1350 $date->endOfMonth();
1643 1351 }
1352 +
1644 1353 if ($this->isDayOfWeekRecurring()) {
1645 1354 $date->modifyWeekOfMonth($startDate);
1646 1355 }
1356 +
1647 1357 $date->setTimeFrom($startDate);
1648 1358
1649 - return $date;
1650 - }, $dates);
1359 + if ($date->greaterThan($now)) {
1651 1360
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;
1361 + if (!$next || $i === 1) {
1362 + return $date;
1668 1363 }
1669 1364
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;
1365 + $i++;
1678 1366 }
1679 1367
1680 - if ($date->greaterThan($now)) {
1681 - $pause_duration = $this->getRecurringPauseDuration(true);
1368 + $period->next();
1369 + }
1682 1370
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 - }
1371 + if ($this->shouldEndRecurringByDate() && $date->lessThan($now)) {
1372 + return $period->getEndDate();
1699 1373 }
1700 - if ($this->shouldEndRecurringByDate() && $out_date->lessThan($now)) {
1701 - // TODO: Use the last recurrence instead of `getEndDate()`
1702 - $out_date = $period->getEndDate();
1703 - }
1704 1374
1705 - wp_cache_set($cached_key, $out_date, 'hurrytimer');
1706 -
1707 - return $out_date;
1375 + return $date;
1708 1376 } // Monthly recurring
1709 1377
1710 1378
1711 1379 // Prevent minutely recurring from affecting performance
@@ -1718,9 +1386,9 @@
1718 1386
1719 1387
1720 1388 list($duration, $pause) = $this->getRecurringDurationInSeconds();
1721 1389
1722 - $period = CarbonPeriod::start($startDate, true);
1390 + $period = CarbonPeriod::start($startDate);
1723 1391
1724 1392 $period->seconds($duration + $pause);
1725 1393
1726 1394 if ($this->shouldEndRecurringByRecurrences()) {
@@ -1726,41 +1394,38 @@
1726 1394 if ($this->shouldEndRecurringByRecurrences()) {
1727 1395 $period->setRecurrences($this->getRecurringRecurrences());
1728 1396 } else {
1729 1397 $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 -
1398 + $period->until($endDate->addDays(7 * $interval));
1734 1399 }
1735 1400
1736 - $date = null;
1401 + $date = false;
1402 + $skip = false;
1403 + while ($period->valid()) {
1737 1404
1738 - $i = 0;
1405 + $date = $period->current();
1406 + $skip_day = $this->should_skip_day($date);
1407 + if ($date->greaterThan($now)) {
1739 1408
1740 - $pause_duration = $this->getRecurringPauseDuration(true);
1409 + if ($skip_day) {
1741 1410
1742 - foreach ($period as $start) {
1411 + $skip = true;
1743 1412
1744 - $start = Carbon::instance($start);
1745 -
1746 - $end = $start->copy()->addSeconds($this->getRecurringFrequencyInSeconds() - 1);
1413 + if ($this->recurringUnselectedDaysAction === 'hide' || $this->isWeeklyRecurring()) {
1414 + return false;
1415 + }
1747 1416
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)) {
1417 + $period->next();
1751 1418 continue;
1752 1419 }
1753 - break;
1754 - }
1755 1420
1756 - if ($now->isBefore($end->copy()->addSeconds($pause_duration))) {
1757 - $date = $start;
1758 - if (!in_array($date->format('w'), $this->recurringDays)) {
1759 - continue;
1421 + if (!$skip) {
1422 + $date = $pause ? $date->subSeconds($pause) : $date;
1760 1423 }
1761 1424 break;
1762 1425 }
1426 +
1427 + $period->next();
1763 1428 }
1764 1429
1765 1430 if (empty($date)) {
1766 1431 return $period->getEndDate();
@@ -1765,14 +1430,13 @@
1765 1430 if (empty($date)) {
1766 1431 return $period->getEndDate();
1767 1432 }
1768 1433
1769 - $stop_date = Carbon::parse($this->recurringUntil, hurryt_tz($this->timezone));
1434 + $stop_date = Carbon::parse($this->recurringUntil, hurryt_tz());
1770 1435
1771 1436 if ($this->shouldEndRecurringByDate() && $stop_date->lessThan($date)) {
1772 1437 return $stop_date;
1773 1438 }
1774 -
1775 1439 return $date;
1776 1440 } catch (\Exception $e) {
1777 1441 return false;
1778 1442 }
@@ -1777,37 +1441,14 @@
1777 1441 return false;
1778 1442 }
1779 1443 }
1780 1444
1781 -
1782 - function should_hide_today($date, $now)
1445 + function should_skip_day($date)
1783 1446 {
1784 - return ($date->format('w') === $now->format('w')) && ($this->isWeeklyRecurring() || $this->recurringUnselectedDaysAction === 'hide');
1785 - }
1786 - function has_skipped_days()
1787 - {
1788 1447 $recur_days = array_map('intval', $this->recurringDays);
1789 - return count($recur_days) < 7;
1448 + return !in_array($date->format('w'), $recur_days);
1790 1449 }
1791 - function can_recur_on($date)
1792 - {
1793 - $recur_days = array_map('intval', $this->recurringDays);
1794 1450
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 1451 /**
1811 1452 * Returns compaign publish datetime.
1812 1453 *
1813 1454 * @return mixed
@@ -1964,12 +1605,8 @@
1964 1605 * @param $labels
1965 1606 */
1966 1607 public function setLabels($labels)
1967 1608 {
1968 - // Sanitize each label to prevent XSS
1969 - if (is_array($labels)) {
1970 - $labels = array_map('sanitize_text_field', $labels);
1971 - }
1972 1609 $labels = wp_parse_args($labels, $this->labels);
1973 1610 update_post_meta($this->id, 'labels', $labels);
1974 1611 }
1975 1612
@@ -2001,13 +1638,13 @@
2001 1638 return true;
2002 1639 }
2003 1640
2004 1641 if ($this->is_recurring()) {
2005 - $start_date = Carbon::parse($this->recurringStartTime, hurryt_tz($this->timezone));
2006 - $now = Carbon::now(hurryt_tz($this->timezone));
1642 + $start_date = Carbon::parse($this->recurringStartTime, hurryt_tz());
1643 + $now = Carbon::now(hurryt_tz());
2007 1644 $now->second = $start_date->second;
1645 +
2008 1646 if ($now->lessThan($start_date)) {
2009 -
2010 1647 return true;
2011 1648 }
2012 1649 }
2013 1650
@@ -2028,160 +1665,8 @@
2028 1665
2029 1666 return $endDate < $today;
2030 1667 }
2031 1668
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 1669 public function is_sticky_dismissed()
2185 1670 {
2186 1671 return isset($_COOKIE[Cookie_Detection::COOKIE_PREFIX . $this->get_id() . '_dismissed'])
2187 1672 && $this->stickyBarDismissible === C::YES
@@ -2267,11 +1752,8 @@
2267 1752 * @return bool
2268 1753 */
2269 1754 public function is_expired()
2270 1755 {
2271 - if ($this->is_one_time() && $this->hasMultipleDateRanges()) {
2272 - return $this->is_all_date_ranges_expired();
2273 - }
2274 1756 return ($this->is_one_time() && $this->is_one_time_expired())
2275 1757 || ($this->is_recurring() && $this->is_recurring_expired());
2276 1758 }
2277 1759
@@ -2341,115 +1823,5 @@
2341 1823 private function isMinutelyRecurring()
2342 1824 {
2343 1825 return $this->recurringFrequency === C::RECURRING_MINUTELY;
2344 1826 }
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 1827 }