Elgg  Version master
Cron.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
8 use Elgg\Traits\Loggable;
9 use Elgg\Traits\TimeUsing;
10 use GO\Job;
11 use GO\Scheduler;
12 
18 class Cron {
19 
20  use Loggable;
21  use TimeUsing;
22 
23  protected const LOG_FILES_TO_KEEP = 5;
24 
25  protected array $default_intervals = [
26  'minute' => '* * * * *',
27  'fiveminute' => '*/5 * * * *',
28  'fifteenmin' => '*/15 * * * *',
29  'halfhour' => '*/30 * * * *',
30  'hourly' => '0 * * * *',
31  'daily' => '0 0 * * *',
32  'weekly' => '0 0 * * 0',
33  'monthly' => '0 0 1 * *',
34  'yearly' => '0 0 1 1 *',
35  ];
36 
43  public function __construct(protected EventsService $events, protected Translator $translator) {
44  }
45 
55  public function run(?array $intervals = null, bool $force = false): array {
56  if (!isset($intervals)) {
57  $intervals = array_keys($this->default_intervals);
58  }
59 
60  $allowed_intervals = $this->getConfiguredIntervals();
61 
62  $scheduler = new Scheduler();
63  $time = $this->getCurrentTime();
64  $immutable = \DateTimeImmutable::createFromInterface($time);
65 
66  foreach ($intervals as $interval) {
67  if (!array_key_exists($interval, $allowed_intervals)) {
68  throw new CronException("{$interval} is not a recognized cron interval. Please use one of the following: " . implode(', ', array_keys($allowed_intervals)));
69  }
70 
71  $cron_interval = $force ? $allowed_intervals['minute'] : $allowed_intervals[$interval];
72  $filename = $this->getLogFilename($interval, $immutable);
73 
74  $cron_logger = \Elgg\Logger\Cron::factory([
75  'interval' => $interval,
76  'filename' => $filename,
77  ]);
78 
79  $scheduler
80  ->call(function () use ($interval, $immutable, $cron_logger, $filename) {
81  return $this->execute($interval, $cron_logger, $filename, $immutable);
82  })
83  ->at($cron_interval)
84  ->before(function () use ($interval, $immutable, $cron_logger) {
85  $this->before($interval, $cron_logger, $immutable);
86  })
87  ->then(function ($output) use ($interval, $cron_logger) {
88  $this->after($output, $interval, $cron_logger);
89  });
90  }
91 
92  return $scheduler->run($time);
93  }
94 
104  protected function before(string $interval, \Elgg\Logger\Cron $cron_logger, \DateTimeImmutable $time): void {
105  try {
106  $this->events->triggerBefore('cron', $interval, $time);
107  } catch (\Throwable $t) {
108  $this->getLogger()->error($t);
109  }
110 
111  // give every period at least 'max_execution_time' (PHP ini setting)
112  set_time_limit((int) ini_get('max_execution_time'));
113 
114  $now = new DateTime();
115 
116  $cron_logger->notice($this->translator->translate('admin:cron:started', [$interval, $time->format(DATE_RFC2822)]));
117  $cron_logger->notice($this->translator->translate('admin:cron:started:actual', [$interval, $now->format(DATE_RFC2822)]));
118  }
119 
130  protected function execute(string $interval, \Elgg\Logger\Cron $cron_logger, string $filename, \DateTimeImmutable $time): string {
131  try {
132  $begin_callback = function (array $params) use ($cron_logger) {
133  $readable_callable = (string) elgg_extract('readable_callable', $params);
134 
135  $cron_logger->notice("Starting {$readable_callable}");
136  };
137 
138  $end_callback = function (array $params) use ($cron_logger) {
139  $readable_callable = (string) elgg_extract('readable_callable', $params);
140 
141  $cron_logger->notice("Finished {$readable_callable}");
142  };
143 
144  // for BC this needs to be a triggerResults
145  $this->events->triggerResults('cron', $interval, [
146  'time' => $time->getTimestamp(),
147  'dt' => $time,
148  'logger' => $cron_logger,
149  ], null, [
150  EventsService::OPTION_BEGIN_CALLBACK => $begin_callback,
151  EventsService::OPTION_END_CALLBACK => $end_callback,
152  EventsService::OPTION_CONTINUE_ON_EXCEPTION => true,
153  ]);
154  } catch (\Throwable $t) {
155  $this->getLogger()->error($t);
156  }
157 
158  $now = new DateTime();
159 
160  $complete = $this->translator->translate('admin:cron:complete', [$interval, $now->format(DATE_RFC2822)]);
161  $cron_logger->notice($complete);
162 
163  if (file_exists($filename) && is_readable($filename)) {
164  return file_get_contents($filename);
165  }
166 
167  return '';
168  }
169 
179  protected function after(string $output, string $interval, \Elgg\Logger\Cron $cron_logger): void {
180  $this->getLogger()->info($output);
181 
182  try {
183  $this->events->triggerAfter('cron', $interval, new \DateTime());
184  } catch (\Throwable $t) {
185  $this->getLogger()->error($t);
186  }
187 
188  $cron_logger->close();
189  $this->rotateLogs($interval);
190  $this->logCompletion($interval);
191  }
192 
203  public function getLogs(string $interval, bool $filenames_only = false): array {
204  $fh = new \ElggFile();
205  $fh->owner_guid = elgg_get_site_entity()->guid;
206  $fh->setFilename("cron/{$interval}/dummy.log");
207 
208  $dir = pathinfo($fh->getFilenameOnFilestore(), PATHINFO_DIRNAME);
209  if (!is_dir($dir) || !is_readable($dir)) {
210  return [];
211  }
212 
213  $dh = new \DirectoryIterator($dir);
214  $files = [];
215  /* @var $file \DirectoryIterator */
216  foreach ($dh as $file) {
217  if ($file->isDot() || !$file->isFile() || $file->getExtension() !== 'log') {
218  continue;
219  }
220 
221  if ($filenames_only) {
222  $files[] = $file->getFilename();
223  } else {
224  $files[$file->getFilename()] = file_get_contents($file->getPathname());
225  }
226  }
227 
228  if ($filenames_only) {
229  natcasesort($files);
230  } else {
231  uksort($files, 'strnatcasecmp');
232  }
233 
234  return array_reverse($files);
235  }
236 
244  public function getLastCompletion(string $interval): ?DateTime {
245  $fh = new \ElggFile();
246  $fh->owner_guid = elgg_get_site_entity()->guid;
247  $fh->setFilename("cron/{$interval}.complete");
248 
249  if (!$fh->exists()) {
250  return null;
251  }
252 
253  $date = $fh->grabFile();
254  if (empty($date)) {
255  // how??
256  return null;
257  }
258 
259  try {
260  return Values::normalizeTime($date);
261  } catch (\Elgg\Exceptions\ExceptionInterface $e) {
262  $this->getLogger()->warning($e);
263  }
264 
265  return null;
266  }
267 
276  public function getConfiguredIntervals(bool $only_names = false): array {
277  $result = $this->events->triggerResults('cron:intervals', 'system', [], $this->default_intervals);
278  if (!is_array($result)) {
279  $this->getLogger()->warning("The event 'cron:intervals', 'system' should return an array, " . gettype($result) . ' given');
280 
281  $result = $this->default_intervals;
282  }
283 
284  if ($only_names) {
285  return array_keys($result);
286  }
287 
288  return $result;
289  }
290 
299  protected function getLogFilename(string $interval, \DateTimeInterface $time): string {
300  $date = $time->format(\DateTimeInterface::ATOM);
301  $date = str_replace('+', 'p', $date);
302  $date = preg_replace('/[^a-zA-Z0-9_-]+/', '-', $date);
303 
304  $fh = new \ElggFile();
305  $fh->owner_guid = elgg_get_site_entity()->guid;
306  $fh->setFilename("cron/{$interval}/{$date}.log");
307 
308  return $fh->getFilenameOnFilestore();
309  }
310 
318  protected function rotateLogs(string $interval): void {
319  $files = $this->getLogs($interval, true);
320  if (count($files) <= self::LOG_FILES_TO_KEEP) {
321  return;
322  }
323 
324  $fh = new \ElggFile();
325  $fh->owner_guid = elgg_get_site_entity()->guid;
326 
327  while (count($files) > self::LOG_FILES_TO_KEEP) {
328  $filename = array_pop($files);
329 
330  $fh->setFilename("cron/{$interval}/{$filename}");
331  $fh->delete();
332  }
333  }
334 
342  protected function logCompletion(string $interval): void {
343  $fh = new \ElggFile();
344  $fh->owner_guid = elgg_get_site_entity()->guid;
345  $fh->setFilename("cron/{$interval}.complete");
346 
347  try {
348  if ($fh->open('write') === false) {
349  return;
350  }
351  } catch (\Elgg\Exceptions\ExceptionInterface $e) {
352  $this->getLogger()->warning($e);
353  return;
354  }
355 
356  $now = new DateTime();
357  $fh->write($now->format(\DateTimeInterface::ATOM));
358  $fh->close();
359  }
360 }
getLogger()
Returns logger.
Definition: Loggable.php:37
$params
Saves global plugin settings.
Definition: save.php:13
if(! $annotation instanceof ElggAnnotation) $time
Definition: time.php:20
Cron.
Definition: Cron.php:18
before(string $interval, \Elgg\Logger\Cron $cron_logger, \DateTimeImmutable $time)
Execute commands before cron interval is run.
Definition: Cron.php:104
execute(string $interval, \Elgg\Logger\Cron $cron_logger, string $filename, \DateTimeImmutable $time)
Execute handlers attached to a specific cron interval.
Definition: Cron.php:130
getLogFilename(string $interval, \DateTimeInterface $time)
Get a filename to log in.
Definition: Cron.php:299
logCompletion(string $interval)
Log the completion time of a cron interval.
Definition: Cron.php:342
getLogs(string $interval, bool $filenames_only=false)
Get the log files for a given cron interval.
Definition: Cron.php:203
getLastCompletion(string $interval)
Get the time of the last completion of a cron interval.
Definition: Cron.php:244
run(?array $intervals=null, bool $force=false)
Executes handlers for periods that have elapsed since last cron.
Definition: Cron.php:55
after(string $output, string $interval, \Elgg\Logger\Cron $cron_logger)
Printers handler result.
Definition: Cron.php:179
getConfiguredIntervals(bool $only_names=false)
Get the cron interval configuration.
Definition: Cron.php:276
__construct(protected EventsService $events, protected Translator $translator)
Constructor.
Definition: Cron.php:43
rotateLogs(string $interval)
Rotate the log files.
Definition: Cron.php:318
Events service.
A generic parent class for cron exceptions.
Extension of the DateTime class to support formatting a date using the locale.
Definition: DateTime.php:12
Logger.
Definition: Logger.php:26
$output
Definition: download.php:9
elgg_extract($key, $array, $default=null, bool $strict=true)
Checks for $array[$key] and returns its value if it exists, else returns $default.
Definition: elgglib.php:240
elgg_get_site_entity()
Get the current site entity.
Definition: entities.php:99
Generic interface which allows catching of all exceptions thrown in Elgg.
try
Definition: login_as.php:33
if(parse_url(elgg_get_site_url(), PHP_URL_PATH) !=='/') if(file_exists(elgg_get_root_path() . 'robots.txt'))
Set robots.txt.
Definition: robots.php:10