'la_opt_CronEveryMinute', '*/5 * * * *' => 'la_opt_CronEveryFiveMinutes', '0,30 * * * *' => 'la_opt_CronTwiceAnHour', '0 * * * *' => 'la_opt_CronOnceAnHour', '0 0,12 * * *' => 'la_opt_CronTwiceADay', '0 0 * * *' => 'la_opt_CronOnceADay', '0 0 * * 0' => 'la_opt_CronOnceAWeek', '0 0 1,15 * *' => 'la_opt_CronTwiceAMonth', '0 0 1 * *' => 'la_opt_CronOnceAMonth', '0 0 1 1 *' => 'la_opt_CronOnceAYear', ); protected $minuteSettings = Array ( '*' => 'la_opt_CronEveryMinute', '*/2' => 'la_opt_CronEveryOtherMinute', '*/5' => 'la_opt_CronEveryFiveMinutes', '*/10' => 'la_opt_CronEveryTenMinutes', '*/15' => 'la_opt_CronEveryFifteenMinutes', '0,30' => 'la_opt_CronEveryThirtyMinutes', '--' => 'la_opt_CronMinutes', // minutes added dynamically later ); protected $hourSettings = Array ( '*' => 'la_opt_CronEveryHour', '*/2' => 'la_opt_CronEveryOtherHour', '*/3' => 'la_opt_CronEveryThreeHours', '*/4' => 'la_opt_CronEveryFourHours', '*/6' => 'la_opt_CronEverySixHours', '0,12' => 'la_opt_CronEveryTwelveHours', '--' => 'la_opt_CronHours', // hours added dynamically later ); protected $daySettings = Array ( '*' => 'la_opt_CronEveryDay', '*/2' => 'la_opt_CronEveryOtherDay', '1,15' => 'la_opt_CronTwiceAMonth', '--' => 'la_opt_CronDays', // days added dynamically later ); protected $monthSettings = Array ( '*' => 'la_opt_CronEveryMonth', '*/2' => 'la_opt_CronEveryOtherMonth', '*/4' => 'la_opt_CronEveryThreeMonths', '1,7' => 'la_opt_CronEverySixMonths', '--' => 'la_opt_CronMonths', '1' => 'la_opt_January', '2' => 'la_opt_February', '3' => 'la_opt_March', '4' => 'la_opt_April', '5' => 'la_opt_May', '6' => 'la_opt_June', '7' => 'la_opt_July', '8' => 'la_opt_August', '9' => 'la_opt_September', '10' => 'la_opt_October', '11' => 'la_opt_November', '12' => 'la_opt_December', ); protected $weekdaySettings = Array ( '*' => 'la_opt_CronEveryWeekday', '1-5' => 'la_opt_CronMondayThroughFriday', '0,6' => 'la_opt_CronSaturdayAndSunday', '1,3,5' => 'la_opt_CronMondayWednesdayAndFriday', '2,4' => 'la_opt_CronTuesdayAndThursday', '--' => 'la_opt_CronWeekdays', '0' => 'la_opt_Sunday', '1' => 'la_opt_Monday', '2' => 'la_opt_Tuesday', '3' => 'la_opt_Wednesday', '4' => 'la_opt_Thursday', '5' => 'la_opt_Friday', '6' => 'la_opt_Saturday', ); /** * Returns possible field options by type * * @param int $field_type * @return Array */ public function getOptions($field_type) { $mapping = Array ( self::COMMON => $this->commonSettings, self::MINUTE => $this->minuteSettings, self::HOUR => $this->hourSettings, self::DAY => $this->daySettings, self::MONTH => $this->monthSettings, self::WEEKDAY => $this->weekdaySettings, ); $ret = $mapping[$field_type]; /* @var $ret Array */ foreach ($ret as $option_key => $option_title) { $option_title = substr($option_title, 0, 1) == '+' ? substr($option_title, 1) : $this->Application->Phrase($option_title); $ret[$option_key] = $option_title; if ( "$option_key" !== '--' ) { $ret[$option_key] .= ' (' . $option_key . ')'; } } if ( $field_type == self::MINUTE ) { for ($i = 0; $i <= 59; $i++) { $ret[$i] = ':' . str_pad($i, 2, '0', STR_PAD_LEFT) . ' (' . $i . ')'; } } elseif ( $field_type == self::HOUR ) { $language = $this->Application->recallObject('lang.current'); /* @var $language LanguagesItem */ $short_time_format = str_replace(':s', '', $language->GetDBField('TimeFormat')); for ($i = 0; $i <= 23; $i++) { $ret[$i] = date($short_time_format, mktime($i, 0, 0)) . ' (' . $i . ')'; } } elseif ( $field_type == self::DAY ) { $ml_helper = $this->Application->recallObject('kMultiLanguageHelper'); /* @var $ml_helper kMultiLanguageHelper */ $forms = Array ( 'phrase1' => 'la_NumberSuffixSt', 'phrase2' => 'la_NumberSuffixNd', 'phrase3' => 'la_NumberSuffixRd', 'phrase4' => 'la_NumberSuffixTh', 'phrase5' => 'la_NumberSuffixTh' ); for ($i = 1; $i <= 31; $i++) { $ret[$i] = $i . $ml_helper->getPluralPhrase($i, $forms) . ' (' . $i . ')'; } } return $ret; } /** * Returns field name by type * * @param int $field_type * @param string $field_prefix * @return string * @access protected */ protected function _getFieldNameByType($field_type, $field_prefix) { $field_mapping = Array ( self::MINUTE => 'Minute', self::HOUR => 'Hour', self::DAY => 'Day', self::MONTH => 'Month', self::WEEKDAY => 'Weekday', ); return $field_prefix . $field_mapping[$field_type]; } /** * Creates virtual fields for given unit * * @param string $prefix * @param string $field_prefix * @return void * @access public */ public function initUnit($prefix, $field_prefix = '') { $config = $this->Application->getUnitConfig($prefix); $config->addVirtualFields(Array ( $field_prefix . 'CommonHints' => Array ( 'type' => 'string', 'formatter' => 'kOptionsFormatter', 'options' => $this->getOptions(self::COMMON), 'default' => '' ), )); foreach ($this->fieldTypes as $field_type) { $field_name = $this->_getFieldNameByType($field_type, $field_prefix); $config->addVirtualFields(Array ( $field_name => Array ('type' => 'string', 'max_len' => 30, 'default' => '*'), $field_name . 'Hints' => Array ( 'type' => 'string', 'formatter' => 'kOptionsFormatter', 'options' => $this->getOptions($field_type), 'default' => '' ), )); } } /** * Loads schedule values from database into virtual fields * * @param kDBItem $object * @param string $field_prefix */ public function load(kDBItem $object, $field_prefix = '') { $combined_value = explode(' ', $object->GetDBField($field_prefix)); foreach ($this->fieldTypes as $field_type) { $field_name = $this->_getFieldNameByType($field_type, $field_prefix); $object->SetDBField($field_name, $combined_value[$field_type - 1]); } } /** * Validates schedule values and saves them to database * * @param kDBItem $object * @param string $field_prefix * @return bool * @access public */ public function validateAndSave(kDBItem $object, $field_prefix = '') { $validated = true; $combined_value = Array (); $cron_field = new kCronField(); foreach ($this->fieldTypes as $field_type) { $field_name = $this->_getFieldNameByType($field_type, $field_prefix); $value = preg_replace('/\s+/s', '', mb_strtoupper($object->GetDBField($field_name))); if ( $cron_field->validate($field_type, $value) ) { $object->SetDBField($field_name, $value); } else { $validated = false; $object->SetError($field_name, 'invalid_format'); } $combined_value[$field_type] = $value; } ksort($combined_value); $object->SetDBField($field_prefix, implode(' ', $combined_value)); return $validated; } /** * Replaces aliases in the field * * @param int $field_type * @param string $value * @return string * @access public */ public static function replaceAliases($field_type, $value) { $replacements = Array (); $value = mb_strtolower($value); if ( $field_type == self::MONTH ) { $replacements = 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, ); } elseif ( $field_type == self::WEEKDAY ) { $replacements = Array ('sun' => 0, 'mon' => 1, 'tue' => 2, 'wed' => 3, 'thu' => 4, 'fri' => 5, 'sat' => 6); } if ( $replacements ) { $value = str_replace(array_keys($replacements), array_values($replacements), $value); } return $value; } /** * Returns next (after given one or now) timestamp matching given cron expression * * @param string $expression * @param int $date * @param bool $inverse * @param bool $allow_current_date * @return int * @access public * @throws RuntimeException */ public function getMatch($expression, $date = NULL, $inverse = false, $allow_current_date = false) { if ( !isset($date) ) { $date = TIMENOW; } $next_run = strtotime('-' . (int)date('s', $date) . ' seconds', $date); $expression_parts = explode(' ', $expression); $cron_field = new kCronField(); // set a hard limit to bail on an impossible date for ($i = 0; $i < 1000; $i++) { foreach ($this->fieldTypes as $field_type) { $matched = false; $part = $expression_parts[$field_type - 1]; // check if this is singular or a list if ( strpos($part, ',') === false ) { $matched = $cron_field->match($field_type, $next_run, $part); } else { $rules = explode(',', $part); foreach ($rules as $rule) { if ( $cron_field->match($field_type, $next_run, $rule) ) { $matched = true; break; } } } // if the field is not matched, then start over if ( !$matched ) { $next_run = $cron_field->increment($field_type, $next_run, $inverse); continue 2; } } // Skip this match if needed if ( (!$allow_current_date && $next_run == $date) ) { $next_run = $cron_field->increment(self::MINUTE, $next_run, $inverse); continue; } return $next_run; } throw new RuntimeException('Impossible CRON expression'); } } class kCronField extends kBase { /** * Validates field value * * @param int $field_type * @param string $value * @param bool $asterisk_allowed * @return bool * @access public */ public function validate($field_type, $value, $asterisk_allowed = true) { $rules = explode(',', kCronHelper::replaceAliases($field_type, $value)); foreach ($rules as $rule) { if ( $this->_isIncrementRule($rule) ) { if ( !$this->_validateIncrementRule($field_type, $rule) ) { return false; } } elseif ( $this->_isRangeRule($rule) ) { if ( !$this->_validateRangeRule($field_type, $rule) ) { return false; } } elseif ( !$this->_validateNumberRule($field_type, $rule, $asterisk_allowed) ) { return false; } } return true; } /** * Determines if expression is range * * @param string $rule * @return bool * @access protected */ protected function _isRangeRule($rule) { return strpos($rule, '-') !== false; } /** * Validates range rule * * @param int $field_type * @param string $rule * @return bool * @access protected */ protected function _validateRangeRule($field_type, $rule) { $parts = explode('-', $rule); if ( count($parts) != 2 ) { return false; } $min_value = $parts[0]; $max_value = $parts[1]; if ( !$this->_validateNumberRule($field_type, $min_value) || !$this->_validateNumberRule($field_type, $max_value) || $min_value >= $max_value ) { return false; } return true; } /** * Determines if expression is increment * * @param string $rule * @return bool * @access protected */ protected function _isIncrementRule($rule) { return strpos($rule, '/') !== false; } /** * Validates increment rule * * @param int $field_type * @param string $rule * @return bool * @access protected */ protected function _validateIncrementRule($field_type, $rule) { $parts = explode('/', $rule); if ( count($parts) != 2 ) { return false; } $interval = $parts[0]; $increment = $parts[1]; if ( $this->_isRangeRule($interval) ) { if ( !$this->_validateRangeRule($field_type, $interval) ) { return false; } } elseif ( !$this->_validateNumberRule($field_type, $interval, true) ) { return false; } if ( !$this->_validateNumberRule($field_type, $increment) ) { return false; } return true; } /** * Validates, that number within range OR an asterisk is given * * @param int $field_type * @param string $rule * @param bool $asterisk_allowed * @return bool * @access protected */ protected function _validateNumberRule($field_type, $rule, $asterisk_allowed = false) { if ( "$rule" === '*' ) { return $asterisk_allowed; } $int_rule = (int)$rule; if ( !is_numeric($rule) || "$int_rule" !== "$rule" ) { // not integer return false; } $range_mapping = Array ( kCronHelper::MINUTE => Array ('from' => 0, 'to' => 59), kCronHelper::HOUR => Array ('from' => 0, 'to' => 23), kCronHelper::DAY => Array ('from' => 1, 'to' => 31), kCronHelper::MONTH => Array ('from' => 1, 'to' => 12), kCronHelper::WEEKDAY => Array ('from' => 0, 'to' => 7), ); return $int_rule >= $range_mapping[$field_type]['from'] && $int_rule <= $range_mapping[$field_type]['to']; } /** * Tries to match given date to given expression * * @param int $field_type * @param int $date * @param string $rule * @return bool * @access public */ public function match($field_type, $date, $rule) { $date_part = $this->_getDatePart($field_type, $date, $rule); if ( $this->_isIncrementRule($rule) ) { return $this->_isInIncrement($date_part, $rule); } elseif ( $this->_isRangeRule($rule) ) { return $this->_isInRange($date_part, $rule); } return $rule == '*' || $date_part == $rule; } /** * Returns only part, needed based on field type of date in timestamp * * @param int $field_type * @param int $date * @param string $rule * @return int * @access protected */ protected function _getDatePart($field_type, $date, $rule) { $mapping = Array ( kCronHelper::MINUTE => 'i', kCronHelper::HOUR => 'G', kCronHelper::DAY => 'j', kCronHelper::MONTH => 'n', kCronHelper::WEEKDAY => 'N', ); if ( $field_type == kCronHelper::WEEKDAY ) { // Test to see which Sunday to use -- 0 == 7 == Sunday $mapping[$field_type] = in_array(7, str_split($rule)) ? 'N' : 'w'; } return (int)date($mapping[$field_type], $date); } /** * Test if a value is within a range * * @param string $date_value Set date value * @param string $rule Value to test * @return bool * @access protected */ protected function _isInRange($date_value, $rule) { $parts = array_map('trim', explode('-', $rule, 2)); return $date_value >= $parts[0] && $date_value <= $parts[1]; } /** * Test if a value is within an increments of ranges (offset[-to]/step size) * * @param string $date_value Set date value * @param string $rule Value to test * @return bool * @access protected */ protected function _isInIncrement($date_value, $rule) { $parts = array_map('trim', explode('/', $rule, 2)); $stepSize = isset($parts[1]) ? $parts[1] : 0; if ( $parts[0] == '*' || $parts[0] == 0 ) { return (int)$date_value % $stepSize == 0; } $range = explode('-', $parts[0], 2); $offset = $range[0]; $to = isset($range[1]) ? $range[1] : $date_value; // Ensure that the date value is within the range if ( $date_value < $offset || $date_value > $to ) { return false; } for ($i = $offset; $i <= $to; $i += $stepSize) { if ( $i == $date_value ) { return true; } } return false; } /** * Increments/decrements given date for 1 unit based on field type * * @param int $field_type * @param int $date * @param bool $inverse * @return int * @access public */ public function increment($field_type, $date, $inverse = false) { $mapping = Array ( kCronHelper::MINUTE => '1 minute', kCronHelper::HOUR => '1 hour', kCronHelper::DAY => '1 day', kCronHelper::MONTH => '1 month', kCronHelper::WEEKDAY => '1 day', ); return $this->_resetTime($field_type, strtotime(($inverse ? '-' : '+') . $mapping[$field_type], $date), $inverse); } /** * Resets time based on field type * * @param int $field_type * @param int $date * @param bool $inverse * @return int * @access public */ protected function _resetTime($field_type, $date, $inverse = false) { if ( $field_type == kCronHelper::MONTH || $field_type == kCronHelper::WEEKDAY || $field_type == kCronHelper::DAY ) { if ( $inverse ) { $date = strtotime(date('Y-m-d 23:59:59', $date)); // set time 23:59:00 } else { // set time 00:00:00 $date = strtotime(date('Y-m-d 00:00:00', $date)); } } elseif ( $field_type == kCronHelper::HOUR ) { if ( $inverse ) { // set time :59:00 $date = strtotime(date('Y-m-d H:59:59', $date)); } else { // set time :00:00 $date = strtotime(date('Y-m-d H:00:00', $date)); } } return $date; } }