From 6fffcbb295cdd071e24e878894b269b17e757d33 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Tim=20D=C3=BCsterhus?= Date: Fri, 21 Apr 2023 12:59:09 +0200 Subject: [PATCH] Support randomized cronjobs in cronjob PIP This uses an additional attribute on the `` for a clear migration path forward, if the cron expression library gains native support. With PHP 8.2+ a seedable engine is used, ensuring the values stay the same after a reimport of the same cronjob. Resolves #5202 --- ...CronjobPackageInstallationPlugin.class.php | 59 ++++++++++++++++--- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/wcfsetup/install/files/lib/system/package/plugin/CronjobPackageInstallationPlugin.class.php b/wcfsetup/install/files/lib/system/package/plugin/CronjobPackageInstallationPlugin.class.php index 893a35160f..9fdf7e357d 100644 --- a/wcfsetup/install/files/lib/system/package/plugin/CronjobPackageInstallationPlugin.class.php +++ b/wcfsetup/install/files/lib/system/package/plugin/CronjobPackageInstallationPlugin.class.php @@ -46,14 +46,23 @@ class CronjobPackageInstallationPlugin extends AbstractXMLPackageInstallationPlu */ protected function getElement(\DOMXPath $xpath, array &$elements, \DOMElement $element) { - if ($element->tagName == 'description') { - if (!isset($elements['description'])) { - $elements['description'] = []; - } + switch ($element->tagName) { + case 'description': + if (!isset($elements['description'])) { + $elements['description'] = []; + } - $elements['description'][$element->getAttribute('language')] = $element->nodeValue; - } else { - parent::getElement($xpath, $elements, $element); + $elements['description'][$element->getAttribute('language')] = $element->nodeValue; + break; + case 'expression': + $elements['expression'] = [ + 'type' => $element->getAttribute('type') ?? '', + 'value' => $element->nodeValue, + ]; + break; + default: + parent::getElement($xpath, $elements, $element); + break; } } @@ -98,13 +107,47 @@ class CronjobPackageInstallationPlugin extends AbstractXMLPackageInstallationPlu } } + private function getRandomExpression(string $name, string $expression): CronExpression + { + if (\class_exists(\Random\Engine\Xoshiro256StarStar::class, false)) { + // Generate stable, but differing values for each (instance, cronjob) pair. + $randomizer = new \Random\Randomizer(new \Random\Engine\Xoshiro256StarStar( + \hash('sha256', \sprintf( + '%s:%s:%d:%s', + \WCF_UUID, + self::class, + $this->installation->getPackageID(), + $name + ), true) + )); + $engine = static fn (int $min, int $max) => $randomizer->getInt($min, $max); + } else { + // A seedable engine is not available, use completely random values. + $engine = \random_int(...); + } + + return new CronExpression(match ($expression) { + '@hourly' => \sprintf('%d * * * *', $engine(0, 59)), + '@daily' => \sprintf('%d %d * * *', $engine(0, 59), $engine(0, 23)), + '@weekly' => \sprintf('%d %d * * %d', $engine(0, 59), $engine(0, 23), $engine(0, 6)), + '@monthly' => \sprintf('%d %d %d * *', $engine(0, 59), $engine(0, 23), $engine(1, 28)), + }); + } + /** * @inheritDoc */ protected function prepareImport(array $data) { if (isset($data['elements']['expression'])) { - $expression = new CronExpression($data['elements']['expression']); + $expression = match ($data['elements']['expression']['type']) { + '' => new CronExpression($data['elements']['expression']['value']), + 'random' => $this->getRandomExpression( + $data['attributes']['name'], + $data['elements']['expression']['value'] + ), + }; + $data['elements']['startdom'] = $expression->getExpression(CronExpression::DAY); $data['elements']['startdow'] = $expression->getExpression(CronExpression::WEEKDAY); $data['elements']['starthour'] = $expression->getExpression(CronExpression::HOUR); -- 2.20.1