diff options
Diffstat (limited to 'vendor/dragonmantank/cron-expression')
13 files changed, 1575 insertions, 0 deletions
diff --git a/vendor/dragonmantank/cron-expression/CHANGELOG.md b/vendor/dragonmantank/cron-expression/CHANGELOG.md new file mode 100644 index 0000000..4e207aa --- /dev/null +++ b/vendor/dragonmantank/cron-expression/CHANGELOG.md @@ -0,0 +1,84 @@ +# Change Log + +## [2.3.1] - 2020-10-12 +### Added +- Added support for PHP 8 (#92) +### Changed +- N/A +### Fixed +- N/A + +## [2.3.0] - 2019-03-30 +### Added +- Added support for DateTimeImmutable via DateTimeInterface +- Added support for PHP 7.3 +- Started listing projects that use the library +### Changed +- Errors should now report a human readable position in the cron expression, instead of starting at 0 +### Fixed +- N/A + +## [2.2.0] - 2018-06-05 +### Added +- Added support for steps larger than field ranges (#6) +## Changed +- N/A +### Fixed +- Fixed validation for numbers with leading 0s (#12) + +## [2.1.0] - 2018-04-06 +### Added +- N/A +### Changed +- Upgraded to PHPUnit 6 (#2) +### Fixed +- Refactored timezones to deal with some inconsistent behavior (#3) +- Allow ranges and lists in same expression (#5) +- Fixed regression where literals were not converted to their numerical counterpart (#) + +## [2.0.0] - 2017-10-12 +### Added +- N/A + +### Changed +- Dropped support for PHP 5.x +- Dropped support for the YEAR field, as it was not part of the cron standard + +### Fixed +- Reworked validation for all the field types +- Stepping should now work for 1-indexed fields like Month (#153) + +## [1.2.0] - 2017-01-22 +### Added +- Added IDE, CodeSniffer, and StyleCI.IO support + +### Changed +- Switched to PSR-4 Autoloading + +### Fixed +- 0 step expressions are handled better +- Fixed `DayOfMonth` validation to be more strict +- Typos + +## [1.1.0] - 2016-01-26 +### Added +- Support for non-hourly offset timezones +- Checks for valid expressions + +### Changed +- Max Iterations no longer hardcoded for `getRunDate()` +- Supports DateTimeImmutable for newer PHP verions + +### Fixed +- Fixed looping bug for PHP 7 when determining the last specified weekday of a month + +## [1.0.3] - 2013-11-23 +### Added +- Now supports expressions with any number of extra spaces, tabs, or newlines + +### Changed +- Using static instead of self in `CronExpression::factory` + +### Fixed +- Fixes issue [#28](https://github.com/mtdowling/cron-expression/issues/28) where PHP increments of ranges were failing due to PHP casting hyphens to 0 +- Only set default timezone if the given $currentTime is not a DateTime instance ([#34](https://github.com/mtdowling/cron-expression/issues/34)) diff --git a/vendor/dragonmantank/cron-expression/LICENSE b/vendor/dragonmantank/cron-expression/LICENSE new file mode 100644 index 0000000..3e38bbc --- /dev/null +++ b/vendor/dragonmantank/cron-expression/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2011 Michael Dowling <mtdowling@gmail.com>, 2016 Chris Tankersley <chris@ctankersley.com>, and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/dragonmantank/cron-expression/README.md b/vendor/dragonmantank/cron-expression/README.md new file mode 100644 index 0000000..8e8021b --- /dev/null +++ b/vendor/dragonmantank/cron-expression/README.md @@ -0,0 +1,78 @@ +PHP Cron Expression Parser +========================== + +[![Latest Stable Version](https://poser.pugx.org/dragonmantank/cron-expression/v/stable.png)](https://packagist.org/packages/dragonmantank/cron-expression) [![Total Downloads](https://poser.pugx.org/dragonmantank/cron-expression/downloads.png)](https://packagist.org/packages/dragonmantank/cron-expression) [![Build Status](https://secure.travis-ci.org/dragonmantank/cron-expression.png)](http://travis-ci.org/dragonmantank/cron-expression) + +The PHP cron expression parser can parse a CRON expression, determine if it is +due to run, calculate the next run date of the expression, and calculate the previous +run date of the expression. You can calculate dates far into the future or past by +skipping **n** number of matching dates. + +The parser can handle increments of ranges (e.g. */12, 2-59/3), intervals (e.g. 0-9), +lists (e.g. 1,2,3), **W** to find the nearest weekday for a given day of the month, **L** to +find the last day of the month, **L** to find the last given weekday of a month, and hash +(#) to find the nth weekday of a given month. + +More information about this fork can be found in the blog post [here](http://ctankersley.com/2017/10/12/cron-expression-update/). tl;dr - v2.0.0 is a major breaking change, and @dragonmantank can better take care of the project in a separate fork. + +Installing +========== + +Add the dependency to your project: + +```bash +composer require dragonmantank/cron-expression +``` + +Usage +===== +```php +<?php + +require_once '/vendor/autoload.php'; + +// Works with predefined scheduling definitions +$cron = Cron\CronExpression::factory('@daily'); +$cron->isDue(); +echo $cron->getNextRunDate()->format('Y-m-d H:i:s'); +echo $cron->getPreviousRunDate()->format('Y-m-d H:i:s'); + +// Works with complex expressions +$cron = Cron\CronExpression::factory('3-59/15 6-12 */15 1 2-5'); +echo $cron->getNextRunDate()->format('Y-m-d H:i:s'); + +// Calculate a run date two iterations into the future +$cron = Cron\CronExpression::factory('@daily'); +echo $cron->getNextRunDate(null, 2)->format('Y-m-d H:i:s'); + +// Calculate a run date relative to a specific time +$cron = Cron\CronExpression::factory('@monthly'); +echo $cron->getNextRunDate('2010-01-12 00:00:00')->format('Y-m-d H:i:s'); +``` + +CRON Expressions +================ + +A CRON expression is a string representing the schedule for a particular command to execute. The parts of a CRON schedule are as follows: + + * * * * * + - - - - - + | | | | | + | | | | | + | | | | +----- day of week (0 - 7) (Sunday=0 or 7) + | | | +---------- month (1 - 12) + | | +--------------- day of month (1 - 31) + | +-------------------- hour (0 - 23) + +------------------------- min (0 - 59) + +Requirements +============ + +- PHP 7.0+ +- PHPUnit is required to run the unit tests +- Composer is required to run the unit tests + +Projects that Use cron-expression +================================= +* Part of the [Laravel Framework](https://github.com/laravel/framework/) +* Available as a [Symfony Bundle - setono/cron-expression-bundle](https://github.com/Setono/CronExpressionBundle)
\ No newline at end of file diff --git a/vendor/dragonmantank/cron-expression/composer.json b/vendor/dragonmantank/cron-expression/composer.json new file mode 100644 index 0000000..6fcf818 --- /dev/null +++ b/vendor/dragonmantank/cron-expression/composer.json @@ -0,0 +1,40 @@ +{ + "name": "dragonmantank/cron-expression", + "type": "library", + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": ["cron", "schedule"], + "license": "MIT", + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "require": { + "php": "^7.0|^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.4|^7.0|^8.0|^9.0" + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/Cron/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "2.3-dev" + } + } +} diff --git a/vendor/dragonmantank/cron-expression/src/Cron/AbstractField.php b/vendor/dragonmantank/cron-expression/src/Cron/AbstractField.php new file mode 100644 index 0000000..8b1072a --- /dev/null +++ b/vendor/dragonmantank/cron-expression/src/Cron/AbstractField.php @@ -0,0 +1,286 @@ +<?php + +namespace Cron; + +/** + * Abstract CRON expression field + */ +abstract class AbstractField implements FieldInterface +{ + /** + * Full range of values that are allowed for this field type + * @var array + */ + protected $fullRange = []; + + /** + * Literal values we need to convert to integers + * @var array + */ + protected $literals = []; + + /** + * Start value of the full range + * @var integer + */ + protected $rangeStart; + + /** + * End value of the full range + * @var integer + */ + protected $rangeEnd; + + /** + * Constructor + */ + public function __construct() + { + $this->fullRange = range($this->rangeStart, $this->rangeEnd); + } + + /** + * Check to see if a field is satisfied by a value + * + * @param string $dateValue Date value to check + * @param string $value Value to test + * + * @return bool + */ + public function isSatisfied($dateValue, $value) + { + if ($this->isIncrementsOfRanges($value)) { + return $this->isInIncrementsOfRanges($dateValue, $value); + } elseif ($this->isRange($value)) { + return $this->isInRange($dateValue, $value); + } + + return $value == '*' || $dateValue == $value; + } + + /** + * Check if a value is a range + * + * @param string $value Value to test + * + * @return bool + */ + public function isRange($value) + { + return strpos($value, '-') !== false; + } + + /** + * Check if a value is an increments of ranges + * + * @param string $value Value to test + * + * @return bool + */ + public function isIncrementsOfRanges($value) + { + return strpos($value, '/') !== false; + } + + /** + * Test if a value is within a range + * + * @param string $dateValue Set date value + * @param string $value Value to test + * + * @return bool + */ + public function isInRange($dateValue, $value) + { + $parts = array_map(function($value) { + $value = trim($value); + $value = $this->convertLiterals($value); + return $value; + }, + explode('-', $value, 2) + ); + + + return $dateValue >= $parts[0] && $dateValue <= $parts[1]; + } + + /** + * Test if a value is within an increments of ranges (offset[-to]/step size) + * + * @param string $dateValue Set date value + * @param string $value Value to test + * + * @return bool + */ + public function isInIncrementsOfRanges($dateValue, $value) + { + $chunks = array_map('trim', explode('/', $value, 2)); + $range = $chunks[0]; + $step = isset($chunks[1]) ? $chunks[1] : 0; + + // No step or 0 steps aren't cool + if (is_null($step) || '0' === $step || 0 === $step) { + return false; + } + + // Expand the * to a full range + if ('*' == $range) { + $range = $this->rangeStart . '-' . $this->rangeEnd; + } + + // Generate the requested small range + $rangeChunks = explode('-', $range, 2); + $rangeStart = $rangeChunks[0]; + $rangeEnd = isset($rangeChunks[1]) ? $rangeChunks[1] : $rangeStart; + + if ($rangeStart < $this->rangeStart || $rangeStart > $this->rangeEnd || $rangeStart > $rangeEnd) { + throw new \OutOfRangeException('Invalid range start requested'); + } + + if ($rangeEnd < $this->rangeStart || $rangeEnd > $this->rangeEnd || $rangeEnd < $rangeStart) { + throw new \OutOfRangeException('Invalid range end requested'); + } + + // Steps larger than the range need to wrap around and be handled slightly differently than smaller steps + if ($step >= $this->rangeEnd) { + $thisRange = [$this->fullRange[$step % count($this->fullRange)]]; + } else { + $thisRange = range($rangeStart, $rangeEnd, $step); + } + + return in_array($dateValue, $thisRange); + } + + /** + * Returns a range of values for the given cron expression + * + * @param string $expression The expression to evaluate + * @param int $max Maximum offset for range + * + * @return array + */ + public function getRangeForExpression($expression, $max) + { + $values = array(); + $expression = $this->convertLiterals($expression); + + if (strpos($expression, ',') !== false) { + $ranges = explode(',', $expression); + $values = []; + foreach ($ranges as $range) { + $expanded = $this->getRangeForExpression($range, $this->rangeEnd); + $values = array_merge($values, $expanded); + } + return $values; + } + + if ($this->isRange($expression) || $this->isIncrementsOfRanges($expression)) { + if (!$this->isIncrementsOfRanges($expression)) { + list ($offset, $to) = explode('-', $expression); + $offset = $this->convertLiterals($offset); + $to = $this->convertLiterals($to); + $stepSize = 1; + } + else { + $range = array_map('trim', explode('/', $expression, 2)); + $stepSize = isset($range[1]) ? $range[1] : 0; + $range = $range[0]; + $range = explode('-', $range, 2); + $offset = $range[0]; + $to = isset($range[1]) ? $range[1] : $max; + } + $offset = $offset == '*' ? $this->rangeStart : $offset; + if ($stepSize >= $this->rangeEnd) { + $values = [$this->fullRange[$stepSize % count($this->fullRange)]]; + } else { + for ($i = $offset; $i <= $to; $i += $stepSize) { + $values[] = (int)$i; + } + } + sort($values); + } + else { + $values = array($expression); + } + + return $values; + } + + /** + * Convert literal + * + * @param string $value + * @return string + */ + protected function convertLiterals($value) + { + if (count($this->literals)) { + $key = array_search($value, $this->literals); + if ($key !== false) { + return (string) $key; + } + } + + return $value; + } + + /** + * Checks to see if a value is valid for the field + * + * @param string $value + * @return bool + */ + public function validate($value) + { + $value = $this->convertLiterals($value); + + // All fields allow * as a valid value + if ('*' === $value) { + return true; + } + + if (strpos($value, '/') !== false) { + list($range, $step) = explode('/', $value); + return $this->validate($range) && filter_var($step, FILTER_VALIDATE_INT); + } + + // Validate each chunk of a list individually + if (strpos($value, ',') !== false) { + foreach (explode(',', $value) as $listItem) { + if (!$this->validate($listItem)) { + return false; + } + } + return true; + } + + if (strpos($value, '-') !== false) { + if (substr_count($value, '-') > 1) { + return false; + } + + $chunks = explode('-', $value); + $chunks[0] = $this->convertLiterals($chunks[0]); + $chunks[1] = $this->convertLiterals($chunks[1]); + + if ('*' == $chunks[0] || '*' == $chunks[1]) { + return false; + } + + return $this->validate($chunks[0]) && $this->validate($chunks[1]); + } + + if (!is_numeric($value)) { + return false; + } + + if (is_float($value) || strpos($value, '.') !== false) { + return false; + } + + // We should have a numeric by now, so coerce this into an integer + $value = (int) $value; + + return in_array($value, $this->fullRange, true); + } +} diff --git a/vendor/dragonmantank/cron-expression/src/Cron/CronExpression.php b/vendor/dragonmantank/cron-expression/src/Cron/CronExpression.php new file mode 100644 index 0000000..dbb93ce --- /dev/null +++ b/vendor/dragonmantank/cron-expression/src/Cron/CronExpression.php @@ -0,0 +1,413 @@ +<?php + +namespace Cron; + +use DateTime; +use DateTimeImmutable; +use DateTimeInterface; +use DateTimeZone; +use Exception; +use InvalidArgumentException; +use RuntimeException; + +/** + * CRON expression parser that can determine whether or not a CRON expression is + * due to run, the next run date and previous run date of a CRON expression. + * The determinations made by this class are accurate if checked run once per + * minute (seconds are dropped from date time comparisons). + * + * Schedule parts must map to: + * minute [0-59], hour [0-23], day of month, month [1-12|JAN-DEC], day of week + * [1-7|MON-SUN], and an optional year. + * + * @link http://en.wikipedia.org/wiki/Cron + */ +class CronExpression +{ + const MINUTE = 0; + const HOUR = 1; + const DAY = 2; + const MONTH = 3; + const WEEKDAY = 4; + const YEAR = 5; + + /** + * @var array CRON expression parts + */ + private $cronParts; + + /** + * @var FieldFactory CRON field factory + */ + private $fieldFactory; + + /** + * @var int Max iteration count when searching for next run date + */ + private $maxIterationCount = 1000; + + /** + * @var array Order in which to test of cron parts + */ + private static $order = array(self::YEAR, self::MONTH, self::DAY, self::WEEKDAY, self::HOUR, self::MINUTE); + + /** + * Factory method to create a new CronExpression. + * + * @param string $expression The CRON expression to create. There are + * several special predefined values which can be used to substitute the + * CRON expression: + * + * `@yearly`, `@annually` - Run once a year, midnight, Jan. 1 - 0 0 1 1 * + * `@monthly` - Run once a month, midnight, first of month - 0 0 1 * * + * `@weekly` - Run once a week, midnight on Sun - 0 0 * * 0 + * `@daily` - Run once a day, midnight - 0 0 * * * + * `@hourly` - Run once an hour, first minute - 0 * * * * + * @param FieldFactory|null $fieldFactory Field factory to use + * + * @return CronExpression + */ + public static function factory($expression, FieldFactory $fieldFactory = null) + { + $mappings = array( + '@yearly' => '0 0 1 1 *', + '@annually' => '0 0 1 1 *', + '@monthly' => '0 0 1 * *', + '@weekly' => '0 0 * * 0', + '@daily' => '0 0 * * *', + '@hourly' => '0 * * * *' + ); + + if (isset($mappings[$expression])) { + $expression = $mappings[$expression]; + } + + return new static($expression, $fieldFactory ?: new FieldFactory()); + } + + /** + * Validate a CronExpression. + * + * @param string $expression The CRON expression to validate. + * + * @return bool True if a valid CRON expression was passed. False if not. + * @see \Cron\CronExpression::factory + */ + public static function isValidExpression($expression) + { + try { + self::factory($expression); + } catch (InvalidArgumentException $e) { + return false; + } + + return true; + } + + /** + * Parse a CRON expression + * + * @param string $expression CRON expression (e.g. '8 * * * *') + * @param FieldFactory|null $fieldFactory Factory to create cron fields + */ + public function __construct($expression, FieldFactory $fieldFactory = null) + { + $this->fieldFactory = $fieldFactory ?: new FieldFactory(); + $this->setExpression($expression); + } + + /** + * Set or change the CRON expression + * + * @param string $value CRON expression (e.g. 8 * * * *) + * + * @return CronExpression + * @throws \InvalidArgumentException if not a valid CRON expression + */ + public function setExpression($value) + { + $this->cronParts = preg_split('/\s/', $value, -1, PREG_SPLIT_NO_EMPTY); + if (count($this->cronParts) < 5) { + throw new InvalidArgumentException( + $value . ' is not a valid CRON expression' + ); + } + + foreach ($this->cronParts as $position => $part) { + $this->setPart($position, $part); + } + + return $this; + } + + /** + * Set part of the CRON expression + * + * @param int $position The position of the CRON expression to set + * @param string $value The value to set + * + * @return CronExpression + * @throws \InvalidArgumentException if the value is not valid for the part + */ + public function setPart($position, $value) + { + if (!$this->fieldFactory->getField($position)->validate($value)) { + throw new InvalidArgumentException( + 'Invalid CRON field value ' . $value . ' at position ' . $position + ); + } + + $this->cronParts[$position] = $value; + + return $this; + } + + /** + * Set max iteration count for searching next run dates + * + * @param int $maxIterationCount Max iteration count when searching for next run date + * + * @return CronExpression + */ + public function setMaxIterationCount($maxIterationCount) + { + $this->maxIterationCount = $maxIterationCount; + + return $this; + } + + /** + * Get a next run date relative to the current date or a specific date + * + * @param string|\DateTimeInterface $currentTime Relative calculation date + * @param int $nth Number of matches to skip before returning a + * matching next run date. 0, the default, will return the + * current date and time if the next run date falls on the + * current date and time. Setting this value to 1 will + * skip the first match and go to the second match. + * Setting this value to 2 will skip the first 2 + * matches and so on. + * @param bool $allowCurrentDate Set to TRUE to return the current date if + * it matches the cron expression. + * @param null|string $timeZone TimeZone to use instead of the system default + * + * @return \DateTime + * @throws \RuntimeException on too many iterations + */ + public function getNextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false, $timeZone = null) + { + return $this->getRunDate($currentTime, $nth, false, $allowCurrentDate, $timeZone); + } + + /** + * Get a previous run date relative to the current date or a specific date + * + * @param string|\DateTimeInterface $currentTime Relative calculation date + * @param int $nth Number of matches to skip before returning + * @param bool $allowCurrentDate Set to TRUE to return the + * current date if it matches the cron expression + * @param null|string $timeZone TimeZone to use instead of the system default + * + * @return \DateTime + * @throws \RuntimeException on too many iterations + * @see \Cron\CronExpression::getNextRunDate + */ + public function getPreviousRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false, $timeZone = null) + { + return $this->getRunDate($currentTime, $nth, true, $allowCurrentDate, $timeZone); + } + + /** + * Get multiple run dates starting at the current date or a specific date + * + * @param int $total Set the total number of dates to calculate + * @param string|\DateTimeInterface $currentTime Relative calculation date + * @param bool $invert Set to TRUE to retrieve previous dates + * @param bool $allowCurrentDate Set to TRUE to return the + * current date if it matches the cron expression + * @param null|string $timeZone TimeZone to use instead of the system default + * + * @return \DateTime[] Returns an array of run dates + */ + public function getMultipleRunDates($total, $currentTime = 'now', $invert = false, $allowCurrentDate = false, $timeZone = null) + { + $matches = array(); + for ($i = 0; $i < max(0, $total); $i++) { + try { + $matches[] = $this->getRunDate($currentTime, $i, $invert, $allowCurrentDate, $timeZone); + } catch (RuntimeException $e) { + break; + } + } + + return $matches; + } + + /** + * Get all or part of the CRON expression + * + * @param string $part Specify the part to retrieve or NULL to get the full + * cron schedule string. + * + * @return string|null Returns the CRON expression, a part of the + * CRON expression, or NULL if the part was specified but not found + */ + public function getExpression($part = null) + { + if (null === $part) { + return implode(' ', $this->cronParts); + } elseif (array_key_exists($part, $this->cronParts)) { + return $this->cronParts[$part]; + } + + return null; + } + + /** + * Helper method to output the full expression. + * + * @return string Full CRON expression + */ + public function __toString() + { + return $this->getExpression(); + } + + /** + * Determine if the cron is due to run based on the current date or a + * specific date. This method assumes that the current number of + * seconds are irrelevant, and should be called once per minute. + * + * @param string|\DateTimeInterface $currentTime Relative calculation date + * @param null|string $timeZone TimeZone to use instead of the system default + * + * @return bool Returns TRUE if the cron is due to run or FALSE if not + */ + public function isDue($currentTime = 'now', $timeZone = null) + { + $timeZone = $this->determineTimeZone($currentTime, $timeZone); + + if ('now' === $currentTime) { + $currentTime = new DateTime(); + } elseif ($currentTime instanceof DateTime) { + // + } elseif ($currentTime instanceof DateTimeImmutable) { + $currentTime = DateTime::createFromFormat('U', $currentTime->format('U')); + } else { + $currentTime = new DateTime($currentTime); + } + $currentTime->setTimeZone(new DateTimeZone($timeZone)); + + // drop the seconds to 0 + $currentTime = DateTime::createFromFormat('Y-m-d H:i', $currentTime->format('Y-m-d H:i')); + + try { + return $this->getNextRunDate($currentTime, 0, true)->getTimestamp() === $currentTime->getTimestamp(); + } catch (Exception $e) { + return false; + } + } + + /** + * Get the next or previous run date of the expression relative to a date + * + * @param string|\DateTimeInterface $currentTime Relative calculation date + * @param int $nth Number of matches to skip before returning + * @param bool $invert Set to TRUE to go backwards in time + * @param bool $allowCurrentDate Set to TRUE to return the + * current date if it matches the cron expression + * @param string|null $timeZone TimeZone to use instead of the system default + * + * @return \DateTime + * @throws \RuntimeException on too many iterations + */ + protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $allowCurrentDate = false, $timeZone = null) + { + $timeZone = $this->determineTimeZone($currentTime, $timeZone); + + if ($currentTime instanceof DateTime) { + $currentDate = clone $currentTime; + } elseif ($currentTime instanceof DateTimeImmutable) { + $currentDate = DateTime::createFromFormat('U', $currentTime->format('U')); + } else { + $currentDate = new DateTime($currentTime ?: 'now'); + } + + $currentDate->setTimeZone(new DateTimeZone($timeZone)); + $currentDate->setTime($currentDate->format('H'), $currentDate->format('i'), 0); + $nextRun = clone $currentDate; + $nth = (int) $nth; + + // We don't have to satisfy * or null fields + $parts = array(); + $fields = array(); + foreach (self::$order as $position) { + $part = $this->getExpression($position); + if (null === $part || '*' === $part) { + continue; + } + $parts[$position] = $part; + $fields[$position] = $this->fieldFactory->getField($position); + } + + // Set a hard limit to bail on an impossible date + for ($i = 0; $i < $this->maxIterationCount; $i++) { + + foreach ($parts as $position => $part) { + $satisfied = false; + // Get the field object used to validate this part + $field = $fields[$position]; + // Check if this is singular or a list + if (strpos($part, ',') === false) { + $satisfied = $field->isSatisfiedBy($nextRun, $part); + } else { + foreach (array_map('trim', explode(',', $part)) as $listPart) { + if ($field->isSatisfiedBy($nextRun, $listPart)) { + $satisfied = true; + break; + } + } + } + + // If the field is not satisfied, then start over + if (!$satisfied) { + $field->increment($nextRun, $invert, $part); + continue 2; + } + } + + // Skip this match if needed + if ((!$allowCurrentDate && $nextRun == $currentDate) || --$nth > -1) { + $this->fieldFactory->getField(0)->increment($nextRun, $invert, isset($parts[0]) ? $parts[0] : null); + continue; + } + + return $nextRun; + } + + // @codeCoverageIgnoreStart + throw new RuntimeException('Impossible CRON expression'); + // @codeCoverageIgnoreEnd + } + + /** + * Workout what timeZone should be used. + * + * @param string|\DateTimeInterface $currentTime Relative calculation date + * @param string|null $timeZone TimeZone to use instead of the system default + * + * @return string + */ + protected function determineTimeZone($currentTime, $timeZone) + { + if (! is_null($timeZone)) { + return $timeZone; + } + + if ($currentTime instanceOf DateTimeInterface) { + return $currentTime->getTimeZone()->getName(); + } + + return date_default_timezone_get(); + } +} diff --git a/vendor/dragonmantank/cron-expression/src/Cron/DayOfMonthField.php b/vendor/dragonmantank/cron-expression/src/Cron/DayOfMonthField.php new file mode 100644 index 0000000..d4552e0 --- /dev/null +++ b/vendor/dragonmantank/cron-expression/src/Cron/DayOfMonthField.php @@ -0,0 +1,145 @@ +<?php + +namespace Cron; + +use DateTime; +use DateTimeInterface; + +/** + * Day of month field. Allows: * , / - ? L W + * + * 'L' stands for "last" and specifies the last day of the month. + * + * The 'W' character is used to specify the weekday (Monday-Friday) nearest the + * given day. As an example, if you were to specify "15W" as the value for the + * day-of-month field, the meaning is: "the nearest weekday to the 15th of the + * month". So if the 15th is a Saturday, the trigger will fire on Friday the + * 14th. If the 15th is a Sunday, the trigger will fire on Monday the 16th. If + * the 15th is a Tuesday, then it will fire on Tuesday the 15th. However if you + * specify "1W" as the value for day-of-month, and the 1st is a Saturday, the + * trigger will fire on Monday the 3rd, as it will not 'jump' over the boundary + * of a month's days. The 'W' character can only be specified when the + * day-of-month is a single day, not a range or list of days. + * + * @author Michael Dowling <mtdowling@gmail.com> + */ +class DayOfMonthField extends AbstractField +{ + /** + * @inheritDoc + */ + protected $rangeStart = 1; + + /** + * @inheritDoc + */ + protected $rangeEnd = 31; + + /** + * Get the nearest day of the week for a given day in a month + * + * @param int $currentYear Current year + * @param int $currentMonth Current month + * @param int $targetDay Target day of the month + * + * @return \DateTime Returns the nearest date + */ + private static function getNearestWeekday($currentYear, $currentMonth, $targetDay) + { + $tday = str_pad($targetDay, 2, '0', STR_PAD_LEFT); + $target = DateTime::createFromFormat('Y-m-d', "$currentYear-$currentMonth-$tday"); + $currentWeekday = (int) $target->format('N'); + + if ($currentWeekday < 6) { + return $target; + } + + $lastDayOfMonth = $target->format('t'); + + foreach (array(-1, 1, -2, 2) as $i) { + $adjusted = $targetDay + $i; + if ($adjusted > 0 && $adjusted <= $lastDayOfMonth) { + $target->setDate($currentYear, $currentMonth, $adjusted); + if ($target->format('N') < 6 && $target->format('m') == $currentMonth) { + return $target; + } + } + } + } + + /** + * @inheritDoc + */ + public function isSatisfiedBy(DateTimeInterface $date, $value) + { + // ? states that the field value is to be skipped + if ($value == '?') { + return true; + } + + $fieldValue = $date->format('d'); + + // Check to see if this is the last day of the month + if ($value == 'L') { + return $fieldValue == $date->format('t'); + } + + // Check to see if this is the nearest weekday to a particular value + if (strpos($value, 'W')) { + // Parse the target day + $targetDay = substr($value, 0, strpos($value, 'W')); + // Find out if the current day is the nearest day of the week + return $date->format('j') == self::getNearestWeekday( + $date->format('Y'), + $date->format('m'), + $targetDay + )->format('j'); + } + + return $this->isSatisfied($date->format('d'), $value); + } + + /** + * @inheritDoc + * + * @param \DateTime|\DateTimeImmutable &$date + */ + public function increment(DateTimeInterface &$date, $invert = false) + { + if ($invert) { + $date = $date->modify('previous day')->setTime(23, 59); + } else { + $date = $date->modify('next day')->setTime(0, 0); + } + + return $this; + } + + /** + * @inheritDoc + */ + public function validate($value) + { + $basicChecks = parent::validate($value); + + // Validate that a list don't have W or L + if (strpos($value, ',') !== false && (strpos($value, 'W') !== false || strpos($value, 'L') !== false)) { + return false; + } + + if (!$basicChecks) { + + if ($value === 'L') { + return true; + } + + if (preg_match('/^(.*)W$/', $value, $matches)) { + return $this->validate($matches[1]); + } + + return false; + } + + return $basicChecks; + } +} diff --git a/vendor/dragonmantank/cron-expression/src/Cron/DayOfWeekField.php b/vendor/dragonmantank/cron-expression/src/Cron/DayOfWeekField.php new file mode 100644 index 0000000..d4ba315 --- /dev/null +++ b/vendor/dragonmantank/cron-expression/src/Cron/DayOfWeekField.php @@ -0,0 +1,196 @@ +<?php + +namespace Cron; + +use DateTime; +use DateTimeInterface; +use InvalidArgumentException; + +/** + * Day of week field. Allows: * / , - ? L # + * + * Days of the week can be represented as a number 0-7 (0|7 = Sunday) + * or as a three letter string: SUN, MON, TUE, WED, THU, FRI, SAT. + * + * 'L' stands for "last". It allows you to specify constructs such as + * "the last Friday" of a given month. + * + * '#' is allowed for the day-of-week field, and must be followed by a + * number between one and five. It allows you to specify constructs such as + * "the second Friday" of a given month. + */ +class DayOfWeekField extends AbstractField +{ + /** + * @inheritDoc + */ + protected $rangeStart = 0; + + /** + * @inheritDoc + */ + protected $rangeEnd = 7; + + /** + * @var array Weekday range + */ + protected $nthRange; + + /** + * @inheritDoc + */ + protected $literals = [1 => 'MON', 2 => 'TUE', 3 => 'WED', 4 => 'THU', 5 => 'FRI', 6 => 'SAT', 7 => 'SUN']; + + /** + * Constructor + */ + public function __construct() + { + $this->nthRange = range(1, 5); + parent::__construct(); + } + + /** + * @inheritDoc + * + * @param \DateTime|\DateTimeImmutable $date + */ + public function isSatisfiedBy(DateTimeInterface $date, $value) + { + if ($value == '?') { + return true; + } + + // Convert text day of the week values to integers + $value = $this->convertLiterals($value); + + $currentYear = $date->format('Y'); + $currentMonth = $date->format('m'); + $lastDayOfMonth = $date->format('t'); + + // Find out if this is the last specific weekday of the month + if (strpos($value, 'L')) { + $weekday = (int) $this->convertLiterals(substr($value, 0, strpos($value, 'L'))); + $weekday %= 7; + + $tdate = clone $date; + $tdate = $tdate->setDate($currentYear, $currentMonth, $lastDayOfMonth); + while ($tdate->format('w') != $weekday) { + $tdateClone = new DateTime(); + $tdate = $tdateClone + ->setTimezone($tdate->getTimezone()) + ->setDate($currentYear, $currentMonth, --$lastDayOfMonth); + } + + return $date->format('j') == $lastDayOfMonth; + } + + // Handle # hash tokens + if (strpos($value, '#')) { + list($weekday, $nth) = explode('#', $value); + + if (!is_numeric($nth)) { + throw new InvalidArgumentException("Hashed weekdays must be numeric, {$nth} given"); + } else { + $nth = (int) $nth; + } + + // 0 and 7 are both Sunday, however 7 matches date('N') format ISO-8601 + if ($weekday === '0') { + $weekday = 7; + } + + $weekday = $this->convertLiterals($weekday); + + // Validate the hash fields + if ($weekday < 0 || $weekday > 7) { + throw new InvalidArgumentException("Weekday must be a value between 0 and 7. {$weekday} given"); + } + + if (!in_array($nth, $this->nthRange)) { + throw new InvalidArgumentException("There are never more than 5 or less than 1 of a given weekday in a month, {$nth} given"); + } + + // The current weekday must match the targeted weekday to proceed + if ($date->format('N') != $weekday) { + return false; + } + + $tdate = clone $date; + $tdate = $tdate->setDate($currentYear, $currentMonth, 1); + $dayCount = 0; + $currentDay = 1; + while ($currentDay < $lastDayOfMonth + 1) { + if ($tdate->format('N') == $weekday) { + if (++$dayCount >= $nth) { + break; + } + } + $tdate = $tdate->setDate($currentYear, $currentMonth, ++$currentDay); + } + + return $date->format('j') == $currentDay; + } + + // Handle day of the week values + if (strpos($value, '-')) { + $parts = explode('-', $value); + if ($parts[0] == '7') { + $parts[0] = '0'; + } elseif ($parts[1] == '0') { + $parts[1] = '7'; + } + $value = implode('-', $parts); + } + + // Test to see which Sunday to use -- 0 == 7 == Sunday + $format = in_array(7, str_split($value)) ? 'N' : 'w'; + $fieldValue = $date->format($format); + + return $this->isSatisfied($fieldValue, $value); + } + + /** + * @inheritDoc + * + * @param \DateTime|\DateTimeImmutable &$date + */ + public function increment(DateTimeInterface &$date, $invert = false) + { + if ($invert) { + $date = $date->modify('-1 day')->setTime(23, 59, 0); + } else { + $date = $date->modify('+1 day')->setTime(0, 0, 0); + } + + return $this; + } + + /** + * @inheritDoc + */ + public function validate($value) + { + $basicChecks = parent::validate($value); + + if (!$basicChecks) { + // Handle the # value + if (strpos($value, '#') !== false) { + $chunks = explode('#', $value); + $chunks[0] = $this->convertLiterals($chunks[0]); + + if (parent::validate($chunks[0]) && is_numeric($chunks[1]) && in_array($chunks[1], $this->nthRange)) { + return true; + } + } + + if (preg_match('/^(.*)L$/', $value, $matches)) { + return $this->validate($matches[1]); + } + + return false; + } + + return $basicChecks; + } +} diff --git a/vendor/dragonmantank/cron-expression/src/Cron/FieldFactory.php b/vendor/dragonmantank/cron-expression/src/Cron/FieldFactory.php new file mode 100644 index 0000000..545e4b8 --- /dev/null +++ b/vendor/dragonmantank/cron-expression/src/Cron/FieldFactory.php @@ -0,0 +1,54 @@ +<?php + +namespace Cron; + +use InvalidArgumentException; + +/** + * CRON field factory implementing a flyweight factory + * @link http://en.wikipedia.org/wiki/Cron + */ +class FieldFactory +{ + /** + * @var array Cache of instantiated fields + */ + private $fields = array(); + + /** + * Get an instance of a field object for a cron expression position + * + * @param int $position CRON expression position value to retrieve + * + * @return FieldInterface + * @throws InvalidArgumentException if a position is not valid + */ + public function getField($position) + { + if (!isset($this->fields[$position])) { + switch ($position) { + case 0: + $this->fields[$position] = new MinutesField(); + break; + case 1: + $this->fields[$position] = new HoursField(); + break; + case 2: + $this->fields[$position] = new DayOfMonthField(); + break; + case 3: + $this->fields[$position] = new MonthField(); + break; + case 4: + $this->fields[$position] = new DayOfWeekField(); + break; + default: + throw new InvalidArgumentException( + ($position + 1) . ' is not a valid position' + ); + } + } + + return $this->fields[$position]; + } +} diff --git a/vendor/dragonmantank/cron-expression/src/Cron/FieldInterface.php b/vendor/dragonmantank/cron-expression/src/Cron/FieldInterface.php new file mode 100644 index 0000000..f8366ea --- /dev/null +++ b/vendor/dragonmantank/cron-expression/src/Cron/FieldInterface.php @@ -0,0 +1,41 @@ +<?php + +namespace Cron; + +use DateTimeInterface; + +/** + * CRON field interface + */ +interface FieldInterface +{ + /** + * Check if the respective value of a DateTime field satisfies a CRON exp + * + * @param DateTimeInterface $date DateTime object to check + * @param string $value CRON expression to test against + * + * @return bool Returns TRUE if satisfied, FALSE otherwise + */ + public function isSatisfiedBy(DateTimeInterface $date, $value); + + /** + * When a CRON expression is not satisfied, this method is used to increment + * or decrement a DateTime object by the unit of the cron field + * + * @param DateTimeInterface &$date DateTime object to change + * @param bool $invert (optional) Set to TRUE to decrement + * + * @return FieldInterface + */ + public function increment(DateTimeInterface &$date, $invert = false); + + /** + * Validates a CRON expression for a given field + * + * @param string $value CRON expression value to validate + * + * @return bool Returns TRUE if valid, FALSE otherwise + */ + public function validate($value); +} diff --git a/vendor/dragonmantank/cron-expression/src/Cron/HoursField.php b/vendor/dragonmantank/cron-expression/src/Cron/HoursField.php new file mode 100644 index 0000000..628218a --- /dev/null +++ b/vendor/dragonmantank/cron-expression/src/Cron/HoursField.php @@ -0,0 +1,85 @@ +<?php + +namespace Cron; + +use DateTimeInterface; +use DateTimeZone; + +/** + * Hours field. Allows: * , / - + */ +class HoursField extends AbstractField +{ + /** + * @inheritDoc + */ + protected $rangeStart = 0; + + /** + * @inheritDoc + */ + protected $rangeEnd = 23; + + /** + * @inheritDoc + */ + public function isSatisfiedBy(DateTimeInterface $date, $value) + { + if ($value == '?') { + return true; + } + + return $this->isSatisfied($date->format('H'), $value); + } + + /** + * {@inheritDoc} + * + * @param \DateTime|\DateTimeImmutable &$date + * @param string|null $parts + */ + public function increment(DateTimeInterface &$date, $invert = false, $parts = null) + { + // Change timezone to UTC temporarily. This will + // allow us to go back or forwards and hour even + // if DST will be changed between the hours. + if (is_null($parts) || $parts == '*') { + $timezone = $date->getTimezone(); + $date = $date->setTimezone(new DateTimeZone('UTC')); + $date = $date->modify(($invert ? '-' : '+') . '1 hour'); + $date = $date->setTimezone($timezone); + + $date = $date->setTime($date->format('H'), $invert ? 59 : 0); + return $this; + } + + $parts = strpos($parts, ',') !== false ? explode(',', $parts) : array($parts); + $hours = array(); + foreach ($parts as $part) { + $hours = array_merge($hours, $this->getRangeForExpression($part, 23)); + } + + $current_hour = $date->format('H'); + $position = $invert ? count($hours) - 1 : 0; + if (count($hours) > 1) { + for ($i = 0; $i < count($hours) - 1; $i++) { + if ((!$invert && $current_hour >= $hours[$i] && $current_hour < $hours[$i + 1]) || + ($invert && $current_hour > $hours[$i] && $current_hour <= $hours[$i + 1])) { + $position = $invert ? $i : $i + 1; + break; + } + } + } + + $hour = $hours[$position]; + if ((!$invert && $date->format('H') >= $hour) || ($invert && $date->format('H') <= $hour)) { + $date = $date->modify(($invert ? '-' : '+') . '1 day'); + $date = $date->setTime($invert ? 23 : 0, $invert ? 59 : 0); + } + else { + $date = $date->setTime($hour, $invert ? 59 : 0); + } + + return $this; + } +} diff --git a/vendor/dragonmantank/cron-expression/src/Cron/MinutesField.php b/vendor/dragonmantank/cron-expression/src/Cron/MinutesField.php new file mode 100644 index 0000000..fecc9b6 --- /dev/null +++ b/vendor/dragonmantank/cron-expression/src/Cron/MinutesField.php @@ -0,0 +1,75 @@ +<?php + +namespace Cron; + +use DateTimeInterface; + +/** + * Minutes field. Allows: * , / - + */ +class MinutesField extends AbstractField +{ + /** + * @inheritDoc + */ + protected $rangeStart = 0; + + /** + * @inheritDoc + */ + protected $rangeEnd = 59; + + /** + * @inheritDoc + */ + public function isSatisfiedBy(DateTimeInterface $date, $value) + { + if ($value == '?') { + return true; + } + + return $this->isSatisfied($date->format('i'), $value); + } + + /** + * {@inheritDoc} + * + * @param \DateTime|\DateTimeImmutable &$date + * @param string|null $parts + */ + public function increment(DateTimeInterface &$date, $invert = false, $parts = null) + { + if (is_null($parts)) { + $date = $date->modify(($invert ? '-' : '+') . '1 minute'); + return $this; + } + + $parts = strpos($parts, ',') !== false ? explode(',', $parts) : array($parts); + $minutes = array(); + foreach ($parts as $part) { + $minutes = array_merge($minutes, $this->getRangeForExpression($part, 59)); + } + + $current_minute = $date->format('i'); + $position = $invert ? count($minutes) - 1 : 0; + if (count($minutes) > 1) { + for ($i = 0; $i < count($minutes) - 1; $i++) { + if ((!$invert && $current_minute >= $minutes[$i] && $current_minute < $minutes[$i + 1]) || + ($invert && $current_minute > $minutes[$i] && $current_minute <= $minutes[$i + 1])) { + $position = $invert ? $i : $i + 1; + break; + } + } + } + + if ((!$invert && $current_minute >= $minutes[$position]) || ($invert && $current_minute <= $minutes[$position])) { + $date = $date->modify(($invert ? '-' : '+') . '1 hour'); + $date = $date->setTime($date->format('H'), $invert ? 59 : 0); + } + else { + $date = $date->setTime($date->format('H'), $minutes[$position]); + } + + return $this; + } +} diff --git a/vendor/dragonmantank/cron-expression/src/Cron/MonthField.php b/vendor/dragonmantank/cron-expression/src/Cron/MonthField.php new file mode 100644 index 0000000..afc9caf --- /dev/null +++ b/vendor/dragonmantank/cron-expression/src/Cron/MonthField.php @@ -0,0 +1,59 @@ +<?php + +namespace Cron; + +use DateTimeInterface; + +/** + * Month field. Allows: * , / - + */ +class MonthField extends AbstractField +{ + /** + * @inheritDoc + */ + protected $rangeStart = 1; + + /** + * @inheritDoc + */ + protected $rangeEnd = 12; + + /** + * @inheritDoc + */ + protected $literals = [1 => 'JAN', 2 => 'FEB', 3 => 'MAR', 4 => 'APR', 5 => 'MAY', 6 => 'JUN', 7 => 'JUL', + 8 => 'AUG', 9 => 'SEP', 10 => 'OCT', 11 => 'NOV', 12 => 'DEC']; + + /** + * @inheritDoc + */ + public function isSatisfiedBy(DateTimeInterface $date, $value) + { + if ($value == '?') { + return true; + } + + $value = $this->convertLiterals($value); + + return $this->isSatisfied($date->format('m'), $value); + } + + /** + * @inheritDoc + * + * @param \DateTime|\DateTimeImmutable &$date + */ + public function increment(DateTimeInterface &$date, $invert = false) + { + if ($invert) { + $date = $date->modify('last day of previous month')->setTime(23, 59); + } else { + $date = $date->modify('first day of next month')->setTime(0, 0); + } + + return $this; + } + + +} |