From 8c189fd9eb22724a459d439872f5159bab4d43e1 Mon Sep 17 00:00:00 2001
From: Baoshuo
Date: Mon, 13 Feb 2023 20:29:32 +0800
Subject: [PATCH 1/3] feat: email notice
---
db/app_uoj233.sql | 28 +
web/app/cli.php | 54 +-
web/app/composer.json | 3 +-
web/app/controllers/forgot_pw.php | 66 +-
web/app/controllers/login.php | 16 +
web/app/controllers/subdomain/blog/blog.php | 5 +-
web/app/libs/uoj-html-lib.php | 7 +-
web/app/libs/uoj-utility-lib.php | 10 +
web/app/models/UOJContest.php | 5 +-
web/app/models/UOJMail.php | 86 +-
web/app/scheduler.php | 31 +
web/app/vendor/composer/autoload_psr4.php | 3 +
web/app/vendor/composer/autoload_static.php | 18 +
web/app/vendor/composer/installed.json | 187 +
web/app/vendor/composer/installed.php | 37 +-
web/app/vendor/composer/platform_check.php | 4 +-
.../cron-expression/CHANGELOG.md | 228 +
.../dragonmantank/cron-expression/LICENSE | 19 +
.../dragonmantank/cron-expression/README.md | 87 +
.../cron-expression/composer.json | 47 +
.../cron-expression/phpstan.neon | 15 +
.../src/Cron/AbstractField.php | 346 ++
.../src/Cron/CronExpression.php | 568 ++
.../src/Cron/DayOfMonthField.php | 164 +
.../src/Cron/DayOfWeekField.php | 194 +
.../cron-expression/src/Cron/FieldFactory.php | 52 +
.../src/Cron/FieldFactoryInterface.php | 8 +
.../src/Cron/FieldInterface.php | 46 +
.../cron-expression/src/Cron/HoursField.php | 212 +
.../cron-expression/src/Cron/MinutesField.php | 96 +
.../cron-expression/src/Cron/MonthField.php | 61 +
.../php-cron-scheduler/.coveralls.yml | 3 +
.../peppeocchi/php-cron-scheduler/.gitignore | 10 +
.../peppeocchi/php-cron-scheduler/.travis.yml | 23 +
.../php-cron-scheduler/CODE_OF_CONDUCT.md | 46 +
.../peppeocchi/php-cron-scheduler/LICENSE | 21 +
.../peppeocchi/php-cron-scheduler/README.md | 502 ++
.../php-cron-scheduler/composer.json | 41 +
.../peppeocchi/php-cron-scheduler/phpunit.xml | 20 +
.../php-cron-scheduler/src/GO/FailedJob.php | 32 +
.../php-cron-scheduler/src/GO/Job.php | 590 ++
.../php-cron-scheduler/src/GO/Scheduler.php | 327 ++
.../src/GO/Traits/Interval.php | 418 ++
.../src/GO/Traits/Mailer.php | 62 +
.../tests/GO/IntervalTest.php | 274 +
.../tests/GO/JobOutputFilesTest.php | 201 +
.../php-cron-scheduler/tests/GO/JobTest.php | 470 ++
.../tests/GO/MailerTest.php | 166 +
.../tests/GO/SchedulerTest.php | 381 ++
.../php-cron-scheduler/tests/async_job.php | 4 +
.../php-cron-scheduler/tests/test_job.php | 7 +
.../php-cron-scheduler/tests/tmp/.gitignore | 2 +
web/app/vendor/webmozart/assert/CHANGELOG.md | 207 +
web/app/vendor/webmozart/assert/LICENSE | 20 +
web/app/vendor/webmozart/assert/README.md | 287 +
web/app/vendor/webmozart/assert/composer.json | 43 +
.../vendor/webmozart/assert/src/Assert.php | 2080 +++++++
.../assert/src/InvalidArgumentException.php | 16 +
web/app/vendor/webmozart/assert/src/Mixin.php | 5089 +++++++++++++++++
web/install.sh | 2 +
60 files changed, 13962 insertions(+), 85 deletions(-)
create mode 100644 web/app/scheduler.php
create mode 100644 web/app/vendor/dragonmantank/cron-expression/CHANGELOG.md
create mode 100644 web/app/vendor/dragonmantank/cron-expression/LICENSE
create mode 100644 web/app/vendor/dragonmantank/cron-expression/README.md
create mode 100644 web/app/vendor/dragonmantank/cron-expression/composer.json
create mode 100644 web/app/vendor/dragonmantank/cron-expression/phpstan.neon
create mode 100644 web/app/vendor/dragonmantank/cron-expression/src/Cron/AbstractField.php
create mode 100644 web/app/vendor/dragonmantank/cron-expression/src/Cron/CronExpression.php
create mode 100644 web/app/vendor/dragonmantank/cron-expression/src/Cron/DayOfMonthField.php
create mode 100644 web/app/vendor/dragonmantank/cron-expression/src/Cron/DayOfWeekField.php
create mode 100644 web/app/vendor/dragonmantank/cron-expression/src/Cron/FieldFactory.php
create mode 100644 web/app/vendor/dragonmantank/cron-expression/src/Cron/FieldFactoryInterface.php
create mode 100644 web/app/vendor/dragonmantank/cron-expression/src/Cron/FieldInterface.php
create mode 100644 web/app/vendor/dragonmantank/cron-expression/src/Cron/HoursField.php
create mode 100644 web/app/vendor/dragonmantank/cron-expression/src/Cron/MinutesField.php
create mode 100644 web/app/vendor/dragonmantank/cron-expression/src/Cron/MonthField.php
create mode 100644 web/app/vendor/peppeocchi/php-cron-scheduler/.coveralls.yml
create mode 100644 web/app/vendor/peppeocchi/php-cron-scheduler/.gitignore
create mode 100644 web/app/vendor/peppeocchi/php-cron-scheduler/.travis.yml
create mode 100644 web/app/vendor/peppeocchi/php-cron-scheduler/CODE_OF_CONDUCT.md
create mode 100644 web/app/vendor/peppeocchi/php-cron-scheduler/LICENSE
create mode 100644 web/app/vendor/peppeocchi/php-cron-scheduler/README.md
create mode 100644 web/app/vendor/peppeocchi/php-cron-scheduler/composer.json
create mode 100644 web/app/vendor/peppeocchi/php-cron-scheduler/phpunit.xml
create mode 100644 web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/FailedJob.php
create mode 100644 web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Job.php
create mode 100644 web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Scheduler.php
create mode 100644 web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Traits/Interval.php
create mode 100644 web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Traits/Mailer.php
create mode 100644 web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/IntervalTest.php
create mode 100644 web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/JobOutputFilesTest.php
create mode 100644 web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/JobTest.php
create mode 100644 web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/MailerTest.php
create mode 100644 web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/SchedulerTest.php
create mode 100644 web/app/vendor/peppeocchi/php-cron-scheduler/tests/async_job.php
create mode 100644 web/app/vendor/peppeocchi/php-cron-scheduler/tests/test_job.php
create mode 100644 web/app/vendor/peppeocchi/php-cron-scheduler/tests/tmp/.gitignore
create mode 100644 web/app/vendor/webmozart/assert/CHANGELOG.md
create mode 100644 web/app/vendor/webmozart/assert/LICENSE
create mode 100644 web/app/vendor/webmozart/assert/README.md
create mode 100644 web/app/vendor/webmozart/assert/composer.json
create mode 100644 web/app/vendor/webmozart/assert/src/Assert.php
create mode 100644 web/app/vendor/webmozart/assert/src/InvalidArgumentException.php
create mode 100644 web/app/vendor/webmozart/assert/src/Mixin.php
diff --git a/db/app_uoj233.sql b/db/app_uoj233.sql
index 1d2c42f..70b46ed 100644
--- a/db/app_uoj233.sql
+++ b/db/app_uoj233.sql
@@ -429,6 +429,34 @@ LOCK TABLES `custom_test_submissions` WRITE;
/*!40000 ALTER TABLE `custom_test_submissions` ENABLE KEYS */;
UNLOCK TABLES;
+--
+-- Table structure for table `emails`
+--
+
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8mb4 */;
+CREATE TABLE `emails` (
+ `id` int UNSIGNED NOT NULL AUTO_INCREMENT,
+ `receiver` varchar(20) NOT NULL,
+ `subject` varchar(100) NOT NULL,
+ `content` text NOT NULL,
+ `created_at` datetime NOT NULL,
+ `send_time` datetime DEFAULT NULL,
+ `priority` int NOT NULL DEFAULT 0,
+ PRIMARY KEY (`id`),
+ KEY `send_time` (`send_time`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Dumping data for table `emails`
+--
+
+LOCK TABLES `emails` WRITE;
+/*!40000 ALTER TABLE `emails` DISABLE KEYS */;
+/*!40000 ALTER TABLE `emails` ENABLE KEYS */;
+UNLOCK TABLES;
+
--
-- Table structure for table `friend_links`
--
diff --git a/web/app/cli.php b/web/app/cli.php
index 280a7b6..c836c5b 100644
--- a/web/app/cli.php
+++ b/web/app/cli.php
@@ -10,65 +10,79 @@ require $_SERVER['DOCUMENT_ROOT'] . '/app/libs/uoj-lib.php';
$handlers = [
'upgrade:up' => function ($name) {
if (func_num_args() != 1) {
- die("php7.4 cli.php upgrade:up \n");
+ print("php cli.php upgrade:up \n");
+ exit(1);
}
- Upgrader::transaction(function() use ($name) {
+ Upgrader::transaction(function () use ($name) {
Upgrader::up($name);
});
- die("finished!\n");
+ print("finished!\n");
},
'upgrade:down' => function ($name) {
if (func_num_args() != 1) {
- die("php7.4 cli.php upgrade:down \n");
+ print("php cli.php upgrade:down \n");
+ exit(1);
}
- Upgrader::transaction(function() use ($name) {
+ Upgrader::transaction(function () use ($name) {
Upgrader::down($name);
});
- die("finished!\n");
+ print("finished!\n");
},
'upgrade:refresh' => function ($name) {
if (func_num_args() != 1) {
- die("php7.4 cli.php upgrade:refresh \n");
+ print("php cli.php upgrade:refresh \n");
+ exit(1);
}
- Upgrader::transaction(function() use ($name) {
+ Upgrader::transaction(function () use ($name) {
Upgrader::refresh($name);
});
- die("finished!\n");
+ print("finished!\n");
},
'upgrade:remove' => function ($name) {
if (func_num_args() != 1) {
- die("php7.4 cli.php upgrade:remove \n");
+ print("php cli.php upgrade:remove \n");
+ exit(1);
}
- Upgrader::transaction(function() use ($name) {
+ Upgrader::transaction(function () use ($name) {
Upgrader::remove($name);
});
- die("finished!\n");
+ print("finished!\n");
},
'upgrade:latest' => function () {
if (func_num_args() != 0) {
- die("php7.4 cli.php upgrade:latest\n");
+ print("php cli.php upgrade:latest\n");
+ exit(1);
}
- Upgrader::transaction(function() {
+ Upgrader::transaction(function () {
Upgrader::upgradeToLatest();
});
- die("finished!\n");
+ print("finished!\n");
},
'upgrade:remove-all' => function () {
if (func_num_args() != 0) {
- die("php7.4 cli.php upgrade:remove-all\n");
+ print("php cli.php upgrade:remove-all\n");
+ exit(1);
}
- Upgrader::transaction(function() {
+ Upgrader::transaction(function () {
Upgrader::removeAll();
});
- die("finished!\n");
+ print("finished!\n");
},
- 'help' => 'showHelp'
+ 'email:send-all' => function () {
+ if (func_num_args() != 0) {
+ print("php cli.php email:send-all\n");
+ exit(1);
+ }
+ UOJMail::cronSendEmail();
+ print("finished!\n");
+ },
+ 'help' => 'showHelp',
];
function showHelp() {
global $handlers;
echo "UOJ Command-Line Interface\n";
- echo "php7.4 cli.php params1 params2 ...\n";
+ echo "php cli.php params1 params2 ...\n";
echo "\n";
echo "The following tasks are available:\n";
foreach ($handlers as $cmd => $handler) {
diff --git a/web/app/composer.json b/web/app/composer.json
index c857c4d..c248713 100644
--- a/web/app/composer.json
+++ b/web/app/composer.json
@@ -6,7 +6,8 @@
"erusev/parsedown": "^1.7",
"php-curl-class/php-curl-class": "^9.13",
"ext-dom": "20031129",
- "ivopetkov/html5-dom-document-php": "2.*"
+ "ivopetkov/html5-dom-document-php": "2.*",
+ "peppeocchi/php-cron-scheduler": "^4.0"
},
"autoload": {
"classmap": [
diff --git a/web/app/controllers/forgot_pw.php b/web/app/controllers/forgot_pw.php
index 838f591..5488b48 100644
--- a/web/app/controllers/forgot_pw.php
+++ b/web/app/controllers/forgot_pw.php
@@ -46,7 +46,7 @@ $forgot_form->handle = function (&$vdata) {
unset($_SESSION['phrase']);
if (!$user['email']) {
- becomeMsgPage('用户未填写邮件地址,请联系管理员重置!');
+ becomeMsgPage('用户未填写邮件地址,请联系管理员重置密码!');
}
$oj_name = UOJConfig::$data['profile']['oj-name'];
@@ -54,7 +54,6 @@ $forgot_form->handle = function (&$vdata) {
$check_code = md5($user['username'] . "+" . $password . '+' . UOJTime::$time_now_str);
$sufs = base64url_encode($user['username'] . "." . $check_code);
$url = HTML::url("/reset_password", ['params' => ['p' => $sufs]]);
- $oj_url = HTML::url('/');
$name = $user['username'];
$remote_addr = UOJContext::remoteAddr();
$http_x_forwarded_for = UOJContext::httpXForwardedFor();
@@ -64,53 +63,28 @@ $forgot_form->handle = function (&$vdata) {
$name .= ' (' . $user['realname'] . ')';
}
- $html = <<
+ sendEmail($user['username'], $oj_name_short . ' 密码找回', <<您最近告知我们需要重置您在 {$oj_name_short} 上账号的密码。请访问以下链接:{$url} (如果无法点击链接,请试着复制链接并粘贴至浏览器中打开。)
+ 如果您没有请求重置密码,则忽略此信息。该链接将在 72 小时后自动过期失效。
-{$name} 您好,
+
+ - 请求 IP: {$remote_addr}
+ - 转发源 IP:{$http_x_forwarded_for}
+ - 用户代理: {$user_agent}
+
+ EOD);
-您最近告知我们需要重置您在 {$oj_name_short} 上账号的密码。请访问以下链接:{$url} (如果无法点击链接,请试着复制链接并粘贴至浏览器中打开。)
-如果您没有请求重置密码,则忽略此信息。该链接将在 72 小时后自动过期失效。
+ DB::update([
+ "update user_info",
+ "set", [
+ 'extra' => DB::json_set('extra', '$.reset_password_check_code', $check_code, '$.reset_password_time', UOJTime::$time_now_str),
+ ],
+ "where", [
+ "username" => $user['username'],
+ ],
+ ]);
-
-- 请求 IP: {$remote_addr} (转发来源: {$http_x_forwarded_for})
-- 用户代理: {$user_agent}
-
-
-{$oj_name}
-{$oj_url}
-EOD;
-
- $mailer = UOJMail::noreply();
- $mailer->addAddress($user['email'], $user['username']);
- $mailer->Subject = $oj_name_short . " 密码找回";
- $mailer->msgHTML($html);
-
- $res = retry_loop(function () use (&$mailer) {
- $res = $mailer->send();
-
- if ($res) return true;
-
- UOJLog::error($mailer->ErrorInfo);
-
- return false;
- });
-
- if (!$res) {
- becomeMsgPage('');
- } else {
- DB::update([
- "update user_info",
- "set", [
- 'extra' => DB::json_set('extra', '$.reset_password_check_code', $check_code, '$.reset_password_time', UOJTime::$time_now_str),
- ],
- "where", [
- "username" => $user['username'],
- ],
- ]);
-
- becomeMsgPage('邮件发送成功,请检查收件箱!
如果邮件未出现在收件箱中,请检查垃圾箱。');
- }
+ becomeMsgPage('邮件已发送,请检查收件箱!
如果邮件未出现在收件箱中,请检查垃圾箱。');
};
$forgot_form->runAtServer();
?>
diff --git a/web/app/controllers/login.php b/web/app/controllers/login.php
index ff97b1f..b8d8c38 100644
--- a/web/app/controllers/login.php
+++ b/web/app/controllers/login.php
@@ -42,6 +42,22 @@ function handleLoginPost() {
}
Auth::login($user['username']);
+
+ $remote_addr = UOJContext::remoteAddr();
+ $http_x_forwarded_for = UOJContext::httpXForwardedFor();
+ $user_agent = UOJContext::httpUserAgent();
+ sendEmail($user['username'], '新登录', <<您收到这封邮件是因为有人通过以下方式登录了您的帐户:
+
+
+ - 请求 IP: {$remote_addr}
+ - 转发源 IP:{$http_x_forwarded_for}
+ - 用户代理: {$user_agent}
+
+
+ 如果这是您进行的登录操作,请忽略此邮件。如果您没有进行过登录操作,请立即重置您账号的密码。
+ EOD);
+
return "ok";
}
diff --git a/web/app/controllers/subdomain/blog/blog.php b/web/app/controllers/subdomain/blog/blog.php
index 89259d4..d0b84aa 100644
--- a/web/app/controllers/subdomain/blog/blog.php
+++ b/web/app/controllers/subdomain/blog/blog.php
@@ -153,7 +153,7 @@ $reply_form->handle = function (&$vdata) {
}
if ($blog['poster'] !== Auth::id() && !in_array($blog['poster'], $notified)) {
$notified[] = $blog['poster'];
- $content = $user_link . '回复了您的博客 ' . $blog['title'] . ' :点击此处查看';
+ $content = $user_link . ' 回复了您的博客 ' . $blog['title'] . ' :点击此处查看';
sendSystemMsg($blog['poster'], '博客新回复通知', $content);
}
@@ -195,8 +195,7 @@ if (UOJUserBlog::userHasManagePermission(Auth::user())) {
sendSystemMsg(
$comment->info['poster'],
'评论隐藏通知',
- "" . UOJUser::getLink($comment->info['poster'], ['color' => false]) . " 您好:
" .
- "您为博客 " . UOJBlog::cur()->getLink() . " 回复的评论 “" . substr($comment->info['content'], 0, 30) . "……” 已被管理员隐藏,隐藏原因为 “{$reason}”。
"
+ "您为博客 " . UOJBlog::cur()->getLink() . " 回复的评论 “" . substr($comment->info['content'], 0, 30) . "……” 已被管理员隐藏,隐藏原因为 “{$reason}”。"
);
}
};
diff --git a/web/app/libs/uoj-html-lib.php b/web/app/libs/uoj-html-lib.php
index 37d07c7..308294c 100644
--- a/web/app/libs/uoj-html-lib.php
+++ b/web/app/libs/uoj-html-lib.php
@@ -106,12 +106,9 @@ function getLongTablePageRawUri($page) {
unset($param['page']);
}
- if ($param) {
- return $path . '?' . http_build_query($param);
- } else {
- return $path;
- }
+ return HTML::url($path, ['params' => $param]);
}
+
function getLongTablePageUri($page) {
return HTML::escape(getLongTablePageRawUri($page));
}
diff --git a/web/app/libs/uoj-utility-lib.php b/web/app/libs/uoj-utility-lib.php
index 5a9f027..a4e69e7 100644
--- a/web/app/libs/uoj-utility-lib.php
+++ b/web/app/libs/uoj-utility-lib.php
@@ -206,6 +206,16 @@ function sendSystemMsg($username, $title, $content) {
"(receiver, title, content, send_time)",
"values", DB::tuple([$username, $title, $content, DB::now()])
]);
+
+ sendEmail($username, $title, $content);
+}
+
+function sendEmail($username, $title, $content) {
+ DB::insert([
+ "insert into emails",
+ "(receiver, subject, content, created_at)",
+ "values", DB::tuple([$username, $title, $content, DB::now()])
+ ]);
}
function retry_loop(callable $f, $retry = 5, $ms = 10) {
diff --git a/web/app/models/UOJContest.php b/web/app/models/UOJContest.php
index 4605ce2..3b74b85 100644
--- a/web/app/models/UOJContest.php
+++ b/web/app/models/UOJContest.php
@@ -66,12 +66,9 @@ class UOJContest {
calcStandings($contest, $data, $score, $standings, ['update_contests_submissions' => true]);
for ($i = 0; $i < count($standings); $i++) {
- $user_link = UOJUser::getLink($standings[$i][2][0], ['color' => false]);
$tail = $standings[$i][0] == $total_score ? ',请继续保持。' : ',请继续努力!';
- $content = '' . $user_link . ' 您好:
';
- $content .= '' . '您参与的比赛 ' . $contest['name'] . ' 现已公布成绩,您的成绩为 ' . $standings[$i][0] . '' . $tail . '
';
- sendSystemMsg($standings[$i][2][0], '比赛成绩公布通知', $content);
+ sendSystemMsg($standings[$i][2][0], '比赛成绩公布通知', '您参与的比赛 ' . $contest['name'] . ' 现已公布成绩,您的成绩为 ' . $standings[$i][0] . '' . $tail);
DB::update([
"update contests_registrants",
"set", ["final_rank" => $standings[$i][3]],
diff --git a/web/app/models/UOJMail.php b/web/app/models/UOJMail.php
index d9978a6..4ed1f68 100644
--- a/web/app/models/UOJMail.php
+++ b/web/app/models/UOJMail.php
@@ -4,7 +4,7 @@ use PHPMailer\PHPMailer\PHPMailer;
class UOJMail {
public static function noreply() {
- $mailer = new PHPMailer();
+ $mailer = new PHPMailer();
$mailer->isSMTP();
$mailer->Host = UOJConfig::$data['mail']['noreply']['host'];
$mailer->Port = UOJConfig::$data['mail']['noreply']['port'];
@@ -12,9 +12,91 @@ class UOJMail {
$mailer->SMTPSecure = UOJConfig::$data['mail']['noreply']['secure'];
$mailer->Username = UOJConfig::$data['mail']['noreply']['username'];
$mailer->Password = UOJConfig::$data['mail']['noreply']['password'];
- $mailer->setFrom(UOJConfig::$data['mail']['noreply']['username'], "UOJ noreply");
+ $mailer->setFrom(UOJConfig::$data['mail']['noreply']['username'], UOJConfig::$data['profile']['oj-name-short']);
$mailer->CharSet = "utf-8";
$mailer->Encoding = "base64";
return $mailer;
}
+
+ public static function cronSendEmail() {
+ $emails = DB::selectAll([
+ "select * from emails",
+ "where", DB::land([
+ ["created_at", ">=", DB::raw("addtime(now(), '-24:00:00')")],
+ "send_time" => null,
+ ]),
+ "order by priority desc",
+ ]);
+
+ $oj_name = UOJConfig::$data['profile']['oj-name'];
+ $oj_name_short = UOJConfig::$data['profile']['oj-name-short'];
+ $oj_url = HTML::url('/');
+ $oj_logo_url = HTML::url('/images/logo_small.png');
+
+ foreach ($emails as $email) {
+ $user = UOJUser::query($email['receiver']);
+ $name = $user['username'];
+
+ if ($user['realname']) {
+ $name .= ' (' . $user['realname'] . ')';
+ }
+
+ if ($user['email']) {
+ $mailer = UOJMail::noreply();
+ $mailer->addAddress($user['email'], $user['username']);
+ $mailer->Subject = $email['subject'];
+ $mailer->msgHTML(<<
+
+
+
+
{$oj_name_short}
+
+
+
+
+
+
{$email['subject']}
+
{$name} 您好,
+
+
+
+ {$email['content']}
+
+
+
+
+
+
+
+
+
+ 您之所以收到本邮件,是因为您是 {$oj_name} 的用户。
+
+ 本邮件由系统自动发送,请勿回复。
+
+
+ EOD);
+
+ $res = retry_loop(function () use (&$mailer) {
+ $res = $mailer->send();
+
+ if ($res) return true;
+
+ UOJLog::error($mailer->ErrorInfo);
+
+ return false;
+ });
+
+ if ($res) {
+ DB::update("update emails set send_time = now() where id = {$email['id']}");
+ echo '[UOJMail::cronSendEmail] ID: ' . $email['id'] . ' sent.' . "\n";
+ }
+ }
+ }
+
+ echo '[UOJMail::cronSendEmail] Done.' . "\n";
+ }
}
diff --git a/web/app/scheduler.php b/web/app/scheduler.php
new file mode 100644
index 0000000..ea7a43b
--- /dev/null
+++ b/web/app/scheduler.php
@@ -0,0 +1,31 @@
+ '/tmp'
+]);
+
+echo '[UOJScheduler] Init', "\n";
+
+// =========== JOBS ===========
+
+// Email
+$scheduler->call('UOJMail::cronSendEmail', [], 'cronSendEmail')
+ ->at('* * * * *')
+ ->onlyOne()
+ ->before(function () {
+ echo "[cronSendEmail] started at " . time() . "\n";
+ })
+ ->then(function () {
+ echo "[cronSendEmail] ended at " . time() . "\n";
+ });
+
+// ============================
+
+// Let the scheduler execute jobs which are due.
+$scheduler->run();
diff --git a/web/app/vendor/composer/autoload_psr4.php b/web/app/vendor/composer/autoload_psr4.php
index 6f487a5..8047cb1 100644
--- a/web/app/vendor/composer/autoload_psr4.php
+++ b/web/app/vendor/composer/autoload_psr4.php
@@ -6,9 +6,12 @@ $vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
+ 'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'),
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Component\\Finder\\' => array($vendorDir . '/symfony/finder'),
'PHPMailer\\PHPMailer\\' => array($vendorDir . '/phpmailer/phpmailer/src'),
'Gregwar\\' => array($vendorDir . '/gregwar/captcha/src/Gregwar'),
+ 'GO\\' => array($vendorDir . '/peppeocchi/php-cron-scheduler/src/GO'),
'Curl\\' => array($vendorDir . '/php-curl-class/php-curl-class/src/Curl'),
+ 'Cron\\' => array($vendorDir . '/dragonmantank/cron-expression/src/Cron'),
);
diff --git a/web/app/vendor/composer/autoload_static.php b/web/app/vendor/composer/autoload_static.php
index 87966e2..5cafcee 100644
--- a/web/app/vendor/composer/autoload_static.php
+++ b/web/app/vendor/composer/autoload_static.php
@@ -14,6 +14,10 @@ class ComposerStaticInit0d7c2cd5c2dbf2120e4372996869e900
);
public static $prefixLengthsPsr4 = array (
+ 'W' =>
+ array (
+ 'Webmozart\\Assert\\' => 17,
+ ),
'S' =>
array (
'Symfony\\Polyfill\\Php80\\' => 23,
@@ -26,14 +30,20 @@ class ComposerStaticInit0d7c2cd5c2dbf2120e4372996869e900
'G' =>
array (
'Gregwar\\' => 8,
+ 'GO\\' => 3,
),
'C' =>
array (
'Curl\\' => 5,
+ 'Cron\\' => 5,
),
);
public static $prefixDirsPsr4 = array (
+ 'Webmozart\\Assert\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/webmozart/assert/src',
+ ),
'Symfony\\Polyfill\\Php80\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
@@ -50,10 +60,18 @@ class ComposerStaticInit0d7c2cd5c2dbf2120e4372996869e900
array (
0 => __DIR__ . '/..' . '/gregwar/captcha/src/Gregwar',
),
+ 'GO\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/peppeocchi/php-cron-scheduler/src/GO',
+ ),
'Curl\\' =>
array (
0 => __DIR__ . '/..' . '/php-curl-class/php-curl-class/src/Curl',
),
+ 'Cron\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/dragonmantank/cron-expression/src/Cron',
+ ),
);
public static $prefixesPsr0 = array (
diff --git a/web/app/vendor/composer/installed.json b/web/app/vendor/composer/installed.json
index 483f472..fdc2dca 100644
--- a/web/app/vendor/composer/installed.json
+++ b/web/app/vendor/composer/installed.json
@@ -1,5 +1,69 @@
{
"packages": [
+ {
+ "name": "dragonmantank/cron-expression",
+ "version": "v3.3.2",
+ "version_normalized": "3.3.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/dragonmantank/cron-expression.git",
+ "reference": "782ca5968ab8b954773518e9e49a6f892a34b2a8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/782ca5968ab8b954773518e9e49a6f892a34b2a8",
+ "reference": "782ca5968ab8b954773518e9e49a6f892a34b2a8",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2|^8.0",
+ "webmozart/assert": "^1.0"
+ },
+ "replace": {
+ "mtdowling/cron-expression": "^1.0"
+ },
+ "require-dev": {
+ "phpstan/extension-installer": "^1.0",
+ "phpstan/phpstan": "^1.0",
+ "phpstan/phpstan-webmozart-assert": "^1.0",
+ "phpunit/phpunit": "^7.0|^8.0|^9.0"
+ },
+ "time": "2022-09-10T18:51:20+00:00",
+ "type": "library",
+ "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.3.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/dragonmantank",
+ "type": "github"
+ }
+ ],
+ "install-path": "../dragonmantank/cron-expression"
+ },
{
"name": "erusev/parsedown",
"version": "1.7.4",
@@ -213,6 +277,68 @@
},
"install-path": "../ivopetkov/html5-dom-document-php"
},
+ {
+ "name": "peppeocchi/php-cron-scheduler",
+ "version": "v4.0",
+ "version_normalized": "4.0.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/peppeocchi/php-cron-scheduler.git",
+ "reference": "0acfa032e60f0ea22a27b96a6b15a673a31d3448"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/peppeocchi/php-cron-scheduler/zipball/0acfa032e60f0ea22a27b96a6b15a673a31d3448",
+ "reference": "0acfa032e60f0ea22a27b96a6b15a673a31d3448",
+ "shasum": ""
+ },
+ "require": {
+ "dragonmantank/cron-expression": "^3.0",
+ "php": "^7.3 || ^8.0"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.4",
+ "phpunit/phpunit": "~9.5",
+ "swiftmailer/swiftmailer": "~5.4 || ^6.0"
+ },
+ "suggest": {
+ "swiftmailer/swiftmailer": "Required to send the output of a job to email address/es (~5.4 || ^6.0)."
+ },
+ "time": "2021-04-22T21:32:03+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "GO\\": "src/GO/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Giuseppe Occhipinti",
+ "email": "peppeocchi@gmail.com"
+ },
+ {
+ "name": "Carsten Windler",
+ "email": "carsten@carstenwindler.de",
+ "homepage": "http://carstenwindler.de",
+ "role": "Contributor"
+ }
+ ],
+ "description": "PHP Cron Job Scheduler",
+ "keywords": [
+ "cron job",
+ "scheduler"
+ ],
+ "support": {
+ "issues": "https://github.com/peppeocchi/php-cron-scheduler/issues",
+ "source": "https://github.com/peppeocchi/php-cron-scheduler/tree/v4.0"
+ },
+ "install-path": "../peppeocchi/php-cron-scheduler"
+ },
{
"name": "php-curl-class/php-curl-class",
"version": "9.13.1",
@@ -580,6 +706,67 @@
}
],
"install-path": "../symfony/polyfill-php80"
+ },
+ {
+ "name": "webmozart/assert",
+ "version": "1.11.0",
+ "version_normalized": "1.11.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/webmozarts/assert.git",
+ "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991",
+ "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "php": "^7.2 || ^8.0"
+ },
+ "conflict": {
+ "phpstan/phpstan": "<0.12.20",
+ "vimeo/psalm": "<4.6.1 || 4.6.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^8.5.13"
+ },
+ "time": "2022-06-03T18:03:27+00:00",
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.10-dev"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "Webmozart\\Assert\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Assertions to validate method input/output with nice error messages.",
+ "keywords": [
+ "assert",
+ "check",
+ "validate"
+ ],
+ "support": {
+ "issues": "https://github.com/webmozarts/assert/issues",
+ "source": "https://github.com/webmozarts/assert/tree/1.11.0"
+ },
+ "install-path": "../webmozart/assert"
}
],
"dev": true,
diff --git a/web/app/vendor/composer/installed.php b/web/app/vendor/composer/installed.php
index e4ba758..8f3b441 100644
--- a/web/app/vendor/composer/installed.php
+++ b/web/app/vendor/composer/installed.php
@@ -5,7 +5,7 @@
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
- 'reference' => 'aa6bf6a363501f9836d2bb26a513c2ac3985de83',
+ 'reference' => '8314eb67602af0416e50f8514f64e1d9d1a3acd8',
'name' => '__root__',
'dev' => true,
),
@@ -16,7 +16,16 @@
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
- 'reference' => 'aa6bf6a363501f9836d2bb26a513c2ac3985de83',
+ 'reference' => '8314eb67602af0416e50f8514f64e1d9d1a3acd8',
+ 'dev_requirement' => false,
+ ),
+ 'dragonmantank/cron-expression' => array(
+ 'pretty_version' => 'v3.3.2',
+ 'version' => '3.3.2.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../dragonmantank/cron-expression',
+ 'aliases' => array(),
+ 'reference' => '782ca5968ab8b954773518e9e49a6f892a34b2a8',
'dev_requirement' => false,
),
'erusev/parsedown' => array(
@@ -55,6 +64,21 @@
'reference' => '32c5ba748d661a9654c190bf70ce2854eaf5ad22',
'dev_requirement' => false,
),
+ 'mtdowling/cron-expression' => array(
+ 'dev_requirement' => false,
+ 'replaced' => array(
+ 0 => '^1.0',
+ ),
+ ),
+ 'peppeocchi/php-cron-scheduler' => array(
+ 'pretty_version' => 'v4.0',
+ 'version' => '4.0.0.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../peppeocchi/php-cron-scheduler',
+ 'aliases' => array(),
+ 'reference' => '0acfa032e60f0ea22a27b96a6b15a673a31d3448',
+ 'dev_requirement' => false,
+ ),
'php-curl-class/php-curl-class' => array(
'pretty_version' => '9.13.1',
'version' => '9.13.1.0',
@@ -100,5 +124,14 @@
'reference' => 'cfa0ae98841b9e461207c13ab093d76b0fa7bace',
'dev_requirement' => false,
),
+ 'webmozart/assert' => array(
+ 'pretty_version' => '1.11.0',
+ 'version' => '1.11.0.0',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../webmozart/assert',
+ 'aliases' => array(),
+ 'reference' => '11cb2199493b2f8a3b53e7f19068fc6aac760991',
+ 'dev_requirement' => false,
+ ),
),
);
diff --git a/web/app/vendor/composer/platform_check.php b/web/app/vendor/composer/platform_check.php
index a8b98d5..92370c5 100644
--- a/web/app/vendor/composer/platform_check.php
+++ b/web/app/vendor/composer/platform_check.php
@@ -4,8 +4,8 @@
$issues = array();
-if (!(PHP_VERSION_ID >= 70205)) {
- $issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.5". You are running ' . PHP_VERSION . '.';
+if (!(PHP_VERSION_ID >= 70300)) {
+ $issues[] = 'Your Composer dependencies require a PHP version ">= 7.3.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
diff --git a/web/app/vendor/dragonmantank/cron-expression/CHANGELOG.md b/web/app/vendor/dragonmantank/cron-expression/CHANGELOG.md
new file mode 100644
index 0000000..7b6df4b
--- /dev/null
+++ b/web/app/vendor/dragonmantank/cron-expression/CHANGELOG.md
@@ -0,0 +1,228 @@
+# Change Log
+
+## [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))
diff --git a/web/app/vendor/dragonmantank/cron-expression/LICENSE b/web/app/vendor/dragonmantank/cron-expression/LICENSE
new file mode 100644
index 0000000..3e38bbc
--- /dev/null
+++ b/web/app/vendor/dragonmantank/cron-expression/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2011 Michael Dowling , 2016 Chris Tankersley , and contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/web/app/vendor/dragonmantank/cron-expression/README.md b/web/app/vendor/dragonmantank/cron-expression/README.md
new file mode 100644
index 0000000..e853ad4
--- /dev/null
+++ b/web/app/vendor/dragonmantank/cron-expression/README.md
@@ -0,0 +1,87 @@
+PHP Cron Expression Parser
+==========================
+
+[![Latest Stable Version](https://poser.pugx.org/dragonmantank/cron-expression/v/stable.png)](https://packagist.org/packages/dragonmantank/cron-expression) [![Total Downloads](https://poser.pugx.org/dragonmantank/cron-expression/downloads.png)](https://packagist.org/packages/dragonmantank/cron-expression) [![Build Status](https://secure.travis-ci.org/dragonmantank/cron-expression.png)](http://travis-ci.org/dragonmantank/cron-expression) [![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
+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)
+ | | | +---------- month (1 - 12)
+ | | +--------------- day of month (1 - 31)
+ | +-------------------- hour (0 - 23)
+ +------------------------- min (0 - 59)
+
+This library also supports a few macros:
+
+* `@yearly`, `@annually` - Run once a year, midnight, Jan. 1 - `0 0 1 1 *`
+* `@monthly` - Run once a month, midnight, first of month - `0 0 1 * *`
+* `@weekly` - Run once a week, midnight on Sun - `0 0 * * 0`
+* `@daily`, `@midnight` - Run once a day, midnight - `0 0 * * *`
+* `@hourly` - Run once an hour, first minute - `0 * * * *`
+
+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/lavary/crunz)
diff --git a/web/app/vendor/dragonmantank/cron-expression/composer.json b/web/app/vendor/dragonmantank/cron-expression/composer.json
new file mode 100644
index 0000000..657a5b4
--- /dev/null
+++ b/web/app/vendor/dragonmantank/cron-expression/composer.json
@@ -0,0 +1,47 @@
+{
+ "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": "^7.2|^8.0",
+ "webmozart/assert": "^1.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.0",
+ "phpunit/phpunit": "^7.0|^8.0|^9.0",
+ "phpstan/phpstan-webmozart-assert": "^1.0",
+ "phpstan/extension-installer": "^1.0"
+ },
+ "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"
+ },
+ "config": {
+ "allow-plugins": {
+ "ocramius/package-versions": true,
+ "phpstan/extension-installer": true
+ }
+ }
+}
diff --git a/web/app/vendor/dragonmantank/cron-expression/phpstan.neon b/web/app/vendor/dragonmantank/cron-expression/phpstan.neon
new file mode 100644
index 0000000..bea9cb0
--- /dev/null
+++ b/web/app/vendor/dragonmantank/cron-expression/phpstan.neon
@@ -0,0 +1,15 @@
+parameters:
+ checkMissingIterableValueType: false
+
+ ignoreErrors:
+ - '#Call to an undefined method DateTimeInterface::add\(\)#'
+ - '#Call to an undefined method DateTimeInterface::modify\(\)#'
+ - '#Call to an undefined method DateTimeInterface::setDate\(\)#'
+ - '#Call to an undefined method DateTimeInterface::setTime\(\)#'
+ - '#Call to an undefined method DateTimeInterface::setTimezone\(\)#'
+ - '#Call to an undefined method DateTimeInterface::sub\(\)#'
+
+ level: max
+
+ paths:
+ - src/
diff --git a/web/app/vendor/dragonmantank/cron-expression/src/Cron/AbstractField.php b/web/app/vendor/dragonmantank/cron-expression/src/Cron/AbstractField.php
new file mode 100644
index 0000000..df2848d
--- /dev/null
+++ b/web/app/vendor/dragonmantank/cron-expression/src/Cron/AbstractField.php
@@ -0,0 +1,346 @@
+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
+ /** @phpstan-ignore-next-line */
+ 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[$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
+ */
+ 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[$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;
+ }
+}
diff --git a/web/app/vendor/dragonmantank/cron-expression/src/Cron/CronExpression.php b/web/app/vendor/dragonmantank/cron-expression/src/Cron/CronExpression.php
new file mode 100644
index 0000000..d5337cc
--- /dev/null
+++ b/web/app/vendor/dragonmantank/cron-expression/src/Cron/CronExpression.php
@@ -0,0 +1,568 @@
+ '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 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 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
+ */
+ 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
+ */
+ 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
+ */
+ 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);
+ Assert::isArray($split);
+
+ $this->cronParts = $split;
+ if (\count($this->cronParts) < 5) {
+ throw new InvalidArgumentException(
+ $value . ' is not a valid CRON expression'
+ );
+ }
+
+ foreach ($this->cronParts as $position => $part) {
+ $this->setPart($position, $part);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set part of the CRON expression.
+ *
+ * @param int $position The position of the CRON expression to set
+ * @param string $value The value to set
+ *
+ * @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);
+ }
+
+ Assert::isInstanceOf($currentTime, DateTime::class);
+ $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);
+ }
+
+ Assert::isInstanceOf($currentTime, DateTime::class);
+ $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');
+ }
+
+ Assert::isInstanceOf($currentDate, DateTime::class);
+ $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();
+ }
+}
diff --git a/web/app/vendor/dragonmantank/cron-expression/src/Cron/DayOfMonthField.php b/web/app/vendor/dragonmantank/cron-expression/src/Cron/DayOfMonthField.php
new file mode 100644
index 0000000..39ff597
--- /dev/null
+++ b/web/app/vendor/dragonmantank/cron-expression/src/Cron/DayOfMonthField.php
@@ -0,0 +1,164 @@
+
+ */
+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;
+ }
+}
diff --git a/web/app/vendor/dragonmantank/cron-expression/src/Cron/DayOfWeekField.php b/web/app/vendor/dragonmantank/cron-expression/src/Cron/DayOfWeekField.php
new file mode 100644
index 0000000..b9bbf48
--- /dev/null
+++ b/web/app/vendor/dragonmantank/cron-expression/src/Cron/DayOfWeekField.php
@@ -0,0 +1,194 @@
+ '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 = $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;
+ }
+}
diff --git a/web/app/vendor/dragonmantank/cron-expression/src/Cron/FieldFactory.php b/web/app/vendor/dragonmantank/cron-expression/src/Cron/FieldFactory.php
new file mode 100644
index 0000000..839b275
--- /dev/null
+++ b/web/app/vendor/dragonmantank/cron-expression/src/Cron/FieldFactory.php
@@ -0,0 +1,52 @@
+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'
+ );
+ }
+}
diff --git a/web/app/vendor/dragonmantank/cron-expression/src/Cron/FieldFactoryInterface.php b/web/app/vendor/dragonmantank/cron-expression/src/Cron/FieldFactoryInterface.php
new file mode 100644
index 0000000..8bd3c65
--- /dev/null
+++ b/web/app/vendor/dragonmantank/cron-expression/src/Cron/FieldFactoryInterface.php
@@ -0,0 +1,8 @@
+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;
+ }
+
+ 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');
+
+ $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");
+ }
+
+ $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;
+ }
+}
diff --git a/web/app/vendor/dragonmantank/cron-expression/src/Cron/MinutesField.php b/web/app/vendor/dragonmantank/cron-expression/src/Cron/MinutesField.php
new file mode 100644
index 0000000..eda9109
--- /dev/null
+++ b/web/app/vendor/dragonmantank/cron-expression/src/Cron/MinutesField.php
@@ -0,0 +1,96 @@
+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];
+ $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;
+ }
+}
diff --git a/web/app/vendor/dragonmantank/cron-expression/src/Cron/MonthField.php b/web/app/vendor/dragonmantank/cron-expression/src/Cron/MonthField.php
new file mode 100644
index 0000000..5a15fbb
--- /dev/null
+++ b/web/app/vendor/dragonmantank/cron-expression/src/Cron/MonthField.php
@@ -0,0 +1,61 @@
+ '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;
+ }
+}
diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/.coveralls.yml b/web/app/vendor/peppeocchi/php-cron-scheduler/.coveralls.yml
new file mode 100644
index 0000000..4eecff5
--- /dev/null
+++ b/web/app/vendor/peppeocchi/php-cron-scheduler/.coveralls.yml
@@ -0,0 +1,3 @@
+service_name: travis-ci
+coverage_clover: clover.xml
+json_path: coveralls-upload.json
diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/.gitignore b/web/app/vendor/peppeocchi/php-cron-scheduler/.gitignore
new file mode 100644
index 0000000..11e7bbf
--- /dev/null
+++ b/web/app/vendor/peppeocchi/php-cron-scheduler/.gitignore
@@ -0,0 +1,10 @@
+.DS_Store
+
+/vendor
+composer.lock
+
+/examples
+*.old
+
+# PHPUnit coverage file
+clover.xml
diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/.travis.yml b/web/app/vendor/peppeocchi/php-cron-scheduler/.travis.yml
new file mode 100644
index 0000000..0b462f2
--- /dev/null
+++ b/web/app/vendor/peppeocchi/php-cron-scheduler/.travis.yml
@@ -0,0 +1,23 @@
+language: php
+php:
+ - '7.3'
+ - '7.4'
+ - '8.0'
+ - hhvm
+
+matrix:
+ allow_failures:
+ - php: hhvm
+ fast_finish: true
+
+sudo: false
+
+install:
+ - curl -s http://getcomposer.org/installer | php
+ - php composer.phar install --no-interaction
+
+script:
+ - XDEBUG_MODE=coverage php vendor/bin/phpunit -c phpunit.xml --coverage-clover clover.xml
+
+after_success:
+ - travis_retry php vendor/bin/php-coveralls -v
diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/CODE_OF_CONDUCT.md b/web/app/vendor/peppeocchi/php-cron-scheduler/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..b34773d
--- /dev/null
+++ b/web/app/vendor/peppeocchi/php-cron-scheduler/CODE_OF_CONDUCT.md
@@ -0,0 +1,46 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at peppeocchi@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
+
+[homepage]: http://contributor-covenant.org
+[version]: http://contributor-covenant.org/version/1/4/
diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/LICENSE b/web/app/vendor/peppeocchi/php-cron-scheduler/LICENSE
new file mode 100644
index 0000000..e77fea8
--- /dev/null
+++ b/web/app/vendor/peppeocchi/php-cron-scheduler/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 Giuseppe Occhipinti
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/README.md b/web/app/vendor/peppeocchi/php-cron-scheduler/README.md
new file mode 100644
index 0000000..f594990
--- /dev/null
+++ b/web/app/vendor/peppeocchi/php-cron-scheduler/README.md
@@ -0,0 +1,502 @@
+PHP Cron Scheduler
+==
+
+[![Latest Stable Version](https://poser.pugx.org/peppeocchi/php-cron-scheduler/v/stable)](https://packagist.org/packages/peppeocchi/php-cron-scheduler) [![License](https://poser.pugx.org/peppeocchi/php-cron-scheduler/license)](https://packagist.org/packages/peppeocchi/php-cron-scheduler) [![Build Status](https://travis-ci.org/peppeocchi/php-cron-scheduler.svg)](https://travis-ci.org/peppeocchi/php-cron-scheduler) [![Coverage Status](https://coveralls.io/repos/github/peppeocchi/php-cron-scheduler/badge.svg?branch=v3.x)](https://coveralls.io/github/peppeocchi/php-cron-scheduler?branch=v3.x) [![StyleCI](https://styleci.io/repos/38302733/shield)](https://styleci.io/repos/38302733) [![Total Downloads](https://poser.pugx.org/peppeocchi/php-cron-scheduler/downloads)](https://packagist.org/packages/peppeocchi/php-cron-scheduler)
+
+This is a framework agnostic cron jobs scheduler that can be easily integrated with your project or run as a standalone command scheduler.
+The idea was originally inspired by the [Laravel Task Scheduling](http://laravel.com/docs/5.1/scheduling).
+
+## Installing via Composer
+The recommended way is to install the php-cron-scheduler is through [Composer](https://getcomposer.org/).
+Please refer to [Getting Started](https://getcomposer.org/doc/00-intro.md) on how to download and install Composer.
+
+After you have downloaded/installed Composer, run
+
+`php composer.phar require peppeocchi/php-cron-scheduler`
+
+or add the package to your `composer.json`
+```json
+{
+ "require": {
+ "peppeocchi/php-cron-scheduler": "3.*"
+ }
+}
+```
+
+Scheduler V4 requires php >= 7.3, please use the [v3 branch](https://github.com/peppeocchi/php-cron-scheduler/tree/v3.x) for php versions < 7.3 and > 7.1 or the [v2 branch](https://github.com/peppeocchi/php-cron-scheduler/tree/v2.x) for php versions < 7.1.
+
+## How it works
+
+Create a `scheduler.php` file in the root your project with the following content.
+```php
+run();
+```
+
+Then add a new entry to your crontab to run `scheduler.php` every minute.
+
+````
+* * * * * path/to/phpbin path/to/scheduler.php 1>> /dev/null 2>&1
+````
+
+That's it! Your scheduler is up and running, now you can add your jobs without worring anymore about the crontab.
+
+## Scheduling jobs
+
+By default all your jobs will try to run in background.
+PHP scripts and raw commands will run in background by default, while functions will always run in foreground.
+You can force a command to run in foreground by calling the `inForeground()` method.
+**Jobs that have to send the output to email, will run foreground**.
+
+### Schedule a php script
+
+```php
+$scheduler->php('path/to/my/script.php');
+```
+The `php` method accepts 4 arguments:
+- The path to your php script
+- The PHP binary to use
+- Arguments to be passed to the script (**NOTE**: You need to have **register_argc_argv** enable in your php.ini for this to work ([ref](https://github.com/peppeocchi/php-cron-scheduler/issues/88)). Don't worry it's enabled by default, so unlessy you've intentionally disabled it or your host has it disabled by default, you can ignore it.)
+- Identifier
+```php
+$scheduler->php(
+ 'path/to/my/script.php', // The script to execute
+ 'path/to/my/custom/bin/php', // The PHP bin
+ [
+ '-c' => 'ignore',
+ '--merge' => null,
+ ],
+ 'myCustomIdentifier'
+);
+```
+
+### Schedule a raw command
+
+```php
+$scheduler->raw('ps aux | grep httpd');
+```
+The `raw` method accepts 3 arguments:
+- Your command
+- Arguments to be passed to the command
+- Identifier
+```php
+$scheduler->raw(
+ 'mycommand | myOtherCommand',
+ [
+ '-v' => '6',
+ '--silent' => null,
+ ],
+ 'myCustomIdentifier'
+);
+```
+
+### Schedule a function
+
+```php
+$scheduler->call(function () {
+ return true;
+});
+```
+The `call` method accepts 3 arguments:
+- Your function
+- Arguments to be passed to the function
+- Identifier
+```php
+$scheduler->call(
+ function ($args) {
+ return $args['user'];
+ },
+ [
+ ['user' => $user],
+ ],
+ 'myCustomIdentifier'
+);
+```
+
+All of the arguments you pass in the array will be injected to your function.
+For example
+
+```php
+$scheduler->call(
+ function ($firstName, $lastName) {
+ return implode(' ', [$firstName, $lastName]);
+ },
+ [
+ 'John',
+ 'last_name' => 'Doe', // The keys are being ignored
+ ],
+ 'myCustomIdentifier'
+);
+```
+
+If you want to pass a key => value pair, please pass an array within the arguments array
+
+```php
+$scheduler->call(
+ function ($user, $role) {
+ return implode(' ', [$user['first_name'], $user['last_name']]) . " has role: '{$role}'";
+ },
+ [
+ [
+ 'first_name' => 'John',
+ 'last_name' => 'Doe',
+ ],
+ 'Admin'
+ ],
+ 'myCustomIdentifier'
+);
+```
+
+### Schedules execution time
+
+There are a few methods to help you set the execution time of your schedules.
+If you don't call any of this method, the job will run every minute (* * * * *).
+
+- `at` - This method accepts any expression supported by [dragonmantank/cron-expression](https://github.com/dragonmantank/cron-expression)
+ ```php
+ $scheduler->php('script.php')->at('* * * * *');
+ ```
+- `everyMinute` - Run every minute. You can optionally pass a `$minute` to specify the job runs every `$minute` minutes.
+ ```php
+ $scheduler->php('script.php')->everyMinute();
+ $scheduler->php('script.php')->everyMinute(5);
+ ```
+- `hourly` - Run once per hour. You can optionally pass the `$minute` you want to run, by default it will run every hour at minute '00'.
+ ```php
+ $scheduler->php('script.php')->hourly();
+ $scheduler->php('script.php')->hourly(53);
+ ```
+- `daily` - Run once per day. You can optionally pass `$hour` and `$minute` to have more granular control (or a string `hour:minute`)
+ ```php
+ $scheduler->php('script.php')->daily();
+ $scheduler->php('script.php')->daily(22, 03);
+ $scheduler->php('script.php')->daily('22:03');
+ ```
+
+There are additional helpers for weekdays (all accepting optionals hour and minute - defaulted at 00:00)
+- `sunday`
+- `monday`
+- `tuesday`
+- `wednesday`
+- `thursday`
+- `friday`
+- `saturday`
+
+```php
+$scheduler->php('script.php')->saturday();
+$scheduler->php('script.php')->friday(18);
+$scheduler->php('script.php')->sunday(12, 30);
+```
+
+And additional helpers for months (all accepting optionals day, hour and minute - defaulted to the 1st of the month at 00:00)
+- `january`
+- `february`
+- `march`
+- `april`
+- `may`
+- `june`
+- `july`
+- `august`
+- `september`
+- `october`
+- `november`
+- `december`
+
+```php
+$scheduler->php('script.php')->january();
+$scheduler->php('script.php')->december(25);
+$scheduler->php('script.php')->august(15, 20, 30);
+```
+
+You can also specify a `date` for when the job should run.
+The date can be specified as string or as instance of `DateTime`. In both cases you can specify the date only (e.g. 2018-01-01) or the time as well (e.g. 2018-01-01 10:30), if you don't specify the time it will run at 00:00 on that date.
+If you're providing a date in a "non standard" format, it is strongly adviced to pass an instance of `DateTime`. If you're using `createFromFormat` without specifying a time, and you want to default it to 00:00, just make sure to add a `!` to the date format, otherwise the time would be the current time. [Read more](http://php.net/manual/en/datetime.createfromformat.php)
+
+```php
+$scheduler->php('script.php')->date('2018-01-01 12:20');
+$scheduler->php('script.php')->date(new DateTime('2018-01-01'));
+$scheduler->php('script.php')->date(DateTime::createFromFormat('!d/m Y', '01/01 2018'));
+```
+
+### Send output to file/s
+
+You can define one or multiple files where you want the output of your script/command/function execution to be sent to.
+
+```php
+$scheduler->php('script.php')->output([
+ 'my_file1.log', 'my_file2.log'
+]);
+
+// The scheduler catches both stdout and function return and send
+// those values to the output file
+$scheduler->call(function () {
+ echo "Hello";
+
+ return " world!";
+})->output('my_file.log');
+```
+
+### Send output to email/s
+
+You can define one or multiple email addresses where you want the output of your script/command/function execution to be sent to.
+In order for the email to be sent, the output of the job needs to be sent first to a file.
+In fact, the files will be attached to your email address.
+In order for this to work, you need to install [swiftmailer/swiftmailer](https://github.com/swiftmailer/swiftmailer)
+
+```php
+$scheduler->php('script.php')->output([
+ // If you specify multiple files, both will be attached to the email
+ 'my_file1.log', 'my_file2.log'
+])->email([
+ 'someemail@mail.com' => 'My custom name',
+ 'someotheremail@mail.com'
+]);
+```
+
+You can optionally customize the `Swift_Mailer` instance with a custom `Swift_Transport`.
+You can configure:
+- `subject` - The subject of the email sent
+- `from` - The email address set as sender
+- `body` - The body of the email
+- `transport` - The transport to use. For example if you want to use your gmail account or any other SMTP account. The value should be an instance of `Swift_Tranport`
+- `ignore_empty_output` - If this is set to `true`, jobs that return no output won't fire any email.
+
+The configuration can be set "globally" for all the scheduler commands, when creating the scheduler.
+
+```php
+$scheduler = new Scheduler([
+ 'email' => [
+ 'subject' => 'Visitors count',
+ 'from' => 'cron@email.com',
+ 'body' => 'This is the daily visitors count',
+ 'transport' => Swift_SmtpTransport::newInstance('smtp.gmail.com', 465, 'ssl')
+ ->setUsername('username')
+ ->setPassword('password'),
+ 'ignore_empty_output' => false,
+ ]
+]);
+```
+
+Or can be set on a job per job basis.
+
+```php
+$scheduler = new Scheduler();
+
+$scheduler->php('myscript.php')->configure([
+ 'email' => [
+ 'subject' => 'Visitors count',
+ ]
+]);
+
+$scheduler->php('my_other_script.php')->configure([
+ 'email' => [
+ 'subject' => 'Page views count',
+ ]
+]);
+```
+
+### Schedule conditional execution
+
+Sometimes you might want to execute a schedule not only when the execution is due, but also depending on some other condition.
+
+You can delegate the execution of a cronjob to a truthful test with the method `when`.
+
+```php
+$scheduler->php('script.php')->when(function () {
+ // The job will run (if due) only when
+ // this function returns true
+ return true;
+});
+```
+
+### Schedules execution order
+
+The jobs that are due to run are being ordered by their execution: jobs that can run in **background** will be executed **first**.
+
+### Schedules overlapping
+
+To prevent the execution of a schedule while the previous execution is still in progress, use the method `onlyOne`. To avoid overlapping, the Scheduler needs to create **lock files**.
+By default it will be used the directory path used for temporary files.
+
+You can specify a custom directory path globally, when creating a new Scheduler instance.
+
+```php
+$scheduler = new Scheduler([
+ 'tempDir' => 'path/to/my/tmp/dir'
+]);
+
+$scheduler->php('script.php')->onlyOne();
+```
+
+Or you can define the directory path on a job per job basis.
+
+```php
+$scheduler = new Scheduler();
+
+// This will use the default directory path
+$scheduler->php('script.php')->onlyOne();
+
+$scheduler->php('script.php')->onlyOne('path/to/my/tmp/dir');
+$scheduler->php('other_script.php')->onlyOne('path/to/my/other/tmp/dir');
+```
+
+In some cases you might want to run the job also if it's overlapping.
+For example if the last execution was more that 5 minutes ago.
+You can pass a function as a second parameter, the last execution time will be injected.
+The job will not run until this function returns `false`. If it returns `true`, the job will run if overlapping.
+
+```php
+$scheduler->php('script.php')->onlyOne(null, function ($lastExecutionTime) {
+ return (time() - $lastExecutionTime) > (60 * 5);
+});
+```
+
+### Before job execution
+
+In some cases you might want to run some code, if the job is due to run, before it's being executed.
+For example you might want to add a log entry, ping a url or anything else.
+To do so, you can call the `before` like the example below.
+
+```php
+// $logger here is your own implementation
+$scheduler->php('script.php')->before(function () use ($logger) {
+ $logger->info("script.php started at " . time());
+});
+```
+
+### After job execution
+
+Sometime you might wish to do something after a job runs. The `then` methods provides you the flexibility to do anything you want after the job execution. The output of the job will be injected to this function.
+For example you might want to add an entry to you logs, ping a url etc...
+By default, the job will be forced to run in foreground (because the output is injected to the function), if you don't need the output, you can pass `true` as a second parameter to allow the execution in background (in this case `$output` will be empty).
+
+```php
+// $logger and $messenger here are your own implementation
+$scheduler->php('script.php')->then(function ($output) use ($logger, $messenger) {
+ $logger->info($output);
+
+ $messenger->ping('myurl.com', $output);
+});
+
+$scheduler->php('script.php')->then(function ($output) use ($logger) {
+ $logger->info('Job executed!');
+}, true);
+```
+
+#### Using "before" and "then" together
+
+```php
+// $logger here is your own implementation
+$scheduler->php('script.php')
+ ->before(function () use ($logger) {
+ $logger->info("script.php started at " . time());
+ })
+ ->then(function ($output) use ($logger) {
+ $logger->info("script.php completed at " . time(), [
+ 'output' => $output,
+ ]);
+ });
+```
+
+### Multiple scheduler runs
+In some cases you might need to run the scheduler multiple times in the same script.
+Although this is not a common case, the following methods will allow you to re-use the same instance of the scheduler.
+```php
+# some code
+$scheduler->run();
+# ...
+
+// Reset the scheduler after a previous run
+$scheduler->resetRun()
+ ->run(); // now we can run it again
+```
+
+Another handy method if you are re-using the same instance of the scheduler with different jobs (e.g. job coming from an external source - db, file ...) on every run, is to clear the current scheduled jobs.
+```php
+$scheduler->clearJobs();
+
+$jobsFromDb = $db->query(/*...*/);
+foreach ($jobsFromDb as $job) {
+ $scheduler->php($job->script)->at($job->schedule);
+}
+
+$scheduler->resetRun()
+ ->run();
+```
+
+### Faking scheduler run time
+When running the scheduler you might pass an `DateTime` to fake the scheduler run time.
+The resons for this feature are described [here](https://github.com/peppeocchi/php-cron-scheduler/pull/28);
+
+```
+// ...
+$fakeRunTime = new DateTime('2017-09-13 00:00:00');
+$scheduler->run($fakeRunTime);
+```
+
+### Job failures
+If some job fails, you can access list of failed jobs and reasons for failures.
+
+```php
+// get all failed jobs and select first
+$failedJob = $scheduler->getFailedJobs()[0];
+
+// exception that occurred during job
+$exception = $failedJob->getException();
+
+// job that failed
+$job = $failedJob->getJob();
+```
+
+### Worker
+You can simulate a cronjob by starting a worker. Let's see a simple example
+```php
+$scheduler = new Scheduler();
+$scheduler->php('some/script.php');
+$scheduler->work();
+```
+The above code starts a worker that will run your job/s every minute.
+This is meant to be a testing/debugging tool, but you're free to use it however you like.
+You can optionally pass an array of "seconds" of when you want the worker to run your jobs, for example by passing `[0, 30]`, the worker will run your jobs at second **0** and at second **30** of the minute.
+```php
+$scheduler->work([0, 10, 25, 50, 55]);
+```
+
+It is highly advisable that you run your worker separately from your scheduler, although you can run the worker within your scheduler. The problem comes when your scheduler has one or more synchronous job, and the worker will have to wait for your job to complete before continuing the loop. For example
+```php
+$scheduler->call(function () {
+ sleep(120);
+});
+$scheduler->work();
+```
+The above will skip more than one execution, so it won't run anymore every minute but it will run probably every 2 or 3 minutes.
+Instead the preferred approach would be to separate the worker from your scheduler.
+```php
+// File scheduler.php
+$scheduler = new Scheduler();
+$scheduler->call(function () {
+ sleep(120);
+});
+$scheduler->run();
+```
+```php
+// File worker.php
+$scheduler = new Scheduler();
+$scheduler->php('scheduler.php');
+$scheduler->work();
+```
+Then in your command line run `php worker.php`. This will start a foreground process that you can kill by simply exiting the command.
+
+The worker is not meant to collect any data about your runs, and as already said it is meant to be a testing/debugging tool.
+
+## License
+[The MIT License (MIT)](LICENSE)
diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/composer.json b/web/app/vendor/peppeocchi/php-cron-scheduler/composer.json
new file mode 100644
index 0000000..b848b9f
--- /dev/null
+++ b/web/app/vendor/peppeocchi/php-cron-scheduler/composer.json
@@ -0,0 +1,41 @@
+{
+ "name": "peppeocchi/php-cron-scheduler",
+ "description": "PHP Cron Job Scheduler",
+ "license": "MIT",
+ "keywords": ["cron job", "scheduler"],
+ "authors": [
+ {
+ "name": "Giuseppe Occhipinti",
+ "email": "peppeocchi@gmail.com"
+ },
+ {
+ "name": "Carsten Windler",
+ "email": "carsten@carstenwindler.de",
+ "homepage": "http://carstenwindler.de",
+ "role": "Contributor"
+ }
+ ],
+ "minimum-stability": "dev",
+ "require": {
+ "php": "^7.3 || ^8.0",
+ "dragonmantank/cron-expression": "^3.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~9.5",
+ "php-coveralls/php-coveralls": "^2.4",
+ "swiftmailer/swiftmailer": "~5.4 || ^6.0"
+ },
+ "suggest": {
+ "swiftmailer/swiftmailer": "Required to send the output of a job to email address/es (~5.4 || ^6.0)."
+ },
+ "autoload": {
+ "psr-4": {
+ "GO\\": "src/GO/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Tests\\": "tests/GO/"
+ }
+ }
+}
diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/phpunit.xml b/web/app/vendor/peppeocchi/php-cron-scheduler/phpunit.xml
new file mode 100644
index 0000000..c29142a
--- /dev/null
+++ b/web/app/vendor/peppeocchi/php-cron-scheduler/phpunit.xml
@@ -0,0 +1,20 @@
+
+
+
+
+ src/GO
+
+
+
+
+
+
+
+ ./tests/
+
+
+
+
+
+
+
diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/FailedJob.php b/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/FailedJob.php
new file mode 100644
index 0000000..776f825
--- /dev/null
+++ b/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/FailedJob.php
@@ -0,0 +1,32 @@
+job = $job;
+ $this->exception = $exception;
+ }
+
+ public function getJob(): Job
+ {
+ return $this->job;
+ }
+
+ public function getException(): Exception
+ {
+ return $this->exception;
+ }
+}
diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Job.php b/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Job.php
new file mode 100644
index 0000000..b666930
--- /dev/null
+++ b/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Job.php
@@ -0,0 +1,590 @@
+id = $id;
+ } else {
+ if (is_string($command)) {
+ $this->id = md5($command);
+ } elseif (is_array($command)) {
+ $this->id = md5(serialize($command));
+ } else {
+ /* @var object $command */
+ $this->id = spl_object_hash($command);
+ }
+ }
+
+ $this->creationTime = new DateTime('now');
+
+ // initialize the directory path for lock files
+ $this->tempDir = sys_get_temp_dir();
+
+ $this->command = $command;
+ $this->args = $args;
+ }
+
+ /**
+ * Get the Job id.
+ *
+ * @return string
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * Check if the Job is due to run.
+ * It accepts as input a DateTime used to check if
+ * the job is due. Defaults to job creation time.
+ * It also defaults the execution time if not previously defined.
+ *
+ * @param DateTime $date
+ * @return bool
+ */
+ public function isDue(DateTime $date = null)
+ {
+ // The execution time is being defaulted if not defined
+ if (! $this->executionTime) {
+ $this->at('* * * * *');
+ }
+
+ $date = $date !== null ? $date : $this->creationTime;
+
+ if ($this->executionYear && $this->executionYear !== $date->format('Y')) {
+ return false;
+ }
+
+ return $this->executionTime->isDue($date);
+ }
+
+ /**
+ * Check if the Job is overlapping.
+ *
+ * @return bool
+ */
+ public function isOverlapping()
+ {
+ return $this->lockFile &&
+ file_exists($this->lockFile) &&
+ call_user_func($this->whenOverlapping, filemtime($this->lockFile)) === false;
+ }
+
+ /**
+ * Force the Job to run in foreground.
+ *
+ * @return self
+ */
+ public function inForeground()
+ {
+ $this->runInBackground = false;
+
+ return $this;
+ }
+
+ /**
+ * Check if the Job can run in background.
+ *
+ * @return bool
+ */
+ public function canRunInBackground()
+ {
+ if (is_callable($this->command) || $this->runInBackground === false) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * This will prevent the Job from overlapping.
+ * It prevents another instance of the same Job of
+ * being executed if the previous is still running.
+ * The job id is used as a filename for the lock file.
+ *
+ * @param string $tempDir The directory path for the lock files
+ * @param callable $whenOverlapping A callback to ignore job overlapping
+ * @return self
+ */
+ public function onlyOne($tempDir = null, callable $whenOverlapping = null)
+ {
+ if ($tempDir === null || ! is_dir($tempDir)) {
+ $tempDir = $this->tempDir;
+ }
+
+ $this->lockFile = implode('/', [
+ trim($tempDir),
+ trim($this->id) . '.lock',
+ ]);
+
+ if ($whenOverlapping) {
+ $this->whenOverlapping = $whenOverlapping;
+ } else {
+ $this->whenOverlapping = function () {
+ return false;
+ };
+ }
+
+ return $this;
+ }
+
+ /**
+ * Compile the Job command.
+ *
+ * @return mixed
+ */
+ public function compile()
+ {
+ $compiled = $this->command;
+
+ // If callable, return the function itself
+ if (is_callable($compiled)) {
+ return $compiled;
+ }
+
+ // Augment with any supplied arguments
+ foreach ($this->args as $key => $value) {
+ $compiled .= ' ' . escapeshellarg($key);
+ if ($value !== null) {
+ $compiled .= ' ' . escapeshellarg($value);
+ }
+ }
+
+ // Add the boilerplate to redirect the output to file/s
+ if (count($this->outputTo) > 0) {
+ $compiled .= ' | tee ';
+ $compiled .= $this->outputMode === 'a' ? '-a ' : '';
+ foreach ($this->outputTo as $file) {
+ $compiled .= $file . ' ';
+ }
+
+ $compiled = trim($compiled);
+ }
+
+ // Add boilerplate to remove lockfile after execution
+ if ($this->lockFile) {
+ $compiled .= '; rm ' . $this->lockFile;
+ }
+
+ // Add boilerplate to run in background
+ if ($this->canRunInBackground()) {
+ // Parentheses are need execute the chain of commands in a subshell
+ // that can then run in background
+ $compiled = '(' . $compiled . ') > /dev/null 2>&1 &';
+ }
+
+ return trim($compiled);
+ }
+
+ /**
+ * Configure the job.
+ *
+ * @param array $config
+ * @return self
+ */
+ public function configure(array $config = [])
+ {
+ if (isset($config['email'])) {
+ if (! is_array($config['email'])) {
+ throw new InvalidArgumentException('Email configuration should be an array.');
+ }
+ $this->emailConfig = $config['email'];
+ }
+
+ // Check if config has defined a tempDir
+ if (isset($config['tempDir']) && is_dir($config['tempDir'])) {
+ $this->tempDir = $config['tempDir'];
+ }
+
+ return $this;
+ }
+
+ /**
+ * Truth test to define if the job should run if due.
+ *
+ * @param callable $fn
+ * @return self
+ */
+ public function when(callable $fn)
+ {
+ $this->truthTest = $fn();
+
+ return $this;
+ }
+
+ /**
+ * Run the job.
+ *
+ * @return bool
+ */
+ public function run()
+ {
+ // If the truthTest failed, don't run
+ if ($this->truthTest !== true) {
+ return false;
+ }
+
+ // If overlapping, don't run
+ if ($this->isOverlapping()) {
+ return false;
+ }
+
+ $compiled = $this->compile();
+
+ // Write lock file if necessary
+ $this->createLockFile();
+
+ if (is_callable($this->before)) {
+ call_user_func($this->before);
+ }
+
+ if (is_callable($compiled)) {
+ $this->output = $this->exec($compiled);
+ } else {
+ exec($compiled, $this->output, $this->returnCode);
+ }
+
+ $this->finalise();
+
+ return true;
+ }
+
+ /**
+ * Create the job lock file.
+ *
+ * @param mixed $content
+ * @return void
+ */
+ private function createLockFile($content = null)
+ {
+ if ($this->lockFile) {
+ if ($content === null || ! is_string($content)) {
+ $content = $this->getId();
+ }
+
+ file_put_contents($this->lockFile, $content);
+ }
+ }
+
+ /**
+ * Remove the job lock file.
+ *
+ * @return void
+ */
+ private function removeLockFile()
+ {
+ if ($this->lockFile && file_exists($this->lockFile)) {
+ unlink($this->lockFile);
+ }
+ }
+
+ /**
+ * Execute a callable job.
+ *
+ * @param callable $fn
+ * @throws Exception
+ * @return string
+ */
+ private function exec(callable $fn)
+ {
+ ob_start();
+
+ try {
+ $returnData = call_user_func_array($fn, $this->args);
+ } catch (Exception $e) {
+ ob_end_clean();
+ throw $e;
+ }
+
+ $outputBuffer = ob_get_clean();
+
+ foreach ($this->outputTo as $filename) {
+ if ($outputBuffer) {
+ file_put_contents($filename, $outputBuffer, $this->outputMode === 'a' ? FILE_APPEND : 0);
+ }
+
+ if ($returnData) {
+ file_put_contents($filename, $returnData, FILE_APPEND);
+ }
+ }
+
+ $this->removeLockFile();
+
+ return $outputBuffer . (is_string($returnData) ? $returnData : '');
+ }
+
+ /**
+ * Set the file/s where to write the output of the job.
+ *
+ * @param string|array $filename
+ * @param bool $append
+ * @return self
+ */
+ public function output($filename, $append = false)
+ {
+ $this->outputTo = is_array($filename) ? $filename : [$filename];
+ $this->outputMode = $append === false ? 'w' : 'a';
+
+ return $this;
+ }
+
+ /**
+ * Get the job output.
+ *
+ * @return mixed
+ */
+ public function getOutput()
+ {
+ return $this->output;
+ }
+
+ /**
+ * Set the emails where the output should be sent to.
+ * The Job should be set to write output to a file
+ * for this to work.
+ *
+ * @param string|array $email
+ * @return self
+ */
+ public function email($email)
+ {
+ if (! is_string($email) && ! is_array($email)) {
+ throw new InvalidArgumentException('The email can be only string or array');
+ }
+
+ $this->emailTo = is_array($email) ? $email : [$email];
+
+ // Force the job to run in foreground
+ $this->inForeground();
+
+ return $this;
+ }
+
+ /**
+ * Finilise the job after execution.
+ *
+ * @return void
+ */
+ private function finalise()
+ {
+ // Send output to email
+ $this->emailOutput();
+
+ // Call any callback defined
+ if (is_callable($this->after)) {
+ call_user_func($this->after, $this->output, $this->returnCode);
+ }
+ }
+
+ /**
+ * Email the output of the job, if any.
+ *
+ * @return bool
+ */
+ private function emailOutput()
+ {
+ if (! count($this->outputTo) || ! count($this->emailTo)) {
+ return false;
+ }
+
+ if (isset($this->emailConfig['ignore_empty_output']) &&
+ $this->emailConfig['ignore_empty_output'] === true &&
+ empty($this->output)
+ ) {
+ return false;
+ }
+
+ $this->sendToEmails($this->outputTo);
+
+ return true;
+ }
+
+ /**
+ * Set function to be called before job execution
+ * Job object is injected as a parameter to callable function.
+ *
+ * @param callable $fn
+ * @return self
+ */
+ public function before(callable $fn)
+ {
+ $this->before = $fn;
+
+ return $this;
+ }
+
+ /**
+ * Set a function to be called after job execution.
+ * By default this will force the job to run in foreground
+ * because the output is injected as a parameter of this
+ * function, but it could be avoided by passing true as a
+ * second parameter. The job will run in background if it
+ * meets all the other criteria.
+ *
+ * @param callable $fn
+ * @param bool $runInBackground
+ * @return self
+ */
+ public function then(callable $fn, $runInBackground = false)
+ {
+ $this->after = $fn;
+
+ // Force the job to run in foreground
+ if ($runInBackground === false) {
+ $this->inForeground();
+ }
+
+ return $this;
+ }
+}
diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Scheduler.php b/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Scheduler.php
new file mode 100644
index 0000000..0c5fba6
--- /dev/null
+++ b/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Scheduler.php
@@ -0,0 +1,327 @@
+config = $config;
+ }
+
+ /**
+ * Queue a job for execution in the correct queue.
+ *
+ * @param Job $job
+ * @return void
+ */
+ private function queueJob(Job $job)
+ {
+ $this->jobs[] = $job;
+ }
+
+ /**
+ * Prioritise jobs in background.
+ *
+ * @return array
+ */
+ private function prioritiseJobs()
+ {
+ $background = [];
+ $foreground = [];
+
+ foreach ($this->jobs as $job) {
+ if ($job->canRunInBackground()) {
+ $background[] = $job;
+ } else {
+ $foreground[] = $job;
+ }
+ }
+
+ return array_merge($background, $foreground);
+ }
+
+ /**
+ * Get the queued jobs.
+ *
+ * @return array
+ */
+ public function getQueuedJobs()
+ {
+ return $this->prioritiseJobs();
+ }
+
+ /**
+ * Queues a function execution.
+ *
+ * @param callable $fn The function to execute
+ * @param array $args Optional arguments to pass to the php script
+ * @param string $id Optional custom identifier
+ * @return Job
+ */
+ public function call(callable $fn, $args = [], $id = null)
+ {
+ $job = new Job($fn, $args, $id);
+
+ $this->queueJob($job->configure($this->config));
+
+ return $job;
+ }
+
+ /**
+ * Queues a php script execution.
+ *
+ * @param string $script The path to the php script to execute
+ * @param string $bin Optional path to the php binary
+ * @param array $args Optional arguments to pass to the php script
+ * @param string $id Optional custom identifier
+ * @return Job
+ */
+ public function php($script, $bin = null, $args = [], $id = null)
+ {
+ if (! is_string($script)) {
+ throw new InvalidArgumentException('The script should be a valid path to a file.');
+ }
+
+ $bin = $bin !== null && is_string($bin) && file_exists($bin) ?
+ $bin : (PHP_BINARY === '' ? '/usr/bin/php' : PHP_BINARY);
+
+ $job = new Job($bin . ' ' . $script, $args, $id);
+
+ if (! file_exists($script)) {
+ $this->pushFailedJob(
+ $job,
+ new InvalidArgumentException('The script should be a valid path to a file.')
+ );
+ }
+
+ $this->queueJob($job->configure($this->config));
+
+ return $job;
+ }
+
+ /**
+ * Queue a raw shell command.
+ *
+ * @param string $command The command to execute
+ * @param array $args Optional arguments to pass to the command
+ * @param string $id Optional custom identifier
+ * @return Job
+ */
+ public function raw($command, $args = [], $id = null)
+ {
+ $job = new Job($command, $args, $id);
+
+ $this->queueJob($job->configure($this->config));
+
+ return $job;
+ }
+
+ /**
+ * Run the scheduler.
+ *
+ * @param DateTime $runTime Optional, run at specific moment
+ * @return array Executed jobs
+ */
+ public function run(Datetime $runTime = null)
+ {
+ $jobs = $this->getQueuedJobs();
+
+ if (is_null($runTime)) {
+ $runTime = new DateTime('now');
+ }
+
+ foreach ($jobs as $job) {
+ if ($job->isDue($runTime)) {
+ try {
+ $job->run();
+ $this->pushExecutedJob($job);
+ } catch (\Exception $e) {
+ $this->pushFailedJob($job, $e);
+ }
+ }
+ }
+
+ return $this->getExecutedJobs();
+ }
+
+ /**
+ * Reset all collected data of last run.
+ *
+ * Call before run() if you call run() multiple times.
+ */
+ public function resetRun()
+ {
+ // Reset collected data of last run
+ $this->executedJobs = [];
+ $this->failedJobs = [];
+ $this->outputSchedule = [];
+
+ return $this;
+ }
+
+ /**
+ * Add an entry to the scheduler verbose output array.
+ *
+ * @param string $string
+ * @return void
+ */
+ private function addSchedulerVerboseOutput($string)
+ {
+ $now = '[' . (new DateTime('now'))->format('c') . '] ';
+ $this->outputSchedule[] = $now . $string;
+
+ // Print to stdoutput in light gray
+ // echo "\033[37m{$string}\033[0m\n";
+ }
+
+ /**
+ * Push a succesfully executed job.
+ *
+ * @param Job $job
+ * @return Job
+ */
+ private function pushExecutedJob(Job $job)
+ {
+ $this->executedJobs[] = $job;
+
+ $compiled = $job->compile();
+
+ // If callable, log the string Closure
+ if (is_callable($compiled)) {
+ $compiled = 'Closure';
+ }
+
+ $this->addSchedulerVerboseOutput("Executing {$compiled}");
+
+ return $job;
+ }
+
+ /**
+ * Get the executed jobs.
+ *
+ * @return array
+ */
+ public function getExecutedJobs()
+ {
+ return $this->executedJobs;
+ }
+
+ /**
+ * Push a failed job.
+ *
+ * @param Job $job
+ * @param Exception $e
+ * @return Job
+ */
+ private function pushFailedJob(Job $job, Exception $e)
+ {
+ $this->failedJobs[] = new FailedJob($job, $e);
+
+ $compiled = $job->compile();
+
+ // If callable, log the string Closure
+ if (is_callable($compiled)) {
+ $compiled = 'Closure';
+ }
+
+ $this->addSchedulerVerboseOutput("{$e->getMessage()}: {$compiled}");
+
+ return $job;
+ }
+
+ /**
+ * Get the failed jobs.
+ *
+ * @return FailedJob[]
+ */
+ public function getFailedJobs()
+ {
+ return $this->failedJobs;
+ }
+
+ /**
+ * Get the scheduler verbose output.
+ *
+ * @param string $type Allowed: text, html, array
+ * @return mixed The return depends on the requested $type
+ */
+ public function getVerboseOutput($type = 'text')
+ {
+ switch ($type) {
+ case 'text':
+ return implode("\n", $this->outputSchedule);
+ case 'html':
+ return implode('
', $this->outputSchedule);
+ case 'array':
+ return $this->outputSchedule;
+ default:
+ throw new InvalidArgumentException('Invalid output type');
+ }
+ }
+
+ /**
+ * Remove all queued Jobs.
+ */
+ public function clearJobs()
+ {
+ $this->jobs = [];
+
+ return $this;
+ }
+
+ /**
+ * Start a worker.
+ *
+ * @param array $seconds - When the scheduler should run
+ */
+ public function work(array $seconds = [0])
+ {
+ while (true) {
+ if (in_array((int) date('s'), $seconds)) {
+ $this->run();
+ sleep(1);
+ }
+ }
+ }
+}
diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Traits/Interval.php b/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Traits/Interval.php
new file mode 100644
index 0000000..c80a4d9
--- /dev/null
+++ b/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Traits/Interval.php
@@ -0,0 +1,418 @@
+executionTime = CronExpression::factory($expression);
+
+ return $this;
+ }
+
+ /**
+ * Run the Job at a specific date.
+ *
+ * @param string/DateTime $date
+ * @return self
+ */
+ public function date($date)
+ {
+ if (! $date instanceof DateTime) {
+ $date = new DateTime($date);
+ }
+
+ $this->executionYear = $date->format('Y');
+
+ return $this->at("{$date->format('i')} {$date->format('H')} {$date->format('d')} {$date->format('m')} *");
+ }
+
+ /**
+ * Set the execution time to every minute.
+ *
+ * @param int|string|null When set, specifies that the job will be run every $minute minutes
+ *
+ * @return self
+ */
+ public function everyMinute($minute = null)
+ {
+ $minuteExpression = '*';
+ if ($minute !== null) {
+ $c = $this->validateCronSequence($minute);
+ $minuteExpression = '*/' . $c['minute'];
+ }
+
+ return $this->at($minuteExpression . ' * * * *');
+ }
+
+ /**
+ * Set the execution time to every hour.
+ *
+ * @param int|string $minute
+ * @return self
+ */
+ public function hourly($minute = 0)
+ {
+ $c = $this->validateCronSequence($minute);
+
+ return $this->at("{$c['minute']} * * * *");
+ }
+
+ /**
+ * Set the execution time to once a day.
+ *
+ * @param int|string $hour
+ * @param int|string $minute
+ * @return self
+ */
+ public function daily($hour = 0, $minute = 0)
+ {
+ if (is_string($hour)) {
+ $parts = explode(':', $hour);
+ $hour = $parts[0];
+ $minute = isset($parts[1]) ? $parts[1] : '0';
+ }
+
+ $c = $this->validateCronSequence($minute, $hour);
+
+ return $this->at("{$c['minute']} {$c['hour']} * * *");
+ }
+
+ /**
+ * Set the execution time to once a week.
+ *
+ * @param int|string $weekday
+ * @param int|string $hour
+ * @param int|string $minute
+ * @return self
+ */
+ public function weekly($weekday = 0, $hour = 0, $minute = 0)
+ {
+ if (is_string($hour)) {
+ $parts = explode(':', $hour);
+ $hour = $parts[0];
+ $minute = isset($parts[1]) ? $parts[1] : '0';
+ }
+
+ $c = $this->validateCronSequence($minute, $hour, null, null, $weekday);
+
+ return $this->at("{$c['minute']} {$c['hour']} * * {$c['weekday']}");
+ }
+
+ /**
+ * Set the execution time to once a month.
+ *
+ * @param int|string $month
+ * @param int|string $day
+ * @param int|string $hour
+ * @param int|string $minute
+ * @return self
+ */
+ public function monthly($month = '*', $day = 1, $hour = 0, $minute = 0)
+ {
+ if (is_string($hour)) {
+ $parts = explode(':', $hour);
+ $hour = $parts[0];
+ $minute = isset($parts[1]) ? $parts[1] : '0';
+ }
+
+ $c = $this->validateCronSequence($minute, $hour, $day, $month);
+
+ return $this->at("{$c['minute']} {$c['hour']} {$c['day']} {$c['month']} *");
+ }
+
+ /**
+ * Set the execution time to every Sunday.
+ *
+ * @param int|string $hour
+ * @param int|string $minute
+ * @return self
+ */
+ public function sunday($hour = 0, $minute = 0)
+ {
+ return $this->weekly(0, $hour, $minute);
+ }
+
+ /**
+ * Set the execution time to every Monday.
+ *
+ * @param int|string $hour
+ * @param int|string $minute
+ * @return self
+ */
+ public function monday($hour = 0, $minute = 0)
+ {
+ return $this->weekly(1, $hour, $minute);
+ }
+
+ /**
+ * Set the execution time to every Tuesday.
+ *
+ * @param int|string $hour
+ * @param int|string $minute
+ * @return self
+ */
+ public function tuesday($hour = 0, $minute = 0)
+ {
+ return $this->weekly(2, $hour, $minute);
+ }
+
+ /**
+ * Set the execution time to every Wednesday.
+ *
+ * @param int|string $hour
+ * @param int|string $minute
+ * @return self
+ */
+ public function wednesday($hour = 0, $minute = 0)
+ {
+ return $this->weekly(3, $hour, $minute);
+ }
+
+ /**
+ * Set the execution time to every Thursday.
+ *
+ * @param int|string $hour
+ * @param int|string $minute
+ * @return self
+ */
+ public function thursday($hour = 0, $minute = 0)
+ {
+ return $this->weekly(4, $hour, $minute);
+ }
+
+ /**
+ * Set the execution time to every Friday.
+ *
+ * @param int|string $hour
+ * @param int|string $minute
+ * @return self
+ */
+ public function friday($hour = 0, $minute = 0)
+ {
+ return $this->weekly(5, $hour, $minute);
+ }
+
+ /**
+ * Set the execution time to every Saturday.
+ *
+ * @param int|string $hour
+ * @param int|string $minute
+ * @return self
+ */
+ public function saturday($hour = 0, $minute = 0)
+ {
+ return $this->weekly(6, $hour, $minute);
+ }
+
+ /**
+ * Set the execution time to every January.
+ *
+ * @param int|string $day
+ * @param int|string $hour
+ * @param int|string $minute
+ * @return self
+ */
+ public function january($day = 1, $hour = 0, $minute = 0)
+ {
+ return $this->monthly(1, $day, $hour, $minute);
+ }
+
+ /**
+ * Set the execution time to every February.
+ *
+ * @param int|string $day
+ * @param int|string $hour
+ * @param int|string $minute
+ * @return self
+ */
+ public function february($day = 1, $hour = 0, $minute = 0)
+ {
+ return $this->monthly(2, $day, $hour, $minute);
+ }
+
+ /**
+ * Set the execution time to every March.
+ *
+ * @param int|string $day
+ * @param int|string $hour
+ * @param int|string $minute
+ * @return self
+ */
+ public function march($day = 1, $hour = 0, $minute = 0)
+ {
+ return $this->monthly(3, $day, $hour, $minute);
+ }
+
+ /**
+ * Set the execution time to every April.
+ *
+ * @param int|string $day
+ * @param int|string $hour
+ * @param int|string $minute
+ * @return self
+ */
+ public function april($day = 1, $hour = 0, $minute = 0)
+ {
+ return $this->monthly(4, $day, $hour, $minute);
+ }
+
+ /**
+ * Set the execution time to every May.
+ *
+ * @param int|string $day
+ * @param int|string $hour
+ * @param int|string $minute
+ * @return self
+ */
+ public function may($day = 1, $hour = 0, $minute = 0)
+ {
+ return $this->monthly(5, $day, $hour, $minute);
+ }
+
+ /**
+ * Set the execution time to every June.
+ *
+ * @param int|string $day
+ * @param int|string $hour
+ * @param int|string $minute
+ * @return self
+ */
+ public function june($day = 1, $hour = 0, $minute = 0)
+ {
+ return $this->monthly(6, $day, $hour, $minute);
+ }
+
+ /**
+ * Set the execution time to every July.
+ *
+ * @param int|string $day
+ * @param int|string $hour
+ * @param int|string $minute
+ * @return self
+ */
+ public function july($day = 1, $hour = 0, $minute = 0)
+ {
+ return $this->monthly(7, $day, $hour, $minute);
+ }
+
+ /**
+ * Set the execution time to every August.
+ *
+ * @param int|string $day
+ * @param int|string $hour
+ * @param int|string $minute
+ * @return self
+ */
+ public function august($day = 1, $hour = 0, $minute = 0)
+ {
+ return $this->monthly(8, $day, $hour, $minute);
+ }
+
+ /**
+ * Set the execution time to every September.
+ *
+ * @param int|string $day
+ * @param int|string $hour
+ * @param int|string $minute
+ * @return self
+ */
+ public function september($day = 1, $hour = 0, $minute = 0)
+ {
+ return $this->monthly(9, $day, $hour, $minute);
+ }
+
+ /**
+ * Set the execution time to every October.
+ *
+ * @param int|string $day
+ * @param int|string $hour
+ * @param int|string $minute
+ * @return self
+ */
+ public function october($day = 1, $hour = 0, $minute = 0)
+ {
+ return $this->monthly(10, $day, $hour, $minute);
+ }
+
+ /**
+ * Set the execution time to every November.
+ *
+ * @param int|string $day
+ * @param int|string $hour
+ * @param int|string $minute
+ * @return self
+ */
+ public function november($day = 1, $hour = 0, $minute = 0)
+ {
+ return $this->monthly(11, $day, $hour, $minute);
+ }
+
+ /**
+ * Set the execution time to every December.
+ *
+ * @param int|string $day
+ * @param int|string $hour
+ * @param int|string $minute
+ * @return self
+ */
+ public function december($day = 1, $hour = 0, $minute = 0)
+ {
+ return $this->monthly(12, $day, $hour, $minute);
+ }
+
+ /**
+ * Validate sequence of cron expression.
+ *
+ * @param int|string $minute
+ * @param int|string $hour
+ * @param int|string $day
+ * @param int|string $month
+ * @param int|string $weekday
+ * @return array
+ */
+ private function validateCronSequence($minute = null, $hour = null, $day = null, $month = null, $weekday = null)
+ {
+ return [
+ 'minute' => $this->validateCronRange($minute, 0, 59),
+ 'hour' => $this->validateCronRange($hour, 0, 23),
+ 'day' => $this->validateCronRange($day, 1, 31),
+ 'month' => $this->validateCronRange($month, 1, 12),
+ 'weekday' => $this->validateCronRange($weekday, 0, 6),
+ ];
+ }
+
+ /**
+ * Validate sequence of cron expression.
+ *
+ * @param int|string $value
+ * @param int $min
+ * @param int $max
+ * @return mixed
+ */
+ private function validateCronRange($value, $min, $max)
+ {
+ if ($value === null || $value === '*') {
+ return '*';
+ }
+
+ if (! is_numeric($value) ||
+ ! ($value >= $min && $value <= $max)
+ ) {
+ throw new InvalidArgumentException(
+ "Invalid value: it should be '*' or between {$min} and {$max}."
+ );
+ }
+
+ return (int) $value;
+ }
+}
diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Traits/Mailer.php b/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Traits/Mailer.php
new file mode 100644
index 0000000..27b009e
--- /dev/null
+++ b/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Traits/Mailer.php
@@ -0,0 +1,62 @@
+emailConfig['subject']) ||
+ ! is_string($this->emailConfig['subject'])
+ ) {
+ $this->emailConfig['subject'] = 'Cronjob execution';
+ }
+
+ if (! isset($this->emailConfig['from'])) {
+ $this->emailConfig['from'] = ['cronjob@server.my' => 'My Email Server'];
+ }
+
+ if (! isset($this->emailConfig['body']) ||
+ ! is_string($this->emailConfig['body'])
+ ) {
+ $this->emailConfig['body'] = 'Cronjob output attached';
+ }
+
+ if (! isset($this->emailConfig['transport']) ||
+ ! ($this->emailConfig['transport'] instanceof \Swift_Transport)
+ ) {
+ $this->emailConfig['transport'] = new \Swift_SendmailTransport();
+ }
+
+ return $this->emailConfig;
+ }
+
+ /**
+ * Send files to emails.
+ *
+ * @param array $files
+ * @return void
+ */
+ private function sendToEmails(array $files)
+ {
+ $config = $this->getEmailConfig();
+
+ $mailer = new \Swift_Mailer($config['transport']);
+
+ $message = (new \Swift_Message())
+ ->setSubject($config['subject'])
+ ->setFrom($config['from'])
+ ->setTo($this->emailTo)
+ ->setBody($config['body'])
+ ->addPart('Cronjob output attached
', 'text/html');
+
+ foreach ($files as $filename) {
+ $message->attach(\Swift_Attachment::fromPath($filename));
+ }
+
+ $mailer->send($message);
+ }
+}
diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/IntervalTest.php b/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/IntervalTest.php
new file mode 100644
index 0000000..9519a73
--- /dev/null
+++ b/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/IntervalTest.php
@@ -0,0 +1,274 @@
+assertTrue($job->everyMinute()->isDue(\DateTime::createFromFormat('H:i', '00:00')));
+ }
+
+ public function testShouldRunHourly()
+ {
+ $job = new Job('ls');
+
+ // Default run is at minute 00 every hour
+ $this->assertTrue($job->hourly()->isDue(\DateTime::createFromFormat('H:i', '10:00')));
+ $this->assertFalse($job->hourly()->isDue(\DateTime::createFromFormat('H:i', '10:01')));
+ $this->assertTrue($job->hourly()->isDue(\DateTime::createFromFormat('H:i', '11:00')));
+ }
+
+ public function testShouldRunHourlyWithCustomInput()
+ {
+ $job = new Job('ls');
+
+ $this->assertTrue($job->hourly(19)->isDue(\DateTime::createFromFormat('H:i', '10:19')));
+ $this->assertTrue($job->hourly('07')->isDue(\DateTime::createFromFormat('H:i', '10:07')));
+ $this->assertFalse($job->hourly(19)->isDue(\DateTime::createFromFormat('H:i', '10:01')));
+ $this->assertTrue($job->hourly(19)->isDue(\DateTime::createFromFormat('H:i', '11:19')));
+ }
+
+ public function testShouldThrowExceptionWithInvalidHourlyMinuteInput()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+
+ $job = new Job('ls');
+ $job->hourly('abc');
+ }
+
+ public function testShouldRunDaily()
+ {
+ $job = new Job('ls');
+
+ // Default run is at 00:00 every day
+ $this->assertTrue($job->daily()->isDue(\DateTime::createFromFormat('H:i', '00:00')));
+ }
+
+ public function testShouldRunDailyWithCustomInput()
+ {
+ $job = new Job('ls');
+
+ $this->assertTrue($job->daily(19)->isDue(\DateTime::createFromFormat('H:i', '19:00')));
+ $this->assertTrue($job->daily(19, 53)->isDue(\DateTime::createFromFormat('H:i', '19:53')));
+ $this->assertFalse($job->daily(19)->isDue(\DateTime::createFromFormat('H:i', '18:00')));
+ $this->assertFalse($job->daily(19, 53)->isDue(\DateTime::createFromFormat('H:i', '19:52')));
+
+ // A string is also acceptable
+ $this->assertTrue($job->daily('19')->isDue(\DateTime::createFromFormat('H:i', '19:00')));
+ $this->assertTrue($job->daily('19:53')->isDue(\DateTime::createFromFormat('H:i', '19:53')));
+ }
+
+ public function testShouldThrowExceptionWithInvalidDailyHourInput()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+
+ $job = new Job('ls');
+ $job->daily('abc');
+ }
+
+ public function testShouldThrowExceptionWithInvalidDailyMinuteInput()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+
+ $job = new Job('ls');
+ $job->daily(2, 'abc');
+ }
+
+ public function testShouldRunWeekly()
+ {
+ $job = new Job('ls');
+
+ // Default run is every Sunday at 00:00
+ $this->assertTrue($job->weekly()->isDue(
+ new \DateTime('Sunday'))
+ );
+
+ $this->assertFalse($job->weekly()->isDue(
+ new \DateTime('Tuesday'))
+ );
+ }
+
+ public function testShouldRunWeeklyOnCustomDay()
+ {
+ $job = new Job('ls');
+
+ $this->assertTrue($job->weekly(6)->isDue(
+ new \DateTime('Saturday'))
+ );
+
+ // Testing also the helpers to run weekly on custom day
+ $this->assertTrue($job->monday()->isDue(
+ new \DateTime('Monday'))
+ );
+ $this->assertFalse($job->monday()->isDue(
+ new \DateTime('Saturday'))
+ );
+
+ $this->assertTrue($job->tuesday()->isDue(
+ new \DateTime('Tuesday'))
+ );
+ $this->assertTrue($job->wednesday()->isDue(
+ new \DateTime('Wednesday'))
+ );
+ $this->assertTrue($job->thursday()->isDue(
+ new \DateTime('Thursday'))
+ );
+ $this->assertTrue($job->friday()->isDue(
+ new \DateTime('Friday'))
+ );
+ $this->assertTrue($job->saturday()->isDue(
+ new \DateTime('Saturday'))
+ );
+ $this->assertTrue($job->sunday()->isDue(
+ new \DateTime('Sunday'))
+ );
+ }
+
+ public function testShouldRunWeeklyOnCustomDayAndTime()
+ {
+ $job = new Job('ls');
+
+ $date1 = new \DateTime('Saturday 03:45');
+ $date2 = new \DateTime('Saturday 03:46');
+
+ $this->assertTrue($job->weekly(6, 3, 45)->isDue($date1));
+ $this->assertTrue($job->weekly(6, '03:45')->isDue($date1));
+ $this->assertFalse($job->weekly(6, '03:45')->isDue($date2));
+ }
+
+ public function testShouldRunMonthly()
+ {
+ $job = new Job('ls');
+
+ // Default run is every 1st of the month at 00:00
+ $this->assertTrue($job->monthly()->isDue(
+ new \DateTime('01 January'))
+ );
+ $this->assertTrue($job->monthly()->isDue(
+ new \DateTime('01 December'))
+ );
+
+ $this->assertFalse($job->monthly()->isDue(
+ new \DateTime('02 January'))
+ );
+ }
+
+ public function testShouldRunMonthlyOnCustomMonth()
+ {
+ $job = new Job('ls');
+
+ $this->assertTrue($job->monthly()->isDue(
+ new \DateTime('01 January'))
+ );
+
+ // Testing also the helpers to run weekly on custom day
+ $this->assertTrue($job->january()->isDue(
+ new \DateTime('01 January'))
+ );
+ $this->assertFalse($job->january()->isDue(
+ new \DateTime('01 February'))
+ );
+
+ $this->assertTrue($job->february()->isDue(
+ new \DateTime('01 February'))
+ );
+
+ $this->assertTrue($job->march()->isDue(
+ new \DateTime('01 March'))
+ );
+ $this->assertTrue($job->april()->isDue(
+ new \DateTime('01 April'))
+ );
+ $this->assertTrue($job->may()->isDue(
+ new \DateTime('01 May'))
+ );
+ $this->assertTrue($job->june()->isDue(
+ new \DateTime('01 June'))
+ );
+ $this->assertTrue($job->july()->isDue(
+ new \DateTime('01 July'))
+ );
+ $this->assertTrue($job->august()->isDue(
+ new \DateTime('01 August'))
+ );
+ $this->assertTrue($job->september()->isDue(
+ new \DateTime('01 September'))
+ );
+ $this->assertTrue($job->october()->isDue(
+ new \DateTime('01 October'))
+ );
+ $this->assertTrue($job->november()->isDue(
+ new \DateTime('01 November'))
+ );
+ $this->assertTrue($job->december()->isDue(
+ new \DateTime('01 December'))
+ );
+ }
+
+ public function testShouldRunMonthlyOnCustomDayAndTime()
+ {
+ $job = new Job('ls');
+
+ $date1 = new \DateTime('May 15 12:21');
+ $date2 = new \DateTime('February 15 12:21');
+ $date3 = new \DateTime('February 16 12:21');
+
+ $this->assertTrue($job->monthly(5, 15, 12, 21)->isDue($date1));
+ $this->assertTrue($job->monthly(5, 15, '12:21')->isDue($date1));
+ $this->assertFalse($job->monthly(5, 15, '12:21')->isDue($date2));
+ // Every 15th at 12:21
+ $this->assertTrue($job->monthly(null, 15, '12:21')->isDue($date1));
+ $this->assertTrue($job->monthly(null, 15, '12:21')->isDue($date2));
+ $this->assertFalse($job->monthly(null, 15, '12:21')->isDue($date3));
+ }
+
+ public function testShouldRunAtSpecificDate()
+ {
+ $job = new Job('ls');
+
+ $date = '2018-01-01';
+
+ // As instance of datetime
+ $this->assertTrue($job->date(new \DateTime($date))->isDue(new \DateTime($date)));
+ // As date string
+ $this->assertTrue($job->date($date)->isDue(new \DateTime($date)));
+ // Fail for different day
+ $this->assertFalse($job->date($date)->isDue(new \DateTime('2018-01-02')));
+ }
+
+ public function testShouldRunAtSpecificDateTime()
+ {
+ $job = new Job('ls');
+
+ $date = '2018-01-01 12:20';
+
+ // As instance of datetime
+ $this->assertTrue($job->date(new \DateTime($date))->isDue(new \DateTime($date)));
+ // As date string
+ $this->assertTrue($job->date($date)->isDue(new \DateTime($date)));
+ // Fail for different time
+ $this->assertFalse($job->date($date)->isDue(new \DateTime('2018-01-01 12:21')));
+ }
+
+ public function testShouldFailIfDifferentYear()
+ {
+ $job = new Job('ls');
+
+ // As instance of datetime
+ $this->assertFalse($job->date('2018-01-01')->isDue(new \DateTime('2019-01-01')));
+ }
+
+ public function testEveryMinuteWithParameter()
+ {
+ $job = new Job('ls');
+
+ // Job should run at 10:00, 10:05, 10:10 etc., but not at 10:02
+ $this->assertTrue($job->everyMinute(5)->isDue(\DateTime::createFromFormat('H:i', '10:00')));
+ $this->assertFalse($job->everyMinute(5)->isDue(\DateTime::createFromFormat('H:i', '10:02')));
+ $this->assertTrue($job->everyMinute(5)->isDue(\DateTime::createFromFormat('H:i', '10:05')));
+ }
+}
diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/JobOutputFilesTest.php b/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/JobOutputFilesTest.php
new file mode 100644
index 0000000..dad4c8d
--- /dev/null
+++ b/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/JobOutputFilesTest.php
@@ -0,0 +1,201 @@
+assertFalse(file_exists($outputFile));
+ $job->output($outputFile)->run();
+
+ sleep(2);
+ $this->assertTrue(file_exists($outputFile));
+
+ // Content should be 'hi'
+ $this->assertEquals('hi', file_get_contents($outputFile));
+
+ unlink($outputFile);
+ }
+
+ public function testShouldWriteCommandOutputToMultipleFiles()
+ {
+ $command = PHP_BINARY . ' ' . __DIR__ . '/../test_job.php';
+ $job = new Job($command);
+ $outputFile1 = __DIR__ . '/../tmp/output1.log';
+ $outputFile2 = __DIR__ . '/../tmp/output2.log';
+ $outputFile3 = __DIR__ . '/../tmp/output3.log';
+
+ @unlink($outputFile1);
+ @unlink($outputFile2);
+ @unlink($outputFile3);
+
+ // Test fist that the file doesn't exist yet
+ $this->assertFalse(file_exists($outputFile1));
+ $this->assertFalse(file_exists($outputFile2));
+ $this->assertFalse(file_exists($outputFile3));
+ $job->output([
+ $outputFile1,
+ $outputFile2,
+ $outputFile3,
+ ])->run();
+
+ sleep(2);
+ $this->assertTrue(file_exists($outputFile1));
+ $this->assertTrue(file_exists($outputFile2));
+ $this->assertTrue(file_exists($outputFile3));
+
+ $this->assertEquals('hi', file_get_contents($outputFile1));
+ $this->assertEquals('hi', file_get_contents($outputFile2));
+ $this->assertEquals('hi', file_get_contents($outputFile3));
+
+ unlink($outputFile1);
+ unlink($outputFile2);
+ unlink($outputFile3);
+ }
+
+ public function testShouldWriteFunctionOutputToSingleFile()
+ {
+ $job = new Job(function () {
+ echo 'Hello ';
+
+ return 'World!';
+ });
+ $outputFile = __DIR__ . '/../tmp/output.log';
+
+ @unlink($outputFile);
+
+ // Test fist that the file doesn't exist yet
+ $this->assertFalse(file_exists($outputFile));
+ $job->output($outputFile)->run();
+
+ sleep(2);
+ $this->assertTrue(file_exists($outputFile));
+
+ $this->assertEquals('Hello World!', file_get_contents($outputFile));
+
+ unlink($outputFile);
+ }
+
+ public function testShouldWriteFunctionOutputToMultipleFiles()
+ {
+ $job = new Job(function () {
+ echo 'Hello';
+ });
+ $outputFile1 = __DIR__ . '/../tmp/output1.log';
+ $outputFile2 = __DIR__ . '/../tmp/output2.log';
+ $outputFile3 = __DIR__ . '/../tmp/output3.log';
+
+ @unlink($outputFile1);
+ @unlink($outputFile2);
+ @unlink($outputFile3);
+
+ // Test fist that the file doesn't exist yet
+ $this->assertFalse(file_exists($outputFile1));
+ $this->assertFalse(file_exists($outputFile2));
+ $this->assertFalse(file_exists($outputFile3));
+ $job->output([
+ $outputFile1,
+ $outputFile2,
+ $outputFile3,
+ ])->run();
+
+ sleep(2);
+ $this->assertTrue(file_exists($outputFile1));
+ $this->assertTrue(file_exists($outputFile2));
+ $this->assertTrue(file_exists($outputFile3));
+
+ $this->assertEquals('Hello', file_get_contents($outputFile1));
+ $this->assertEquals('Hello', file_get_contents($outputFile2));
+ $this->assertEquals('Hello', file_get_contents($outputFile3));
+
+ unlink($outputFile1);
+ unlink($outputFile2);
+ unlink($outputFile3);
+ }
+
+ public function testShouldWriteFunctionReturnToSingleFile()
+ {
+ $job = new Job(function () {
+ return 'Hello World!';
+ });
+ $outputFile = __DIR__ . '/../tmp/output1.log';
+
+ // Test fist that the file doesn't exist yet
+ $this->assertFalse(file_exists($outputFile));
+ $job->output($outputFile)->run();
+
+ sleep(2);
+ $this->assertTrue(file_exists($outputFile));
+
+ $this->assertEquals('Hello World!', file_get_contents($outputFile));
+
+ unlink($outputFile);
+ }
+
+ public function testShouldWriteFunctionReturnToMultipleFiles()
+ {
+ $job = new Job(function () {
+ return ['Hello ', 'World!'];
+ });
+ $outputFile1 = __DIR__ . '/../tmp/output1.log';
+ $outputFile2 = __DIR__ . '/../tmp/output2.log';
+ $outputFile3 = __DIR__ . '/../tmp/output3.log';
+
+ @unlink($outputFile1);
+ @unlink($outputFile2);
+ @unlink($outputFile3);
+
+ // Test fist that the file doesn't exist yet
+ $this->assertFalse(file_exists($outputFile1));
+ $this->assertFalse(file_exists($outputFile2));
+ $this->assertFalse(file_exists($outputFile3));
+ $job->output([
+ $outputFile1,
+ $outputFile2,
+ $outputFile3,
+ ])->run();
+
+ sleep(2);
+ $this->assertTrue(file_exists($outputFile1));
+ $this->assertTrue(file_exists($outputFile2));
+ $this->assertTrue(file_exists($outputFile3));
+
+ $this->assertEquals('Hello World!', file_get_contents($outputFile1));
+ $this->assertEquals('Hello World!', file_get_contents($outputFile2));
+ $this->assertEquals('Hello World!', file_get_contents($outputFile3));
+
+ unlink($outputFile1);
+ unlink($outputFile2);
+ unlink($outputFile3);
+ }
+
+ public function testShouldWriteFunctionOutputAndReturnToFile()
+ {
+ $job = new Job(function () {
+ echo 'Hello ';
+
+ return 'World!';
+ });
+ $outputFile = __DIR__ . '/../tmp/output1.log';
+
+ // Test fist that the file doesn't exist yet
+ $this->assertFalse(file_exists($outputFile));
+ $job->output($outputFile)->run();
+
+ sleep(2);
+ $this->assertTrue(file_exists($outputFile));
+
+ $this->assertEquals('Hello World!', file_get_contents($outputFile));
+
+ unlink($outputFile);
+ }
+}
diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/JobTest.php b/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/JobTest.php
new file mode 100644
index 0000000..1b84cb2
--- /dev/null
+++ b/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/JobTest.php
@@ -0,0 +1,470 @@
+assertTrue(is_string($job1->getId()));
+
+ $job2 = new Job(function () {
+ return true;
+ });
+ $this->assertTrue(is_string($job2->getId()));
+
+ $job3 = new Job(['MyClass', 'myMethod']);
+ $this->assertTrue(is_string($job3->getId()));
+ }
+
+ public function testShouldGenerateIdFromSignature()
+ {
+ $job1 = new Job('ls');
+ $this->assertEquals(md5('ls'), $job1->getId());
+
+ $job2 = new Job('whoami');
+ $this->assertNotEquals($job1->getId(), $job2->getId());
+
+ $job3 = new Job(['MyClass', 'myMethod']);
+ $this->assertNotEquals($job1->getId(), $job3->getId());
+ }
+
+ public function testShouldAllowCustomId()
+ {
+ $job = new Job('ls', [], 'aCustomId');
+
+ $this->assertNotEquals(md5('ls'), $job->getId());
+ $this->assertEquals('aCustomId', $job->getId());
+
+ $job2 = new Job(['MyClass', 'myMethod'], null, 'myCustomId');
+ $this->assertEquals('myCustomId', $job2->getId());
+ }
+
+ public function testShouldKnowIfDue()
+ {
+ $job1 = new Job('ls');
+ $this->assertTrue($job1->isDue());
+
+ $job2 = new Job('ls');
+ $job2->at('* * * * *');
+ $this->assertTrue($job2->isDue());
+
+ $job3 = new Job('ls');
+ $job3->at('10 * * * *');
+ $this->assertTrue($job3->isDue(\DateTime::createFromFormat('i', '10')));
+ $this->assertFalse($job3->isDue(\DateTime::createFromFormat('i', '12')));
+ }
+
+ public function testShouldKnowIfCanRunInBackground()
+ {
+ $job = new Job('ls');
+ $this->assertTrue($job->canRunInBackground());
+
+ $job2 = new Job(function () {
+ return "I can't run in background";
+ });
+ $this->assertFalse($job2->canRunInBackground());
+ }
+
+ public function testShouldForceTheJobToRunInForeground()
+ {
+ $job = new Job('ls');
+
+ $this->assertTrue($job->canRunInBackground());
+ $this->assertFalse($job->inForeground()->canRunInBackground());
+ }
+
+ public function testShouldReturnCompiledJobCommand()
+ {
+ $job1 = new Job('ls');
+ $this->assertEquals('ls', $job1->inForeground()->compile());
+
+ $fn = function () {
+ return true;
+ };
+ $job2 = new Job($fn);
+ $this->assertEquals($fn, $job2->compile());
+ }
+
+ public function testShouldCompileWithArguments()
+ {
+ $job = new Job('ls', [
+ '-l' => null,
+ '-arg' => 'value',
+ ]);
+
+ $this->assertEquals("ls '-l' '-arg' 'value'", $job->inForeground()->compile());
+ }
+
+ public function testShouldCompileCommandInBackground()
+ {
+ $job1 = new Job('ls');
+ $job1->at('* * * * *');
+
+ $this->assertEquals('(ls) > /dev/null 2>&1 &', $job1->compile());
+ }
+
+ public function testShouldRunInBackground()
+ {
+ // This script has a 5 seconds sleep
+ $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php';
+ $job = new Job($command);
+
+ $startTime = microtime(true);
+ $job->at('* * * * *')->run();
+ $endTime = microtime(true);
+
+ $this->assertTrue(5 > ($endTime - $startTime));
+
+ $startTime = microtime(true);
+ $job->at('* * * * *')->inForeground()->run();
+ $endTime = microtime(true);
+
+ $this->assertTrue(($endTime - $startTime) >= 5);
+ }
+
+ public function testShouldRunInForegroundIfSendsEmails()
+ {
+ $job = new Job('ls');
+ $job->email('test@mail.com');
+
+ $this->assertFalse($job->canRunInBackground());
+ }
+
+ public function testShouldAcceptSingleOrMultipleEmails()
+ {
+ $job = new Job('ls');
+
+ $this->assertInstanceOf(Job::class, $job->email('test@mail.com'));
+ $this->assertInstanceOf(Job::class, $job->email(['test@mail.com', 'other@mail.com']));
+ }
+
+ public function testShouldFailIfEmailInputIsNotStringOrArray()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+
+ $job = new Job('ls');
+
+ $job->email(1);
+ }
+
+ public function testShouldAcceptEmailConfigurationAndItShouldBeChainable()
+ {
+ $job = new Job('ls');
+ $this->assertInstanceOf(Job::class, $job->configure([
+ 'email' => [],
+ ]));
+ }
+
+ public function testShouldFailIfEmailConfigurationIsNotArray()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+
+ $job = new Job('ls');
+ $job->configure([
+ 'email' => 123,
+ ]);
+ }
+
+ public function testShouldCreateLockFileIfOnlyOne()
+ {
+ $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php';
+ $job = new Job($command);
+
+ // Default temp dir
+ $tmpDir = sys_get_temp_dir();
+ $lockFile = $tmpDir . '/' . $job->getId() . '.lock';
+
+ @unlink($lockFile);
+
+ $this->assertFalse(file_exists($lockFile));
+
+ $job->onlyOne()->run();
+
+ $this->assertTrue(file_exists($lockFile));
+ }
+
+ public function testShouldCreateLockFilesInCustomPath()
+ {
+ $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php';
+ $job = new Job($command);
+
+ // Default temp dir
+ $tmpDir = __DIR__ . '/../tmp';
+ $lockFile = $tmpDir . '/' . $job->getId() . '.lock';
+
+ @unlink($lockFile);
+
+ $this->assertFalse(file_exists($lockFile));
+
+ $job->onlyOne($tmpDir)->run();
+
+ $this->assertTrue(file_exists($lockFile));
+ }
+
+ public function testShouldRemoveLockFileAfterRunningClosures()
+ {
+ $job = new Job(function () {
+ sleep(3);
+ });
+
+ // Default temp dir
+ $tmpDir = __DIR__ . '/../tmp';
+ $lockFile = $tmpDir . '/' . $job->getId() . '.lock';
+
+ $job->onlyOne($tmpDir)->run();
+
+ $this->assertFalse(file_exists($lockFile));
+ }
+
+ public function testShouldRemoveLockFileAfterRunningCommands()
+ {
+ $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php';
+ $job = new Job($command);
+
+ // Default temp dir
+ $tmpDir = __DIR__ . '/../tmp';
+ $lockFile = $tmpDir . '/' . $job->getId() . '.lock';
+
+ $job->onlyOne($tmpDir)->run();
+
+ sleep(1);
+
+ $this->assertTrue(file_exists($lockFile));
+
+ sleep(5);
+
+ $this->assertFalse(file_exists($lockFile));
+ }
+
+ public function testShouldKnowIfOverlapping()
+ {
+ $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php';
+ $job = new Job($command);
+
+ $this->assertFalse($job->isOverlapping());
+
+ $tmpDir = __DIR__ . '/../tmp';
+
+ $job->onlyOne($tmpDir)->run();
+
+ sleep(1);
+
+ $this->assertTrue($job->isOverlapping());
+
+ sleep(5);
+
+ $this->assertFalse($job->isOverlapping());
+ }
+
+ public function testShouldNotRunIfOverlapping()
+ {
+ $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php';
+ $job = new Job($command);
+
+ $this->assertFalse($job->isOverlapping());
+
+ $tmpDir = __DIR__ . '/../tmp';
+
+ $job->onlyOne($tmpDir);
+
+ sleep(1);
+
+ $this->assertTrue($job->run());
+ $this->assertFalse($job->run());
+
+ sleep(6);
+ $this->assertTrue($job->run());
+ }
+
+ public function testShouldRunIfOverlappingCallbackReturnsTrue()
+ {
+ $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php';
+ $job = new Job($command);
+
+ $this->assertFalse($job->isOverlapping());
+
+ $tmpDir = __DIR__ . '/../tmp';
+
+ $job->onlyOne($tmpDir, function ($lastExecution) {
+ return time() - $lastExecution > 2;
+ })->run();
+
+ // The job should not run as it is overlapping
+ $this->assertFalse($job->run());
+ sleep(3);
+ // The job should run now as the function should now return true,
+ // while it's still being executed
+ $lockFile = $tmpDir . '/' . $job->getId() . '.lock';
+ $this->assertTrue(file_exists($lockFile));
+ $this->assertTrue($job->run());
+ }
+
+ public function testShouldAcceptTempDirInConfiguration()
+ {
+ $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php';
+ $job = new Job($command);
+
+ $tmpDir = __DIR__ . '/../tmp';
+
+ $job->configure([
+ 'tempDir' => $tmpDir,
+ ])->onlyOne()->run();
+
+ sleep(1);
+
+ $this->assertTrue(file_exists($tmpDir . '/' . $job->getId() . '.lock'));
+ }
+
+ public function testWhenMethodShouldBeChainable()
+ {
+ $job = new Job('ls');
+
+ $this->assertInstanceOf(Job::class, $job->when(function () {
+ return true;
+ }));
+ }
+
+ public function testShouldNotRunIfTruthTestFails()
+ {
+ $job = new Job('ls');
+
+ $this->assertFalse($job->when(function () {
+ return false;
+ })->run());
+
+ $this->assertTrue($job->when(function () {
+ return true;
+ })->run());
+ }
+
+ public function testShouldReturnOutputOfJobExecution()
+ {
+ $job1 = new Job(function () {
+ echo 'hi';
+ });
+ $job1->run();
+ $this->assertEquals('hi', $job1->getOutput());
+
+ $job2 = new Job(function () {
+ return 'hello';
+ });
+ $job2->run();
+ $this->assertEquals('hello', $job2->getOutput());
+
+ $command = PHP_BINARY . ' ' . __DIR__ . '/../test_job.php';
+ $job3 = new Job($command);
+ $job3->inForeground()->run();
+ $this->assertEquals(['hi'], $job3->getOutput());
+ }
+
+ public function testShouldRunCallbackBeforeJobExecution()
+ {
+ $job = new Job(function () {
+ return 'Job for testing before function';
+ });
+
+ $callbackWasExecuted = false;
+ $outputWasSet = false;
+
+ $job->before(function () use ($job, &$callbackWasExecuted, &$outputWasSet) {
+ $callbackWasExecuted = true;
+ $outputWasSet = ! is_null($job->getOutput());
+ })->run();
+
+ $this->assertTrue($callbackWasExecuted);
+ $this->assertFalse($outputWasSet);
+ }
+
+ public function testShouldRunCallbackAfterJobExecution()
+ {
+ $job = new Job(function () {
+ $visitors = 1000;
+
+ return 'Daily visitors: ' . $visitors;
+ });
+
+ $jobResult = null;
+
+ $job->then(function ($output) use (&$jobResult) {
+ $jobResult = $output;
+ })->run();
+
+ $this->assertEquals($jobResult, $job->getOutput());
+
+ $command = PHP_BINARY . ' ' . __DIR__ . '/../test_job.php';
+ $job2 = new Job($command);
+
+ $job2Result = null;
+
+ $job2->then(function ($output) use (&$job2Result) {
+ $job2Result = $output;
+ }, true)->run();
+
+ // Commands in background should return an empty string
+ $this->assertTrue(empty($job2Result));
+
+ $job2Result = null;
+ $job2->then(function ($output) use (&$job2Result) {
+ $job2Result = $output;
+ })->inForeground()->run();
+ $this->assertTrue(! empty($job2Result) &&
+ $job2Result === $job2->getOutput());
+ }
+
+ public function testThenMethodShouldPassReturnCode()
+ {
+ $command_success = PHP_BINARY . ' ' . __DIR__ . '/../test_job.php';
+ $command_fail = $command_success . ' fail';
+
+ $run = function ($command) {
+ $job = new Job($command);
+ $testReturnCode = null;
+
+ $job->then(function ($output, $returnCode) use (&$testReturnCode, &$testOutput) {
+ $testReturnCode = $returnCode;
+ })->run();
+
+ return $testReturnCode;
+ };
+
+ $this->assertEquals(0, $run($command_success));
+ $this->assertNotEquals(0, $run($command_fail));
+ }
+
+ public function testThenMethodShouldBeChainable()
+ {
+ $job = new Job('ls');
+
+ $this->assertInstanceOf(Job::class, $job->then(function () {
+ return true;
+ }));
+ }
+
+ public function testShouldDefaultExecutionInForegroundIfMethodThenIsDefined()
+ {
+ $job = new Job('ls');
+
+ $job->then(function () {
+ return true;
+ });
+
+ $this->assertFalse($job->canRunInBackground());
+ }
+
+ public function testShouldAllowForcingTheJobToRunInBackgroundIfMethodThenIsDefined()
+ {
+ // This is a use case when you want to execute a callback every time your
+ // job is executed, but you don't care about the output of the job
+
+ $job = new Job('ls');
+
+ $job->then(function () {
+ return true;
+ }, true);
+
+ $this->assertTrue($job->canRunInBackground());
+ }
+}
diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/MailerTest.php b/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/MailerTest.php
new file mode 100644
index 0000000..3ae1f60
--- /dev/null
+++ b/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/MailerTest.php
@@ -0,0 +1,166 @@
+getEmailConfig();
+
+ $this->assertTrue(isset($config['subject']));
+ $this->assertTrue(isset($config['from']));
+ $this->assertTrue(isset($config['body']));
+ $this->assertTrue(isset($config['transport']));
+ }
+
+ public function testShouldAllowCustomTransportWhenSendingEmails()
+ {
+ $job = new Job(function () {
+ return 'hi';
+ });
+
+ $job->configure([
+ 'email' => [
+ 'transport' => new \Swift_NullTransport(),
+ ],
+ ]);
+
+ $this->assertInstanceOf(\Swift_NullTransport::class, $job->getEmailConfig()['transport']);
+ }
+
+ public function testEmailTransportShouldAlwaysBeInstanceOfSwift_Transport()
+ {
+ $job = new Job(function () {
+ return 'hi';
+ });
+
+ $job->configure([
+ 'email' => [
+ 'transport' => 'Something not allowed',
+ ],
+ ]);
+
+ $this->assertInstanceOf(\Swift_Transport::class, $job->getEmailConfig()['transport']);
+ }
+
+ public function testShouldSendJobOutputToEmail()
+ {
+ $emailAddress = 'local@localhost.com';
+ $command = PHP_BINARY . ' ' . __DIR__ . '/../test_job.php';
+ $job1 = new Job($command);
+
+ $job2 = new Job(function () {
+ return 'Hello World!';
+ });
+
+ $nullTransportConfig = [
+ 'email' => [
+ 'transport' => new \Swift_NullTransport(),
+ ],
+ ];
+ $job1->configure($nullTransportConfig);
+ $job2->configure($nullTransportConfig);
+
+ $outputFile1 = __DIR__ . '/../tmp/output001.log';
+ $this->assertTrue($job1->output($outputFile1)->email($emailAddress)->run());
+ $outputFile2 = __DIR__ . '/../tmp/output002.log';
+ $this->assertTrue($job2->output($outputFile2)->email($emailAddress)->run());
+
+ unlink($outputFile1);
+ unlink($outputFile2);
+ }
+
+ public function testShouldSendMultipleFilesToEmail()
+ {
+ $emailAddress = 'local@localhost.com';
+ $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php';
+ $job = new Job($command);
+
+ $outputFile1 = __DIR__ . '/../tmp/output003.log';
+ $outputFile2 = __DIR__ . '/../tmp/output004.log';
+
+ $nullTransportConfig = [
+ 'email' => [
+ 'transport' => new \Swift_NullTransport(),
+ ],
+ ];
+ $job->configure($nullTransportConfig);
+
+ $this->assertTrue($job->output([
+ $outputFile1, $outputFile2,
+ ])->email([$emailAddress])->run());
+
+ unlink($outputFile1);
+ unlink($outputFile2);
+ }
+
+ public function testShouldSendToMultipleEmails()
+ {
+ $emailAddress1 = 'local@localhost.com';
+ $emailAddress2 = 'local1@localhost.com';
+ $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php';
+ $job = new Job($command);
+
+ $outputFile = __DIR__ . '/../tmp/output005.log';
+
+ $nullTransportConfig = [
+ 'email' => [
+ 'transport' => new \Swift_NullTransport(),
+ ],
+ ];
+ $job->configure($nullTransportConfig);
+
+ $this->assertTrue($job->output($outputFile)->email([
+ $emailAddress1, $emailAddress2,
+ ])->run());
+
+ unlink($outputFile);
+ }
+
+ public function testShouldAcceptCustomEmailConfig()
+ {
+ $emailAddress = 'local@localhost.com';
+ $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php';
+ $job = new Job($command);
+
+ $outputFile = __DIR__ . '/../tmp/output6.log';
+
+ $this->assertTrue(
+ $job->output($outputFile)->email($emailAddress)
+ ->configure([
+ 'email' => [
+ 'subject' => 'My custom subject',
+ 'from' => 'my@custom.from',
+ 'body' => 'My custom body',
+ 'transport' => new \Swift_NullTransport(),
+ ],
+ ])->run()
+ );
+
+ unlink($outputFile);
+ }
+
+ public function testShouldIgnoreEmailIfSpecifiedInConfig()
+ {
+ $job = new Job(function () {
+ $tot = 1 + 2;
+ // Return nothing....
+ });
+
+ $nullTransportConfig = [
+ 'email' => [
+ 'transport' => new \Swift_NullTransport(),
+ 'ignore_empty_output' => true,
+ ],
+ ];
+ $job->configure($nullTransportConfig);
+
+ $outputFile = __DIR__ . '/../tmp/output.log';
+ $this->assertTrue($job->output($outputFile)->email('local@localhost.com')->run());
+
+ @unlink($outputFile);
+ }
+}
diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/SchedulerTest.php b/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/SchedulerTest.php
new file mode 100644
index 0000000..9f2204d
--- /dev/null
+++ b/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/SchedulerTest.php
@@ -0,0 +1,381 @@
+assertEquals(count($scheduler->getQueuedJobs()), 0);
+
+ $scheduler->raw('ls');
+
+ $this->assertEquals(count($scheduler->getQueuedJobs()), 1);
+ }
+
+ public function testShouldQueueAPhpScript()
+ {
+ $scheduler = new Scheduler();
+
+ $script = __DIR__ . '/../test_job.php';
+
+ $this->assertEquals(count($scheduler->getQueuedJobs()), 0);
+
+ $scheduler->php($script);
+
+ $this->assertEquals(count($scheduler->getQueuedJobs()), 1);
+ }
+
+ public function testShouldAllowCustomPhpBin()
+ {
+ $scheduler = new Scheduler();
+ $script = __DIR__ . '/../test_job.php';
+
+ // Create fake bin
+ $bin = __DIR__ . '/../custom_bin';
+ touch($bin);
+
+ $job = $scheduler->php($script, $bin)->inForeground();
+
+ unlink($bin);
+
+ $this->assertEquals($bin . ' ' . $script, $job->compile());
+ }
+
+ public function testShouldUseSystemPhpBinIfCustomBinDoesNotExist()
+ {
+ $scheduler = new Scheduler();
+ $script = __DIR__ . '/../test_job.php';
+
+ // Create fake bin
+ $bin = '/my/custom/php/bin';
+
+ $job = $scheduler->php($script, $bin)->inForeground();
+
+ $this->assertNotEquals($bin . ' ' . $script, $job->compile());
+ $this->assertEquals(PHP_BINARY . ' ' . $script, $job->compile());
+ }
+
+ public function testShouldThrowExceptionIfScriptIsNotAString()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+
+ $scheduler = new Scheduler();
+ $scheduler->php(function () {
+ return false;
+ });
+
+ $scheduler->run();
+ }
+
+ public function testShouldMarkJobAsFailedIfScriptPathIsInvalid()
+ {
+ $scheduler = new Scheduler();
+ $scheduler->php('someInvalidPathToAScript');
+
+ $scheduler->run();
+ $fail = $scheduler->getFailedJobs();
+ $this->assertCount(1, $fail);
+ $this->assertContainsOnlyInstancesOf(FailedJob::class, $fail);
+ }
+
+ public function testShouldQueueAShellCommand()
+ {
+ $scheduler = new Scheduler();
+
+ $this->assertEquals(count($scheduler->getQueuedJobs()), 0);
+
+ $scheduler->raw('ls');
+
+ $this->assertEquals(count($scheduler->getQueuedJobs()), 1);
+ }
+
+ public function testShouldQueueAFunction()
+ {
+ $scheduler = new Scheduler();
+
+ $this->assertEquals(count($scheduler->getQueuedJobs()), 0);
+
+ $scheduler->call(function () {
+ return true;
+ });
+
+ $this->assertEquals(count($scheduler->getQueuedJobs()), 1);
+ }
+
+ public function testShouldKeepTrackOfExecutedJobs()
+ {
+ $scheduler = new Scheduler();
+
+ $scheduler->call(function () {
+ return true;
+ });
+
+ $this->assertEquals(count($scheduler->getQueuedJobs()), 1);
+ $this->assertEquals(count($scheduler->getExecutedJobs()), 0);
+
+ $scheduler->run();
+
+ $this->assertEquals(count($scheduler->getExecutedJobs()), 1);
+ }
+
+ public function testShouldPassParametersToAFunction()
+ {
+ $scheduler = new Scheduler();
+
+ $outputFile = __DIR__ . '/../tmp/output.txt';
+ $scheduler->call(function ($phrase) {
+ return $phrase;
+ }, [
+ 'Hello World!',
+ ])->output($outputFile);
+
+ @unlink($outputFile);
+
+ $this->assertFalse(file_exists($outputFile));
+
+ $scheduler->run();
+
+ $this->assertNotEquals('Hello', file_get_contents($outputFile));
+ $this->assertEquals('Hello World!', file_get_contents($outputFile));
+
+ @unlink($outputFile);
+ }
+
+ public function testShouldKeepTrackOfFailedJobs()
+ {
+ $scheduler = new Scheduler();
+
+ $exception = new \Exception('Something failed');
+ $scheduler->call(function () use ($exception) {
+ throw $exception;
+ });
+
+ $this->assertEquals(count($scheduler->getFailedJobs()), 0);
+
+ $scheduler->run();
+
+ $this->assertEquals(count($scheduler->getExecutedJobs()), 0);
+ $this->assertEquals(count($scheduler->getFailedJobs()), 1);
+ $failedJob = $scheduler->getFailedJobs()[0];
+ $this->assertInstanceOf(FailedJob::class, $failedJob);
+ $this->assertSame($exception, $failedJob->getException());
+ $this->assertInstanceOf(Job::class, $failedJob->getJob());
+ }
+
+ public function testShouldKeepExecutingJobsIfOneFails()
+ {
+ $scheduler = new Scheduler();
+
+ $scheduler->call(function () {
+ throw new \Exception('Something failed');
+ });
+
+ $scheduler->call(function () {
+ return true;
+ });
+
+ $scheduler->run();
+
+ $this->assertEquals(count($scheduler->getExecutedJobs()), 1);
+ $this->assertEquals(count($scheduler->getFailedJobs()), 1);
+ }
+
+ public function testShouldInjectConfigToTheJobs()
+ {
+ $schedulerConfig = [
+ 'email' => [
+ 'subject' => 'My custom subject',
+ ],
+ ];
+ $scheduler = new Scheduler($schedulerConfig);
+
+ $job = $scheduler->raw('ls');
+
+ $this->assertEquals($job->getEmailConfig()['subject'], $schedulerConfig['email']['subject']);
+ }
+
+ public function testShouldPrioritizeJobConfigOverSchedulerConfig()
+ {
+ $schedulerConfig = [
+ 'email' => [
+ 'subject' => 'My custom subject',
+ ],
+ ];
+ $scheduler = new Scheduler($schedulerConfig);
+
+ $jobConfig = [
+ 'email' => [
+ 'subject' => 'My job subject',
+ ],
+ ];
+ $job = $scheduler->raw('ls')->configure($jobConfig);
+
+ $this->assertNotEquals($job->getEmailConfig()['subject'], $schedulerConfig['email']['subject']);
+ $this->assertEquals($job->getEmailConfig()['subject'], $jobConfig['email']['subject']);
+ }
+
+ public function testShouldShowClosuresVerboseOutputAsText()
+ {
+ $scheduler = new Scheduler();
+
+ $scheduler->call(function ($phrase) {
+ return $phrase;
+ }, [
+ 'Hello World!',
+ ]);
+
+ $scheduler->run();
+
+ $this->assertMatchesRegularExpression('/ Executing Closure$/', $scheduler->getVerboseOutput());
+ $this->assertMatchesRegularExpression('/ Executing Closure$/', $scheduler->getVerboseOutput('text'));
+ }
+
+ public function testShouldShowClosuresVerboseOutputAsHtml()
+ {
+ $scheduler = new Scheduler();
+
+ $scheduler->call(function ($phrase) {
+ return $phrase;
+ }, [
+ 'Hello World!',
+ ]);
+
+ $scheduler->call(function () {
+ return true;
+ });
+
+ $scheduler->run();
+
+ $this->assertMatchesRegularExpression('/
/', $scheduler->getVerboseOutput('html'));
+ }
+
+ public function testShouldShowClosuresVerboseOutputAsArray()
+ {
+ $scheduler = new Scheduler();
+
+ $scheduler->call(function ($phrase) {
+ return $phrase;
+ }, [
+ 'Hello World!',
+ ]);
+
+ $scheduler->call(function () {
+ return true;
+ });
+
+ $scheduler->run();
+
+ $this->assertTrue(is_array($scheduler->getVerboseOutput('array')));
+ $this->assertEquals(count($scheduler->getVerboseOutput('array')), 2);
+ }
+
+ public function testShouldThrowExceptionWithInvalidOutputType()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+
+ $scheduler = new Scheduler();
+
+ $scheduler->call(function ($phrase) {
+ return $phrase;
+ }, [
+ 'Hello World!',
+ ]);
+
+ $scheduler->call(function () {
+ return true;
+ });
+
+ $scheduler->run();
+
+ $scheduler->getVerboseOutput('multiline');
+ }
+
+ public function testShouldPrioritizeJobsInBackround()
+ {
+ $scheduler = new Scheduler();
+
+ $scheduler->php(__DIR__ . '/../async_job.php', null, null, 'async_foreground')->then(function () {
+ return true;
+ });
+
+ $scheduler->php(__DIR__ . '/../async_job.php', null, null, 'async_background');
+
+ $jobs = $scheduler->getQueuedJobs();
+
+ $this->assertEquals('async_background', $jobs[0]->getId());
+ $this->assertEquals('async_foreground', $jobs[1]->getId());
+ }
+
+ public function testCouldRunTwice()
+ {
+ $scheduler = new Scheduler();
+
+ $scheduler->call(function () {
+ return true;
+ });
+
+ $scheduler->run();
+
+ $this->assertCount(1, $scheduler->getExecutedJobs(), 'Number of executed jobs');
+
+ $scheduler->resetRun();
+ $scheduler->run();
+
+ $this->assertCount(1, $scheduler->getExecutedJobs(), 'Number of executed jobs');
+ }
+
+ public function testClearJobs()
+ {
+ $scheduler = new Scheduler();
+
+ $scheduler->call(function () {
+ return true;
+ });
+
+ $this->assertCount(1, $scheduler->getQueuedJobs(), 'Number of queued jobs');
+
+ $scheduler->clearJobs();
+
+ $this->assertCount(0, $scheduler->getQueuedJobs(), 'Number of queued jobs');
+ }
+
+ public function testShouldRunDelayedJobsIfDueWhenCreated()
+ {
+ $scheduler = new Scheduler();
+ $currentTime = date('H:i');
+
+ $scheduler->call(function () {
+ $s = (int) date('s');
+ sleep(60 - $s + 1);
+ })->daily($currentTime);
+
+ $scheduler->call(function () {
+ // do nothing
+ })->daily($currentTime);
+
+ $executed = $scheduler->run();
+
+ $this->assertEquals(2, count($executed));
+ }
+
+ public function testShouldRunAtSpecificTime()
+ {
+ $scheduler = new Scheduler();
+ $runTime = new DateTime('2017-09-13 00:00:00');
+
+ $scheduler->call(function () {
+ // do nothing
+ })->daily('00:00');
+
+ $executed = $scheduler->run($runTime);
+
+ $this->assertEquals(1, count($executed));
+ }
+}
diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/tests/async_job.php b/web/app/vendor/peppeocchi/php-cron-scheduler/tests/async_job.php
new file mode 100644
index 0000000..8e77317
--- /dev/null
+++ b/web/app/vendor/peppeocchi/php-cron-scheduler/tests/async_job.php
@@ -0,0 +1,4 @@
+ Webmozart\Assert\InvalidArgumentException:
+// The employee ID must be an integer. Got: string
+
+new Employee(-10);
+// => Webmozart\Assert\InvalidArgumentException:
+// The employee ID must be a positive integer. Got: -10
+```
+
+Assertions
+----------
+
+The [`Assert`] class provides the following assertions:
+
+### Type Assertions
+
+Method | Description
+-------------------------------------------------------- | --------------------------------------------------
+`string($value, $message = '')` | Check that a value is a string
+`stringNotEmpty($value, $message = '')` | Check that a value is a non-empty string
+`integer($value, $message = '')` | Check that a value is an integer
+`integerish($value, $message = '')` | Check that a value casts to an integer
+`positiveInteger($value, $message = '')` | Check that a value is a positive (non-zero) integer
+`float($value, $message = '')` | Check that a value is a float
+`numeric($value, $message = '')` | Check that a value is numeric
+`natural($value, $message= ''')` | Check that a value is a non-negative integer
+`boolean($value, $message = '')` | Check that a value is a boolean
+`scalar($value, $message = '')` | Check that a value is a scalar
+`object($value, $message = '')` | Check that a value is an object
+`resource($value, $type = null, $message = '')` | Check that a value is a resource
+`isCallable($value, $message = '')` | Check that a value is a callable
+`isArray($value, $message = '')` | Check that a value is an array
+`isTraversable($value, $message = '')` (deprecated) | Check that a value is an array or a `\Traversable`
+`isIterable($value, $message = '')` | Check that a value is an array or a `\Traversable`
+`isCountable($value, $message = '')` | Check that a value is an array or a `\Countable`
+`isInstanceOf($value, $class, $message = '')` | Check that a value is an `instanceof` a class
+`isInstanceOfAny($value, array $classes, $message = '')` | Check that a value is an `instanceof` at least one class on the array of classes
+`notInstanceOf($value, $class, $message = '')` | Check that a value is not an `instanceof` a class
+`isAOf($value, $class, $message = '')` | Check that a value is of the class or has one of its parents
+`isAnyOf($value, array $classes, $message = '')` | Check that a value is of at least one of the classes or has one of its parents
+`isNotA($value, $class, $message = '')` | Check that a value is not of the class or has not one of its parents
+`isArrayAccessible($value, $message = '')` | Check that a value can be accessed as an array
+`uniqueValues($values, $message = '')` | Check that the given array contains unique values
+
+### Comparison Assertions
+
+Method | Description
+----------------------------------------------- | ------------------------------------------------------------------
+`true($value, $message = '')` | Check that a value is `true`
+`false($value, $message = '')` | Check that a value is `false`
+`notFalse($value, $message = '')` | Check that a value is not `false`
+`null($value, $message = '')` | Check that a value is `null`
+`notNull($value, $message = '')` | Check that a value is not `null`
+`isEmpty($value, $message = '')` | Check that a value is `empty()`
+`notEmpty($value, $message = '')` | Check that a value is not `empty()`
+`eq($value, $value2, $message = '')` | Check that a value equals another (`==`)
+`notEq($value, $value2, $message = '')` | Check that a value does not equal another (`!=`)
+`same($value, $value2, $message = '')` | Check that a value is identical to another (`===`)
+`notSame($value, $value2, $message = '')` | Check that a value is not identical to another (`!==`)
+`greaterThan($value, $value2, $message = '')` | Check that a value is greater than another
+`greaterThanEq($value, $value2, $message = '')` | Check that a value is greater than or equal to another
+`lessThan($value, $value2, $message = '')` | Check that a value is less than another
+`lessThanEq($value, $value2, $message = '')` | Check that a value is less than or equal to another
+`range($value, $min, $max, $message = '')` | Check that a value is within a range
+`inArray($value, array $values, $message = '')` | Check that a value is one of a list of values
+`oneOf($value, array $values, $message = '')` | Check that a value is one of a list of values (alias of `inArray`)
+
+### String Assertions
+
+You should check that a value is a string with `Assert::string()` before making
+any of the following assertions.
+
+Method | Description
+--------------------------------------------------- | -----------------------------------------------------------------
+`contains($value, $subString, $message = '')` | Check that a string contains a substring
+`notContains($value, $subString, $message = '')` | Check that a string does not contain a substring
+`startsWith($value, $prefix, $message = '')` | Check that a string has a prefix
+`notStartsWith($value, $prefix, $message = '')` | Check that a string does not have a prefix
+`startsWithLetter($value, $message = '')` | Check that a string starts with a letter
+`endsWith($value, $suffix, $message = '')` | Check that a string has a suffix
+`notEndsWith($value, $suffix, $message = '')` | Check that a string does not have a suffix
+`regex($value, $pattern, $message = '')` | Check that a string matches a regular expression
+`notRegex($value, $pattern, $message = '')` | Check that a string does not match a regular expression
+`unicodeLetters($value, $message = '')` | Check that a string contains Unicode letters only
+`alpha($value, $message = '')` | Check that a string contains letters only
+`digits($value, $message = '')` | Check that a string contains digits only
+`alnum($value, $message = '')` | Check that a string contains letters and digits only
+`lower($value, $message = '')` | Check that a string contains lowercase characters only
+`upper($value, $message = '')` | Check that a string contains uppercase characters only
+`length($value, $length, $message = '')` | Check that a string has a certain number of characters
+`minLength($value, $min, $message = '')` | Check that a string has at least a certain number of characters
+`maxLength($value, $max, $message = '')` | Check that a string has at most a certain number of characters
+`lengthBetween($value, $min, $max, $message = '')` | Check that a string has a length in the given range
+`uuid($value, $message = '')` | Check that a string is a valid UUID
+`ip($value, $message = '')` | Check that a string is a valid IP (either IPv4 or IPv6)
+`ipv4($value, $message = '')` | Check that a string is a valid IPv4
+`ipv6($value, $message = '')` | Check that a string is a valid IPv6
+`email($value, $message = '')` | Check that a string is a valid e-mail address
+`notWhitespaceOnly($value, $message = '')` | Check that a string contains at least one non-whitespace character
+
+### File Assertions
+
+Method | Description
+----------------------------------- | --------------------------------------------------
+`fileExists($value, $message = '')` | Check that a value is an existing path
+`file($value, $message = '')` | Check that a value is an existing file
+`directory($value, $message = '')` | Check that a value is an existing directory
+`readable($value, $message = '')` | Check that a value is a readable path
+`writable($value, $message = '')` | Check that a value is a writable path
+
+### Object Assertions
+
+Method | Description
+----------------------------------------------------- | --------------------------------------------------
+`classExists($value, $message = '')` | Check that a value is an existing class name
+`subclassOf($value, $class, $message = '')` | Check that a class is a subclass of another
+`interfaceExists($value, $message = '')` | Check that a value is an existing interface name
+`implementsInterface($value, $class, $message = '')` | Check that a class implements an interface
+`propertyExists($value, $property, $message = '')` | Check that a property exists in a class/object
+`propertyNotExists($value, $property, $message = '')` | Check that a property does not exist in a class/object
+`methodExists($value, $method, $message = '')` | Check that a method exists in a class/object
+`methodNotExists($value, $method, $message = '')` | Check that a method does not exist in a class/object
+
+### Array Assertions
+
+Method | Description
+-------------------------------------------------- | ------------------------------------------------------------------
+`keyExists($array, $key, $message = '')` | Check that a key exists in an array
+`keyNotExists($array, $key, $message = '')` | Check that a key does not exist in an array
+`validArrayKey($key, $message = '')` | Check that a value is a valid array key (int or string)
+`count($array, $number, $message = '')` | Check that an array contains a specific number of elements
+`minCount($array, $min, $message = '')` | Check that an array contains at least a certain number of elements
+`maxCount($array, $max, $message = '')` | Check that an array contains at most a certain number of elements
+`countBetween($array, $min, $max, $message = '')` | Check that an array has a count in the given range
+`isList($array, $message = '')` | Check that an array is a non-associative list
+`isNonEmptyList($array, $message = '')` | Check that an array is a non-associative list, and not empty
+`isMap($array, $message = '')` | Check that an array is associative and has strings as keys
+`isNonEmptyMap($array, $message = '')` | Check that an array is associative and has strings as keys, and is not empty
+
+### Function Assertions
+
+Method | Description
+------------------------------------------- | -----------------------------------------------------------------------------------------------------
+`throws($closure, $class, $message = '')` | Check that a function throws a certain exception. Subclasses of the exception class will be accepted.
+
+### Collection Assertions
+
+All of the above assertions can be prefixed with `all*()` to test the contents
+of an array or a `\Traversable`:
+
+```php
+Assert::allIsInstanceOf($employees, 'Acme\Employee');
+```
+
+### Nullable Assertions
+
+All of the above assertions can be prefixed with `nullOr*()` to run the
+assertion only if it the value is not `null`:
+
+```php
+Assert::nullOrString($middleName, 'The middle name must be a string or null. Got: %s');
+```
+
+### Extending Assert
+
+The `Assert` class comes with a few methods, which can be overridden to change the class behaviour. You can also extend it to
+add your own assertions.
+
+#### Overriding methods
+
+Overriding the following methods in your assertion class allows you to change the behaviour of the assertions:
+
+* `public static function __callStatic($name, $arguments)`
+ * This method is used to 'create' the `nullOr` and `all` versions of the assertions.
+* `protected static function valueToString($value)`
+ * This method is used for error messages, to convert the value to a string value for displaying. You could use this for representing a value object with a `__toString` method for example.
+* `protected static function typeToString($value)`
+ * This method is used for error messages, to convert the a value to a string representing its type.
+* `protected static function strlen($value)`
+ * This method is used to calculate string length for relevant methods, using the `mb_strlen` if available and useful.
+* `protected static function reportInvalidArgument($message)`
+ * This method is called when an assertion fails, with the specified error message. Here you can throw your own exception, or log something.
+
+## Static analysis support
+
+Where applicable, assertion functions are annotated to support Psalm's
+[Assertion syntax](https://psalm.dev/docs/annotating_code/assertion_syntax/).
+A dedicated [PHPStan Plugin](https://github.com/phpstan/phpstan-webmozart-assert) is
+required for proper type support.
+
+Authors
+-------
+
+* [Bernhard Schussek] a.k.a. [@webmozart]
+* [The Community Contributors]
+
+Contribute
+----------
+
+Contributions to the package are always welcome!
+
+* Report any bugs or issues you find on the [issue tracker].
+* You can grab the source code at the package's [Git repository].
+
+License
+-------
+
+All contents of this package are licensed under the [MIT license].
+
+[beberlei/assert]: https://github.com/beberlei/assert
+[assert package]: https://github.com/beberlei/assert
+[Composer]: https://getcomposer.org
+[Bernhard Schussek]: https://webmozarts.com
+[The Community Contributors]: https://github.com/webmozart/assert/graphs/contributors
+[issue tracker]: https://github.com/webmozart/assert/issues
+[Git repository]: https://github.com/webmozart/assert
+[@webmozart]: https://twitter.com/webmozart
+[MIT license]: LICENSE
+[`Assert`]: src/Assert.php
diff --git a/web/app/vendor/webmozart/assert/composer.json b/web/app/vendor/webmozart/assert/composer.json
new file mode 100644
index 0000000..b340452
--- /dev/null
+++ b/web/app/vendor/webmozart/assert/composer.json
@@ -0,0 +1,43 @@
+{
+ "name": "webmozart/assert",
+ "description": "Assertions to validate method input/output with nice error messages.",
+ "license": "MIT",
+ "keywords": [
+ "assert",
+ "check",
+ "validate"
+ ],
+ "authors": [
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "require": {
+ "php": "^7.2 || ^8.0",
+ "ext-ctype": "*"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^8.5.13"
+ },
+ "conflict": {
+ "phpstan/phpstan": "<0.12.20",
+ "vimeo/psalm": "<4.6.1 || 4.6.2"
+ },
+ "autoload": {
+ "psr-4": {
+ "Webmozart\\Assert\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Webmozart\\Assert\\Tests\\": "tests/",
+ "Webmozart\\Assert\\Bin\\": "bin/src"
+ }
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.10-dev"
+ }
+ }
+}
diff --git a/web/app/vendor/webmozart/assert/src/Assert.php b/web/app/vendor/webmozart/assert/src/Assert.php
new file mode 100644
index 0000000..db1f3a5
--- /dev/null
+++ b/web/app/vendor/webmozart/assert/src/Assert.php
@@ -0,0 +1,2080 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Webmozart\Assert;
+
+use ArrayAccess;
+use BadMethodCallException;
+use Closure;
+use Countable;
+use DateTime;
+use DateTimeImmutable;
+use Exception;
+use ResourceBundle;
+use SimpleXMLElement;
+use Throwable;
+use Traversable;
+
+/**
+ * Efficient assertions to validate the input/output of your methods.
+ *
+ * @since 1.0
+ *
+ * @author Bernhard Schussek
+ */
+class Assert
+{
+ use Mixin;
+
+ /**
+ * @psalm-pure
+ * @psalm-assert string $value
+ *
+ * @param mixed $value
+ * @param string $message
+ *
+ * @throws InvalidArgumentException
+ */
+ public static function string($value, $message = '')
+ {
+ if (!\is_string($value)) {
+ static::reportInvalidArgument(\sprintf(
+ $message ?: 'Expected a string. Got: %s',
+ static::typeToString($value)
+ ));
+ }
+ }
+
+ /**
+ * @psalm-pure
+ * @psalm-assert non-empty-string $value
+ *
+ * @param mixed $value
+ * @param string $message
+ *
+ * @throws InvalidArgumentException
+ */
+ public static function stringNotEmpty($value, $message = '')
+ {
+ static::string($value, $message);
+ static::notEq($value, '', $message);
+ }
+
+ /**
+ * @psalm-pure
+ * @psalm-assert int $value
+ *
+ * @param mixed $value
+ * @param string $message
+ *
+ * @throws InvalidArgumentException
+ */
+ public static function integer($value, $message = '')
+ {
+ if (!\is_int($value)) {
+ static::reportInvalidArgument(\sprintf(
+ $message ?: 'Expected an integer. Got: %s',
+ static::typeToString($value)
+ ));
+ }
+ }
+
+ /**
+ * @psalm-pure
+ * @psalm-assert numeric $value
+ *
+ * @param mixed $value
+ * @param string $message
+ *
+ * @throws InvalidArgumentException
+ */
+ public static function integerish($value, $message = '')
+ {
+ if (!\is_numeric($value) || $value != (int) $value) {
+ static::reportInvalidArgument(\sprintf(
+ $message ?: 'Expected an integerish value. Got: %s',
+ static::typeToString($value)
+ ));
+ }
+ }
+
+ /**
+ * @psalm-pure
+ * @psalm-assert positive-int $value
+ *
+ * @param mixed $value
+ * @param string $message
+ *
+ * @throws InvalidArgumentException
+ */
+ public static function positiveInteger($value, $message = '')
+ {
+ if (!(\is_int($value) && $value > 0)) {
+ static::reportInvalidArgument(\sprintf(
+ $message ?: 'Expected a positive integer. Got: %s',
+ static::valueToString($value)
+ ));
+ }
+ }
+
+ /**
+ * @psalm-pure
+ * @psalm-assert float $value
+ *
+ * @param mixed $value
+ * @param string $message
+ *
+ * @throws InvalidArgumentException
+ */
+ public static function float($value, $message = '')
+ {
+ if (!\is_float($value)) {
+ static::reportInvalidArgument(\sprintf(
+ $message ?: 'Expected a float. Got: %s',
+ static::typeToString($value)
+ ));
+ }
+ }
+
+ /**
+ * @psalm-pure
+ * @psalm-assert numeric $value
+ *
+ * @param mixed $value
+ * @param string $message
+ *
+ * @throws InvalidArgumentException
+ */
+ public static function numeric($value, $message = '')
+ {
+ if (!\is_numeric($value)) {
+ static::reportInvalidArgument(\sprintf(
+ $message ?: 'Expected a numeric. Got: %s',
+ static::typeToString($value)
+ ));
+ }
+ }
+
+ /**
+ * @psalm-pure
+ * @psalm-assert positive-int|0 $value
+ *
+ * @param mixed $value
+ * @param string $message
+ *
+ * @throws InvalidArgumentException
+ */
+ public static function natural($value, $message = '')
+ {
+ if (!\is_int($value) || $value < 0) {
+ static::reportInvalidArgument(\sprintf(
+ $message ?: 'Expected a non-negative integer. Got: %s',
+ static::valueToString($value)
+ ));
+ }
+ }
+
+ /**
+ * @psalm-pure
+ * @psalm-assert bool $value
+ *
+ * @param mixed $value
+ * @param string $message
+ *
+ * @throws InvalidArgumentException
+ */
+ public static function boolean($value, $message = '')
+ {
+ if (!\is_bool($value)) {
+ static::reportInvalidArgument(\sprintf(
+ $message ?: 'Expected a boolean. Got: %s',
+ static::typeToString($value)
+ ));
+ }
+ }
+
+ /**
+ * @psalm-pure
+ * @psalm-assert scalar $value
+ *
+ * @param mixed $value
+ * @param string $message
+ *
+ * @throws InvalidArgumentException
+ */
+ public static function scalar($value, $message = '')
+ {
+ if (!\is_scalar($value)) {
+ static::reportInvalidArgument(\sprintf(
+ $message ?: 'Expected a scalar. Got: %s',
+ static::typeToString($value)
+ ));
+ }
+ }
+
+ /**
+ * @psalm-pure
+ * @psalm-assert object $value
+ *
+ * @param mixed $value
+ * @param string $message
+ *
+ * @throws InvalidArgumentException
+ */
+ public static function object($value, $message = '')
+ {
+ if (!\is_object($value)) {
+ static::reportInvalidArgument(\sprintf(
+ $message ?: 'Expected an object. Got: %s',
+ static::typeToString($value)
+ ));
+ }
+ }
+
+ /**
+ * @psalm-pure
+ * @psalm-assert resource $value
+ *
+ * @param mixed $value
+ * @param string|null $type type of resource this should be. @see https://www.php.net/manual/en/function.get-resource-type.php
+ * @param string $message
+ *
+ * @throws InvalidArgumentException
+ */
+ public static function resource($value, $type = null, $message = '')
+ {
+ if (!\is_resource($value)) {
+ static::reportInvalidArgument(\sprintf(
+ $message ?: 'Expected a resource. Got: %s',
+ static::typeToString($value)
+ ));
+ }
+
+ if ($type && $type !== \get_resource_type($value)) {
+ static::reportInvalidArgument(\sprintf(
+ $message ?: 'Expected a resource of type %2$s. Got: %s',
+ static::typeToString($value),
+ $type
+ ));
+ }
+ }
+
+ /**
+ * @psalm-pure
+ * @psalm-assert callable $value
+ *
+ * @param mixed $value
+ * @param string $message
+ *
+ * @throws InvalidArgumentException
+ */
+ public static function isCallable($value, $message = '')
+ {
+ if (!\is_callable($value)) {
+ static::reportInvalidArgument(\sprintf(
+ $message ?: 'Expected a callable. Got: %s',
+ static::typeToString($value)
+ ));
+ }
+ }
+
+ /**
+ * @psalm-pure
+ * @psalm-assert array $value
+ *
+ * @param mixed $value
+ * @param string $message
+ *
+ * @throws InvalidArgumentException
+ */
+ public static function isArray($value, $message = '')
+ {
+ if (!\is_array($value)) {
+ static::reportInvalidArgument(\sprintf(
+ $message ?: 'Expected an array. Got: %s',
+ static::typeToString($value)
+ ));
+ }
+ }
+
+ /**
+ * @psalm-pure
+ * @psalm-assert iterable $value
+ *
+ * @deprecated use "isIterable" or "isInstanceOf" instead
+ *
+ * @param mixed $value
+ * @param string $message
+ *
+ * @throws InvalidArgumentException
+ */
+ public static function isTraversable($value, $message = '')
+ {
+ @\trigger_error(
+ \sprintf(
+ 'The "%s" assertion is deprecated. You should stop using it, as it will soon be removed in 2.0 version. Use "isIterable" or "isInstanceOf" instead.',
+ __METHOD__
+ ),
+ \E_USER_DEPRECATED
+ );
+
+ if (!\is_array($value) && !($value instanceof Traversable)) {
+ static::reportInvalidArgument(\sprintf(
+ $message ?: 'Expected a traversable. Got: %s',
+ static::typeToString($value)
+ ));
+ }
+ }
+
+ /**
+ * @psalm-pure
+ * @psalm-assert array|ArrayAccess $value
+ *
+ * @param mixed $value
+ * @param string $message
+ *
+ * @throws InvalidArgumentException
+ */
+ public static function isArrayAccessible($value, $message = '')
+ {
+ if (!\is_array($value) && !($value instanceof ArrayAccess)) {
+ static::reportInvalidArgument(\sprintf(
+ $message ?: 'Expected an array accessible. Got: %s',
+ static::typeToString($value)
+ ));
+ }
+ }
+
+ /**
+ * @psalm-pure
+ * @psalm-assert countable $value
+ *
+ * @param mixed $value
+ * @param string $message
+ *
+ * @throws InvalidArgumentException
+ */
+ public static function isCountable($value, $message = '')
+ {
+ if (
+ !\is_array($value)
+ && !($value instanceof Countable)
+ && !($value instanceof ResourceBundle)
+ && !($value instanceof SimpleXMLElement)
+ ) {
+ static::reportInvalidArgument(\sprintf(
+ $message ?: 'Expected a countable. Got: %s',
+ static::typeToString($value)
+ ));
+ }
+ }
+
+ /**
+ * @psalm-pure
+ * @psalm-assert iterable $value
+ *
+ * @param mixed $value
+ * @param string $message
+ *
+ * @throws InvalidArgumentException
+ */
+ public static function isIterable($value, $message = '')
+ {
+ if (!\is_array($value) && !($value instanceof Traversable)) {
+ static::reportInvalidArgument(\sprintf(
+ $message ?: 'Expected an iterable. Got: %s',
+ static::typeToString($value)
+ ));
+ }
+ }
+
+ /**
+ * @psalm-pure
+ * @psalm-template ExpectedType of object
+ * @psalm-param class-string $class
+ * @psalm-assert ExpectedType $value
+ *
+ * @param mixed $value
+ * @param string|object $class
+ * @param string $message
+ *
+ * @throws InvalidArgumentException
+ */
+ public static function isInstanceOf($value, $class, $message = '')
+ {
+ if (!($value instanceof $class)) {
+ static::reportInvalidArgument(\sprintf(
+ $message ?: 'Expected an instance of %2$s. Got: %s',
+ static::typeToString($value),
+ $class
+ ));
+ }
+ }
+
+ /**
+ * @psalm-pure
+ * @psalm-template ExpectedType of object
+ * @psalm-param class-string $class
+ * @psalm-assert !ExpectedType $value
+ *
+ * @param mixed $value
+ * @param string|object $class
+ * @param string $message
+ *
+ * @throws InvalidArgumentException
+ */
+ public static function notInstanceOf($value, $class, $message = '')
+ {
+ if ($value instanceof $class) {
+ static::reportInvalidArgument(\sprintf(
+ $message ?: 'Expected an instance other than %2$s. Got: %s',
+ static::typeToString($value),
+ $class
+ ));
+ }
+ }
+
+ /**
+ * @psalm-pure
+ * @psalm-param array $classes
+ *
+ * @param mixed $value
+ * @param array