File: /home/thwoodlg/public_html/wp-content/plugins/duplicator-pro/lib/cron/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;
}
}
?>