EVOLUTION-MANAGER
Edit File: csd_parser.php
<?php defined("ABSPATH") or die(""); /** * Lightweight Class for easy calculation of UNIX timestamps from cron scheduling definitions. * * This class can calculate a (range of) unix timestamps of either future or past occurences relative * to a given time considering a single or a combination of multiple scheduling definitions. * * The special character "*", "/", "," and "-" and their functions are fully supported. The "?" * will be treated as a "*". Special characters "L", "W" and "#" are not (yet) supported, while "%" * isn't and will - logically - never be supported. * * <b>Example</b><br /> * Let's take a look at an example:<code> * $test = new csd_parser('1 3 2-8/6 * 2,3', '27-05-2013 23:00'); * for($i = 0; $i < 3; $i++) { * echo date('d-m-Y H:i (w)', $test->get($i)).'<br />'; * }</code> * this will output:<pre> * 02-07-2013 03:01 (2) * 02-10-2013 03:01 (3) * 08-10-2013 03:01 (2)</pre> * * <b>Combining definitions</b><br /> * Now would i use '0 0 15 * *' it would output the 15th of each month. This date would thus "sandwich" * the first result above. You can provide such a combined definition by seperating them with a newline * or putting each statement in an array. E.g.:<code> * $test = new csd_parser(array('1 3 2-8/6 * 2,3', '0 0 15 * *'), '27-05-2013 23:00'); * for($i = 0; $i < 3; $i++) { * echo date('d-m-Y H:i (w)', $test->get($i)).'<br />'; * }</code> * will output:<pre> * 15-06-2013 00:00 (6) * 02-07-2013 03:01 (2) * 15-07-2013 00:00 (1)</pre> * * <b>Some selection options</b><br /><code> * // single result, single command * echo date('d-m-Y H:i (w)', csd_parser::calc('1 3 2-8/6 * 2,3')).'<br />'; * // all occurences in a year * $test = new csd_parser(array('0 0 15 * *'), '27-05-2013 23:00'); * $data = $test->get('01-01-2013 00:00', '01-01-2012 00:00'); * foreach($data as $time) { * echo date('d-m-Y H:i (w)', $time).'<br />'; * } * // last run * echo date('d-m-Y H:i (w)', $test->get('last')).'<br />'; * // five runs back * echo date('d-m-Y H:i (w)', $test->get(-5)).'<br />';</code> * * <b>Cron syntax</b><br /><pre> * d d d d d d * │ │ │ │ │ └─ year (is optional, range is 1970 - 2099) * │ │ │ │ └─── day of week (0 - 7, or sun - sat) (0 or 7 are Sunday to Saturday) * │ │ │ └───── month (1 - 12, or jan - dec) * │ │ └─────── day of month (1 - 31) * │ └───────── hour (0 - 23) * └─────────── min (0 - 59)</pre> * For more info see (@link http://en.wikipedia.org/wiki/Cron the Wikipedia article on crons) or Google. * * @name CSD_Parser * @author Chris Volwerk * @copyright Copyright © 2014, Chris Volwerk * @license LGPLv3 http://www.opensource.org/licenses/lgpl-3.0 * @version 2 */ class csd_parser { /** * Cron scheduling definition(s) string * * @var string */ protected $csds; /** * Time set constructino * * @var int */ protected $base_time; /** * Array with parsed time data * * @var array */ protected $times = array(); /** * Array with known times relative to base_time * * @var array */ protected $known = array(); /** * Names of the fields * * @var str[] */ static protected $fields = array('minute', 'hour', 'day', 'month', 'weekday', 'year'); /** * Mapping for month-names and day names * * @var array */ static protected $mapping = array( 'day' => array('sun' => 0, 'mon' => 1, 'tue' => 2, 'wed' => 3, 'thu' => 4, 'fri' => 5, 'sat' => 6, 'sunday' => 0, 'monday' => 1, 'tuesday' => 2, 'wednesday' => 3, 'thursday' => 4, 'friday' => 5, 'saturday' => 6), 'month' => array('jan' => 1, 'feb' => 2, 'mar' => 3, 'apr' => 4, 'may' => 5, 'jun' => 6, 'jul' => 7, 'aug' => 8, 'sep' => 9, 'oct' => 10, 'nov' => 11, 'dec' => 12, 'january' => 1, 'february' => 2, 'march' => 3, 'april' => 4, 'june' => 6, 'july' => 7, 'august' => 8, 'september' => 9, 'october' => 10, 'november' => 11, 'december' => 12) ); /** * Long month mapping * 0 doubles for 12 (for 'january - 1') * * @var bool[] */ static protected $long = array(true, true, false, true, false, true, false, true, true, false, true, false, true); /** * Create a new csd_parser * * @param mixed $csd Array of definition(s) or a (multiline) string with definition(s) * @param mixed $time Set a custom base time for this parser (unix timestamp or parsable string) * * @throws Exception If none of the definitions were valid * @return csd_parser */ public function __construct($csd, $time = null) { $this->csds = is_array($csd) ? implode("\n", $csd) : $csd; $this->times = self::prep_csds($csd); if ($this->times === false) { throw new Exception(__METHOD__.': No valid definition(s) provided. Check error log for details.'); } $this->base_time = self::parse_time($time); } /** * Calculate a single runtime for the provided definition(s) * * @param mixed $which Which occurence ('last', 'next' or an integer: 0 for next, -1 for last, -2 for the one before that�) * Additionaly when you provide a string: it will be than be treated as a time (unix stamp or parsable * string) up until which to calculate times. This results in an array of times being returned. * @param mixed $time Calculate relative to this time instead of the base time of this parser * * @return mixed The unix timestamp of the runtime (or null if it doesn't exist) */ public function get($which = 'next', $time = null) { $which = $which !== 'next' ? $which !== 'prev' ? $which !== 'last' ? $which : -1 : -1 : 0; if (!is_int($which)) { if (is_string($which)) { $till = preg_match('/^\d+$/', $which) ? $which * 1 : strtotime($which); } if (!isset($till) || empty($till)) { $trace = debug_backtrace(); trigger_error( __METHOD__ .": Incorrect value given for \$which in {$trace[0]['file']} on line {$trace[0]['line']}. . Using \"next\" instead.", E_USER_WARNING ); $which = 0; } } if ($time === null) { $time = $this->base_time; } else { $time = self::parse_time($time); } $store = $time === $this->base_time; if (isset($till)) { $times = array(); $up = $till >= $time ? true : false; $index = $up ? 0 : -1; do { if ($store && isset($this->known[$index])) { $time = $this->known[$index]; } else { $time = self::find_time($this, $time, $up); if ($time === null) { break; } if ($store) { $this->known[$index] = $time; } } $times[] = $time; $time = $up ? $time + 60 : $time - 60; $index = $up ? $index + 1 : $index - 1; } while (($up && $time < $till) || (!$up && $time > $till)); array_pop($times); return $times; } else { $which = (int)$which; $up = $which >= 0 ? true : false; $runs = $up ? $which + 1 : $which * -1; for ($i = 1; $i <= $runs; $i++) { if ($store && isset($this->known[$up ? $i - 1 : -$i])) { $time = $this->known[$up ? $i - 1 : -$i]; } else { $time = self::find_time($this, $time, $up); if ($time === null) { break; } if ($store) { $this->known[$up ? $i - 1 : -$i] = $time; } } if ($i !== $runs) { $time = $up ? $time + 60 : $time - 60; } } } return $time; } /** * Calculate a single runtime for the given definition(s) * * @param mixed $csd Array of definition(s) or a (multiline) string with definition(s) * @param mixed $which Which occurence ('last', 'next' or an integer: 0 for next, -1 for last, -2 for the one before that�) * Additionaly when you provide a string: it will be than be treated as a time (unix stamp or parsable * string) up until which to calculate times. This results in an array of times being returned. * @param mixed $time Set a custom base time to calculate agains (unix timestamp or parsable string) * * @return int The unix timestamp of the runtime (or null if it doesn't exist) */ static public function calc($csd, $which = 'next', $time = null) { $parser = new self($csd, $time); return $parser->get($which); } /** * Calculate a single time for a parser * * @param csd_parser $parser The parser for which to calculate * @param int $time Calculate relative to this time * @param bool $up Find next * * @return int|null */ static protected function find_time(csd_parser $parser, $time, $up = true) { $next = null; foreach ($parser->times as $time_data) { $option = self::find_time_l1($time_data, $time, $up); if ($option !== null) { if ($next === null) { $next = $option; } elseif ($up) { if ($next > $option) { $next = $option; } } else { if ($next < $option) { $next = $option; } } } } return $next; } /** * Find a time * * @param mixed $time_data * @param mixed $time * @param mixed $up * * @return int|null */ static protected function find_time_l1($time_data, $time, $up = true) { if (is_array($time)) { $time = mktime($time[1], $time[0], 0, $time[3], $time[2], $time[5]); } $rel_times = explode(' ', date('i H d m w Y', $time)); foreach ($rel_times as $key => $val) { $rel_times[$key] = (int)$val; } $with = $rel_times; $limit = 100; if (!self::find_time_l2($time_data, $with, 5, $up)) { return null; } if (!self::find_time_l2($time_data, $with, 3, $up)) { return null; } do { if (!self::find_time_l2($time_data, $with, 2, $up)) { return null; } if (!self::find_time_l2($time_data, $with, 1, $up)) { return null; } if (!self::find_time_l2($time_data, $with, 0, $up)) { return null; } $weekday = (int)date('w', mktime($with[1], $with[0], 0, $with[3], $with[2], $with[5])); $with[4] = $weekday; if (self::find_time_l2($time_data, $with, 4, $up)) { $offset = $with[4] - $weekday; } else { if ($up) { $offset = $with[4] + (7 - $weekday); } else { $offset = -($weekday + (7 - $with[4])); } } $with[2] += $offset; } while ($offset !== 0 && --$limit !== 0); return mktime($with[1], $with[0], 0, $with[3], $with[2], $with[5]); } /** * Flood time array with the closest value of a field from a time data array * * @param array $for Time data * @param array $with Time array (to be set) * @param int $field_id Field ID * @param bool $up Whether direction is up or not * @param bool $force Whether to force update (use after manually setting the field's value); * * @return bool Whether there actually is a possible time or not in that direction */ static protected function find_time_l2($for, &$with, $field_id, $up = true, $force = false) { if ($field_id === 5 && !isset($for[$field_id])) { return true; } # years are optional $index = $up ? $for[$field_id]->index : array_reverse($for[$field_id]->index); $curr = $with[$field_id]; $spill = true; $value = null; # scan for next option without spill if ($up) { foreach ($index as $value) { if ($value >= $curr) { if ($field_id !== 2 || # not a day, $value < 29 || (self::$long[$with[3]] && $value <= 31) || ($with[3] !== 2 && $value <= 30) || (self::ly($with[5])) ) { $spill = false; if ($value === $curr && !$force) { return true; } else { break; } } } } } else { foreach ($index as $value) { if ($value <= $curr) { $spill = false; if ($value === $curr && !$force) { return true; } else { break; } } } } # set value, and spill if needed if (!$spill) { $with[$field_id] = $value; if ($field_id === 4) { return true; } } else { if ($field_id > 3) { $with[$field_id] = $value; return false; } for ($i = $field_id + 1; $i <= 5; $i++) { if ($i === 4) { continue; } $with[$i] = $up ? $with[$i] + 1 : $with[$i] - 1; $break = self::make_valid($with, $i, $up); self::find_time_l2($for, $with, $i, $up); if ($break) { break; } } $with[$field_id] = $index[0]; self::make_valid($with, $field_id, $up); } # finally, adjust lower params for ($i = $field_id - 1; $i >= 0; $i--) { switch ($i) { case 3: $with[3] = $up ? 1 : 12; break; case 2: if ($up) { $with[2] = 1; break; } if (self::$long[$with[3]]) { $with[2] = 31; } # long month: 31 elseif ($with[3] !== 2) { $with[2] = 30; } elseif (self::ly($with[5])) { $with[2] = 29; } # feb in a leapyear: 29 else { $with[2] = 28; } break; case 1: $with[1] = $up ? 0 : 23; break; case 0: $with[0] = $up ? 0 : 59; break; } } return true; } /** * Get the next valid value for a field in a certain direction * * @param array $with Time array (to be set) * @param int $field_id Field ID * @param bool $up Whether direction is up or not * * @return bool Found without need for spill */ static protected function make_valid(&$with, $field_id, $up) { $want =& $with[$field_id]; switch ($field_id) { case 4: if ($want < 0 || $want > 6) { $want = $up ? 0 : 6; return false; } break; case 3: if ($want < 1 || $want > 12) { $want = $up ? 1 : 12; return false; } break; case 2: if ($up) { if ($want < 29 || (self::$long[$with[3]] && $want <= 31) || ($with[3] !== 2 && $want <= 30) || (self::ly($with[5])) ) { break; } $want = 1; return false; } elseif ($want < 1) { if (self::$long[$with[3] - 1]) { $want = 31; } elseif ($with[3] !== 3) { $want = 30; } elseif (self::ly($with[5])) { $want = 29; } else { $want = 28; } return false; } break; case 1: if ($want < 0 || $want > 23) { $want = $up ? 0 : 23; return false; } break; case 0: if ($want < 0 || $want > 59) { $want = $up ? 0 : 59; return false; } break; } return true; } /** * Prepare definitions * * @param mixed Array of definition(s) or a (multiline) string with definition(s) * * @return stdClass[] Array with object with timing details */ static protected function prep_csds($csds) { if (!is_array($csds)) { $csds = preg_split('/\s*(?:\r\n|\r|\n)\s*/', trim($csds), null, PREG_SPLIT_NO_EMPTY); } $index = 0; $data = array(); foreach ($csds as $line => $csd) { $fields = preg_split('/\s+/', $csd, null, 1); $times = array(); # convert special statements if (!isset($fields[1])) { switch ($fields[0]) { case '@yearly': case '@annually': $csd = '0 0 1 1 *'; break; case '@monthly': $csd = '0 0 1 * *'; break; case '@weekly': $csd = '0 0 * * 0'; break; case '@daily': case '@midnight': $csd = '0 0 * * *'; break; case '@hourly': $csd = '0 * * * *'; break; } $fields = explode(' ', $csd); } if (!isset($fields[4]) || isset($fields[6])) { $trace = debug_backtrace(); trigger_error( "{$trace[1]['function']}: Skipping incorrect definition '".implode(' ', $fields) ."' found on line $line of the definitions provided in {$trace[1]['file']} on line {$trace[1]['line']}.", E_USER_WARNING ); continue; } # process the separate fields foreach ($fields as $field_id => $field) { $error = false; $type = self::$fields[$field_id]; $field = str_replace('?', '*', $field); if (isset(self::$mapping[$type])) { # if we have a mapping replace according to it $trace = debug_backtrace(); $field = str_ireplace( array_keys(self::$mapping[$type]), array_values(self::$mapping[$type]), $field ); } if ($field_id === 4) { $old = $field; $field = strtr($field, array('6-7' => '6,0', '-7' => '-6,0', '7' => '0')); if ($old !== $field) { /** @noinspection PhpUndefinedVariableInspection */ trigger_error( "{$trace[1]['function']}: Use of incorrect value for (and/or place of) 7, 'sun' or 'sunday' in the definition-field value for $type ('" .$fields[$field_id]."' in definition '".implode(' ', $fields) ."') on line $line of the definitions provided in {$trace[1]['file']} on line {$trace[1]['line']}.", E_USER_NOTICE ); } } $sets = preg_split('/,/', $field, null); $times[$field_id] = new stdClass(); $times[$field_id]->index = array(); # process the sets in the fields foreach ($sets as $set) { if ($set === '' || !self::parse_set($times[$field_id], $type, $set)) { $error = true; } } if ($error) { $trace = debug_backtrace(); if (empty($times[$field_id]->index)) { trigger_error( "{$trace[1]['function']}: Skipping incorrect definition '".implode(' ', $fields)."' due to unparseable definition-field value for $type ('" .$fields[$field_id] ."') on line $line of the definitions provided in {$trace[1]['file']} on line {$trace[1]['line']}.", E_USER_WARNING ); continue 2; } else { trigger_error( "{$trace[1]['function']}: Ignored incorrect part(s) of the definition-field value for $type ('" .$fields[$field_id]."' in definition '".implode(' ', $fields) ."') on line $line of the definitions provided in {$trace[1]['file']} on line {$trace[1]['line']}.", E_USER_NOTICE ); } } $times[$field_id]->index = array_keys($times[$field_id]->index); sort($times[$field_id]->index); } # apparently it all went quite well $data[$index] = $times; $index++; } if (empty($data)) { return false; } return $data; } /** * Parse a single set of a field of an definition into $data * * @param stdClass $data The time data object * @param string $type The type of field we're parsing here * @param string $set The set that is to be parsed * * @return bool */ static protected function parse_set(&$data, $type, $set) { $min = 0; $max = 0; switch ($type) { case 'minute': $min = 0; $max = 59; break; case 'hour': $min = 0; $max = 23; break; case 'day': $min = 1; $max = 31; break; case 'month': $min = 1; $max = 12; break; case 'weekday': $min = 0; $max = 6; break; case 'year': $min = 1970; $max = 2099; break; } # parse default definitions if ($set === '*') { # asterisk $add = array_fill($min, $max - $min + 1, true); } elseif (preg_match('/^\d+$/', $set)) { # single digit if ($set >= $min && $set <= $max) { $add = array_fill($set, 1, true); } } elseif (preg_match('/^\d+\-\d+$/', $set)) { # digit range $set = explode('-', $set); if ($set[0] < $min) { $set[0] = $min; } if ($set[1] > $max) { $set[1] = $max; } if ($set[0] <= $set[1]) { $add = array_fill($set[0], $set[1] - $set[0] + 1, true); } } elseif (preg_match('/^(\*|(\d+\-\d+))\/\d+$/', $set)) { # incremental range $set = explode('/', $set); if ($set[0] === '*') { $set[0] = array($min, $max); } else { $set[0] = explode('-', $set[0]); if ($set[0][0] < $min) { $set[0][0] = $min; } if ($set[0][1] > $max) { $set[0][1] = $max; } } if ($set[0][0] <= $set[0][1]) { for ($i = $set[0][0]; $i <= $set[0][1]; $i += $set[1]) { $add[(int)$i] = true; } } } # if the default definition yielded result if (isset($add)) { foreach ($add as $key => $val) { $data->index[$key] = 1; } return true; } return false; } /** * Parse a given time * * @param mixed $time Unix timestamp or parseble string * * @return int Unix timestamp */ static protected function parse_time($time) { if ($time === null) { $time = time(); } elseif (is_string($time) || is_numeric($time)) { if (preg_match('/^\d*$/', $time)) { $time = (int)$time; } else { $time = @strtotime($time); } } if (!is_int($time)) { $trace = debug_backtrace(); trigger_error( "{$trace[1]['function']}: Incorrect value given for \$time in {$trace[1]['file']} on line {$trace[1]['line']}. Using current time instead.", E_USER_NOTICE ); $time = time(); } return $time; } /** * Check if a year is a leap year * * @param int $year The year to be checked * * @return bool Whether the year is a leap year */ static protected function ly($year) { if ($year % 100 === 0) { return $year % 400 === 0 ? true : false; } if ($year % 4 === 0) { return true; } return true; } } ?>