CronRule.class.php 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. <?php
  2. /**
  3. * @file
  4. *
  5. * This class parses cron rules and determines last execution time using least case integer comparison.
  6. */
  7. class CronRule {
  8. public $rule = NULL;
  9. public $allow_shorthand = FALSE;
  10. private static $ranges = array(
  11. 'minutes' => array(0, 59),
  12. 'hours' => array(0, 23),
  13. 'days' => array(1, 31),
  14. 'months' => array(1, 12),
  15. 'weekdays' => array(0, 6),
  16. );
  17. private $parsed_rule = array();
  18. public $offset = 0;
  19. /**
  20. * Constructor
  21. */
  22. function __construct($rule = NULL) {
  23. $this->rule = $rule;
  24. }
  25. /**
  26. * Expand interval from cronrule part
  27. *
  28. * @param $matches (e.g. 4-43/5+2)
  29. * array of matches:
  30. * [1] = lower
  31. * [2] = upper
  32. * [5] = step
  33. * [7] = offset
  34. *
  35. * @return
  36. * (string) comma-separated list of values
  37. */
  38. function expandInterval($matches) {
  39. $result = array();
  40. $lower = $matches[1];
  41. $upper = $matches[2];
  42. $step = isset($matches[5]) ? $matches[5] : 1;
  43. $offset = isset($matches[7]) ? $matches[7] : 0;
  44. if ($step <= 0) return '';
  45. $step = ($step > 0) ? $step : 1;
  46. for ($i = $lower; $i <= $upper; $i+=$step) {
  47. $result[] = ($i + $offset) % ($upper + 1);
  48. }
  49. return implode(',', $result);
  50. }
  51. /**
  52. * Expand range from cronrule part
  53. *
  54. * @param $rule
  55. * (string) cronrule part, e.g.: 1,2,3,4-43/5
  56. * @param $max
  57. * (string) boundaries, e.g.: 0-59
  58. * @param $digits
  59. * (int) number of digits of value (leading zeroes)
  60. * @return
  61. * (array) array of valid values
  62. */
  63. function expandRange($rule, $type) {
  64. $max = implode('-', self::$ranges[$type]);
  65. $rule = str_replace("*", $max, $rule);
  66. $rule = str_replace("@", $this->offset % (self::$ranges[$type][1] + 1), $rule);
  67. $this->parsed_rule[$type] = $rule;
  68. $rule = preg_replace_callback('!(\d+)-(\d+)((/(\d+))?(\+(\d+))?)?!', array($this, 'expandInterval'), $rule);
  69. if (!preg_match('/([^0-9\,])/', $rule)) {
  70. $rule = explode(',', $rule);
  71. rsort($rule);
  72. }
  73. else {
  74. $rule = array();
  75. }
  76. return $rule;
  77. }
  78. /**
  79. * Pre process rule.
  80. *
  81. * @param array &$parts
  82. */
  83. function preProcessRule(&$parts) {
  84. // Allow JAN-DEC
  85. $months = array(1 => 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec');
  86. $parts[3] = strtr(strtolower($parts[3]), array_flip($months));
  87. // Allow SUN-SUN
  88. $days = array('sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat');
  89. $parts[4] = strtr(strtolower($parts[4]), array_flip($days));
  90. $parts[4] = str_replace('7', '0', $parts[4]);
  91. }
  92. /**
  93. * Post process rule
  94. *
  95. * @param array $intervals
  96. */
  97. function postProcessRule(&$intervals) {
  98. }
  99. /**
  100. * Generate regex rules
  101. *
  102. * @param $rule
  103. * (string) cronrule, e.g: 1,2,3,4-43/5 * * * 2,5
  104. * @return
  105. * (array) date and time regular expression for mathing rule
  106. */
  107. function getIntervals($rule = NULL) {
  108. $parts = preg_split('/\s+/', isset($rule) ? $rule : $this->rule);
  109. if ($this->allow_shorthand) $parts += array('*', '*', '*', '*', '*'); // Allow short rules by appending wildcards?
  110. if (count($parts) != 5) return FALSE;
  111. $this->preProcessRule($parts);
  112. $intervals = array();
  113. $intervals['minutes'] = $this->expandRange($parts[0], 'minutes');
  114. if (empty($intervals['minutes'])) return FALSE;
  115. $intervals['hours'] = $this->expandRange($parts[1], 'hours');
  116. if (empty($intervals['hours'])) return FALSE;
  117. $intervals['days'] = $this->expandRange($parts[2], 'days');
  118. if (empty($intervals['days'])) return FALSE;
  119. $intervals['months'] = $this->expandRange($parts[3], 'months');
  120. if (empty($intervals['months'])) return FALSE;
  121. $intervals['weekdays'] = $this->expandRange($parts[4], 'weekdays');
  122. if (empty($intervals['weekdays'])) return FALSE;
  123. $intervals['weekdays'] = array_flip($intervals['weekdays']);
  124. $this->postProcessRule($intervals);
  125. return $intervals;
  126. }
  127. /**
  128. * Convert intervals back into crontab rule format
  129. */
  130. function rebuildRule($intervals) {
  131. $parts = array();
  132. foreach ($intervals as $type => $interval) {
  133. $parts[] = $this->parsed_rule[$type];
  134. }
  135. return implode(' ', $parts);
  136. }
  137. /**
  138. * Parse rule. Run through parser expanding expression, and recombine into crontab syntax.
  139. */
  140. function parseRule() {
  141. return $this->rebuildRule($this->getIntervals());
  142. }
  143. /**
  144. * Get last execution time of rule in unix timestamp format
  145. *
  146. * @param $time
  147. * (int) time to use as relative time (default now)
  148. * @return
  149. * (int) unix timestamp of last execution time
  150. */
  151. function getLastRan($time = NULL) {
  152. // Current time round to last minute
  153. if (!isset($time)) $time = time();
  154. $time = floor($time / 60) * 60;
  155. // Generate regular expressions from rule
  156. $intervals = $this->getIntervals();
  157. if ($intervals === FALSE) return FALSE;
  158. // Get starting points
  159. $start_year = date('Y', $time);
  160. $end_year = $start_year - 28; // Go back max 28 years (leapyear * weekdays)
  161. $start_month = date('n', $time);
  162. $start_day = date('j', $time);
  163. $start_hour = date('G', $time);
  164. $start_minute = (int)date('i', $time);
  165. // If both weekday and days are restricted, then use either or
  166. // otherwise, use and ... when using or, we have to try out all the days in the month
  167. // and not just to the ones restricted
  168. $check_both = (count($intervals['days']) != 31 && count($intervals['weekdays']) != 7) ? FALSE : TRUE;
  169. $days = $check_both ? $intervals['days'] : range(31, 1);
  170. // Find last date and time this rule was run
  171. for ($year = $start_year; $year > $end_year; $year--) {
  172. foreach ($intervals['months'] as $month) {
  173. if ($month < 1 || $month > 12) continue;
  174. if ($year >= $start_year && $month > $start_month) continue;
  175. foreach ($days as $day) {
  176. if ($day < 1 || $day > 31) continue;
  177. if ($year >= $start_year && $month >= $start_month && $day > $start_day) continue;
  178. if (!checkdate($month, $day, $year)) continue;
  179. // Check days and weekdays using and/or logic
  180. $date_array = getdate(mktime(0, 0, 0, $month, $day, $year));
  181. if ($check_both) {
  182. if (!isset($intervals['weekdays'][$date_array['wday']])) continue;
  183. }
  184. else {
  185. if (
  186. !in_array($day, $intervals['days']) &&
  187. !isset($intervals['weekdays'][$date_array['wday']])
  188. ) continue;
  189. }
  190. if ($day != $start_day || $month != $start_month || $year != $start_year) {
  191. $start_hour = 23;
  192. $start_minute = 59;
  193. }
  194. foreach ($intervals['hours'] as $hour) {
  195. if ($hour < 0 || $hour > 23) continue;
  196. if ($hour > $start_hour) continue;
  197. if ($hour < $start_hour) $start_minute = 59;
  198. foreach ($intervals['minutes'] as $minute) {
  199. if ($minute < 0 || $minute > 59) continue;
  200. if ($minute > $start_minute) continue;
  201. break 5;
  202. }
  203. }
  204. }
  205. }
  206. }
  207. // Create unix timestamp from derived date+time
  208. $time = mktime($hour, $minute, 0, $month, $day, $year);
  209. return $time;
  210. }
  211. /**
  212. * Check if a rule is valid
  213. */
  214. function isValid($time = NULL) {
  215. return $this->getLastRan($time) === FALSE ? FALSE : TRUE;
  216. }
  217. }