Support randomized cronjobs in cronjob PIP
authorTim Düsterhus <duesterhus@woltlab.com>
Fri, 21 Apr 2023 10:59:09 +0000 (12:59 +0200)
committerTim Düsterhus <duesterhus@woltlab.com>
Fri, 21 Apr 2023 10:59:09 +0000 (12:59 +0200)
This uses an additional attribute on the `<expression>` 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

wcfsetup/install/files/lib/system/package/plugin/CronjobPackageInstallationPlugin.class.php

index 893a35160fcd37ebad96229586934b117f1a4a7a..9fdf7e357d9bd0e2c711174bb0553cc49dda0b6f 100644 (file)
@@ -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);