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  ]);
153  } catch (\Throwable $t) {
154  $this->getLogger()->error($t);
155  }
156 
157  $now = new DateTime();
158 
159  $complete = $this->translator->translate('admin:cron:complete', [$interval, $now->format(DATE_RFC2822)]);
160  $cron_logger->notice($complete);
161 
162  if (file_exists($filename) && is_readable($filename)) {
163  return file_get_contents($filename);
164  }
165 
166  return '';
167  }
168 
178  protected function after(string $output, string $interval, \Elgg\Logger\Cron $cron_logger): void {
179  $this->getLogger()->info($output);
180 
181  try {
182  $this->events->triggerAfter('cron', $interval, new \DateTime());
183  } catch (\Throwable $t) {
184  $this->getLogger()->error($t);
185  }
186 
187  $cron_logger->close();
188  $this->rotateLogs($interval);
189  $this->logCompletion($interval);
190  }
191 
202  public function getLogs(string $interval, bool $filenames_only = false): array {
203  $fh = new \ElggFile();
204  $fh->owner_guid = elgg_get_site_entity()->guid;
205  $fh->setFilename("cron/{$interval}/dummy.log");
206 
207  $dir = pathinfo($fh->getFilenameOnFilestore(), PATHINFO_DIRNAME);
208  if (!is_dir($dir) || !is_readable($dir)) {
209  return [];
210  }
211 
212  $dh = new \DirectoryIterator($dir);
213  $files = [];
214  /* @var $file \DirectoryIterator */
215  foreach ($dh as $file) {
216  if ($file->isDot() || !$file->isFile() || $file->getExtension() !== 'log') {
217  continue;
218  }
219 
220  if ($filenames_only) {
221  $files[] = $file->getFilename();
222  } else {
223  $files[$file->getFilename()] = file_get_contents($file->getPathname());
224  }
225  }
226 
227  if ($filenames_only) {
228  natcasesort($files);
229  } else {
230  uksort($files, 'strnatcasecmp');
231  }
232 
233  return array_reverse($files);
234  }
235 
243  public function getLastCompletion(string $interval): ?DateTime {
244  $fh = new \ElggFile();
245  $fh->owner_guid = elgg_get_site_entity()->guid;
246  $fh->setFilename("cron/{$interval}.complete");
247 
248  if (!$fh->exists()) {
249  return null;
250  }
251 
252  $date = $fh->grabFile();
253  if (empty($date)) {
254  // how??
255  return null;
256  }
257 
258  try {
259  return Values::normalizeTime($date);
260  } catch (\Elgg\Exceptions\ExceptionInterface $e) {
261  $this->getLogger()->warning($e);
262  }
263 
264  return null;
265  }
266 
275  public function getConfiguredIntervals(bool $only_names = false): array {
276  $result = $this->events->triggerResults('cron:intervals', 'system', [], $this->default_intervals);
277  if (!is_array($result)) {
278  $this->getLogger()->warning("The event 'cron:intervals', 'system' should return an array, " . gettype($result) . ' given');
279 
280  $result = $this->default_intervals;
281  }
282 
283  if ($only_names) {
284  return array_keys($result);
285  }
286 
287  return $result;
288  }
289 
298  protected function getLogFilename(string $interval, \DateTimeInterface $time): string {
299  $date = $time->format(\DateTimeInterface::ATOM);
300  $date = str_replace('+', 'p', $date);
301  $date = preg_replace('/[^a-zA-Z0-9_-]+/', '-', $date);
302 
303  $fh = new \ElggFile();
304  $fh->owner_guid = elgg_get_site_entity()->guid;
305  $fh->setFilename("cron/{$interval}/{$date}.log");
306 
307  return $fh->getFilenameOnFilestore();
308  }
309 
317  protected function rotateLogs(string $interval): void {
318  $files = $this->getLogs($interval, true);
319  if (count($files) <= self::LOG_FILES_TO_KEEP) {
320  return;
321  }
322 
323  $fh = new \ElggFile();
324  $fh->owner_guid = elgg_get_site_entity()->guid;
325 
326  while (count($files) > self::LOG_FILES_TO_KEEP) {
327  $filename = array_pop($files);
328 
329  $fh->setFilename("cron/{$interval}/{$filename}");
330  $fh->delete();
331  }
332  }
333 
341  protected function logCompletion(string $interval): void {
342  $fh = new \ElggFile();
343  $fh->owner_guid = elgg_get_site_entity()->guid;
344  $fh->setFilename("cron/{$interval}.complete");
345 
346  try {
347  if ($fh->open('write') === false) {
348  return;
349  }
350  } catch (\Elgg\Exceptions\ExceptionInterface $e) {
351  $this->getLogger()->warning($e);
352  return;
353  }
354 
355  $now = new DateTime();
356  $fh->write($now->format(\DateTimeInterface::ATOM));
357  $fh->close();
358  }
359 }
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:298
logCompletion(string $interval)
Log the completion time of a cron interval.
Definition: Cron.php:341
getLogs(string $interval, bool $filenames_only=false)
Get the log files for a given cron interval.
Definition: Cron.php:202
getLastCompletion(string $interval)
Get the time of the last completion of a cron interval.
Definition: Cron.php:243
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:178
getConfiguredIntervals(bool $only_names=false)
Get the cron interval configuration.
Definition: Cron.php:275
__construct(protected EventsService $events, protected Translator $translator)
Constructor.
Definition: Cron.php:43
rotateLogs(string $interval)
Rotate the log files.
Definition: Cron.php:317
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:256
elgg_get_site_entity()
Get the current site entity.
Definition: entities.php:101
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