Merge pull request #1714 from amitsharma322/gli-3006

Added: Add CronTaskManager module in default QloApps
This commit is contained in:
Rohit Jain
2026-08-07 12:03:38 +05:30
committed by GitHub
65 changed files with 6356 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
# v1.0.0 - 2026-03-24
- Initial release
- Centralized cron task management for QloApps modules
- Single server cron entry dispatches all module tasks
- Admin panel for viewing tasks, logs, and configuration
- Automatic task discovery on module install/uninstall
- Cron expression support via dragonmantank/cron-expression library

View File

@@ -0,0 +1,15 @@
SOFTWARE LICENCE AGREEMENT
===============================
This source file is subject to the Open Software License version 3.0
that is bundled with this package in the file LICENSE.md
It is also available through the world-wide-web at this URL:
https://opensource.org/license/osl-3-0-php
If you did not receive a copy of the license and are unable to
obtain it through the world-wide-web, please send an email
to support@qloapps.com so we can send you a copy immediately.
@author Webkul IN
@copyright Since 2010 Webkul
@license https://opensource.org/license/osl-3-0-php Open Software License version 3.0

View File

@@ -0,0 +1,42 @@
## Overview
The **QloApps Cron Task Manager** module enables centralized management and execution of scheduled (cron) tasks within QloApps. It allows developers and administrators to define, register, and execute automated jobs efficiently.
**QloApps Cron Task Manager 1.0.0**
- Current version: 1.0.0
- Module V1.0.0 compatible with QloApps version 1.7.0 and V1.8.x.
## Usage (QloApps version 1.7.0)
To register a cron task, add a new hook inside your module's main class file.
### Step 1: Register Cron Tasks Hook
```php
public function hookRegisterCronTasks()
{
return array(
array(
'name' => 'cron_success_probe',
'description' => $this->l('Sample cron task for scheduler verification.'),
'cron' => '* * * * *',
'callback' => 'cronSuccessProbe',
)
);
}
```
### Support Policy:
https://store.webkul.com/support.html
### Explore Addons:
https://qloapps.com/addons/
### Refund Policy:
https://store.webkul.com/refund-policy.html/

View File

@@ -0,0 +1,370 @@
<?php
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
if (!defined('_PS_VERSION_')) {
exit;
}
class QctmCronExpressionTranslator
{
protected $module;
public function __construct()
{
$this->module = Module::getInstanceByName('qlocrontaskmanager');
}
public function getCronExpressionTranslation($expression)
{
$parts = preg_split('/\s+/', trim($expression));
if (count($parts) !== 5) {
return $this->module->l('Invalid cron expression');
}
list($minute, $hour, $day, $month, $weekday) = $parts;
$hasFixedTime = $this->isNumeric($hour) && $this->isNumeric($minute);
$dayMonthWildcard = ($day == '*' && $month == '*');
$monthWeekdayWildcard = ($month == '*' && $weekday == '*');
$everyMinuteWildcard = ($minute == '*' && $hour == '*');
if ($expression === '* * * * *') {
return $this->module->l('Every minute');
}
if (preg_match('/^\*\/(\d+)$/', $minute, $m) && $hour == '*' && $dayMonthWildcard && $weekday == '*') {
return ((int) $m[1] === 1)
? $this->module->l('Every minute')
: vsprintf($this->module->l('Every %d minutes'), array((int) $m[1]));
}
if ($minute == '0' && $hour == '*' && $dayMonthWildcard && $weekday == '*') {
return $this->module->l('Every hour');
}
if ($minute == '0' && preg_match('/^\*\/(\d+)$/', $hour, $m) && $dayMonthWildcard && $weekday == '*') {
return ((int) $m[1] === 1)
? $this->module->l('Every hour')
: vsprintf($this->module->l('Every %d hours'), array((int) $m[1]));
}
if ($hasFixedTime && $dayMonthWildcard && $weekday == '*') {
return vsprintf($this->module->l('Every day at %s'), array($this->hourLabel($hour, $minute)));
}
if ($hasFixedTime && $day == '*' && $weekday == '*' && preg_match('/^\*\/(\d+)$/', $month, $mm)) {
return ((int) $mm[1] === 1)
? vsprintf($this->module->l('Every day every month at %s'), array($this->hourLabel($hour, $minute)))
: vsprintf(
$this->module->l('Every day every %1$d months at %2$s'),
array((int) $mm[1], $this->hourLabel($hour, $minute))
);
}
if ($hasFixedTime && $dayMonthWildcard && $weekday == '1-5') {
return vsprintf($this->module->l('Every weekday at %s'), array($this->hourLabel($hour, $minute)));
}
if ($hasFixedTime && $dayMonthWildcard && $weekday == '0,6') {
return vsprintf($this->module->l('Every weekend at %s'), array($this->hourLabel($hour, $minute)));
}
if ($hasFixedTime && $dayMonthWildcard && $this->isNumeric($weekday)) {
return vsprintf(
$this->module->l('Every %1$s at %2$s'),
array($this->weekdayName($weekday), $this->hourLabel($hour, $minute))
);
}
if (strpos($weekday, ',') !== false && $hasFixedTime && $dayMonthWildcard) {
return vsprintf(
$this->module->l('Every %1$s at %2$s'),
array($this->joinNames($weekday, 'weekdayName'), $this->hourLabel($hour, $minute))
);
}
if ($this->isNumeric($day) && $hasFixedTime && $monthWeekdayWildcard) {
return vsprintf(
$this->module->l('On the %1$s of every month at %2$s'),
array($this->ordinal($day), $this->hourLabel($hour, $minute))
);
}
if (preg_match('/^(\d+)-(\d+)$/', $day, $dm) && $hasFixedTime && $monthWeekdayWildcard) {
return vsprintf(
$this->module->l('Every day from the %1$s to the %2$s of each month at %3$s'),
array($this->ordinal($dm[1]), $this->ordinal($dm[2]), $this->hourLabel($hour, $minute))
);
}
if (strpos($day, ',') !== false && $hasFixedTime && $monthWeekdayWildcard) {
return vsprintf(
$this->module->l('On the %1$s of each month at %2$s'),
array($this->joinOrdinals($day), $this->hourLabel($hour, $minute))
);
}
if ($this->isNumeric($day) && $this->isNumeric($month) && $hasFixedTime && $weekday == '*') {
return vsprintf(
$this->module->l('Every %1$s %2$s at %3$s'),
array($this->monthName($month), $this->ordinal($day), $this->hourLabel($hour, $minute))
);
}
if ($everyMinuteWildcard && $dayMonthWildcard && $this->isNumeric($weekday)) {
return vsprintf($this->module->l('Every minute on %s'), array($this->weekdayName($weekday)));
}
if ($everyMinuteWildcard && $dayMonthWildcard && $weekday == '1-5') {
return $this->module->l('Every minute on weekdays');
}
if ($everyMinuteWildcard && $dayMonthWildcard && $weekday == '0,6') {
return $this->module->l('Every minute on weekends');
}
if ($hasFixedTime) {
$when = vsprintf($this->module->l('At %s'), array($this->hourLabel($hour, $minute)));
} else {
$minuteFreq = null;
if ($minute === '*') {
$minuteFreq = $this->module->l('Every minute');
} elseif (preg_match('/^\*\/(\d+)$/', $minute, $m)) {
$minuteFreq = ((int) $m[1] === 1)
? $this->module->l('Every minute')
: vsprintf($this->module->l('Every %d minutes'), array((int) $m[1]));
}
if (preg_match('/^(\d+)-(\d+)\/(\d+)$/', $hour, $hs) && $this->isNumeric($minute)) {
$when = ((int) $hs[3] === 1)
? vsprintf(
$this->module->l('Every hour between %1$s and %2$s'),
array($this->hourLabel($hs[1], $minute), $this->hourLabel($hs[2], $minute))
)
: vsprintf(
$this->module->l('Every %1$d hours between %2$s and %3$s'),
array((int) $hs[3], $this->hourLabel($hs[1], $minute), $this->hourLabel($hs[2], $minute))
);
} elseif ($minuteFreq !== null) {
if ($hour === '*') {
$when = $minuteFreq;
} elseif ($this->isNumeric($hour)) {
$when = vsprintf($this->module->l('%1$s, during hour %2$s'), array($minuteFreq, $hour));
} elseif (preg_match('/^(\d+)-(\d+)$/', $hour, $hm)) {
$when = vsprintf(
$this->module->l('%1$s between %2$s and %3$s'),
array($minuteFreq, $this->hourLabel($hm[1], 0), $this->hourLabel($hm[2], 59))
);
} else {
$when = vsprintf($this->module->l('%1$s, during hour %2$s'), array($minuteFreq, $hour));
}
} elseif ($this->isNumeric($minute) && $hour === '*') {
$when = vsprintf($this->module->l('Every hour, at minute %s'), array($minute));
} elseif ($this->isNumeric($minute) && preg_match('/^(\d+)-(\d+)$/', $hour, $hm)) {
$when = vsprintf(
$this->module->l('At minute %1$s, between %2$s and %3$s'),
array($minute, $this->hourLabel($hm[1], $minute), $this->hourLabel($hm[2], $minute))
);
} else {
$when = vsprintf($this->module->l('At minute %1$s, hour %2$s'), array($minute, $hour));
}
}
$conditions = array();
if ($day !== '*') {
if (preg_match('/^(\d+)-(\d+)$/', $day, $dm)) {
$conditions[] = vsprintf(
$this->module->l('from the %1$s to the %2$s of the month'),
array($this->ordinal($dm[1]), $this->ordinal($dm[2]))
);
} elseif (strpos($day, ',') !== false) {
$conditions[] = vsprintf($this->module->l('on the %s of the month'), array($this->joinOrdinals($day)));
} elseif ($this->isNumeric($day)) {
$conditions[] = vsprintf($this->module->l('on the %s of the month'), array($this->ordinal($day)));
} else {
$conditions[] = vsprintf($this->module->l('on day %s of the month'), array($day));
}
}
if ($month !== '*') {
if (preg_match('/^\*\/(\d+)$/', $month, $mf)) {
$conditions[] = ((int) $mf[1] === 1)
? $this->module->l('every month')
: vsprintf($this->module->l('every %d months'), array((int) $mf[1]));
} else {
if (preg_match('/^(\d+)-(\d+)$/', $month, $mm)) {
$monthDesc = vsprintf(
$this->module->l('%1$s through %2$s'),
array($this->monthName($mm[1]), $this->monthName($mm[2]))
);
} elseif (strpos($month, ',') !== false) {
$monthDesc = $this->joinNames($month, 'monthName');
} else {
$monthDesc = $this->monthName($month);
}
$conditions[] = vsprintf($this->module->l('in %s'), array($monthDesc));
}
}
if ($weekday !== '*') {
if ($weekday === '1-5') {
$weekdayDesc = $this->module->l('weekdays');
} elseif ($weekday === '0,6' || $weekday === '6,0') {
$weekdayDesc = $this->module->l('weekends');
} elseif (preg_match('/^(\d+)-(\d+)$/', $weekday, $wm)) {
$weekdayDesc = vsprintf(
$this->module->l('%1$s through %2$s'),
array($this->weekdayName($wm[1]), $this->weekdayName($wm[2]))
);
} elseif (strpos($weekday, ',') !== false) {
$weekdayDesc = $this->joinNames($weekday, 'weekdayName');
} else {
$weekdayDesc = $this->weekdayName($weekday);
}
$conditions[] = vsprintf($this->module->l('on %s'), array($weekdayDesc));
}
if (empty($conditions)) {
return $when;
}
return $when . ' ' . implode(', ', $conditions);
}
public function isNumeric($value)
{
return preg_match('/^\d+$/', $value) === 1;
}
public function weekdayName($weekday)
{
$weekdays = array(
0 => $this->module->l('Sunday'),
1 => $this->module->l('Monday'),
2 => $this->module->l('Tuesday'),
3 => $this->module->l('Wednesday'),
4 => $this->module->l('Thursday'),
5 => $this->module->l('Friday'),
6 => $this->module->l('Saturday'),
7 => $this->module->l('Sunday'),
);
return ($this->isNumeric($weekday) && isset($weekdays[(int) $weekday])) ? $weekdays[(int) $weekday] : $weekday;
}
public function monthName($month)
{
$months = array(
1 => $this->module->l('January'),
2 => $this->module->l('February'),
3 => $this->module->l('March'),
4 => $this->module->l('April'),
5 => $this->module->l('May'),
6 => $this->module->l('June'),
7 => $this->module->l('July'),
8 => $this->module->l('August'),
9 => $this->module->l('September'),
10 => $this->module->l('October'),
11 => $this->module->l('November'),
12 => $this->module->l('December'),
);
return ($this->isNumeric($month) && isset($months[(int) $month])) ? $months[(int) $month] : $month;
}
public function hourLabel($hour, $minute = null)
{
$h = (int) $hour;
$suffix = $h >= 12 ? $this->module->l('PM') : $this->module->l('AM');
$h12 = $h % 12;
if ($h12 === 0) {
$h12 = 12;
}
return $minute === null
? sprintf('%d %s', $h12, $suffix)
: sprintf('%d:%02d %s', $h12, (int) $minute, $suffix);
}
public function joinNames($csv, $nameMethod)
{
$names = array();
foreach (explode(',', $csv) as $token) {
$names[] = $this->$nameMethod($token);
}
if (count($names) === 1) {
return $names[0];
}
$last = array_pop($names);
return implode(', ', $names) . ' ' . $this->module->l('and') . ' ' . $last;
}
public function ordinal($number)
{
$number = (int) $number;
if ($number % 100 >= 11 && $number % 100 <= 13) {
$suffix = 'th';
} else {
switch ($number % 10) {
case 1:
$suffix = 'st';
break;
case 2:
$suffix = 'nd';
break;
case 3:
$suffix = 'rd';
break;
default:
$suffix = 'th';
}
}
return $number . $suffix;
}
public function joinOrdinals($csv)
{
$ordinals = array();
foreach (explode(',', $csv) as $token) {
$ordinals[] = $this->isNumeric($token) ? $this->ordinal($token) : $token;
}
if (count($ordinals) === 1) {
return $ordinals[0];
}
$last = array_pop($ordinals);
return implode(', ', $ordinals) . ' ' . $this->module->l('and') . ' ' . $last;
}
}

View File

@@ -0,0 +1,256 @@
<?php
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
if (!defined('_PS_VERSION_')) {
exit;
}
class QctmCronTask extends ObjectModel
{
public $id_cron_task;
public $id_module;
public $task_name;
public $description;
public $cron_expression;
public $callback;
public $active;
public $is_system;
public $date_add;
public $date_upd;
public static $definition = array(
'table' => 'qctm_cron_task',
'primary' => 'id_cron_task',
'fields' => array(
'id_module' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true),
'task_name' => array('type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'size' => 128, 'required' => true),
'description' => array('type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'size' => 255, 'required' => true),
'cron_expression' => array('type' => self::TYPE_STRING, 'validate' => 'isString', 'size' => 64, 'required' => true),
'callback' => array('type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'size' => 128, 'required' => true),
'active' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'),
'is_system' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'),
'date_add' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'),
'date_upd' => array('type' => self::TYPE_DATE, 'validate' => 'isDate'),
),
);
public static function getActiveTasks()
{
return Db::getInstance()->executeS(
'SELECT `id_cron_task`, `id_module`, `task_name`, `description`,
`cron_expression`, `callback`, `active`
FROM `' . _DB_PREFIX_ . 'qctm_cron_task`
WHERE `active` = 1'
);
}
public function dispatch()
{
$tasks = self::getActiveTasks();
if (empty($tasks)) {
return;
}
$now = new DateTime('now', new DateTimeZone(Configuration::get('PS_TIMEZONE') ?: date_default_timezone_get()));
$moduleCache = [];
foreach ($tasks as $task) {
if (!\Cron\CronExpression::isValidExpression($task['cron_expression'])) {
QctmCronTaskLog::addLog(
$task['id_cron_task'],
QctmCronTaskLog::QCTM_CRON_TASK_LOG_STATUS_ERROR,
'Invalid cron expression "' . $task['cron_expression'] . '"'
);
continue;
}
$cron = \Cron\CronExpression::factory($task['cron_expression']);
if (!$cron->isDue($now)) {
continue;
}
$idModule = (int) $task['id_module'];
if (!array_key_exists($idModule, $moduleCache)) {
$moduleCache[$idModule] = Module::getInstanceById($idModule);
}
$module = $moduleCache[$idModule];
if (!$module) {
QctmCronTaskLog::addLog(
$task['id_cron_task'],
QctmCronTaskLog::QCTM_CRON_TASK_LOG_STATUS_ERROR,
'Module with ID "' . (int) $task['id_module'] . '" could not be loaded'
);
continue;
}
if (!$module->active) {
continue;
}
$callback = $task['callback'];
if (!method_exists($module, $callback)) {
QctmCronTaskLog::addLog(
$task['id_cron_task'],
QctmCronTaskLog::QCTM_CRON_TASK_LOG_STATUS_ERROR,
'Callback method "' . $callback . '" not found on module "' . $module->name . '"'
);
continue;
}
$startTime = microtime(true);
try {
$module->{$callback}();
QctmCronTaskLog::addLog(
$task['id_cron_task'],
QctmCronTaskLog::QCTM_CRON_TASK_LOG_STATUS_SUCCESS,
null,
round(microtime(true) - $startTime, 4)
);
} catch (\Throwable $e) {
QctmCronTaskLog::addLog(
$task['id_cron_task'],
QctmCronTaskLog::QCTM_CRON_TASK_LOG_STATUS_ERROR,
$e->getMessage(),
round(microtime(true) - $startTime, 4)
);
}
}
}
public function syncAllExistingModules()
{
$allTasks = Hook::exec('registerCronTasks', array(), null, true);
if (!empty($allTasks) && is_array($allTasks)) {
foreach ($allTasks as $moduleName => $tasks) {
if (!empty($tasks) && is_array($tasks)) {
$module = Module::getInstanceByName($moduleName);
if($module){
$this->registerTasksForModule($module->id, $tasks);
}
}
}
}
}
/**
* Register tasks for a specific module after validation
*
* @param int $idModule
* @param array $tasks
*/
public function registerTasksForModule($idModule, $tasks)
{
$module = Module::getInstanceById($idModule);
if (!$module) {
return;
}
foreach ($tasks as $task) {
if (!$this->validateTask($module, $task)) {
continue;
}
if (QctmCronTask::getByModuleAndName($idModule, $task['name'])) {
continue;
}
$cronTask = new QctmCronTask();
$cronTask->id_module = $idModule;
$cronTask->task_name = $task['name'];
$cronTask->description = $task['description'];
$cronTask->cron_expression = $task['cron'];
$cronTask->callback = $task['callback'];
$cronTask->active = 1;
$cronTask->is_system = 0;
if (!$cronTask->save()) {
PrestaShopLogger::addLog(
'QloCronTaskManager: failed to register task "' . $task['name'] . '" for module "' . $module->name . '"',
3
);
}
}
}
/**
* Validate a task definition array
*
* @param object $module
* @param array $task
* @return bool
*/
public function validateTask(Module $module, $task)
{
if (!Validate::isGenericName($task['name']) || !Validate::isGenericName($task['description'])
|| !Validate::isString($task['cron']) || !Validate::isString($task['callback'])) {
return false;
}
if (!\Cron\CronExpression::isValidExpression($task['cron'])) {
return false;
}
if (!method_exists($module, $task['callback'])) {
return false;
}
return true;
}
/**
* @param int $idModule
* @param string $taskName
* @return int|false
*/
public static function getByModuleAndName($idModule, $taskName)
{
return Db::getInstance()->getValue(
'SELECT `id_cron_task`
FROM `' . _DB_PREFIX_ . 'qctm_cron_task`
WHERE `id_module` = ' . (int) $idModule . '
AND `task_name` = \'' . pSQL($taskName) . '\''
);
}
/**
* Delete all tasks belonging to a module.
* Logs are intentionally preserved for history tracking.
*
* @param int $idModule
* @return bool
*/
public static function deleteByModule($idModule)
{
return Db::getInstance()->execute(
'DELETE FROM `' . _DB_PREFIX_ . 'qctm_cron_task`
WHERE `id_module` = ' . (int) $idModule
);
}
}

View File

@@ -0,0 +1,100 @@
<?php
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
if (!defined('_PS_VERSION_')) {
exit;
}
class QctmCronTaskLog extends ObjectModel
{
public $id_cron_task_log;
public $id_cron_task;
public $status;
public $error_message;
public $execution_time;
public $date_add;
const QCTM_CRON_TASK_LOG_STATUS_SUCCESS = 1;
const QCTM_CRON_TASK_LOG_STATUS_ERROR = 2;
public static $definition = array(
'table' => 'qctm_cron_task_log',
'primary' => 'id_cron_task_log',
'fields' => array(
'id_cron_task' => array('type' => self::TYPE_INT,'validate' => 'isUnsignedId','required' => true),
'status' => array('type' => self::TYPE_INT,'validate' => 'isUnsignedId','required' => true),
'error_message' => array('type' => self::TYPE_STRING,'validate' => 'isString'),
'execution_time' => array('type' => self::TYPE_FLOAT,'validate' => 'isFloat'),
'date_add' => array('type' => self::TYPE_DATE,'validate' => 'isDate'),
),
);
/**
* Add a log entry for a task execution
*
* @param int $idCronTask
* @param int $status
* @param string|null $errorMessage
* @param float $executionTime
* @return bool
*/
public static function addLog($idCronTask, $status, $errorMessage = null, $executionTime = 0)
{
$log = new self();
$log->id_cron_task = (int) $idCronTask;
$log->status = $status;
$log->error_message = $errorMessage;
$log->execution_time = (float) $executionTime;
return $log->add();
}
/**
* Delete logs older than specified number of days
*
* @param int $days
* @return bool
*/
public static function deleteOlderThan($days)
{
return Db::getInstance()->execute(
'DELETE FROM `' . _DB_PREFIX_ . 'qctm_cron_task_log`
WHERE `date_add` < DATE_SUB(NOW(), INTERVAL ' . (int) $days . ' DAY)'
);
}
public static function getStatuses()
{
static $statuses = null;
if ($statuses === null) {
$module = Module::getInstanceByName('qlocrontaskmanager');
$statuses = array(
self::QCTM_CRON_TASK_LOG_STATUS_SUCCESS => $module->l('Success'),
self::QCTM_CRON_TASK_LOG_STATUS_ERROR => $module->l('Error'),
);
}
return $statuses;
}
}

View File

@@ -0,0 +1,102 @@
<?php
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
if (!defined('_PS_VERSION_')) {
exit;
}
class QctmCronTaskManagerDb
{
public function createTables()
{
$sql = array(
'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'qctm_cron_task` (
`id_cron_task` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`id_module` INT(11) UNSIGNED NOT NULL,
`task_name` VARCHAR(128) NOT NULL,
`description` VARCHAR(255) NOT NULL,
`cron_expression` VARCHAR(64) NOT NULL,
`callback` VARCHAR(128) NOT NULL,
`active` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1,
`is_system` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0,
`date_add` DATETIME NOT NULL,
`date_upd` DATETIME NOT NULL,
PRIMARY KEY (`id_cron_task`),
UNIQUE KEY `module_task` (`id_module`, `task_name`)
) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;',
'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'qctm_cron_task_log` (
`id_cron_task_log` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`id_cron_task` INT(11) UNSIGNED NOT NULL,
`status` TINYINT(1) UNSIGNED NOT NULL DEFAULT 1,
`error_message` TEXT DEFAULT NULL,
`execution_time` FLOAT NOT NULL DEFAULT 0,
`date_add` DATETIME NOT NULL,
PRIMARY KEY (`id_cron_task_log`),
KEY `idx_cron_task` (`id_cron_task`),
KEY `idx_date_add` (`date_add`)
) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;'
);
foreach ($sql as $query) {
if (!Db::getInstance()->execute($query)) {
return false;
}
}
return true;
}
public function dropTables()
{
$sql = array(
'DROP TABLE IF EXISTS `' . _DB_PREFIX_ . 'qctm_cron_task_log`',
'DROP TABLE IF EXISTS `' . _DB_PREFIX_ . 'qctm_cron_task`'
);
foreach ($sql as $query) {
if (!Db::getInstance()->execute($query)) {
return false;
}
}
return true;
}
public function installDefaultData()
{
Configuration::updateValue('QCTM_CRON_TASK_MANAGER_TOKEN', Tools::passwdGen(32));
Configuration::updateValue('QCTM_CRON_TASK_MANAGER_LOG_RETENTION_DAYS', 30);
return true;
}
public function deleteConfigurations()
{
Configuration::deleteByName('QCTM_CRON_TASK_MANAGER_TOKEN');
Configuration::deleteByName('QCTM_CRON_TASK_MANAGER_LOG_RETENTION_DAYS');
return true;
}
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
require_once _PS_MODULE_DIR_.'qlocrontaskmanager/classes/QctmCronTaskManagerDb.php';
require_once _PS_MODULE_DIR_.'qlocrontaskmanager/classes/QctmCronTask.php';
require_once _PS_MODULE_DIR_.'qlocrontaskmanager/classes/QctmCronTaskLog.php';
require_once _PS_MODULE_DIR_.'qlocrontaskmanager/classes/QctmCronExpressionTranslator.php';
require_once _PS_MODULE_DIR_.'qlocrontaskmanager/vendor/autoload.php';

View File

@@ -0,0 +1,32 @@
<?php
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../');
exit;

View File

@@ -0,0 +1,7 @@
{
"name": "qloapps/qlocrontaskmanager",
"description": "Centralized cron task manager for QloApps modules",
"require": {
"dragonmantank/cron-expression": "^3.0"
}
}

View File

@@ -0,0 +1,163 @@
<?php
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
if (!defined('_PS_VERSION_')) {
exit;
}
class AdminCronTaskLogsController extends ModuleAdminController
{
public function __construct()
{
$this->bootstrap = true;
$this->list_no_link = true;
$this->table = 'qctm_cron_task_log';
$this->className = 'QctmCronTaskLog';
$this->identifier = 'id_cron_task_log';
parent::__construct();
$this->initLogList();
}
public function init()
{
parent::init();
$idCronTask = (int) Tools::getValue('id_cron_task');
$prefix = $this->getCookieFilterPrefix();
$cookieKey = $prefix . $this->table . 'Filter_ct!task_name';
if ($idCronTask > 0) {
$cronTask = new QctmCronTask($idCronTask);
if (Validate::isLoadedObject($cronTask)) {
$this->_where .= ' AND a.`id_cron_task` = ' . $idCronTask;
$this->context->cookie->{$cookieKey} = $cronTask->task_name;
$this->context->cookie->{'submitFilter' . $this->table} = 1;
$this->toolbar_title = $this->l('Logs for: ') . $cronTask->task_name;
} else {
$this->toolbar_title = $this->l('Cron Task Logs');
}
} else {
$this->context->cookie->{'submitFilter' . $this->table} = 0;
$this->toolbar_title = $this->l('All Cron Task Logs');
}
$this->page_header_toolbar_btn['back_to_tasks'] = array(
'href' => $this->context->link->getAdminLink('AdminCronTaskManager'),
'icon' => 'process-icon-back',
'desc' => $this->l('Back to Tasks'),
);
}
protected function initLogList()
{
$this->lang = false;
$this->_orderBy = 'id_cron_task_log';
$this->_orderWay = 'DESC';
$this->_join = 'LEFT JOIN `' . _DB_PREFIX_ . 'qctm_cron_task` ct
ON (a.`id_cron_task` = ct.`id_cron_task`)
LEFT JOIN `' . _DB_PREFIX_ . 'module` m
ON (m.`id_module` = ct.`id_module`)';
$this->_select = 'm.`name` AS `module_name`, ct.`task_name`, ct.`description` AS task_description';
$cronTaskLogStatuses = QctmCronTaskLog::getStatuses();
$this->fields_list = array(
'id_cron_task_log' => array(
'title' => $this->l('ID'),
'align' => 'center',
'class' => 'fixed-width-xs',
),
'module_name' => array(
'title' => $this->l('Module'),
'filter_key' => 'm!name',
'order_key' => 'm!name',
'callback' => 'displayModuleName',
),
'task_name' => array(
'title' => $this->l('Task Name'),
'filter_key' => 'ct!task_name',
'search' => true,
),
'status' => array(
'title' => $this->l('Status'),
'align' => 'center',
'type' => 'select',
'list' => $cronTaskLogStatuses,
'filter_key' => 'a!status',
'callback' => 'getStatusBadge',
),
'error_message' => array(
'title' => $this->l('Error Message'),
'maxlength' => 80,
),
'execution_time' => array(
'title' => $this->l('Execution Time'),
'align' => 'center',
'suffix' => ' s',
'class' => 'fixed-width-md',
),
'date_add' => array(
'title' => $this->l('Date'),
'type' => 'datetime',
'align' => 'right',
'filter_key' => 'a!date_add',
),
);
}
public function initToolbar()
{
parent::initToolbar();
unset($this->toolbar_btn['new']);
}
public function getStatusBadge($status, $row)
{
$this->context->smarty->assign('status', $status);
return $this->createTemplate('qctm_badge_status.tpl')->fetch();
}
public function displayModuleName($moduleName, $row)
{
$cacheKey = 'displayModuleName_' . $moduleName;
if (!Cache::isStored($cacheKey)) {
$module = Module::getInstanceByName($moduleName);
$displayName = ($module && $module->displayName) ? $module->displayName : $moduleName;
Cache::store($cacheKey, $displayName);
}
return Cache::retrieve($cacheKey);
}
public function setMedia()
{
parent::setMedia();
$this->addCSS($this->module->getPathUri() . 'views/css/admin/qlo_cron_task_manager_admin.css');
}
}

View File

@@ -0,0 +1,446 @@
<?php
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
if (!defined('_PS_VERSION_')) {
exit;
}
class AdminCronTaskManagerController extends ModuleAdminController
{
public function __construct()
{
$this->bootstrap = true;
$this->list_no_link = true;
$this->table = 'qctm_cron_task';
$this->className = 'QctmCronTask';
$this->identifier = 'id_cron_task';
$this->_orderBy = 'id_cron_task';
$this->lang = false;
$this->_orderWay = 'ASC';
parent::__construct();
// Exclude hidden system tasks from the listing
$this->_where = ' AND a.`is_system` = 0';
$this->_join = 'LEFT JOIN (
SELECT ctl.`id_cron_task`, ctl.`date_add` AS `last_run`, ctl.`status` AS `last_status`
FROM `' . _DB_PREFIX_ . 'qctm_cron_task_log` ctl
INNER JOIN (
SELECT `id_cron_task`, MAX(`id_cron_task_log`) AS `max_id`
FROM `' . _DB_PREFIX_ . 'qctm_cron_task_log`
GROUP BY `id_cron_task`
) latest ON ctl.`id_cron_task_log` = latest.`max_id`
) AS llog ON (llog.`id_cron_task` = a.`id_cron_task`)
LEFT JOIN `' . _DB_PREFIX_ . 'module` m ON (m.`id_module` = a.`id_module`)';
$this->_select = 'm.`name` as `module_name`, llog.`last_run`, llog.`last_status`, a.`cron_expression` AS `next_run_expression`';
$this->addRowAction('viewLogs');
$this->addRowAction('edit');
$this->addRowAction('resetModuleCrons');
$this->bulk_actions = array(
'enableSelection' => array(
'text' => $this->l('Enable selected'),
'icon' => 'icon-power-off text-success',
),
'disableSelection' => array(
'text' => $this->l('Disable selected'),
'icon' => 'icon-power-off text-danger',
),
);
$this->fields_list = array(
'id_cron_task' => array(
'title' => $this->l('ID'),
'align' => 'center',
'class' => 'fixed-width-xs',
),
'module_name' => array(
'title' => $this->l('Module Name'),
'filter_key' => 'm!name',
'order_key' => 'm!name',
'callback' => 'displayModuleName',
),
'task_name' => array(
'title' => $this->l('Task Name'),
'filter_key' => 'a!task_name',
),
'cron_expression' => array(
'title' => $this->l('Cron Expression'),
'align' => 'center',
'class' => 'fixed-width-lg',
'search' => false,
'callback' => 'getCronWithReadable',
),
'last_run' => array(
'title' => $this->l('Last Run'),
'align' => 'center',
'filter' => false,
'search' => false,
),
'last_status' => array(
'title' => $this->l('Last Status'),
'align' => 'center',
'filter' => false,
'search' => false,
'callback' => 'getLastStatus',
),
'next_run_expression' => array(
'title' => $this->l('Next Run'),
'align' => 'center',
'filter' => false,
'search' => false,
'callback' => 'getNextRun',
),
'date_add' => array(
'title' => $this->l('Added Time'),
'align' => 'center',
'filter' => false,
'search' => false,
),
'active' => array(
'title' => $this->l('Active'),
'active' => 'status',
'type' => 'bool',
'align' => 'center',
'class' => 'fixed-width-sm',
'filter_key' => 'a!active',
),
);
}
public function init()
{
parent::init();
if(!Validate::isUnsignedInt($idCronTask = Tools::getValue('id_cron_task')) && $this->display == 'edit') {
$this->errors = array($this->l('Invalid Cron Task ID'));
$this->display = 'list';
}else{
if (Validate::isUnsignedInt($idCronTask = Tools::getValue('id_cron_task')) && Validate::isLoadedObject($objCronTask = new QctmCronTask($idCronTask)) && $this->display == 'edit' ) {
$this->toolbar_title = $this->l('Edit Cron Task: ') . $objCronTask->task_name;
}
}
}
public function initToolbar()
{
parent::initToolbar();
unset($this->toolbar_btn['new']);
if($this->display && $this->display == 'list') {
$this->page_header_toolbar_btn['view_all_logs'] = array(
'href' => $this->context->link->getAdminLink('AdminCronTaskLogs'),
'desc' => $this->l('View All Logs'),
'icon' => 'icon-history',
);
}else if($this->display && $this->display == 'edit') {
$idCronTask = Tools::getValue('id_cron_task');
$this->page_header_toolbar_btn['view_logs'] = array(
'href' => $this->context->link->getAdminLink('AdminCronTaskLogs') . '&id_cron_task=' . (int) $idCronTask,
'desc' => $this->l('View Logs'),
'icon' => 'icon-history',
);
}
}
public function renderForm()
{
$cronExpression = '* * * * *';
if(Validate::isLoadedObject($this->object)){
$cronExpression = $this->object->cron_expression;
}
$isValidExpression = \Cron\CronExpression::isValidExpression($cronExpression);
if ($isValidExpression) {
$readable = $this->l('Runs ') . (new QctmCronExpressionTranslator())->getCronExpressionTranslation($cronExpression);
} else {
$readable = $this->l('Invalid cron expression.');
}
$moduleDisplayName = '';
if (Validate::isLoadedObject($this->object) && $this->object->id_module) {
$module = Module::getInstanceById($this->object->id_module);
$moduleDisplayName = ($module && $module->displayName) ? $module->displayName : '#' . (int) $this->object->id_module;
}
$this->fields_value['module_display_name'] = $moduleDisplayName;
$this->context->smarty->assign(array(
'qctmCronExpression' => $cronExpression,
'qctmCronReadable' => $readable,
'qctmAjaxUrl' => $this->context->link->getAdminLink($this->controller_name) . '&ajax=1',
));
$this->fields_form = array(
'legend' => array(
'title' => $this->l('Cron Task'),
'icon' => 'icon-time',
),
'input' => array(
array(
'type' => 'text',
'label' => $this->l('Module Name'),
'name' => 'module_display_name',
'disabled' => true,
'col' => 5,
'desc' => $this->l('The module that owns this task.'),
),
array(
'type' => 'text',
'label' => $this->l('Task Name'),
'name' => 'task_name',
'disabled' => true,
'required' => true,
'col' => 5,
'hint' => $this->l('e.g. send_daily_report'),
'desc' => $this->l('A unique identifier for this task within the module.'),
),
array(
'type' => 'text',
'label' => $this->l('Description'),
'name' => 'description',
'required' => true,
'disabled' => true,
'col' => 7,
'hint' => $this->l('e.g. Sends the daily booking summary report'),
'desc' => $this->l('A short human-readable description of what this task does.'),
),
array(
'type' => 'text',
'label' => $this->l('Cron Expression'),
'name' => 'cron_expression',
'required' => true,
'col' => 4,
'class' => 'qctm-cron-expression',
'hint' => $this->l('e.g. 0 0 * * *'),
'desc' => $readable,
),
array(
'type' => 'html',
'name' => 'qctm_cron_helper',
'html_content' => $this->createTemplate('qctm_cron_expression_helper.tpl')->fetch(),
),
array(
'type' => 'switch',
'label' => $this->l('Active'),
'name' => 'active',
'required' => false,
'desc' => $this->l('Disabled tasks are skipped.'),
'values' => array(
array('id' => 'active_on', 'value' => 1, 'label' => $this->l('Enabled')),
array('id' => 'active_off', 'value' => 0, 'label' => $this->l('Disabled')),
),
),
),
'submit' => array(
'title' => $this->l('Save'),
),
);
return parent::renderForm();
}
public function ajaxProcessGetCronReadable()
{
$expression = Tools::getValue('cron_expression');
$result = array('readable' => $this->l('Invalid cron expression'), 'valid' => false);
if (\Cron\CronExpression::isValidExpression($expression)) {
$result['valid'] = true;
$result['readable'] = $this->l('Runs ') . (new QctmCronExpressionTranslator())->getCronExpressionTranslation($expression);
}
die(Tools::jsonEncode($result));
}
public function processSave()
{
if (!$this->validateCronExpression()) {
$this->display = 'edit';
return false;
}
return parent::processSave();
}
protected function validateCronExpression()
{
$cronExpression = Tools::getValue('cron_expression');
if (!\Cron\CronExpression::isValidExpression($cronExpression)) {
$this->errors[] = $this->l('Invalid cron expression. Must have exactly 5 parts: minute hour day month weekday. Example: * * * * *');
return false;
}
return true;
}
public function processStatus()
{
return parent::processStatus();
}
protected function processBulkEnableSelection()
{
return parent::processBulkEnableSelection();
}
protected function processBulkDisableSelection()
{
return parent::processBulkDisableSelection();
}
public function displayViewLogsLink($token = null, $id = null, $row = array())
{
$this->context->smarty->assign(
'viewLogsLink',
$this->context->link->getAdminLink('AdminCronTaskLogs') . '&id_cron_task=' . (int) $id
);
return $this->createTemplate('qctm_view_logs_link.tpl')->fetch();
}
public function displayResetModuleCronsLink($token = null, $id = null, $row = array())
{
$this->context->smarty->assign(
'resetLink',
$this->context->link->getAdminLink('AdminCronTaskManager')
. '&action=resetModuleCrons&' . $this->identifier . '=' . (int) $id
);
return $this->createTemplate('qctm_reset_module_crons_link.tpl')->fetch();
}
public function processResetModuleCrons()
{
$idCronTask = (int) Tools::getValue($this->identifier);
$cronTask = new QctmCronTask($idCronTask);
if (!Validate::isLoadedObject($cronTask)) {
$this->errors[] = $this->l('Cron task not found.');
return false;
}
$module = Module::getInstanceById($cronTask->id_module);
if (!$module) {
$this->errors[] = $this->l('Module not found.');
return false;
}
if (!method_exists($module, 'hookRegisterCronTasks')) {
$this->errors[] = $this->l('Module does not define any cron tasks.');
return false;
}
$tasks = $module->hookRegisterCronTasks();
if (empty($tasks) || !is_array($tasks)) {
$this->errors[] = $this->l('No tasks returned by module.');
return false;
}
foreach ($tasks as $task) {
if ($task['name'] === $cronTask->task_name) {
if (!$cronTask->validateTask($module, $task)) {
$this->errors[] = $this->l('Module returned an invalid task definition (bad cron expression or missing callback).');
return false;
}
$cronTask->cron_expression = $task['cron'];
$cronTask->description = $task['description'];
$cronTask->callback = $task['callback'];
if (!$cronTask->save()) {
$this->errors[] = $this->l('Failed to reset cron task.');
return false;
}
$this->confirmations[] = $this->l('Cron task reset to module defaults successfully.');
return true;
}
}
$this->errors[] = $this->l('Task definition not found in module.');
return false;
}
public function getCronWithReadable($cronExpression, $row)
{
$readable = \Cron\CronExpression::isValidExpression($cronExpression)
? (new QctmCronExpressionTranslator())->getCronExpressionTranslation($cronExpression)
: $cronExpression;
return '<code>' . htmlspecialchars($cronExpression) . '</code>'
. '<br><small class="text-muted">' . htmlspecialchars($readable) . '</small>';
}
public function displayModuleName($moduleName, $row)
{
$cacheKey = 'displayModuleName_' . $moduleName;
if (!Cache::isStored($cacheKey)) {
$module = Module::getInstanceByName($moduleName);
$displayName = ($module && $module->displayName) ? $module->displayName : $moduleName;
Cache::store($cacheKey, $displayName);
}
return Cache::retrieve($cacheKey);
}
public function getNextRun($cronExpression, $row)
{
if (!$cronExpression || !\Cron\CronExpression::isValidExpression($cronExpression)) {
return '-';
}
try {
$next = \Cron\CronExpression::factory($cronExpression)->getNextRunDate();
return $next->format('Y-m-d H:i:s');
} catch (\Exception $e) {
return '-';
}
}
public function getLastStatus($lastStatus, $row)
{
$this->context->smarty->assign('status', $lastStatus);
return $this->createTemplate('qctm_badge_status.tpl')->fetch();
}
public function setMedia()
{
parent::setMedia();
Media::addJsDef(
array(
'INVALID_CRON_EXPRESSION' => $this->l('Invalid cron expression.'),
)
);
$this->addCSS($this->module->getPathUri() . 'views/css/admin/qlo_cron_task_manager_admin.css');
$this->addJS($this->module->getPathUri() . 'views/js/admin/qlo_cron_task_manager_admin.js');
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../');
exit;

View File

@@ -0,0 +1,32 @@
<?php
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../');
exit;

View File

@@ -0,0 +1,43 @@
<?php
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
require_once dirname(__FILE__) . '/../../config/config.inc.php';
$token = Tools::getValue('token');
$expectedToken = Configuration::get('QCTM_CRON_TASK_MANAGER_TOKEN');
if (!$token || $token !== $expectedToken) {
header('HTTP/1.1 403 Forbidden');
die('Access denied');
}
$module = Module::getInstanceByName('qlocrontaskmanager');
if (!$module || !$module->active) {
die('Cron module not active');
}
require_once dirname(__FILE__) . '/classes/QctmCronTask.php';
$objCronTask = new QctmCronTask();
$objCronTask->dispatch();

View File

@@ -0,0 +1,32 @@
<?php
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../');
exit;

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,340 @@
<?php
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
if (!defined('_PS_VERSION_')) {
exit;
}
require_once dirname(__FILE__).'/classes/QctmRequiredClasses.php';
class QloCronTaskManager extends Module
{
public function __construct()
{
$this->name = 'qlocrontaskmanager';
$this->tab = 'administration';
$this->version = '1.0.0';
$this->author = 'Webkul';
$this->need_instance = 0;
$this->bootstrap = true;
parent::__construct();
$this->displayName = $this->l('Cron Task Manager');
$this->description = $this->l('A centralized cron scheduling system where a single server cron entry manages all module tasks.');
$this->confirmUninstall = $this->l('Are you sure you want to uninstall this module? All cron task data will be lost.');
}
public function getContent()
{
$output = '';
if (Tools::isSubmit('submitCronTaskManagerSettings')) {
$output .= $this->processSaveSettings();
}
if (Tools::isSubmit('regenerateToken')) {
Configuration::updateValue('QCTM_CRON_TASK_MANAGER_TOKEN', Tools::passwdGen(32));
$output .= $this->displayConfirmation($this->l('Token has been regenerated. Update your server cron entry with the new URL.'));
}
if(Tools::getValue('module_name') === $this->name) {
$this->context->controller->addCSS($this->_path . 'views/css/admin/qlo_cron_task_manager_admin.css');
$this->context->controller->addJS($this->_path . 'views/js/admin/qlo_cron_task_manager_admin.js');
}
$output .= $this->renderCronCommandPanel();
$output .= $this->renderSettingsForm();
return $output;
}
protected function processSaveSettings()
{
$logRetention = Tools::getValue('QCTM_CRON_TASK_MANAGER_LOG_RETENTION_DAYS');
if(!Validate::isUnsignedInt($logRetention)) {
return $this->displayError($this->l('Log retention must be an integer value.'));
}
if ($logRetention < 1 || $logRetention > 365) {
return $this->displayError($this->l('Log retention must be between 1 and 365 days.'));
}
Configuration::updateValue('QCTM_CRON_TASK_MANAGER_LOG_RETENTION_DAYS', $logRetention);
return $this->displayConfirmation($this->l('Settings saved successfully.'));
}
protected function renderCronCommandPanel()
{
$token = Configuration::get('QCTM_CRON_TASK_MANAGER_TOKEN');
$cronUrl = Tools::getShopDomainSsl(true) . __PS_BASE_URI__
. 'modules/qlocrontaskmanager/cron.php?token=' . $token;
$cronCommand = '* * * * * curl -s "' . $cronUrl . '"';
$this->context->smarty->assign(array(
'cronCommand' => $cronCommand,
));
return $this->display(__FILE__, 'views/templates/admin/configure.tpl');
}
protected function renderSettingsForm()
{
$token = Configuration::get('QCTM_CRON_TASK_MANAGER_TOKEN');
$helper = new HelperForm();
$helper->module = $this;
$helper->name_controller = $this->name;
$helper->token = Tools::getAdminTokenLite('AdminModules');
$helper->currentIndex = AdminController::$currentIndex . '&configure=' . $this->name;
$helper->default_form_language = (int) Configuration::get('PS_LANG_DEFAULT');
$helper->fields_value = array(
'QCTM_CRON_TASK_MANAGER_TOKEN' => $token,
'QCTM_CRON_TASK_MANAGER_LOG_RETENTION_DAYS' => (int) Configuration::get('QCTM_CRON_TASK_MANAGER_LOG_RETENTION_DAYS'),
);
$fields_form = array(
'form' => array(
'legend' => array(
'title' => $this->l('Settings'),
'icon' => 'icon-cogs',
),
'input' => array(
array(
'type' => 'text',
'label' => $this->l('Secure Token'),
'name' => 'QCTM_CRON_TASK_MANAGER_TOKEN',
'readonly' => true,
'col' => 6,
'desc' => $this->l('This token secures the cron endpoint. After regenerating it, you will need to update your server cron entry.'),
),
array(
'type' => 'text',
'label' => $this->l('Log Retention (days)'),
'name' => 'QCTM_CRON_TASK_MANAGER_LOG_RETENTION_DAYS',
'col' => 2,
'required' => true,
'validation' => 'isUnsignedInt',
'suffix' => $this->l('days'),
'desc' => $this->l('Number of days to keep execution logs.'),
),
),
'submit' => array(
'title' => $this->l('Save'),
'class' => 'btn btn-default pull-right',
'name' => 'submitCronTaskManagerSettings',
),
'buttons' => array(
array(
'title' => $this->l('Regenerate Token'),
'icon' => 'process-icon-refresh',
'name' => 'regenerateToken',
'type' => 'submit',
'class' => 'btn btn-default',
),
),
),
);
return $helper->generateForm(array($fields_form));
}
public function hookActionModuleInstallAfter($params)
{
if (!isset($params['object']) || !($params['object'] instanceof Module)) {
return;
}
$module = $params['object'];
if (method_exists($module, 'hookRegisterCronTasks')) {
$tasks = $module->hookRegisterCronTasks();
if (!empty($tasks) && is_array($tasks)) {
$objCronTask = new QctmCronTask();
$objCronTask->registerTasksForModule($module->id, $tasks);
}
}
}
public function hookDashboardZoneThree($params)
{
if ($this->isCrontabConfigured() !== false) {
return '';
}
$this->context->smarty->assign(array(
'qctmConfigureLink' => $this->context->link->getAdminLink('AdminModules') . '&configure=' . $this->name,
));
return $this->display(__FILE__, 'views/templates/admin/dashboard/qctm_cron_not_configured.tpl');
}
public function isCrontabConfigured()
{
if (!function_exists('shell_exec')) {
return null;
}
$disabled = array_map('trim', explode(',', (string) ini_get('disable_functions')));
if (in_array('shell_exec', $disabled, true)) {
return null;
}
$output = @shell_exec('crontab -l 2>/dev/null');
return strpos((string) $output, 'qlocrontaskmanager/cron.php') !== false;
}
public function hookActionModuleUninstallBefore($params)
{
if (!isset($params['object']) || !($params['object'] instanceof Module) || $params['object']->id == $this->id) {
return;
}
QctmCronTask::deleteByModule($params['object']->id);
}
/**
* Called by the system log-cleanup cron task (daily at midnight).
*/
public function runLogCleanup()
{
$days = (int) Configuration::get('QCTM_CRON_TASK_MANAGER_LOG_RETENTION_DAYS');
if ($days > 0) {
QctmCronTaskLog::deleteOlderThan($days);
}
}
public function install()
{
$db = new QctmCronTaskManagerDb();
$objCronTask = new QctmCronTask();
if (!parent::install()
|| !$this->registerHooks()
|| !$db->createTables()
|| !$db->installDefaultData()
|| !$this->installTabs()
|| !$this->installDefaultTasks()
) {
return false;
}
$objCronTask->syncAllExistingModules();
return true;
}
public function registerHooks()
{
return $this->registerHook(
array(
'actionModuleInstallAfter',
'actionModuleUninstallBefore',
'dashboardZoneThree',
)
);
}
public function uninstall()
{
$db = new QctmCronTaskManagerDb();
if (!$this->uninstallTabs()
|| !$db->deleteConfigurations()
|| !$db->dropTables()
|| !parent::uninstall()
) {
return false;
}
return true;
}
protected function installDefaultTasks()
{
if (QctmCronTask::getByModuleAndName($this->id, 'log_cleanup')) {
return true;
}
$task = new QctmCronTask();
$task->id_module = (int) $this->id;
$task->task_name = 'log_cleanup';
$task->description = 'Automatic log cleanup';
$task->cron_expression = '0 0 * * *';
$task->callback = 'runLogCleanup';
$task->active = 1;
$task->is_system = 1;
return $task->save();
}
protected function installTabs()
{
$tabCronTasks = new Tab();
$tabCronTasks->class_name = 'AdminCronTaskManager';
$tabCronTasks->id_parent = (int) Tab::getIdFromClassName('AdminTools');
$tabCronTasks->module = $this->name;
$tabCronTasks->position = Tab::getNewLastPosition($tabCronTasks->id_parent);
foreach (Language::getLanguages(false) as $language) {
$tabCronTasks->name[$language['id_lang']] = $this->l('Cron Task Manager');
}
if (!$tabCronTasks->add()) {
return false;
}
// Hidden tab — accessible via URL but not shown in navigation menu
$tabLogs = new Tab();
$tabLogs->class_name = 'AdminCronTaskLogs';
$tabLogs->id_parent = -1;
$tabLogs->module = $this->name;
foreach (Language::getLanguages(false) as $language) {
$tabLogs->name[$language['id_lang']] = $this->l('Cron Task Logs');
}
return $tabLogs->add();
}
protected function uninstallTabs()
{
foreach (array('AdminCronTaskManager', 'AdminCronTaskLogs') as $className) {
$idTab = (int) Tab::getIdFromClassName($className);
if ($idTab) {
$tab = new Tab($idTab);
if (!$tab->delete()) {
return false;
}
}
}
return true;
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../');
exit;

View File

@@ -0,0 +1,22 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
throw new RuntimeException($err);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInitc2f08a32af23ee5d749fb28358c620b3::getLoader();

View File

@@ -0,0 +1,579 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}

View File

@@ -0,0 +1,396 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
* @internal
*/
private static $selfDir = null;
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool
*/
private static $installedIsLocalDir;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
// so we have to assume it does not, and that may result in duplicate data being returned when listing
// all installed packages for example
self::$installedIsLocalDir = false;
}
/**
* @return string
*/
private static function getSelfDir()
{
if (self::$selfDir === null) {
self::$selfDir = strtr(__DIR__, '\\', '/');
}
return self::$selfDir;
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
$copiedLocalDir = false;
if (self::$canGetVendors) {
$selfDir = self::getSelfDir();
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
self::$installedByVendor[$vendorDir] = $required;
$installed[] = $required;
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
self::$installed = $required;
self::$installedIsLocalDir = true;
}
}
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
$copiedLocalDir = true;
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array() && !$copiedLocalDir) {
$installed[] = self::$installed;
}
return $installed;
}
}

View File

@@ -0,0 +1,21 @@
Copyright (c) Nils Adermann, Jordi Boggiano
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.

View File

@@ -0,0 +1,10 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
);

View File

@@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);

View File

@@ -0,0 +1,10 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Cron\\' => array($vendorDir . '/dragonmantank/cron-expression/src/Cron'),
);

View File

@@ -0,0 +1,38 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInitc2f08a32af23ee5d749fb28358c620b3
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInitc2f08a32af23ee5d749fb28358c620b3', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInitc2f08a32af23ee5d749fb28358c620b3', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInitc2f08a32af23ee5d749fb28358c620b3::getInitializer($loader));
$loader->register(true);
return $loader;
}
}

View File

@@ -0,0 +1,36 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInitc2f08a32af23ee5d749fb28358c620b3
{
public static $prefixLengthsPsr4 = array (
'C' =>
array (
'Cron\\' => 5,
),
);
public static $prefixDirsPsr4 = array (
'Cron\\' =>
array (
0 => __DIR__ . '/..' . '/dragonmantank/cron-expression/src/Cron',
),
);
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInitc2f08a32af23ee5d749fb28358c620b3::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInitc2f08a32af23ee5d749fb28358c620b3::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInitc2f08a32af23ee5d749fb28358c620b3::$classMap;
}, null, ClassLoader::class);
}
}

View File

@@ -0,0 +1,73 @@
{
"packages": [
{
"name": "dragonmantank/cron-expression",
"version": "v3.6.0",
"version_normalized": "3.6.0.0",
"source": {
"type": "git",
"url": "https://github.com/dragonmantank/cron-expression.git",
"reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013",
"reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013",
"shasum": ""
},
"require": {
"php": "^8.2|^8.3|^8.4|^8.5"
},
"replace": {
"mtdowling/cron-expression": "^1.0"
},
"require-dev": {
"phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan": "^1.12.32|^2.1.31",
"phpunit/phpunit": "^8.5.48|^9.0"
},
"time": "2025-10-31T18:51:33+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Cron\\": "src/Cron/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Chris Tankersley",
"email": "chris@ctankersley.com",
"homepage": "https://github.com/dragonmantank"
}
],
"description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due",
"keywords": [
"cron",
"schedule"
],
"support": {
"issues": "https://github.com/dragonmantank/cron-expression/issues",
"source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0"
},
"funding": [
{
"url": "https://github.com/dragonmantank",
"type": "github"
}
],
"install-path": "../dragonmantank/cron-expression"
}
],
"dev": true,
"dev-package-names": []
}

View File

@@ -0,0 +1,38 @@
<?php return array(
'root' => array(
'name' => 'qloapps/qlocrontaskmanager',
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
'reference' => 'f7d1d629ca72fc5ffd3349dea2877013367cea81',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => true,
),
'versions' => array(
'dragonmantank/cron-expression' => array(
'pretty_version' => 'v3.6.0',
'version' => '3.6.0.0',
'reference' => 'd61a8a9604ec1f8c3d150d09db6ce98b32675013',
'type' => 'library',
'install_path' => __DIR__ . '/../dragonmantank/cron-expression',
'aliases' => array(),
'dev_requirement' => false,
),
'mtdowling/cron-expression' => array(
'dev_requirement' => false,
'replaced' => array(
0 => '^1.0',
),
),
'qloapps/qlocrontaskmanager' => array(
'pretty_version' => 'dev-develop',
'version' => 'dev-develop',
'reference' => 'f7d1d629ca72fc5ffd3349dea2877013367cea81',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
),
);

View File

@@ -0,0 +1,26 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 80200)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.2.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}

View File

@@ -0,0 +1,240 @@
# Change Log
## [3.3.3] - 2024-08-10
### Added
- N/A
### Changed
- N/A
### Fixed
- Added fixes for making sure `?` is not passed for both DOM and DOW (#148, thank you https://github.com/LeoVie)
- Fixed bug in Next Execution Time by sorting minutes properly (#160, thank you https://github.com/imyip)
## [3.3.2] - 2022-09-19
### Added
- N/A
### Changed
- Skip some daylight savings time tests for PHP 8.1 daylight savings time weirdness (#146)
### Fixed
- Changed string interpolations to work better with PHP 8.2 (#142)
## [3.3.1] - 2022-01-18
### Added
- N/A
### Changed
- N/A
### Fixed
- Fixed issue when timezones had no transition, which can occur over very short timespans (#134)
## [3.3.0] - 2022-01-13
### Added
- Added ability to register your own expression aliases (#132)
### Changed
- Changed how Day of Week and Day of Month resolve when one or the other is `*` or `?`
### Fixed
- PHPStan should no longer error out
## [3.2.4] - 2022-01-12
### Added
- N/A
### Changed
- Changed how Day of Week increment/decrement to help with DST changes (#131)
### Fixed
- N/A
## [3.2.3] - 2022-01-05
### Added
- N/A
### Changed
- Changed how minutes and hours increment/decrement to help with DST changes (#131)
### Fixed
- N/A
## [3.2.2] - 2022-01-05
### Added
- N/A
### Changed
- Marked some methods `@internal` (#124)
### Fixed
- Fixed issue with small ranges and large steps that caused an error with `range()` (#88)
- Fixed issue where wraparound logic incorrectly considered high bound on range (#89)
## [3.2.1] - 2022-01-04
### Added
- N/A
### Changed
- Added PHP 8.1 to testing (#125)
### Fixed
- Allow better mixture of ranges, steps, and lists (#122)
- Fixed return order when multiple dates are requested and inverted (#121)
- Better handling over DST (#115)
- Fixed PHPStan tests (#130)
## [3.2.0] - 2022-01-04
### Added
- Added alias for `@midnight` (#117)
### Changed
- Improved testing for instance of field in tests (#105)
- Optimization for determining multiple run dates (#75)
- `CronExpression` properties changed from private to protected (#106)
### Fixed
- N/A
## [3.1.0] - 2020-11-24
### Added
- Added `CronExpression::getParts()` method to get parts of the expression as an array (#83)
### Changed
- Changed to Interfaces for some type hints (#97, #86)
- Dropped minimum PHP version to 7.2
- Few syntax changes for phpstan compatibility (#93)
### Fixed
- N/A
### Deprecated
- Deprecated `CronExpression::factory` in favor of the constructor (#56)
- Deprecated `CronExpression::YEAR` as a formality, the functionality is already removed (#87)
## [3.0.1] - 2020-10-12
### Added
- Added support for PHP 8 (#92)
### Changed
- N/A
### Fixed
- N/A
## [3.0.0] - 2020-03-25
**MAJOR CHANGE** - In previous versions of this library, setting both a "Day of Month" and a "Day of Week" would be interpreted as an `AND` statement, not an `OR` statement. For example:
`30 0 1 * 1`
would evaluate to "Run 30 minutes after the 0 hour when the Day Of Month is 1 AND a Monday" instead of "Run 30 minutes after the 0 hour on Day Of Month 1 OR a Monday", where the latter is more inline with most cron systems. This means that if your cron expression has both of these fields set, you may see your expression fire more often starting with v3.0.0.
### Added
- Additional docblocks for IDE and documentation
- Added phpstan as a development dependency
- Added a `Cron\FieldFactoryInterface` to make migrations easier (#38)
### Changed
- Changed some DI testing during TravisCI runs
- `\Cron\CronExpression::determineTimezone()` now checks for `\DateTimeInterface` instead of just `\DateTime`
- Errors with fields now report a more human-understandable error and are 1-based instead of 0-based
- Better support for `\DateTimeImmutable` across the library by typehinting for `\DateTimeInterface` now
- Literals should now be less case-sensative across the board
- Changed logic for when both a Day of Week and a Day of Month are supplied to now be an OR statement, not an AND
### Fixed
- Fixed infinite loop when determining last day of week from literals
- Fixed bug where single number ranges were allowed (ex: `1/10`)
- Fixed nullable FieldFactory in CronExpression where no factory could be supplied
- Fixed issue where logic for dropping seconds to 0 could lead to a timezone change
## [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))

View File

@@ -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.

View File

@@ -0,0 +1,131 @@
PHP Cron Expression Parser
==========================
[![Latest Stable Version](https://poser.pugx.org/dragonmantank/cron-expression/v)](https://packagist.org/packages/dragonmantank/cron-expression) [![Total Downloads](https://poser.pugx.org/dragonmantank/cron-expression/downloads)](https://packagist.org/packages/dragonmantank/cron-expression) [![Tests](https://github.com/dragonmantank/cron-expression/actions/workflows/tests.yml/badge.svg)](https://github.com/dragonmantank/cron-expression/actions/workflows/tests.yml) [![StyleCI](https://github.styleci.io/repos/103715337/shield?branch=master)](https://github.styleci.io/repos/103715337)
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 = new Cron\CronExpression('@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 = new Cron\CronExpression('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 = new Cron\CronExpression('@daily');
echo $cron->getNextRunDate(null, 2)->format('Y-m-d H:i:s');
// Calculate a run date relative to a specific time
$cron = new Cron\CronExpression('@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) (or SUN-SAT)
| | | +--------- month (1-12) (or JAN-DEC)
| | +------------- day of month (1-31)
| +----------------- hour (0-23)
+--------------------- minute (0-59)
```
Each part of expression can also use wildcard, lists, ranges and steps:
- wildcard - match always
- `* * * * *` - At every minute.
- day of week and day of month also support `?`, an alias to `*`
- lists - match list of values, ranges and steps
- e.g. `15,30 * * * *` - At minute 15 and 30.
- ranges - match values in range
- e.g. `1-9 * * * *` - At every minute from 1 through 9.
- steps - match every nth value in range
- e.g. `*/5 * * * *` - At every 5th minute.
- e.g. `0-30/5 * * * *` - At every 5th minute from 0 through 30.
- combinations
- e.g. `0-14,30-44 * * * *` - At every minute from 0 through 14 and every minute from 30 through 44.
You can also use macro instead of an expression:
- `@yearly`, `@annually` - At 00:00 on 1st of January. (same as `0 0 1 1 *`)
- `@monthly` - At 00:00 on day-of-month 1. (same as `0 0 1 * *`)
- `@weekly` - At 00:00 on Sunday. (same as `0 0 * * 0`)
- `@daily`, `@midnight` - At 00:00. (same as `0 0 * * *`)
- `@hourly` - At minute 0. (same as `0 * * * *`)
Day of month extra features:
- nearest weekday - weekday (Monday-Friday) nearest to the given day
- e.g. `* * 15W * *` - At every minute on a weekday nearest to the 15th.
- If you were to specify `15W` as the value, 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.
- last day of the month
- e.g. `* * L * *` - At every minute on a last day-of-month.
- last weekday of the month
- e.g. `* * LW * *` - At every minute on a last weekday.
Day of week extra features:
- nth day
- e.g. `* * * * 7#4` - At every minute on 4th Sunday.
- 1-5
- Every day of week repeats 4-5 times a month. To target the last one, use "last day" feature instead.
- last day
- e.g. `* * * * 7L` - At every minute on the last Sunday.
Requirements
============
- PHP 7.2+
- 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)
* Framework agnostic, PHP-based job scheduler - [Crunz](https://github.com/crunzphp/crunz)
* Framework agnostic job scheduler - with locks, parallelism, per-second scheduling and more - [orisai/scheduler](https://github.com/orisai/scheduler)
* Explain expression in English (and other languages) with [orisai/cron-expression-explainer](https://github.com/orisai/cron-expression-explainer)

View File

@@ -0,0 +1,50 @@
{
"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": "Chris Tankersley",
"email": "chris@ctankersley.com",
"homepage": "https://github.com/dragonmantank"
}
],
"require": {
"php": "^8.2|^8.3|^8.4|^8.5"
},
"require-dev": {
"phpstan/phpstan": "^1.12.32|^2.1.31",
"phpunit/phpunit": "^8.5.48|^9.0",
"phpstan/extension-installer": "^1.4.3"
},
"autoload": {
"psr-4": {
"Cron\\": "src/Cron/"
}
},
"autoload-dev": {
"psr-4": {
"Cron\\Tests\\": "tests/Cron/"
}
},
"replace": {
"mtdowling/cron-expression": "^1.0"
},
"scripts": {
"phpstan": "./vendor/bin/phpstan analyze",
"test": "phpunit"
},
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"config": {
"allow-plugins": {
"ocramius/package-versions": true,
"phpstan/extension-installer": true
}
}
}

View File

@@ -0,0 +1,345 @@
<?php
declare(strict_types=1);
namespace Cron;
use DateTimeInterface;
/**
* Abstract CRON expression field.
*/
abstract class AbstractField implements FieldInterface
{
/**
* Full range of values that are allowed for this field type.
*
* @var array<int, int>
*/
protected $fullRange = [];
/**
* Literal values we need to convert to integers.
*
* @var array<int, string>
*/
protected $literals = [];
/**
* Start value of the full range.
*
* @var int
*/
protected $rangeStart;
/**
* End value of the full range.
*
* @var int
*/
protected $rangeEnd;
/**
* Constructor
*/
public function __construct()
{
$this->fullRange = range($this->rangeStart, $this->rangeEnd);
}
/**
* Check to see if a field is satisfied by a value.
*
* @internal
* @param int $dateValue Date value to check
* @param string $value Value to test
*
* @return bool
*/
public function isSatisfied(int $dateValue, string $value): bool
{
if ($this->isIncrementsOfRanges($value)) {
return $this->isInIncrementsOfRanges($dateValue, $value);
}
if ($this->isRange($value)) {
return $this->isInRange($dateValue, $value);
}
return '*' === $value || $dateValue === (int) $value;
}
/**
* Check if a value is a range.
*
* @internal
* @param string $value Value to test
*
* @return bool
*/
public function isRange(string $value): bool
{
return false !== strpos($value, '-');
}
/**
* Check if a value is an increments of ranges.
*
* @internal
* @param string $value Value to test
*
* @return bool
*/
public function isIncrementsOfRanges(string $value): bool
{
return false !== strpos($value, '/');
}
/**
* Test if a value is within a range.
*
* @internal
* @param int $dateValue Set date value
* @param string $value Value to test
*
* @return bool
*/
public function isInRange(int $dateValue, $value): bool
{
$parts = array_map(
function ($value) {
$value = trim($value);
return $this->convertLiterals($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).
*
* @internal
* @param int $dateValue Set date value
* @param string $value Value to test
*
* @return bool
*/
public function isInIncrementsOfRanges(int $dateValue, string $value): bool
{
$chunks = array_map('trim', explode('/', $value, 2));
$range = $chunks[0];
$step = $chunks[1] ?? 0;
// No step or 0 steps aren't cool
if (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 = (int) $rangeChunks[0];
$rangeEnd = $rangeChunks[1] ?? $rangeStart;
$rangeEnd = (int) $rangeEnd;
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
// UPDATE - This is actually false. The C implementation will allow a
// larger step as valid syntax, it never wraps around. It will stop
// once it hits the end. Unfortunately this means in future versions
// we will not wrap around. However, because the logic exists today
// per the above documentation, fixing the bug from #89
if ($step > $this->rangeEnd) {
$thisRange = [$this->fullRange[(int) $step % \count($this->fullRange)]];
} else {
if ($step > ($rangeEnd - $rangeStart)) {
$thisRange[$rangeStart] = (int) $rangeStart;
} else {
$thisRange = range($rangeStart, $rangeEnd, (int) $step);
}
}
return \in_array($dateValue, $thisRange, true);
}
/**
* 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<int, int>
*/
public function getRangeForExpression(string $expression, int $max): array
{
$values = [];
$expression = $this->convertLiterals($expression);
if (false !== strpos($expression, ',')) {
$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)) {
[$offset, $to] = explode('-', $expression);
$offset = $this->convertLiterals($offset);
$to = $this->convertLiterals($to);
$stepSize = 1;
} else {
$range = array_map('trim', explode('/', $expression, 2));
$stepSize = $range[1] ?? 0;
$range = $range[0];
$range = explode('-', $range, 2);
$offset = $range[0];
$to = $range[1] ?? $max;
}
$offset = '*' === $offset ? $this->rangeStart : $offset;
if ($stepSize >= $this->rangeEnd) {
$values = [$this->fullRange[(int) $stepSize % \count($this->fullRange)]];
} else {
for ($i = $offset; $i <= $to; $i += $stepSize) {
$values[] = (int) $i;
}
}
sort($values);
} else {
$values = [$expression];
}
return $values;
}
/**
* Convert literal.
*
* @param string $value
*
* @return string
*/
protected function convertLiterals(string $value): string
{
if (\count($this->literals)) {
$key = array_search(strtoupper($value), $this->literals, true);
if (false !== $key) {
return (string) $key;
}
}
return $value;
}
/**
* Checks to see if a value is valid for the field.
*
* @param string $value
*
* @return bool
*/
public function validate(string $value): bool
{
$value = $this->convertLiterals($value);
// All fields allow * as a valid value
if ('*' === $value) {
return true;
}
// Validate each chunk of a list individually
if (false !== strpos($value, ',')) {
foreach (explode(',', $value) as $listItem) {
if (!$this->validate($listItem)) {
return false;
}
}
return true;
}
if (false !== strpos($value, '/')) {
[$range, $step] = explode('/', $value);
// Don't allow numeric ranges
if (is_numeric($range)) {
return false;
}
return $this->validate($range) && filter_var($step, FILTER_VALIDATE_INT);
}
if (false !== strpos($value, '-')) {
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 (false !== strpos($value, '.')) {
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);
}
protected function timezoneSafeModify(DateTimeInterface $dt, string $modification): DateTimeInterface
{
$timezone = $dt->getTimezone();
$dt = $dt->setTimezone(new \DateTimeZone("UTC"));
$dt = $dt->modify($modification);
$dt = $dt->setTimezone($timezone);
return $dt;
}
protected function setTimeHour(DateTimeInterface $date, bool $invert, int $originalTimestamp): DateTimeInterface
{
$date = $date->setTime((int)$date->format('H'), ($invert ? 59 : 0));
// setTime caused the offset to change, moving time in the wrong direction
$actualTimestamp = $date->format('U');
if ((! $invert) && ($actualTimestamp <= $originalTimestamp)) {
$date = $this->timezoneSafeModify($date, "+1 hour");
} elseif ($invert && ($actualTimestamp >= $originalTimestamp)) {
$date = $this->timezoneSafeModify($date, "-1 hour");
}
return $date;
}
}

View File

@@ -0,0 +1,591 @@
<?php
declare(strict_types=1);
namespace Cron;
use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
use Exception;
use InvalidArgumentException;
use LogicException;
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.
*
* @see http://en.wikipedia.org/wiki/Cron
*/
class CronExpression
{
public const MINUTE = 0;
public const HOUR = 1;
public const DAY = 2;
public const MONTH = 3;
public const WEEKDAY = 4;
/** @deprecated */
public const YEAR = 5;
public const MAPPINGS = [
'@yearly' => '0 0 1 1 *',
'@annually' => '0 0 1 1 *',
'@monthly' => '0 0 1 * *',
'@weekly' => '0 0 * * 0',
'@daily' => '0 0 * * *',
'@midnight' => '0 0 * * *',
'@hourly' => '0 * * * *',
];
/**
* @var array<int, string> CRON expression parts
*/
protected $cronParts;
/**
* @var FieldFactoryInterface CRON field factory
*/
protected $fieldFactory;
/**
* @var int Max iteration count when searching for next run date
*/
protected $maxIterationCount = 1000;
/**
* @var array<int, int> Order in which to test of cron parts
*/
protected static $order = [
self::YEAR,
self::MONTH,
self::DAY,
self::WEEKDAY,
self::HOUR,
self::MINUTE,
];
/**
* @var array<string, string>
*/
private static $registeredAliases = self::MAPPINGS;
/**
* Registered a user defined CRON Expression Alias.
*
* @throws LogicException If the expression or the alias name are invalid
* or if the alias is already registered.
*/
public static function registerAlias(string $alias, string $expression): void
{
try {
new self($expression);
} catch (InvalidArgumentException $exception) {
throw new LogicException("The expression `$expression` is invalid", 0, $exception);
}
$shortcut = strtolower($alias);
if (1 !== preg_match('/^@\w+$/', $shortcut)) {
throw new LogicException("The alias `$alias` is invalid. It must start with an `@` character and contain alphanumeric (letters, numbers, regardless of case) plus underscore (_).");
}
if (isset(self::$registeredAliases[$shortcut])) {
throw new LogicException("The alias `$alias` is already registered.");
}
self::$registeredAliases[$shortcut] = $expression;
}
/**
* Unregistered a user defined CRON Expression Alias.
*
* @throws LogicException If the user tries to unregister a built-in alias
*/
public static function unregisterAlias(string $alias): bool
{
$shortcut = strtolower($alias);
if (isset(self::MAPPINGS[$shortcut])) {
throw new LogicException("The alias `$alias` is a built-in alias; it can not be unregistered.");
}
if (!isset(self::$registeredAliases[$shortcut])) {
return false;
}
unset(self::$registeredAliases[$shortcut]);
return true;
}
/**
* Tells whether a CRON Expression alias is registered.
*/
public static function supportsAlias(string $alias): bool
{
return isset(self::$registeredAliases[strtolower($alias)]);
}
/**
* Returns all registered aliases as an associated array where the aliases are the key
* and their associated expressions are the values.
*
* @return array<string, string>
*/
public static function getAliases(): array
{
return self::$registeredAliases;
}
/**
* @deprecated since version 3.0.2, use __construct instead.
*/
public static function factory(string $expression, ?FieldFactoryInterface $fieldFactory = null): CronExpression
{
/** @phpstan-ignore-next-line */
return new static($expression, $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.
*/
public static function isValidExpression(string $expression): bool
{
try {
new CronExpression($expression);
} catch (InvalidArgumentException $e) {
return false;
}
return true;
}
/**
* Parse a CRON expression.
*
* @param string $expression CRON expression (e.g. '8 * * * *')
* @param null|FieldFactoryInterface $fieldFactory Factory to create cron fields
* @throws InvalidArgumentException
*/
public function __construct(string $expression, ?FieldFactoryInterface $fieldFactory = null)
{
$shortcut = strtolower($expression);
$expression = self::$registeredAliases[$shortcut] ?? $expression;
$this->fieldFactory = $fieldFactory ?: new FieldFactory();
$this->setExpression($expression);
}
/**
* Set or change the CRON expression.
*
* @param string $value CRON expression (e.g. 8 * * * *)
*
* @throws \InvalidArgumentException if not a valid CRON expression
*
* @return CronExpression
*/
public function setExpression(string $value): CronExpression
{
$split = preg_split('/\s/', $value, -1, PREG_SPLIT_NO_EMPTY);
if (!\is_array($split)) {
throw new InvalidArgumentException(
$value . ' is not a valid CRON expression'
);
}
$notEnoughParts = \count($split) < 5;
$questionMarkInInvalidPart = array_key_exists(0, $split) && $split[0] === '?'
|| array_key_exists(1, $split) && $split[1] === '?'
|| array_key_exists(3, $split) && $split[3] === '?';
$tooManyQuestionMarks = array_key_exists(2, $split) && $split[2] === '?'
&& array_key_exists(4, $split) && $split[4] === '?';
if ($notEnoughParts || $questionMarkInInvalidPart || $tooManyQuestionMarks) {
throw new InvalidArgumentException(
$value . ' is not a valid CRON expression'
);
}
$this->cronParts = $split;
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
*
* @throws \InvalidArgumentException if the value is not valid for the part
*
* @return CronExpression
*/
public function setPart(int $position, string $value): CronExpression
{
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(int $maxIterationCount): CronExpression
{
$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
*
* @throws \RuntimeException on too many iterations
* @throws \Exception
*
* @return \DateTime
*/
public function getNextRunDate($currentTime = 'now', int $nth = 0, bool $allowCurrentDate = false, $timeZone = null): DateTime
{
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
*
* @throws \RuntimeException on too many iterations
* @throws \Exception
*
* @return \DateTime
*
* @see \Cron\CronExpression::getNextRunDate
*/
public function getPreviousRunDate($currentTime = 'now', int $nth = 0, bool $allowCurrentDate = false, $timeZone = null): DateTime
{
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|null $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(int $total, $currentTime = 'now', bool $invert = false, bool $allowCurrentDate = false, $timeZone = null): array
{
$timeZone = $this->determineTimeZone($currentTime, $timeZone);
if ('now' === $currentTime) {
$currentTime = new DateTime();
} elseif ($currentTime instanceof DateTime) {
$currentTime = clone $currentTime;
} elseif ($currentTime instanceof DateTimeImmutable) {
$currentTime = DateTime::createFromFormat('U', $currentTime->format('U'));
} elseif (\is_string($currentTime)) {
$currentTime = new DateTime($currentTime);
}
if (!$currentTime instanceof DateTime) {
throw new InvalidArgumentException('invalid current time');
}
$currentTime->setTimezone(new DateTimeZone($timeZone));
$matches = [];
for ($i = 0; $i < $total; ++$i) {
try {
$result = $this->getRunDate($currentTime, 0, $invert, $allowCurrentDate, $timeZone);
} catch (RuntimeException $e) {
break;
}
$allowCurrentDate = false;
$currentTime = clone $result;
$matches[] = $result;
}
return $matches;
}
/**
* Get all or part of the CRON expression.
*
* @param int|string|null $part specify the part to retrieve or NULL to get the full
* cron schedule string
*
* @return null|string 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): ?string
{
if (null === $part) {
return implode(' ', $this->cronParts);
}
if (array_key_exists($part, $this->cronParts)) {
return $this->cronParts[$part];
}
return null;
}
/**
* Gets the parts of the cron expression as an array.
*
* @return string[]
* The array of parts that make up this expression.
*/
public function getParts()
{
return $this->cronParts;
}
/**
* Helper method to output the full expression.
*
* @return string Full CRON expression
*/
public function __toString(): string
{
return (string) $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): bool
{
$timeZone = $this->determineTimeZone($currentTime, $timeZone);
if ('now' === $currentTime) {
$currentTime = new DateTime();
} elseif ($currentTime instanceof DateTime) {
$currentTime = clone $currentTime;
} elseif ($currentTime instanceof DateTimeImmutable) {
$currentTime = DateTime::createFromFormat('U', $currentTime->format('U'));
} elseif (\is_string($currentTime)) {
$currentTime = new DateTime($currentTime);
}
if (!$currentTime instanceof DateTime) {
throw new InvalidArgumentException('invalid current time');
}
$currentTime->setTimezone(new DateTimeZone($timeZone));
// drop the seconds to 0
$currentTime->setTime((int) $currentTime->format('H'), (int) $currentTime->format('i'), 0);
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|null $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
*
* @throws \RuntimeException on too many iterations
* @throws Exception
*
* @return \DateTime
*/
protected function getRunDate($currentTime = null, int $nth = 0, bool $invert = false, bool $allowCurrentDate = false, $timeZone = null): DateTime
{
$timeZone = $this->determineTimeZone($currentTime, $timeZone);
if ($currentTime instanceof DateTime) {
$currentDate = clone $currentTime;
} elseif ($currentTime instanceof DateTimeImmutable) {
$currentDate = DateTime::createFromFormat('U', $currentTime->format('U'));
} elseif (\is_string($currentTime)) {
$currentDate = new DateTime($currentTime);
} else {
$currentDate = new DateTime('now');
}
if (!$currentDate instanceof DateTime) {
throw new InvalidArgumentException('invalid current date');
}
$currentDate->setTimezone(new DateTimeZone($timeZone));
// Workaround for setTime causing an offset change: https://bugs.php.net/bug.php?id=81074
$currentDate = DateTime::createFromFormat("!Y-m-d H:iO", $currentDate->format("Y-m-d H:iP"), $currentDate->getTimezone());
if ($currentDate === false) {
throw new \RuntimeException('Unable to create date from format');
}
$currentDate->setTimezone(new DateTimeZone($timeZone));
$nextRun = clone $currentDate;
// We don't have to satisfy * or null fields
$parts = [];
$fields = [];
foreach (self::$order as $position) {
$part = $this->getExpression($position);
if (null === $part || '*' === $part) {
continue;
}
$parts[$position] = $part;
$fields[$position] = $this->fieldFactory->getField($position);
}
if (isset($parts[self::DAY]) && isset($parts[self::WEEKDAY])) {
$domExpression = sprintf('%s %s %s %s *', $this->getExpression(0), $this->getExpression(1), $this->getExpression(2), $this->getExpression(3));
$dowExpression = sprintf('%s %s * %s %s', $this->getExpression(0), $this->getExpression(1), $this->getExpression(3), $this->getExpression(4));
$domExpression = new self($domExpression);
$dowExpression = new self($dowExpression);
$domRunDates = $domExpression->getMultipleRunDates($nth + 1, $currentTime, $invert, $allowCurrentDate, $timeZone);
$dowRunDates = $dowExpression->getMultipleRunDates($nth + 1, $currentTime, $invert, $allowCurrentDate, $timeZone);
if ($parts[self::DAY] === '?' || $parts[self::DAY] === '*') {
$domRunDates = [];
}
if ($parts[self::WEEKDAY] === '?' || $parts[self::WEEKDAY] === '*') {
$dowRunDates = [];
}
$combined = array_merge($domRunDates, $dowRunDates);
usort($combined, function ($a, $b) {
return $a->format('Y-m-d H:i:s') <=> $b->format('Y-m-d H:i:s');
});
if ($invert) {
$combined = array_reverse($combined);
}
return $combined[$nth];
}
// 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 (false === strpos($part, ',')) {
$satisfied = $field->isSatisfiedBy($nextRun, $part, $invert);
} else {
foreach (array_map('trim', explode(',', $part)) as $listPart) {
if ($field->isSatisfiedBy($nextRun, $listPart, $invert)) {
$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(self::MINUTE)->increment($nextRun, $invert, $parts[self::MINUTE] ?? null);
continue;
}
return $nextRun;
}
// @codeCoverageIgnoreStart
throw new RuntimeException('Impossible CRON expression');
// @codeCoverageIgnoreEnd
}
/**
* Workout what timeZone should be used.
*
* @param string|\DateTimeInterface|null $currentTime Relative calculation date
* @param string|null $timeZone TimeZone to use instead of the system default
*
* @return string
*/
protected function determineTimeZone($currentTime, ?string $timeZone): string
{
if (null !== $timeZone) {
return $timeZone;
}
if ($currentTime instanceof DateTimeInterface) {
return $currentTime->getTimezone()->getName();
}
return date_default_timezone_get();
}
}

View File

@@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
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|null Returns the nearest date
*/
private static function getNearestWeekday(int $currentYear, int $currentMonth, int $targetDay): ?DateTime
{
$tday = str_pad((string) $targetDay, 2, '0', STR_PAD_LEFT);
$target = DateTime::createFromFormat('Y-m-d', "{$currentYear}-{$currentMonth}-{$tday}");
if ($target === false) {
return null;
}
$currentWeekday = (int) $target->format('N');
if ($currentWeekday < 6) {
return $target;
}
$lastDayOfMonth = $target->format('t');
foreach ([-1, 1, -2, 2] as $i) {
$adjusted = $targetDay + $i;
if ($adjusted > 0 && $adjusted <= $lastDayOfMonth) {
$target->setDate($currentYear, $currentMonth, $adjusted);
if ((int) $target->format('N') < 6 && (int) $target->format('m') === $currentMonth) {
return $target;
}
}
}
return null;
}
/**
* {@inheritdoc}
*/
public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bool
{
// ? 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 ('L' === $value) {
return $fieldValue === $date->format('t');
}
// Check to see if this is the nearest weekday to a particular value
if ($wPosition = strpos($value, 'W')) {
// Parse the target day
$targetDay = (int) substr($value, 0, $wPosition);
// Find out if the current day is the nearest day of the week
$nearest = self::getNearestWeekday(
(int) $date->format('Y'),
(int) $date->format('m'),
$targetDay
);
if ($nearest) {
return $date->format('j') === $nearest->format('j');
}
throw new \RuntimeException('Unable to return nearest weekday');
}
return $this->isSatisfied((int) $date->format('d'), $value);
}
/**
* @inheritDoc
*
* @param \DateTime|\DateTimeImmutable $date
*/
public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface
{
if (! $invert) {
$date = $date->add(new \DateInterval('P1D'));
$date = $date->setTime(0, 0);
} else {
$date = $date->sub(new \DateInterval('P1D'));
$date = $date->setTime(23, 59);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function validate(string $value): bool
{
$basicChecks = parent::validate($value);
// Validate that a list don't have W or L
if (false !== strpos($value, ',') && (false !== strpos($value, 'W') || false !== strpos($value, 'L'))) {
return false;
}
if (!$basicChecks) {
if ('?' === $value) {
return true;
}
if ('L' === $value) {
return true;
}
if (preg_match('/^(.*)W$/', $value, $matches)) {
return $this->validate($matches[1]);
}
return false;
}
return $basicChecks;
}
}

View File

@@ -0,0 +1,194 @@
<?php
declare(strict_types=1);
namespace Cron;
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<int, int> 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
*/
public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bool
{
if ('?' === $value) {
return true;
}
// Convert text day of the week values to integers
$value = $this->convertLiterals($value);
$currentYear = (int) $date->format('Y');
$currentMonth = (int) $date->format('m');
$lastDayOfMonth = (int) $date->format('t');
// Find out if this is the last specific weekday of the month
if ($lPosition = strpos($value, 'L')) {
$weekday = (int) $this->convertLiterals(substr($value, 0, $lPosition));
$weekday %= 7;
$daysInMonth = (int) $date->format('t');
$remainingDaysInMonth = $daysInMonth - (int) $date->format('d');
return (($weekday === (int) $date->format('w')) && ($remainingDaysInMonth < 7));
}
// Handle # hash tokens
if (strpos($value, '#')) {
[$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 ('0' === $weekday) {
$weekday = 7;
}
$weekday = (int) $this->convertLiterals((string) $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, true)) {
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 ((int) $date->format('N') !== $weekday) {
return false;
}
$tdate = clone $date;
$tdate = $tdate->setDate($currentYear, $currentMonth, 1);
$dayCount = 0;
$currentDay = 1;
while ($currentDay < $lastDayOfMonth + 1) {
if ((int) $tdate->format('N') === $weekday) {
if (++$dayCount >= $nth) {
break;
}
}
$tdate = $tdate->setDate($currentYear, $currentMonth, ++$currentDay);
}
return (int) $date->format('j') === $currentDay;
}
// Handle day of the week values
if (false !== strpos($value, '-')) {
$parts = explode('-', $value);
if ('7' === $parts[0]) {
$parts[0] = 0;
} elseif ('0' === $parts[1]) {
$parts[1] = 7;
}
$value = implode('-', $parts);
}
// Test to see which Sunday to use -- 0 == 7 == Sunday
$format = \in_array(7, array_map(function ($value) {
return (int) $value;
}, str_split($value)), true) ? 'N' : 'w';
$fieldValue = (int) $date->format($format);
return $this->isSatisfied($fieldValue, $value);
}
/**
* @inheritDoc
*/
public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface
{
if (! $invert) {
$date = $date->add(new \DateInterval('P1D'));
$date = $date->setTime(0, 0);
} else {
$date = $date->sub(new \DateInterval('P1D'));
$date = $date->setTime(23, 59);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function validate(string $value): bool
{
$basicChecks = parent::validate($value);
if (!$basicChecks) {
if ('?' === $value) {
return true;
}
// Handle the # value
if (false !== strpos($value, '#')) {
$chunks = explode('#', $value);
$chunks[0] = $this->convertLiterals($chunks[0]);
if (parent::validate($chunks[0]) && is_numeric($chunks[1]) && \in_array((int) $chunks[1], $this->nthRange, true)) {
return true;
}
}
if (preg_match('/^(.*)L$/', $value, $matches)) {
return $this->validate($matches[1]);
}
return false;
}
return $basicChecks;
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Cron;
use InvalidArgumentException;
/**
* CRON field factory implementing a flyweight factory.
*
* @see http://en.wikipedia.org/wiki/Cron
*/
class FieldFactory implements FieldFactoryInterface
{
/**
* @var array<int, FieldInterface> Cache of instantiated fields
*/
private $fields = [];
/**
* Get an instance of a field object for a cron expression position.
*
* @param int $position CRON expression position value to retrieve
*
* @throws InvalidArgumentException if a position is not valid
*/
public function getField(int $position): FieldInterface
{
return $this->fields[$position] ?? $this->fields[$position] = $this->instantiateField($position);
}
private function instantiateField(int $position): FieldInterface
{
switch ($position) {
case CronExpression::MINUTE:
return new MinutesField();
case CronExpression::HOUR:
return new HoursField();
case CronExpression::DAY:
return new DayOfMonthField();
case CronExpression::MONTH:
return new MonthField();
case CronExpression::WEEKDAY:
return new DayOfWeekField();
}
throw new InvalidArgumentException(
($position + 1) . ' is not a valid position'
);
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Cron;
interface FieldFactoryInterface
{
public function getField(int $position): FieldInterface;
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Cron;
use DateTimeInterface;
/**
* CRON field interface.
*/
interface FieldInterface
{
/**
* Check if the respective value of a DateTime field satisfies a CRON exp.
*
* @internal
* @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, bool $invert): bool;
/**
* 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.
*
* @internal
* @param DateTimeInterface $date DateTime object to change
* @param bool $invert (optional) Set to TRUE to decrement
* @param string|null $parts (optional) Set parts to use
*
* @return FieldInterface
*/
public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface;
/**
* 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(string $value): bool;
}

View File

@@ -0,0 +1,221 @@
<?php
declare(strict_types=1);
namespace Cron;
use DateTimeInterface;
use DateTimeZone;
/**
* Hours field. Allows: * , / -.
*/
class HoursField extends AbstractField
{
/**
* {@inheritdoc}
*/
protected $rangeStart = 0;
/**
* {@inheritdoc}
*/
protected $rangeEnd = 23;
/**
* @var list<array<string, bool|int|string>>|null Transitions returned by DateTimeZone::getTransitions()
*/
protected $transitions = [];
/**
* @var int|null Timestamp of the start of the transitions range
*/
protected $transitionsStart = null;
/**
* @var int|null Timestamp of the end of the transitions range
*/
protected $transitionsEnd = null;
/**
* {@inheritdoc}
*/
public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bool
{
$checkValue = (int) $date->format('H');
$retval = $this->isSatisfied($checkValue, $value);
if ($retval) {
return $retval;
}
// Are we on the edge of a transition
$lastTransition = $this->getPastTransition($date);
if (($lastTransition !== null) && ($lastTransition["ts"] > ((int) $date->format('U') - 3600))) {
$dtLastOffset = clone $date;
$this->timezoneSafeModify($dtLastOffset, "-1 hour");
$lastOffset = $dtLastOffset->getOffset();
$dtNextOffset = clone $date;
$this->timezoneSafeModify($dtNextOffset, "+1 hour");
$nextOffset = $dtNextOffset->getOffset();
$offsetChange = $nextOffset - $lastOffset;
if ($offsetChange >= 3600) {
$checkValue -= 1;
return $this->isSatisfied($checkValue, $value);
}
if ((! $invert) && ($offsetChange <= -3600)) {
$checkValue += 1;
return $this->isSatisfied($checkValue, $value);
}
}
return $retval;
}
/**
* @return non-empty-array<string, bool|int|string>|null
*/
public function getPastTransition(DateTimeInterface $date): ?array
{
$currentTimestamp = (int) $date->format('U');
if (
($this->transitions === null)
|| ($this->transitionsStart < ($currentTimestamp + 86400))
|| ($this->transitionsEnd > ($currentTimestamp - 86400))
) {
// We start a day before current time so we can differentiate between the first transition entry
// and a change that happens now
$dtLimitStart = clone $date;
$dtLimitStart = $dtLimitStart->modify("-12 months");
$dtLimitEnd = clone $date;
$dtLimitEnd = $dtLimitEnd->modify('+12 months');
$this->transitions = $date->getTimezone()->getTransitions(
$dtLimitStart->getTimestamp(),
$dtLimitEnd->getTimestamp()
);
if (empty($this->transitions)) {
return null;
}
$this->transitionsStart = $dtLimitStart->getTimestamp();
$this->transitionsEnd = $dtLimitEnd->getTimestamp();
}
$nextTransition = null;
foreach ($this->transitions as $transition) {
if ($transition["ts"] > $currentTimestamp) {
continue;
}
if (($nextTransition !== null) && ($transition["ts"] < $nextTransition["ts"])) {
continue;
}
$nextTransition = $transition;
}
return ($nextTransition ?? null);
}
/**
* {@inheritdoc}
*
* @param string|null $parts
*/
public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface
{
$originalTimestamp = (int) $date->format('U');
// 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 (null === $parts || '*' === $parts) {
if ($invert) {
$date = $date->sub(new \DateInterval('PT1H'));
} else {
$date = $date->add(new \DateInterval('PT1H'));
}
$date = $this->setTimeHour($date, $invert, $originalTimestamp);
return $this;
}
$parts = false !== strpos($parts, ',') ? explode(',', $parts) : [$parts];
$hours = [];
foreach ($parts as $part) {
$hours = array_merge($hours, $this->getRangeForExpression($part, 23));
}
$current_hour = (int) $date->format('H');
$position = $invert ? \count($hours) - 1 : 0;
$countHours = \count($hours);
if ($countHours > 1) {
for ($i = 0; $i < $countHours - 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;
}
}
}
$target = (int) $hours[$position];
$originalHour = (int)$date->format('H');
$originalDst = (int)$date->format('I');
$originalDay = (int)$date->format('d');
$previousOffset = $date->getOffset();
if (! $invert) {
if ($originalHour >= $target) {
$distance = 24 - $originalHour;
$date = $this->timezoneSafeModify($date, "+{$distance} hours");
$actualDay = (int)$date->format('d');
$actualHour = (int)$date->format('H');
if (($actualDay !== ($originalDay + 1)) && ($actualHour !== 0)) {
$offsetChange = ($previousOffset - $date->getOffset());
$date = $this->timezoneSafeModify($date, "+{$offsetChange} seconds");
}
$originalHour = (int)$date->format('H');
}
$distance = $target - $originalHour;
$date = $this->timezoneSafeModify($date, "+{$distance} hours");
} else {
if ($originalHour <= $target) {
$distance = ($originalHour + 1);
$date = $this->timezoneSafeModify($date, "-" . $distance . " hours");
$actualDay = (int)$date->format('d');
$actualHour = (int)$date->format('H');
if (($actualDay !== ($originalDay - 1)) && ($actualHour !== 23)) {
$offsetChange = ($previousOffset - $date->getOffset());
$date = $this->timezoneSafeModify($date, "+{$offsetChange} seconds");
}
$originalHour = (int)$date->format('H');
}
$distance = $originalHour - $target;
$date = $this->timezoneSafeModify($date, "-{$distance} hours");
}
$actualDst = (int)$date->format('I');
if ($originalDst < $actualDst) {
$date = $this->timezoneSafeModify($date, "-1 hours");
}
$date = $this->setTimeHour($date, $invert, $originalTimestamp);
$actualHour = (int)$date->format('H');
if ($invert && ($actualHour === ($target - 1) || (($actualHour === 23) && ($target === 0)))) {
$date = $this->timezoneSafeModify($date, "+1 hour");
}
return $this;
}
}

View File

@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
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, bool $invert):bool
{
if ($value === '?') {
return true;
}
return $this->isSatisfied((int)$date->format('i'), $value);
}
/**
* {@inheritdoc}
* {@inheritDoc}
*
* @param string|null $parts
*/
public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface
{
if (is_null($parts)) {
$date = $this->timezoneSafeModify($date, ($invert ? "-" : "+") ."1 minute");
return $this;
}
$current_minute = (int) $date->format('i');
$parts = false !== strpos($parts, ',') ? explode(',', $parts) : [$parts];
sort($parts);
$minutes = [];
foreach ($parts as $part) {
$minutes = array_merge($minutes, $this->getRangeForExpression($part, 59));
}
$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;
}
}
}
$target = (int) $minutes[$position];
$originalMinute = (int) $date->format("i");
if (! $invert) {
if ($originalMinute >= $target) {
$distance = 60 - $originalMinute;
$date = $this->timezoneSafeModify($date, "+{$distance} minutes");
$originalMinute = (int) $date->format("i");
}
$distance = $target - $originalMinute;
$date = $this->timezoneSafeModify($date, "+{$distance} minutes");
} else {
if ($originalMinute <= $target) {
$distance = ($originalMinute + 1);
$date = $this->timezoneSafeModify($date, "-{$distance} minutes");
$originalMinute = (int) $date->format("i");
}
$distance = $originalMinute - $target;
$date = $this->timezoneSafeModify($date, "-{$distance} minutes");
}
return $this;
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
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, bool $invert): bool
{
if ($value === '?') {
return true;
}
$value = $this->convertLiterals($value);
return $this->isSatisfied((int) $date->format('m'), $value);
}
/**
* @inheritDoc
*
* @param \DateTime|\DateTimeImmutable $date
*/
public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface
{
if (! $invert) {
$date = $date->modify('first day of next month');
$date = $date->setTime(0, 0);
} else {
$date = $date->modify('last day of previous month');
$date = $date->setTime(23, 59);
}
return $this;
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../');
exit;

View File

@@ -0,0 +1,56 @@
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
.label-success {
background-color: #5cb85c;
}
.label-danger {
background-color: #d9534f;
}
.label-default {
background-color: #999;
}
.qctm-cron-fields-table thead th {
text-align: center;
font-size: 14px;
font-weight: bold;
border: 1px solid #ddd!important;
padding: 8px;
}
.qctm-cron-fields-table tbody td {
text-align: center;
font-size: 12px;
color: #666;
border: 1px solid #e5e5e5;
padding: 6px;
}
.cron-expression-invalid{
color: #c33333!important;
}
.cron-expression-valid{
color: #409bbd !important
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../');
exit;

View File

@@ -0,0 +1,32 @@
<?php
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../');
exit;

View File

@@ -0,0 +1,32 @@
<?php
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../');
exit;

View File

@@ -0,0 +1,169 @@
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
var QctmAdmin = {
selectors: {
copyButton: '#qctm-copy-btn',
cronCommandInput: '#qctm-cron-command',
regenerateButton: 'button[name="regenerateToken"], input[name="regenerateToken"]',
textContainer: '#qctm-admin-js-text',
},
init: function () {
this.bindEvents();
},
bindEvents: function () {
var self = this;
$(document).on('click', self.selectors.copyButton, function (e) {
e.preventDefault();
self.copyCronCommand(this);
});
$(document).on('click', self.selectors.regenerateButton, function (e) {
if (!confirm(self.getRegenerateConfirmText())) {
e.preventDefault();
return false;
}
});
},
copyCronCommand: function (button) {
var input = $(this.selectors.cronCommandInput).get(0);
if (!input) {
return;
}
if (this.canUseClipboardApi()) {
this.copyWithClipboardApi(input, button);
} else {
this.copyWithFallback(input, button);
}
},
canUseClipboardApi: function () {
return typeof navigator !== 'undefined'
&& navigator.clipboard
&& typeof navigator.clipboard.writeText === 'function'
&& window.isSecureContext;
},
copyWithClipboardApi: function (input, button) {
var self = this;
self.selectInputValue(input);
navigator.clipboard.writeText(input.value).then(function () {
self.showCopiedState(button);
}).catch(function () {
self.copyWithFallback(input, button);
});
},
copyWithFallback: function (input, button) {
this.selectInputValue(input);
if (document.execCommand('copy')) {
this.showCopiedState(button);
}
},
selectInputValue: function (input) {
input.focus();
input.select();
input.setSelectionRange(0, input.value.length);
},
showCopiedState: function (button) {
var $button = $(button);
var originalHtml = $button.html();
var copiedText = $button.data('copied-text') || 'Copied!';
$button.html('<i class="icon-check"></i> ' + copiedText);
setTimeout(function () {
$button.html(originalHtml);
}, 2000);
},
getRegenerateConfirmText: function () {
return $(this.selectors.textContainer).data('regenerate-confirm') || '';
},
};
var QctmCronHelper = {
$input: null,
cronRegex: /^(\*|[\d\/*,\-]+)\s+(\*|[\d\/*,\-]+)\s+(\*|[\d\/*,\-]+)\s+(\*|[\d\/*,\-]+)\s+(\*|[\d\/*,\-]+)$/,
init: function () {
this.$input = $('.qctm-cron-expression');
if (!this.$input.length) {
return;
}
this.$input.on('input keyup', this.onInput.bind(this));
},
onInput: function (e) {
var $input = $(e.currentTarget);
var val = $input.val();
val = val.replace(/[^0-9*\/,\-\s]/g, '');
$input.val(val);
var $readable = $input.closest('.col-lg-4').find('.help-block');
if (this.cronRegex.test(val)) {
this.fetchReadable(val, $readable);
} else {
$readable.removeClass('cron-expression-valid').addClass('cron-expression-invalid').text(INVALID_CRON_EXPRESSION);
}
},
fetchReadable: function (val, $readable) {
var ajaxUrl = $('#qctm-ajax-url').val();
if (!ajaxUrl || !val) {
return;
}
$.ajax({
url: ajaxUrl,
type: 'GET',
data: {
action: 'getCronReadable',
cron_expression: val
},
dataType: 'json',
success: function (response) {
if (response && response.valid) {
$readable.removeClass('cron-expression-invalid').addClass('cron-expression-valid').text(response.readable );
} else {
$readable.removeClass('cron-expression-valid').addClass('cron-expression-invalid').text(response.readable);
}
}
});
}
};
$(document).ready(function () {
QctmAdmin.init();
QctmCronHelper.init();
});

View File

@@ -0,0 +1,32 @@
<?php
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../');
exit;

View File

@@ -0,0 +1,62 @@
{*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License (AFL 3.0)
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/afl-3.0.php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/licenses/afl-3.0.php Academic Free License 3.0
*}
<div class="panel">
<div class="panel-heading">
<i class="icon-terminal"></i> {l s='Cron Command' mod='qlocrontaskmanager'}
</div>
<div class="panel-body form-horizontal ">
<div class="alert alert-info">
{l s='Add the following cron entry to your server. This single command will manage all module cron tasks automatically.' mod='qlocrontaskmanager'}
</div>
<div class="form-group">
<label class="control-label col-lg-3" data-original-title="Cron Command" data-placement="top" data-toggle="tooltip">
Cron Command
</label>
<div class="col-lg-8">
<div class="input-group">
<input type="text"
id="qctm-cron-command"
class="form-control"
value='{$cronCommand|escape:'html':'UTF-8'}'
readonly="readonly" />
<span class="input-group-btn">
<button type="button"
id="qctm-copy-btn"
class="btn btn-default"
data-copied-text="{l s='Copied!' mod='qlocrontaskmanager'}"
title="{l s='Copy to clipboard' mod='qlocrontaskmanager'}">
<i class="icon-copy"></i> {l s='Copy' mod='qlocrontaskmanager'}
</button>
</span>
</div>
<p class="help-block">
{l s='This command runs every minute and dispatches all registered module tasks based on their cron expressions.' mod='qlocrontaskmanager'}
</p>
</div>
</div>
</div>
</div>
<span id="qctm-admin-js-text"
class="hide"
data-regenerate-confirm="{l s='Are you sure you want to regenerate the token? You will need to update your server cron entry.' mod='qlocrontaskmanager'}"></span>

View File

@@ -0,0 +1,32 @@
<?php
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../');
exit;

View File

@@ -0,0 +1,31 @@
{*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License (AFL 3.0)
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/afl-3.0.php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/licenses/afl-3.0.php Academic Free License 3.0
*}
{if $status == QctmCronTaskLog::QCTM_CRON_TASK_LOG_STATUS_SUCCESS}
<span class="label label-success">{l s='Success' mod='qlocrontaskmanager'}</span>
{elseif $status == QctmCronTaskLog::QCTM_CRON_TASK_LOG_STATUS_ERROR}
<span class="label label-danger">{l s='Error' mod='qlocrontaskmanager'}</span>
{else}
<span class="label label-default">{l s='N/A' mod='qlocrontaskmanager'}</span>
{/if}

View File

@@ -0,0 +1,32 @@
<?php
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../');
exit;

View File

@@ -0,0 +1,31 @@
{*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License (AFL 3.0)
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/afl-3.0.php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/licenses/afl-3.0.php Academic Free License 3.0
*}
{if $status == QctmCronTaskLog::QCTM_CRON_TASK_LOG_STATUS_SUCCESS}
<span class="label label-success">{l s='Success' mod='qlocrontaskmanager'}</span>
{elseif $status == QctmCronTaskLog::QCTM_CRON_TASK_LOG_STATUS_ERROR}
<span class="label label-danger">{l s='Error' mod='qlocrontaskmanager'}</span>
{else}
<span class="label label-default">{l s='N/A' mod='qlocrontaskmanager'}</span>
{/if}

View File

@@ -0,0 +1,48 @@
{*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License (AFL 3.0)
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/afl-3.0.php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/licenses/afl-3.0.php Academic Free License 3.0
*}
<div class="row">
<div class="col-lg-8">
<p>The cron expression is made of five fields. Each field can have the following values.</p>
<table class="table qctm-cron-fields-table">
<thead>
<tr>
<th>*</th>
<th>*</th>
<th>*</th>
<th>*</th>
<th>*</th>
</tr>
</thead>
<tbody>
<tr>
<td>{l s='minute (0-59)' mod='qlocrontaskmanager'}</td>
<td>{l s='hour (0 - 23)' mod='qlocrontaskmanager'}</td>
<td>{l s='day of the month (1 - 31)' mod='qlocrontaskmanager'}</td>
<td>{l s='month (1 - 12)' mod='qlocrontaskmanager'}</td>
<td>{l s='day of the week (0 - 6)' mod='qlocrontaskmanager'}</td>
</tr>
</tbody>
</table>
</div>
</div>
<input type="hidden" id="qctm-ajax-url" value="{$qctmAjaxUrl|escape:'html'}">

View File

@@ -0,0 +1,26 @@
{*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License (AFL 3.0)
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/afl-3.0.php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/licenses/afl-3.0.php Academic Free License 3.0
*}
<a href="{$resetLink|escape:'html'}" title="{l s='Reset Cron Task' mod='qlocrontaskmanager'}"
onclick="return confirm('{l s='Are you sure? This will revert the task settings to the module\'s original values.' mod='qlocrontaskmanager' js=1}');">
<i class="icon-refresh"></i> {l s='Reset Task' mod='qlocrontaskmanager'}
</a>

View File

@@ -0,0 +1,27 @@
{*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License (AFL 3.0)
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/afl-3.0.php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/licenses/afl-3.0.php Academic Free License 3.0
*}
<a href="{$viewLogsLink|escape:'html'}" class="btn btn-default" title="{l s='View Logs' mod='qlocrontaskmanager'}">
<i class="icon-list"></i> {l s='View Logs' mod='qlocrontaskmanager'}
</a>

View File

@@ -0,0 +1,32 @@
<?php
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../');
exit;

View File

@@ -0,0 +1,30 @@
{*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*}
<div class="col-sm-12">
<section class="dash_links widget panel">
<h3><i class="icon-exclamation"></i> {l s='Cron Task Manager' mod='qlocrontaskmanager'}</h3>
<dl>
<dt><a href="{$qctmConfigureLink|escape:'html':'UTF-8'}">{l s='Server cron job not detected' mod='qlocrontaskmanager'}</a></dt>
<dd>{l s='Scheduled tasks will not run automatically until the cron command is added to your server. Click here to view it.' mod='qlocrontaskmanager'}</dd>
</dl>
</section>
</div>

View File

@@ -0,0 +1,32 @@
<?php
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../');
exit;

View File

@@ -0,0 +1,32 @@
<?php
/**
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License version 3.0
* that is bundled with this package in the file LICENSE.md
* It is also available through the world-wide-web at this URL:
* https://opensource.org/license/osl-3-0-php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to support@qloapps.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to a newer
* versions in the future. If you wish to customize this module for your needs
* please refer to https://store.webkul.com/customisation-guidelines for more information.
*
* @author Webkul IN
* @copyright Since 2010 Webkul
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../');
exit;