feat: email notice

This commit is contained in:
Baoshuo Ren 2023-02-13 20:29:32 +08:00
parent b7428e1c85
commit 8c189fd9eb
Signed by: baoshuo
GPG Key ID: 00CB9680AB29F51A
60 changed files with 13962 additions and 85 deletions

View File

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

View File

@ -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 <name>\n");
print("php cli.php upgrade:up <name>\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 <name>\n");
print("php cli.php upgrade:down <name>\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 <name>\n");
print("php cli.php upgrade:refresh <name>\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 <name>\n");
print("php cli.php upgrade:remove <name>\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 <task-name> params1 params2 ...\n";
echo "php cli.php <task-name> params1 params2 ...\n";
echo "\n";
echo "The following tasks are available:\n";
foreach ($handlers as $cmd => $handler) {

View File

@ -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": [

View File

@ -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 = <<<EOD
<base target="_blank" />
sendEmail($user['username'], $oj_name_short . ' 密码找回', <<<EOD
<p>您最近告知我们需要重置您在 {$oj_name_short} 上账号的密码。请访问以下链接:<a href="{$url}">{$url}</a> (如果无法点击链接,请试着复制链接并粘贴至浏览器中打开。)</p>
<p>如果您没有请求重置密码,则忽略此信息。该链接将在 72 小时后自动过期失效。</p>
<p>{$name} 您好,</p>
<ul>
<li>请求 IP: {$remote_addr}</li>
<li>转发源 IP:{$http_x_forwarded_for} </li>
<li>用户代理: {$user_agent}</li>
</ul>
EOD);
<p>您最近告知我们需要重置您在 {$oj_name_short} 上账号的密码。请访问以下链接:<a href="{$url}">{$url}</a> (如果无法点击链接,请试着复制链接并粘贴至浏览器中打开。)</p>
<p>如果您没有请求重置密码,则忽略此信息。该链接将在 72 小时后自动过期失效。</p>
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'],
],
]);
<ul>
<li><small>请求 IP: {$remote_addr} (转发来源: {$http_x_forwarded_for})</small></li>
<li><small>用户代理: {$user_agent}</small></li>
</ul>
<p>{$oj_name}</p>
<p><a href="{$oj_url}">{$oj_url}</a></p>
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('<div class="text-center"><h2>邮件发送失败,请重试!</h2><a href="">返回</a></div>');
} 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('<div class="text-center"><h2>邮件发送成功,请检查收件箱!</h2><span>如果邮件未出现在收件箱中,请检查垃圾箱。</span></div>');
}
becomeMsgPage('<div class="text-center"><h2>邮件已发送,请检查收件箱!</h2><span>如果邮件未出现在收件箱中,请检查垃圾箱。</span></div>');
};
$forgot_form->runAtServer();
?>

View File

@ -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'], '新登录', <<<EOD
<p>您收到这封邮件是因为有人通过以下方式登录了您的帐户:</p>
<ul>
<li>请求 IP: {$remote_addr}</li>
<li>转发源 IP:{$http_x_forwarded_for} </li>
<li>用户代理: {$user_agent}</li>
</ul>
<p>如果这是您进行的登录操作,请忽略此邮件。如果您没有进行过登录操作,请立即重置您账号的密码。</p>
EOD);
return "ok";
}

View File

@ -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'] . ' <a href="' . $uri . '">点击此处查看</a>';
$content = $user_link . ' 回复了您的博客 ' . $blog['title'] . ' <a href="' . $uri . '">点击此处查看</a>';
sendSystemMsg($blog['poster'], '博客新回复通知', $content);
}
@ -195,8 +195,7 @@ if (UOJUserBlog::userHasManagePermission(Auth::user())) {
sendSystemMsg(
$comment->info['poster'],
'评论隐藏通知',
"<p>" . UOJUser::getLink($comment->info['poster'], ['color' => false]) . " 您好:</p>" .
"<p>您为博客 " . UOJBlog::cur()->getLink() . " 回复的评论 “" . substr($comment->info['content'], 0, 30) . "……” 已被管理员隐藏,隐藏原因为 “{$reason}”。</p>"
"您为博客 " . UOJBlog::cur()->getLink() . " 回复的评论 “" . substr($comment->info['content'], 0, 30) . "……” 已被管理员隐藏,隐藏原因为 “{$reason}”。"
);
}
};

View File

@ -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));
}

View File

@ -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) {

View File

@ -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 = '<p>' . $user_link . ' 您好:</p>';
$content .= '<p>' . '您参与的比赛 <a href="/contest/' . $contest['id'] . '">' . $contest['name'] . '</a> 现已公布成绩,您的成绩为 <a class="uoj-score" data-max="' . $total_score . '">' . $standings[$i][0] . '</a>' . $tail . '</p>';
sendSystemMsg($standings[$i][2][0], '比赛成绩公布通知', $content);
sendSystemMsg($standings[$i][2][0], '比赛成绩公布通知', '您参与的比赛 <a href="' . HTML::url('/contest/' . $contest['id']) . '">' . $contest['name'] . '</a> 现已公布成绩,您的成绩为 <a class="uoj-score" data-max="' . $total_score . '">' . $standings[$i][0] . '</a>' . $tail);
DB::update([
"update contests_registrants",
"set", ["final_rank" => $standings[$i][3]],

View File

@ -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(<<<EOD
<base target="_blank" />
<div style="padding: 48px; margin: 60px auto 60px auto; box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.15), inset 0px 0px 1px rgba(0, 0, 0, 0.5); max-width: 700px">
<div style="display: block">
<div style="font-size: 20px; font-weight: bold; display: inline-block">{$oj_name_short}</div>
<img style="float: right" src="{$oj_logo_url}" height="32" width="32" />
</div>
<hr />
<br />
<h1><center>{$email['subject']}</center></h1>
<div style="font-size: 18px">{$name} 您好,</div>
<br />
<div>
{$email['content']}
</div>
<br />
<br />
<div style="text-align: right;">
<a href="{$oj_url}">{$oj_name}</a>
</div>
<hr />
<div style="font-size: 12px; color: grey; text-align: center;">
您之所以收到本邮件,是因为您是 {$oj_name} 的用户。
<br />
本邮件由系统自动发送,请勿回复。
</div>
</div>
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";
}
}

31
web/app/scheduler.php Normal file
View File

@ -0,0 +1,31 @@
<?php
$_SERVER['DOCUMENT_ROOT'] = dirname(__DIR__);
require_once $_SERVER['DOCUMENT_ROOT'] . '/app/vendor/autoload.php';
require $_SERVER['DOCUMENT_ROOT'] . '/app/libs/uoj-lib.php';
// Create a new scheduler
$scheduler = new GO\Scheduler([
'tempDir' => '/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();

View File

@ -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'),
);

View File

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

View File

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

View File

@ -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,
),
),
);

View File

@ -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) {

View File

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

View File

@ -0,0 +1,19 @@
Copyright (c) 2011 Michael Dowling <mtdowling@gmail.com>, 2016 Chris Tankersley <chris@ctankersley.com>, and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,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
<?php
require_once '/vendor/autoload.php';
// Works with predefined scheduling definitions
$cron = new Cron\CronExpression('@daily');
$cron->isDue();
echo $cron->getNextRunDate()->format('Y-m-d H:i:s');
echo $cron->getPreviousRunDate()->format('Y-m-d H:i:s');
// Works with complex expressions
$cron = new Cron\CronExpression('3-59/15 6-12 */15 1 2-5');
echo $cron->getNextRunDate()->format('Y-m-d H:i:s');
// Calculate a run date two iterations into the future
$cron = new Cron\CronExpression('@daily');
echo $cron->getNextRunDate(null, 2)->format('Y-m-d H:i:s');
// Calculate a run date relative to a specific time
$cron = new Cron\CronExpression('@monthly');
echo $cron->getNextRunDate('2010-01-12 00:00:00')->format('Y-m-d H:i:s');
```
CRON Expressions
================
A CRON expression is a string representing the schedule for a particular command to execute. The parts of a CRON schedule are as follows:
* * * * *
- - - - -
| | | | |
| | | | |
| | | | +----- day of week (0 - 7) (Sunday=0 or 7)
| | | +---------- 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)

View File

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

View File

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

View File

@ -0,0 +1,346 @@
<?php
declare(strict_types=1);
namespace Cron;
use DateTimeInterface;
/**
* Abstract CRON expression field.
*/
abstract class AbstractField implements FieldInterface
{
/**
* Full range of values that are allowed for this field type.
*
* @var array
*/
protected $fullRange = [];
/**
* Literal values we need to convert to integers.
*
* @var array
*/
protected $literals = [];
/**
* Start value of the full range.
*
* @var int
*/
protected $rangeStart;
/**
* End value of the full range.
*
* @var int
*/
protected $rangeEnd;
/**
* Constructor
*/
public function __construct()
{
$this->fullRange = range($this->rangeStart, $this->rangeEnd);
}
/**
* Check to see if a field is satisfied by a value.
*
* @internal
* @param int $dateValue Date value to check
* @param string $value Value to test
*
* @return bool
*/
public function isSatisfied(int $dateValue, string $value): bool
{
if ($this->isIncrementsOfRanges($value)) {
return $this->isInIncrementsOfRanges($dateValue, $value);
}
if ($this->isRange($value)) {
return $this->isInRange($dateValue, $value);
}
return '*' === $value || $dateValue === (int) $value;
}
/**
* Check if a value is a range.
*
* @internal
* @param string $value Value to test
*
* @return bool
*/
public function isRange(string $value): bool
{
return false !== strpos($value, '-');
}
/**
* Check if a value is an increments of ranges.
*
* @internal
* @param string $value Value to test
*
* @return bool
*/
public function isIncrementsOfRanges(string $value): bool
{
return false !== strpos($value, '/');
}
/**
* Test if a value is within a range.
*
* @internal
* @param int $dateValue Set date value
* @param string $value Value to test
*
* @return bool
*/
public function isInRange(int $dateValue, $value): bool
{
$parts = array_map(
function ($value) {
$value = trim($value);
return $this->convertLiterals($value);
},
explode('-', $value, 2)
);
return $dateValue >= $parts[0] && $dateValue <= $parts[1];
}
/**
* Test if a value is within an increments of ranges (offset[-to]/step size).
*
* @internal
* @param int $dateValue Set date value
* @param string $value Value to test
*
* @return bool
*/
public function isInIncrementsOfRanges(int $dateValue, string $value): bool
{
$chunks = array_map('trim', explode('/', $value, 2));
$range = $chunks[0];
$step = $chunks[1] ?? 0;
// No step or 0 steps aren't cool
/** @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);