diff --git a/bin/composer.phar b/bin/composer.phar index 041775a12..96fa2df7b 100755 Binary files a/bin/composer.phar and b/bin/composer.phar differ diff --git a/bin/grav b/bin/grav index faa8846f7..4b2483c6a 100755 --- a/bin/grav +++ b/bin/grav @@ -6,7 +6,9 @@ if (!file_exists(__DIR__ . '/../vendor')){ require_once __DIR__ . '/../system/src/Grav/Common/Composer.php'; } +use Grav\Common\Grav; use Grav\Common\Composer; +use Symfony\Component\Console\Application; if (!file_exists(__DIR__ . '/../vendor')){ // Before we can even start, we need to run composer first @@ -16,9 +18,8 @@ if (!file_exists(__DIR__ . '/../vendor')){ echo "\n\n"; } -use Symfony\Component\Console\Application; - -require_once __DIR__ . '/../vendor/autoload.php'; +$autoload = require_once(__DIR__ . '/../vendor/autoload.php'); +Grav::instance(array('loader' => $autoload)); if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) { exit(sprintf("You are running PHP %s, but Grav needs at least PHP %s to run.\n", $ver, $req)); @@ -41,5 +42,6 @@ $app->addCommands(array( new \Grav\Console\Cli\ClearCacheCommand(), new \Grav\Console\Cli\BackupCommand(), new \Grav\Console\Cli\NewProjectCommand(), + new \Grav\Console\Cli\SchedulerCommand(), )); $app->run(); diff --git a/composer.json b/composer.json index 099a40ab8..252224a51 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "symfony/console": "~4.1", "symfony/event-dispatcher": "~4.1", "symfony/var-dumper": "~4.1", + "symfony/process": "~4.1", "doctrine/cache": "^1.8", "doctrine/collections": "^1.5", "guzzlehttp/psr7": "^1.4", @@ -41,7 +42,8 @@ "league/climate": "^3.4", "antoligy/dom-string-iterators": "^1.0", "miljar/php-exif": "^0.6.4", - "composer/ca-bundle": "^1.0" + "composer/ca-bundle": "^1.0", + "dragonmantank/cron-expression": "^1.2" }, "require-dev": { "codeception/codeception": "^2.4", diff --git a/composer.lock b/composer.lock index ad7b42d68..09e8cbb4c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "cb0cecd3938f21e0f46cb68022ab2b88", + "content-hash": "ee3b919ebe2a385d66074d22125c4317", "packages": [ { "name": "antoligy/dom-string-iterators", @@ -299,6 +299,50 @@ ], "time": "2018-06-21T15:54:46+00:00" }, + { + "name": "dragonmantank/cron-expression", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "9504fa9ea681b586028adaaa0877db4aecf32bad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/9504fa9ea681b586028adaaa0877db4aecf32bad", + "reference": "9504fa9ea681b586028adaaa0877db4aecf32bad", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.0|~5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "time": "2017-01-23T04:29:33+00:00" + }, { "name": "erusev/parsedown", "version": "1.6.4", @@ -1731,6 +1775,55 @@ ], "time": "2018-08-06T14:22:27+00:00" }, + { + "name": "symfony/process", + "version": "v4.1.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "86cdb930a6a855b0ab35fb60c1504cb36184f843" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/86cdb930a6a855b0ab35fb60c1504cb36184f843", + "reference": "86cdb930a6a855b0ab35fb60c1504cb36184f843", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "time": "2018-08-03T11:13:38+00:00" + }, { "name": "symfony/var-dumper", "version": "v4.1.4", @@ -3052,16 +3145,16 @@ }, { "name": "phpunit/phpunit", - "version": "7.3.2", + "version": "7.3.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "34705f81bddc3f505b9599a2ef96e2b4315ba9b8" + "reference": "1bd5629cccfb2c0a9ef5474b4ff772349e1ec898" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/34705f81bddc3f505b9599a2ef96e2b4315ba9b8", - "reference": "34705f81bddc3f505b9599a2ef96e2b4315ba9b8", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1bd5629cccfb2c0a9ef5474b4ff772349e1ec898", + "reference": "1bd5629cccfb2c0a9ef5474b4ff772349e1ec898", "shasum": "" }, "require": { @@ -3132,7 +3225,7 @@ "testing", "xunit" ], - "time": "2018-08-22T06:39:21+00:00" + "time": "2018-09-01T15:49:55+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -3913,55 +4006,6 @@ "homepage": "https://symfony.com", "time": "2018-07-26T11:24:31+00:00" }, - { - "name": "symfony/process", - "version": "v4.1.4", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "86cdb930a6a855b0ab35fb60c1504cb36184f843" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/86cdb930a6a855b0ab35fb60c1504cb36184f843", - "reference": "86cdb930a6a855b0ab35fb60c1504cb36184f843", - "shasum": "" - }, - "require": { - "php": "^7.1.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.1-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Process Component", - "homepage": "https://symfony.com", - "time": "2018-08-03T11:13:38+00:00" - }, { "name": "theseer/tokenizer", "version": "1.1.0", diff --git a/composer_BACKUP_46658.json b/composer_BACKUP_46658.json new file mode 100644 index 000000000..e4c680d78 --- /dev/null +++ b/composer_BACKUP_46658.json @@ -0,0 +1,89 @@ +{ + "name": "getgrav/grav", + "type": "project", + "description": "Modern, Crazy Fast, Ridiculously Easy and Amazingly Powerful Flat-File CMS", + "keywords": ["cms","flat-file cms","flat cms","flatfile cms","php"], + "homepage": "http://getgrav.org", + "license": "MIT", + "require": { + "php": ">=7.1.3", + "ext-json": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-curl": "*", + "ext-zip": "*", + "symfony/polyfill-iconv": "^1.9", + "symfony/polyfill-php72": "^1.9", + "symfony/polyfill-php73": "^1.9", + + "psr/simple-cache": "^1.0", + "psr/http-message": "^1.0", + + "twig/twig": "~1.35", + "erusev/parsedown": "1.6.4", + "erusev/parsedown-extra": "~0.7", + "symfony/yaml": "~4.1", + "symfony/console": "~4.1", + "symfony/event-dispatcher": "~4.1", + "symfony/var-dumper": "~4.1", + "doctrine/cache": "^1.7", + "doctrine/collections": "^1.5", + "guzzlehttp/psr7": "^1.4", + "filp/whoops": "~2.2", + + "matthiasmullie/minify": "^1.3", + "monolog/monolog": "~1.0", + "gregwar/image": "2.*", + "donatj/phpuseragentparser": "~0.10", + "pimple/pimple": "~3.2", + "rockettheme/toolbox": "~1.4", + "maximebf/debugbar": "~1.15", + "league/climate": "^3.4", + "antoligy/dom-string-iterators": "^1.0", +<<<<<<< HEAD + "miljar/php-exif": "^0.6.3", + "composer/ca-bundle": "^1.0", + "dragonmantank/cron-expression": "^1.2", + "symfony/process": "^3.4" +======= + "miljar/php-exif": "^0.6.4", + "composer/ca-bundle": "^1.0" +>>>>>>> 1.6 + }, + "require-dev": { + "codeception/codeception": "^2.4", + "phpunit/php-code-coverage": "~6.0", + "fzaninotto/faker": "^1.8", + "victorjonsson/markdowndocs": "dev-master" + }, + "config": { + "platform": { + "php": "7.1.3" + } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/trilbymedia/PHP-Markdown-Documentation-Generator" + } + ], + "autoload": { + "psr-4": { + "Grav\\": "system/src/Grav" + }, + "files": ["system/defines.php"] + }, + "archive": { + "exclude": ["VERSION"] + }, + "scripts": { + "post-create-project-cmd": "bin/grav install", + "test": "vendor/bin/codecept run unit", + "test-windows": "vendor\\bin\\codecept run unit" + }, + "extra": { + "branch-alias": { + "dev-develop": "1.x-dev" + } + } +} diff --git a/composer_BASE_46658.json b/composer_BASE_46658.json new file mode 100644 index 000000000..fbd5106f7 --- /dev/null +++ b/composer_BASE_46658.json @@ -0,0 +1,76 @@ +{ + "name": "getgrav/grav", + "type": "project", + "description": "Modern, Crazy Fast, Ridiculously Easy and Amazingly Powerful Flat-File CMS", + "keywords": ["cms","flat-file cms","flat cms","flatfile cms","php"], + "homepage": "http://getgrav.org", + "license": "MIT", + "require": { + "php": ">=5.6.4", + "twig/twig": "~1.24", + "erusev/parsedown": "1.6.4", + "erusev/parsedown-extra": "~0.7", + "symfony/yaml": "~3.4", + "symfony/console": "~3.4", + "symfony/event-dispatcher": "~3.4", + "symfony/var-dumper": "~3.4", + "symfony/polyfill-iconv": "~1.0", + "doctrine/cache": "^1.6", + "doctrine/collections": "^1.4", + "psr/simple-cache": "^1.0", + "psr/http-message": "^1.0", + "guzzlehttp/psr7": "^1.4", + "filp/whoops": "~2.0", + "matthiasmullie/minify": "^1.3", + "monolog/monolog": "~1.0", + "gregwar/image": "2.*", + "donatj/phpuseragentparser": "~0.3", + "pimple/pimple": "~3.2", + "rockettheme/toolbox": "~1.4", + "maximebf/debugbar": "~1.10", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-curl": "*", + "ext-zip": "*", + "league/climate": "^3.2", + "antoligy/dom-string-iterators": "^1.0", + "miljar/php-exif": "^0.6.3", + "composer/ca-bundle": "^1.0" + }, + "require-dev": { + "codeception/codeception": "^2.1", + "phpunit/php-code-coverage": "~2.0", + "fzaninotto/faker": "^1.5", + "victorjonsson/markdowndocs": "dev-master" + }, + "config": { + "platform": { + "php": "5.6.4" + } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/trilbymedia/PHP-Markdown-Documentation-Generator" + } + ], + "autoload": { + "psr-4": { + "Grav\\": "system/src/Grav" + }, + "files": ["system/defines.php"] + }, + "archive": { + "exclude": ["VERSION"] + }, + "scripts": { + "post-create-project-cmd": "bin/grav install", + "test": "vendor/bin/codecept run unit", + "test-windows": "vendor\\bin\\codecept run unit" + }, + "extra": { + "branch-alias": { + "dev-develop": "1.x-dev" + } + } +} diff --git a/composer_LOCAL_46658.json b/composer_LOCAL_46658.json new file mode 100644 index 000000000..76885b077 --- /dev/null +++ b/composer_LOCAL_46658.json @@ -0,0 +1,78 @@ +{ + "name": "getgrav/grav", + "type": "project", + "description": "Modern, Crazy Fast, Ridiculously Easy and Amazingly Powerful Flat-File CMS", + "keywords": ["cms","flat-file cms","flat cms","flatfile cms","php"], + "homepage": "http://getgrav.org", + "license": "MIT", + "require": { + "php": ">=5.6.4", + "twig/twig": "~1.24", + "erusev/parsedown": "1.6.4", + "erusev/parsedown-extra": "~0.7", + "symfony/yaml": "~3.4", + "symfony/console": "~3.4", + "symfony/event-dispatcher": "~3.4", + "symfony/var-dumper": "~3.4", + "symfony/polyfill-iconv": "~1.0", + "doctrine/cache": "^1.6", + "doctrine/collections": "^1.4", + "psr/simple-cache": "^1.0", + "psr/http-message": "^1.0", + "guzzlehttp/psr7": "^1.4", + "filp/whoops": "~2.0", + "matthiasmullie/minify": "^1.3", + "monolog/monolog": "~1.0", + "gregwar/image": "2.*", + "donatj/phpuseragentparser": "~0.3", + "pimple/pimple": "~3.2", + "rockettheme/toolbox": "~1.4", + "maximebf/debugbar": "~1.10", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-curl": "*", + "ext-zip": "*", + "league/climate": "^3.2", + "antoligy/dom-string-iterators": "^1.0", + "miljar/php-exif": "^0.6.3", + "composer/ca-bundle": "^1.0", + "dragonmantank/cron-expression": "^1.2", + "symfony/process": "^3.4" + }, + "require-dev": { + "codeception/codeception": "^2.1", + "phpunit/php-code-coverage": "~2.0", + "fzaninotto/faker": "^1.5", + "victorjonsson/markdowndocs": "dev-master" + }, + "config": { + "platform": { + "php": "5.6.4" + } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/trilbymedia/PHP-Markdown-Documentation-Generator" + } + ], + "autoload": { + "psr-4": { + "Grav\\": "system/src/Grav" + }, + "files": ["system/defines.php"] + }, + "archive": { + "exclude": ["VERSION"] + }, + "scripts": { + "post-create-project-cmd": "bin/grav install", + "test": "vendor/bin/codecept run unit", + "test-windows": "vendor\\bin\\codecept run unit" + }, + "extra": { + "branch-alias": { + "dev-develop": "1.x-dev" + } + } +} diff --git a/composer_REMOTE_46658.json b/composer_REMOTE_46658.json new file mode 100644 index 000000000..a211b6e82 --- /dev/null +++ b/composer_REMOTE_46658.json @@ -0,0 +1,82 @@ +{ + "name": "getgrav/grav", + "type": "project", + "description": "Modern, Crazy Fast, Ridiculously Easy and Amazingly Powerful Flat-File CMS", + "keywords": ["cms","flat-file cms","flat cms","flatfile cms","php"], + "homepage": "http://getgrav.org", + "license": "MIT", + "require": { + "php": ">=7.1.3", + "ext-json": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-curl": "*", + "ext-zip": "*", + "symfony/polyfill-iconv": "^1.9", + "symfony/polyfill-php72": "^1.9", + "symfony/polyfill-php73": "^1.9", + + "psr/simple-cache": "^1.0", + "psr/http-message": "^1.0", + + "twig/twig": "~1.35", + "erusev/parsedown": "1.6.4", + "erusev/parsedown-extra": "~0.7", + "symfony/yaml": "~4.1", + "symfony/console": "~4.1", + "symfony/event-dispatcher": "~4.1", + "symfony/var-dumper": "~4.1", + "doctrine/cache": "^1.7", + "doctrine/collections": "^1.5", + "guzzlehttp/psr7": "^1.4", + "filp/whoops": "~2.2", + + "matthiasmullie/minify": "^1.3", + "monolog/monolog": "~1.0", + "gregwar/image": "2.*", + "donatj/phpuseragentparser": "~0.10", + "pimple/pimple": "~3.2", + "rockettheme/toolbox": "~1.4", + "maximebf/debugbar": "~1.15", + "league/climate": "^3.4", + "antoligy/dom-string-iterators": "^1.0", + "miljar/php-exif": "^0.6.4", + "composer/ca-bundle": "^1.0" + }, + "require-dev": { + "codeception/codeception": "^2.4", + "phpunit/php-code-coverage": "~6.0", + "fzaninotto/faker": "^1.8", + "victorjonsson/markdowndocs": "dev-master" + }, + "config": { + "platform": { + "php": "7.1.3" + } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/trilbymedia/PHP-Markdown-Documentation-Generator" + } + ], + "autoload": { + "psr-4": { + "Grav\\": "system/src/Grav" + }, + "files": ["system/defines.php"] + }, + "archive": { + "exclude": ["VERSION"] + }, + "scripts": { + "post-create-project-cmd": "bin/grav install", + "test": "vendor/bin/codecept run unit", + "test-windows": "vendor\\bin\\codecept run unit" + }, + "extra": { + "branch-alias": { + "dev-develop": "1.x-dev" + } + } +} diff --git a/system/blueprints/config/scheduler.yaml b/system/blueprints/config/scheduler.yaml new file mode 100644 index 000000000..e0949aa81 --- /dev/null +++ b/system/blueprints/config/scheduler.yaml @@ -0,0 +1,96 @@ +title: PLUGIN_ADMIN.SCHEDULER + +form: + validation: loose + fields: + tabs: + type: tabs + class: subtle + + fields: + status_tab: + type: tab + title: Status + + fields: + setup_title: + type: section + title: PLUGIN_ADMIN.SCHEDULER_SETUP + underline: true + + setup: + type: croninstall + + status_title: + type: section + title: PLUGIN_ADMIN.SCHEDULER_STATUS + underline: true + + status: + type: cronstatus + validate: + type: commalist + + custom_tab: + type: tab + title: Custom + + fields: + jobs_title: + type: section + title: PLUGIN_ADMIN.SCHEDULER_JOBS + underline: false + custom_jobs: + type: list + style: vertical + label: + classes: cron-job-list + key: id + fields: + .id: + type: key + label: ID + placeholder: 'process-name' + validate: + required: true + pattern: '[a-zа-я0-9_\-]+' + max: 20 + message: 'ID must be lowercase with dashes/underscores only and less than 20 characters' + .command: + type: text + label: Command + placeholder: 'cd ~;ls -lah;' + validate: + required: true + .args: + type: text + label: Extra Arguments + .at: + type: cron + label: Run At + help: 'Cron formatted "at" syntax' + placeholder: '* * * * *' + validate: + required: true + .output: + type: text + label: Output File + help: 'The path/filename of the output file (from the root of the Grav installation)' + placeholder: 'logs/ls-cron.out' + .output_mode: + type: select + label: Output Type + help: 'Either append to the same file each run, or overwrite the file with each run' + default: append + options: + append: Append + overwrite: Overwrite + .email: + type: text + label: Email + help: 'Email to send output to. NOTE: requires output file to be set' + placeholder: 'notifications@yoursite.com' + + + + diff --git a/system/languages/en.yaml b/system/languages/en.yaml index ed72c7b55..a9ed9471d 100644 --- a/system/languages/en.yaml +++ b/system/languages/en.yaml @@ -93,6 +93,23 @@ NICETIME: MO_PLURAL: mos YR_PLURAL: yrs DEC_PLURAL: decs +CRON: + EVERY: every + EVERY_HOUR: every hour + EVERY_MINUTE: every minute + EVERY_DAY_OF_WEEK: every day of the week + EVERY_DAY_OF_MONTH: every day of the month + EVERY_MONTH: every month + TEXT_PERIOD: Every + TEXT_MINS: ' at minute(s) past the hour' + TEXT_TIME: ' at :' + TEXT_DOW: ' on ' + TEXT_MONTH: ' of ' + TEXT_DOM: ' on ' + ERROR1: The tag %s is not supported! + ERROR2: Bad number of elements + ERROR3: The jquery_element should be set into jqCron settings + ERROR4: Unrecognized expression FORM: VALIDATION_FAIL: Validation failed: INVALID_INPUT: Invalid input in diff --git a/system/src/Grav/Common/Grav.php b/system/src/Grav/Common/Grav.php index 84260d01c..42e315615 100644 --- a/system/src/Grav/Common/Grav.php +++ b/system/src/Grav/Common/Grav.php @@ -40,6 +40,7 @@ class Grav extends Container 'cache' => 'Grav\Common\Cache', 'Grav\Common\Service\SessionServiceProvider', 'plugins' => 'Grav\Common\Plugins', + 'scheduler' => 'Grav\Common\Scheduler\Scheduler', 'themes' => 'Grav\Common\Themes', 'twig' => 'Grav\Common\Twig\Twig', 'taxonomy' => 'Grav\Common\Taxonomy', @@ -53,6 +54,7 @@ class Grav extends Container 'exif' => 'Grav\Common\Helpers\Exif', 'Grav\Common\Service\StreamsServiceProvider', 'Grav\Common\Service\ConfigServiceProvider', + 'Grav\Common\Service\InflectorServiceProvider', 'inflector' => 'Grav\Common\Inflector', 'siteSetupProcessor' => 'Grav\Common\Processors\SiteSetupProcessor', 'configurationProcessor' => 'Grav\Common\Processors\ConfigurationProcessor', @@ -61,6 +63,7 @@ class Grav extends Container 'initializeProcessor' => 'Grav\Common\Processors\InitializeProcessor', 'pluginsProcessor' => 'Grav\Common\Processors\PluginsProcessor', 'themesProcessor' => 'Grav\Common\Processors\ThemesProcessor', + 'schedulerProcessor' => 'Grav\Common\Processors\SchedulerProcessor', 'tasksProcessor' => 'Grav\Common\Processors\TasksProcessor', 'assetsProcessor' => 'Grav\Common\Processors\AssetsProcessor', 'twigProcessor' => 'Grav\Common\Processors\TwigProcessor', @@ -80,6 +83,7 @@ class Grav extends Container 'initializeProcessor', 'pluginsProcessor', 'themesProcessor', + 'schedulerProcessor', 'tasksProcessor', 'assetsProcessor', 'twigProcessor', diff --git a/system/src/Grav/Common/Processors/AssetsProcessor.php b/system/src/Grav/Common/Processors/AssetsProcessor.php index 6ca952d70..89997d237 100644 --- a/system/src/Grav/Common/Processors/AssetsProcessor.php +++ b/system/src/Grav/Common/Processors/AssetsProcessor.php @@ -10,7 +10,7 @@ namespace Grav\Common\Processors; class AssetsProcessor extends ProcessorBase implements ProcessorInterface { - public $id = 'assets'; + public $id = '_assets'; public $title = 'Assets'; public function process() diff --git a/system/src/Grav/Common/Processors/SchedulerProcessor.php b/system/src/Grav/Common/Processors/SchedulerProcessor.php new file mode 100644 index 000000000..2b3d62506 --- /dev/null +++ b/system/src/Grav/Common/Processors/SchedulerProcessor.php @@ -0,0 +1,24 @@ +container['scheduler']; + $scheduler->loadSavedJobs(); + $this->container->fireEvent('onSchedulerInitialized', new Event(['scheduler' => $scheduler])); + } +} diff --git a/system/src/Grav/Common/Scheduler/Cron.php b/system/src/Grav/Common/Scheduler/Cron.php new file mode 100644 index 000000000..682a12af2 --- /dev/null +++ b/system/src/Grav/Common/Scheduler/Cron.php @@ -0,0 +1,512 @@ + modified for Grav integration + * @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved. + * @license MIT License; see LICENSE file for details. + */ + +namespace Grav\Common\Scheduler; + +/* + * Usage examples : + * ---------------- + * + * $cron = new Cron('10-30/5 12 * * *'); + * + * var_dump($cron->getMinutes()); + * // array(5) { + * // [0]=> int(10) + * // [1]=> int(15) + * // [2]=> int(20) + * // [3]=> int(25) + * // [4]=> int(30) + * // } + * + * var_dump($cron->getText('fr')); + * // string(32) "Chaque jour à 12:10,15,20,25,30" + * + * var_dump($cron->getText('en')); + * // string(30) "Every day at 12:10,15,20,25,30" + * + * var_dump($cron->getType()); + * // string(3) "day" + * + * var_dump($cron->getCronHours()); + * // string(2) "12" + * + * var_dump($cron->matchExact(new \DateTime('2012-07-01 13:25:10'))); + * // bool(false) + * + * var_dump($cron->matchExact(new \DateTime('2012-07-01 12:15:20'))); + * // bool(true) + * + * var_dump($cron->matchWithMargin(new \DateTime('2012-07-01 12:32:50'), -3, 5)); + * // bool(true) + */ +class Cron { + const TYPE_UNDEFINED = ''; + const TYPE_MINUTE = 'minute'; + const TYPE_HOUR = 'hour'; + const TYPE_DAY = 'day'; + const TYPE_WEEK = 'week'; + const TYPE_MONTH = 'month'; + const TYPE_YEAR = 'year'; + /** + * + * @var array + */ + protected $texts = array( + 'fr' => array( + 'empty' => '-tout-', + 'name_minute' => 'minute', + 'name_hour' => 'heure', + 'name_day' => 'jour', + 'name_week' => 'semaine', + 'name_month' => 'mois', + 'name_year' => 'année', + 'text_period' => 'Chaque %s', + 'text_mins' => 'à %s minutes', + 'text_time' => 'à %s:%s', + 'text_dow' => 'le %s', + 'text_month' => 'de %s', + 'text_dom' => 'le %s', + 'weekdays' => array('lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche'), + 'months' => array('janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'), + ), + 'en' => array( + 'empty' => '-all-', + 'name_minute' => 'minute', + 'name_hour' => 'hour', + 'name_day' => 'day', + 'name_week' => 'week', + 'name_month' => 'month', + 'name_year' => 'year', + 'text_period' => 'Every %s', + 'text_mins' => 'at %s minutes past the hour', + 'text_time' => 'at %s:%s', + 'text_dow' => 'on %s', + 'text_month' => 'of %s', + 'text_dom' => 'on the %s', + 'weekdays' => array('monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'), + 'months' => array('january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'), + ), + ); + /** + * min hour dom month dow + * @var string + */ + protected $cron = ''; + /** + * + * @var array + */ + protected $minutes = array(); + /** + * + * @var array + */ + protected $hours = array(); + /** + * + * @var array + */ + protected $months = array(); + /** + * 0-7 : sunday, monday, ... saturday, sunday + * @var array + */ + protected $dow = array(); + /** + * + * @var array + */ + protected $dom = array(); + /** + * + * @param string $cron + */ + public function __construct($cron = null) { + if (!empty($cron)) { + $this->setCron($cron); + } + } + /** + * + * @return string + */ + public function getCron() { + return implode(' ', array( + $this->getCronMinutes(), + $this->getCronHours(), + $this->getCronDaysOfMonth(), + $this->getCronMonths(), + $this->getCronDaysOfWeek(), + )); + } + /** + * + * @param string $lang 'fr' or 'en' + * @return string + */ + public function getText($lang) { + // check lang + if (!isset($this->texts[$lang])) { + return $this->getCron(); + } + $texts = $this->texts[$lang]; + // check type + $type = $this->getType(); + if ($type == self::TYPE_UNDEFINED) { + return $this->getCron(); + } + // init + $elements = array(); + $elements[] = sprintf($texts['text_period'], $texts['name_' . $type]); + // hour + if (in_array($type, array(self::TYPE_HOUR))) { + $elements[] = sprintf($texts['text_mins'], $this->getCronMinutes()); + } + // week + if (in_array($type, array(self::TYPE_WEEK))) { + $dow = $this->getCronDaysOfWeek(); + foreach ($texts['weekdays'] as $i => $wd) { + $dow = str_replace((string) ($i + 1), $wd, $dow); + } + $elements[] = sprintf($texts['text_dow'], $dow); + } + // month + year + if (in_array($type, array(self::TYPE_MONTH, self::TYPE_YEAR))) { + $elements[] = sprintf($texts['text_dom'], $this->getCronDaysOfMonth()); + } + // year + if (in_array($type, array(self::TYPE_YEAR))) { + $months = $this->getCronMonths(); + for ($i = count($texts['months']) - 1; $i >= 0; $i--) { + $months = str_replace((string) ($i + 1), $texts['months'][$i], $months); + } + $elements[] = sprintf($texts['text_month'], $months); + } + // day + week + month + year + if (in_array($type, array(self::TYPE_DAY, self::TYPE_WEEK, self::TYPE_MONTH, self::TYPE_YEAR))) { + $elements[] = sprintf($texts['text_time'], $this->getCronHours(), $this->getCronMinutes()); + } + return str_replace('*', $texts['empty'], implode(' ', $elements)); + } + /** + * + * @return string + */ + public function getType() { + $mask = preg_replace('/[^\* ]/si', '-', $this->getCron()); + $mask = preg_replace('/-+/si', '-', $mask); + $mask = preg_replace('/[^-\*]/si', '', $mask); + if ($mask == '*****') { + return self::TYPE_MINUTE; + } + elseif ($mask == '-****') { + return self::TYPE_HOUR; + } + elseif (substr($mask, -3) == '***') { + return self::TYPE_DAY; + } + elseif (substr($mask, -3) == '-**') { + return self::TYPE_MONTH; + } + elseif (substr($mask, -3) == '**-') { + return self::TYPE_WEEK; + } + elseif (substr($mask, -2) == '-*') { + return self::TYPE_YEAR; + } + return self::TYPE_UNDEFINED; + } + /** + * + * @param string $cron + * @return Cron + */ + public function setCron($cron) { + // sanitize + $cron = trim($cron); + $cron = preg_replace('/\s+/', ' ', $cron); + // explode + $elements = explode(' ', $cron); + if (count($elements) != 5) { + throw new Exception('Bad number of elements'); + } + $this->cron = $cron; + $this->setMinutes($elements[0]); + $this->setHours($elements[1]); + $this->setDaysOfMonth($elements[2]); + $this->setMonths($elements[3]); + $this->setDaysOfWeek($elements[4]); + return $this; + } + /** + * + * @return string + */ + public function getCronMinutes() { + return $this->arrayToCron($this->minutes); + } + /** + * + * @return string + */ + public function getCronHours() { + return $this->arrayToCron($this->hours); + } + /** + * + * @return string + */ + public function getCronDaysOfMonth() { + return $this->arrayToCron($this->dom); + } + /** + * + * @return string + */ + public function getCronMonths() { + return $this->arrayToCron($this->months); + } + /** + * + * @return string + */ + public function getCronDaysOfWeek() { + return $this->arrayToCron($this->dow); + } + /** + * + * @return array + */ + public function getMinutes() { + return $this->minutes; + } + /** + * + * @return array + */ + public function getHours() { + return $this->hours; + } + /** + * + * @return array + */ + public function getDaysOfMonth() { + return $this->dom; + } + /** + * + * @return array + */ + public function getMonths() { + return $this->months; + } + /** + * + * @return array + */ + public function getDaysOfWeek() { + return $this->dow; + } + /** + * + * @param string|array $minutes + * @return Cron + */ + public function setMinutes($minutes) { + $this->minutes = $this->cronToArray($minutes, 0, 59); + return $this; + } + /** + * + * @param string|array $hours + * @return Cron + */ + public function setHours($hours) { + $this->hours = $this->cronToArray($hours, 0, 23); + return $this; + } + /** + * + * @param string|array $months + * @return Cron + */ + public function setMonths($months) { + $this->months = $this->cronToArray($months, 1, 12); + return $this; + } + /** + * + * @param string|array $dow + * @return Cron + */ + public function setDaysOfWeek($dow) { + $this->dow = $this->cronToArray($dow, 0, 7); + return $this; + } + /** + * + * @param string|array $dom + * @return Cron + */ + public function setDaysOfMonth($dom) { + $this->dom = $this->cronToArray($dom, 1, 31); + return $this; + } + /** + * + * @param mixed $date + * @param int $min + * @param int $hour + * @param int $day + * @param int $month + * @param int $weekday + * @return DateTime + */ + protected function parseDate($date, &$min, &$hour, &$day, &$month, &$weekday) { + if (is_numeric($date) && intval($date) == $date) { + $date = new \DateTime('@' . $date); + } + elseif (is_string($date)) { + $date = new \DateTime('@' . strtotime($date)); + } + if ($date instanceof \DateTime) { + $min = intval($date->format('i')); + $hour = intval($date->format('H')); + $day = intval($date->format('d')); + $month = intval($date->format('m')); + $weekday = intval($date->format('w')); // 0-6 + } + else { + throw new Exception('Date format not supported'); + } + return new \DateTime($date->format('Y-m-d H:i:sP')); + } + /** + * + * @param int|string|\Datetime $date + */ + public function matchExact($date) { + $date = $this->parseDate($date, $min, $hour, $day, $month, $weekday); + return + (empty($this->minutes) || in_array($min, $this->minutes)) && + (empty($this->hours) || in_array($hour, $this->hours)) && + (empty($this->dom) || in_array($day, $this->dom)) && + (empty($this->months) || in_array($month, $this->months)) && + (empty($this->dow) || in_array($weekday, $this->dow) || ($weekday == 0 && in_array(7, $this->dow)) || ($weekday == 7 && in_array(0, $this->dow)) + ); + } + /** + * + * @param int|string|\Datetime $date + * @param int $minuteBefore + * @param int $minuteAfter + */ + public function matchWithMargin($date, $minuteBefore = 0, $minuteAfter = 0) { + if ($minuteBefore > 0) { + throw new Exception('MinuteBefore parameter cannot be positive !'); + } + if ($minuteAfter < 0) { + throw new Exception('MinuteAfter parameter cannot be negative !'); + } + $date = $this->parseDate($date, $min, $hour, $day, $month, $weekday); + $interval = new \DateInterval('PT1M'); // 1 min + if ($minuteBefore != 0) { + $date->sub(new \DateInterval('PT' . abs($minuteBefore) . 'M')); + } + $n = $minuteAfter - $minuteBefore + 1; + for ($i = 0; $i < $n; $i++) { + if ($this->matchExact($date)) { + return true; + } + $date->add($interval); + } + return false; + } + /** + * + * @param array $array + * @return string + */ + protected function arrayToCron($array) { + $n = count($array); + if (!is_array($array) || $n == 0) { + return '*'; + } + $cron = array($array[0]); + $s = $c = $array[0]; + for ($i = 1; $i < $n; $i++) { + if ($array[$i] == $c + 1) { + $c = $array[$i]; + $cron[count($cron) - 1] = $s . '-' . $c; + } + else { + $s = $c = $array[$i]; + $cron[] = $c; + } + } + return implode(',', $cron); + } + /** + * + * @param string $string + * @param int $min + * @param int $max + * @return array + */ + protected function cronToArray($string, $min, $max) { + $array = array(); + if (is_array($string)) { + foreach ($string as $val) { + if (is_numeric($val) && intval($val) == $val && $val >= $min && $val <= $max) { + $array[] = intval($val); + } + } + } + else if ($string !== '*') { + while ($string != '') { + // test "*/n" expression + if (preg_match('/^\*\/([0-9]+),?/', $string, $m)) { + for ($i = max(0, $min); $i <= min(59, $max); $i+=$m[1]) { + $array[] = intval($i); + } + $string = substr($string, strlen($m[0])); + continue; + } + // test "a-b/n" expression + if (preg_match('/^([0-9]+)-([0-9]+)\/([0-9]+),?/', $string, $m)) { + for ($i = max($m[1], $min); $i <= min($m[2], $max); $i+=$m[3]) { + $array[] = intval($i); + } + $string = substr($string, strlen($m[0])); + continue; + } + // test "a-b" expression + if (preg_match('/^([0-9]+)-([0-9]+),?/', $string, $m)) { + for ($i = max($m[1], $min); $i <= min($m[2], $max); $i++) { + $array[] = intval($i); + } + $string = substr($string, strlen($m[0])); + continue; + } + // test "c" expression + if (preg_match('/^([0-9]+),?/', $string, $m)) { + if ($m[1] >= $min && $m[1] <= $max) { + $array[] = intval($m[1]); + } + $string = substr($string, strlen($m[0])); + continue; + } + // something goes wrong in the expression + return array(); + } + } + sort($array); + return $array; + } +} diff --git a/system/src/Grav/Common/Scheduler/IntervalTrait.php b/system/src/Grav/Common/Scheduler/IntervalTrait.php new file mode 100644 index 000000000..9df03699c --- /dev/null +++ b/system/src/Grav/Common/Scheduler/IntervalTrait.php @@ -0,0 +1,365 @@ +at = $expression; + $this->executionTime = CronExpression::factory($expression); + return $this; + } + /** + * Set the execution time to every minute. + * + * @return self + */ + public function everyMinute() + { + return $this->at('* * * * *'); + } + /** + * 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 $value; + } +} + diff --git a/system/src/Grav/Common/Scheduler/Job.php b/system/src/Grav/Common/Scheduler/Job.php new file mode 100644 index 000000000..995e69609 --- /dev/null +++ b/system/src/Grav/Common/Scheduler/Job.php @@ -0,0 +1,491 @@ +id = Grav::instance()['inflector']->hyphenize($id); + } else { + if (is_string($command)) { + $this->id = md5($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; + // Set enabled state + $status = Grav::instance()['config']->get('scheduler.status'); + $this->enabled = isset($status[$id]) && $status[$id] === 'disabled' ? false : true; + } + + /** + * Get the command + * + * @return string + */ + public function getCommand() + { + return $this->command; + } + + /** + * Get the cron 'at' syntax for this job + * + * @return string + */ + public function getAt() + { + return $this->at; + } + + /** + * Get the status of this job + * + * @return bool + */ + public function getEnabled() + { + return $this->enabled; + } + + /** + * Get optional arguments + * + * @return array|string|void + */ + public function getArguments() + { + if (is_string($this->args)) { + return $this->args; + } + return; + } + + public function getCronExpression() + { + return CronExpression::factory($this->at); + } + + /** + * Get the status of the last run for this job + * + * @return bool + */ + public function isSuccessful() + { + return $this->successful; + } + + /** + * 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 default 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; + 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 runInBackground() + { + 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; + } + + /** + * Configure the job. + * + * @param array $config + * @return self + */ + public function configure(array $config = []) + { + // 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; + } + // Write lock file if necessary + $this->createLockFile(); + + // Call before if required + if (is_callable($this->before)) { + call_user_func($this->before); + } + // If command is callable... + if (is_callable($this->command)) { + $this->output = $this->exec(); + } else { + /** @var Process process */ + $args = is_string($this->args) ? $this->args : implode(' ', $this->args); + $command = $this->command . ' ' . $args; + $process = new Process($command); + + $this->process = $process; + + if ($this->runInBackground()) { + $process->start(); + } else { + $process->run(); + $this->finalize(); + } + } + return true; + } + + /** + * Finish up processing the job + * + * @return void + */ + public function finalize() + { + /** @var Process $process */ + $process = $this->process; + + if ($process) { + $process->wait(); + + if ($process->isSuccessful()) { + $this->successful = true; + $this->output = $process->getOutput(); + } else { + $this->successful = false; + $this->output = $process->getErrorOutput(); + } + + $this->postRun(); + + unset($this->process); + } + } + + /** + * Things to run after job has run + */ + private function postRun() + { + if (count($this->outputTo) > 0) { + foreach ($this->outputTo as $file) { + $output_mode = $this->outputMode === 'append' ? FILE_APPEND | LOCK_EX : LOCK_EX; + file_put_contents($file, $this->output, $output_mode); + } + } + + // Send output to email + $this->emailOutput(); + + // Call any callback defined + if (is_callable($this->after)) { + call_user_func($this->after, $this->output, $this->returnCode); + } + + $this->removeLockFile(); + } + + /** + * 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. + * + * @throws Exception + * @return string + */ + private function exec() + { + $return_data = ''; + ob_start(); + try { + $return_data = call_user_func_array($this->command, $this->args); + $this->successful = true; + } catch (Exception $e) { + $this->successful = false; + } + $this->output = ob_get_clean() . (is_string($return_data) ? $return_data : ''); + + $this->postRun(); + } + + /** + * 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 ? 'overwrite' : 'append'; + 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; + } + + /** + * Email the output of the job, if any. + * + * @return bool + */ + private function emailOutput() + { + if (!count($this->outputTo) || !count($this->emailTo)) { + return false; + } + + if (is_callable('Grav\Plugin\Email\Utils::sendEmail')) { + $subject ='Grav Scheduled Job [' . $this->getId() . ']'; + $content = "
\n".$this->getOutput()."\n"; + $to = $this->emailTo; + + \Grav\Plugin\Email\Utils::sendEmail($subject, $content, $to); + } + 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/system/src/Grav/Common/Scheduler/Scheduler.php b/system/src/Grav/Common/Scheduler/Scheduler.php new file mode 100644 index 000000000..04b450021 --- /dev/null +++ b/system/src/Grav/Common/Scheduler/Scheduler.php @@ -0,0 +1,354 @@ +get('scheduler.defaults', []); + $this->config = $config; + $this->loadSavedJobs(); + + $this->status_path = Grav::instance()['locator']->findResource('user://data/scheduler', true, true); + if (!file_exists($this->status_path)) { + Folder::create($this->status_path); + } + + } + + /** + * Load saved jobs from config/scheduler.yaml file + */ + public function loadSavedJobs() + { + if (!$this->jobs) { + $saved_jobs = (array) Grav::instance()['config']->get('scheduler.custom_jobs', []); + + foreach ($saved_jobs as $id => $j) { + $args = isset($j['args']) ? $j['args'] : []; + $id = Grav::instance()['inflector']->hyphenize($id); + $job = $this->addCommand($j['command'], $args, $id); + + if (isset($j['at'])) { + $job->at($j['at']); + } + + if (isset($j['output'])) { + $mode = isset($j['output_mode']) && $j['output_mode'] === 'append' ? true : false; + $job->output($j['output'], $mode); + } + + if (isset($j['email'])) { + $job->email($j['email']); + } + + // store in saved_jobs + $this->saved_jobs[] = $job; + } + } + } + + /** + * Get the queued jobs as background/foreground + * + * @param bool $all + * @return array + */ + public function getQueuedJobs($all = false) + { + $background = []; + $foreground = []; + foreach ($this->jobs as $job) { + if ($all || $job->getEnabled()) { + if ($job->runInBackground()) { + $background[] = $job; + } else { + $foreground[] = $job; + } + } + + } + return [$background, $foreground]; + } + + /** + * Get all jobs if they are disabled or not as one array + * + * @param bool $all + * @return array + */ + public function getAllJobs() + { + list($background, $foreground) = $this->getQueuedJobs(true); + return array_merge($background, $foreground); + } + + /** + * Queues a PHP 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 addFunction(callable $fn, $args = [], $id = null) + { + $job = new Job($fn, $args, $id); + $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 addCommand($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) + { + list($background, $foreground) = $this->getQueuedJobs(false); + $alljobs = array_merge($background, $foreground); + + if (is_null($runTime)) { + $runTime = new \DateTime('now'); + } + + // Star processing jobs + foreach ($alljobs as $job) { + if ($job->isDue($runTime)) { + $job->run(); + $this->jobs_run[] = $job; + } + } + + // Finish handling any background jobs + foreach($background as $job) { + $job->finalize(); + } + + // Store states + $this->saveJobStates(); + } + + /** + * 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; + } + + /** + * 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('