1 <?php
2
3 namespace Alo;
4
5 use Alo\Exception\CronException as CE;
6 use Alo\Exception\OSException as OS;
7 use Log;
8
9 if (!defined('GEN_START')) {
10 http_response_code(404);
11 } else {
12
13 /**
14 * The crontab editor class. You must call the commit() method to save your changes.
15 *
16 * @author Art <a.molcanovas@gmail.com>
17 */
18 class Cron {
19
20 /**
21 * Defines the day of the week as Sunday
22 *
23 * @var int
24 */
25 const WEEKDAY_SUN = 0;
26 /**
27 * Defines the day of the week as Monday
28 *
29 * @var int
30 */
31 const WEEKDAY_MON = 1;
32 /**
33 * Defines the day of the week as Tuesday
34 *
35 * @var int
36 */
37 const WEEKDAY_TUE = 2;
38 /**
39 * Defines the day of the week as Wednesday
40 *
41 * @var int
42 */
43 const WEEKDAY_WED = 3;
44 /**
45 * Defines the day of the week as Thursday
46 *
47 * @var int
48 */
49 const WEEKDAY_THU = 4;
50 /**
51 * Defines the day of the week as Friday
52 *
53 * @var int
54 */
55 const WEEKDAY_FRI = 5;
56 /**
57 * Defines the day of the week as Saturday
58 *
59 * @var int
60 */
61 const WEEKDAY_SAT = 6;
62 /**
63 * A pre-setting to run the cronjob yearly at 1 Jan, 00:00
64 *
65 * @var string
66 */
67 const CONST_YEARLY = '0 0 1 1 *';
68 /**
69 * A pre-setting to run the cronjob monthly at 00:00
70 *
71 * @var string
72 */
73 const CONST_MONTHLY = '0 0 1 * *';
74 /**
75 * A pre-setting to run the cronjob weekly on Sunday, 00:00
76 *
77 * @var string
78 */
79 const CONST_WEEKLY = '0 0 * * 0';
80 /**
81 * A pre-setting to run the cronjob daily at 00:00
82 *
83 * @var string
84 */
85 const CONST_DAILY = '0 0 * * *';
86 /**
87 * A pre-setting to run the cronjob hourly at 00 minutes
88 *
89 * @var string
90 */
91 const CONST_HOURLY = '0 * * * *';
92 /**
93 * A pre-setting to run the cronjob on server startup
94 *
95 * @var string
96 */
97 const CONST_REBOOT = '@reboot';
98 /**
99 * Defines the month as January
100 *
101 * @var int
102 */
103 const MONTH_JAN = 1;
104 /**
105 * Defines the month as February
106 *
107 * @var int
108 */
109 const MONTH_FEB = 2;
110 /**
111 * Defines the month as March
112 *
113 * @var int
114 */
115 const MONTH_MAR = 3;
116 /**
117 * Defines the month as April
118 *
119 * @var int
120 */
121 const MONTH_APR = 4;
122 /**
123 * Defines the month as May
124 *
125 * @var int
126 */
127 const MONTH_MAY = 5;
128 /**
129 * Defines the month as June
130 *
131 * @var int
132 */
133 const MONTH_JUN = 6;
134 /**
135 * Defines the month as July
136 *
137 * @var int
138 */
139 const MONTH_JUL = 7;
140 /**
141 * Defines the month as August
142 *
143 * @var int
144 */
145 const MONTH_AUG = 8;
146 /**
147 * Defines the month as September
148 *
149 * @var int
150 */
151 const MONTH_SEP = 9;
152 /**
153 * Defines the month as October
154 *
155 * @var int
156 */
157 const MONTH_OCT = 10;
158 /**
159 * Defines the month as November
160 *
161 * @var int
162 */
163 const MONTH_NOV = 11;
164 /**
165 * Defines the month as December
166 *
167 * @var int
168 */
169 const MONTH_DEC = 12;
170 /**
171 * Static reference to the last instance of the class
172 *
173 * @var Cron
174 */
175 static $this;
176 /**
177 * Array of valid CRON constants
178 *
179 * @var array
180 */
181 protected static $validConstants = ['@yearly',
182 '@annually',
183 '@monthly',
184 '@weekly',
185 '@daily',
186 '@hourly',
187 '@reboot'];
188
189 /**
190 * The current crontab data
191 *
192 * @var array
193 */
194 protected $crontab;
195
196 /**
197 * Whether changes should be autocommited automatically
198 *
199 * @var bool
200 */
201 protected $autocommit;
202
203 /**
204 * Instantiates the crontab handler
205 *
206 * @author Art <a.molcanovas@gmail.com>
207 * @throws OS When the machine is running Windows
208 */
209 function __construct() {
210 if (\serverIsWindows()) {
211 throw new OS('Windows does not support cron!', OS::E_UNSUPPORTED);
212 } else {
213 $this->autocommit = false;
214 $this->reloadCrontab();
215 }
216 self::$this = &$this;
217 }
218
219 /**
220 * (re)loads the cron job array
221 *
222 * @author Art <a.molcanovas@gmail.com>
223 * @return Cron
224 */
225 function reloadCrontab() {
226 $this->crontab = shell_exec('crontab -l');
227
228 if ($this->crontab) {
229 $this->crontab = trim($this->crontab);
230 $this->crontab = explode("\n", $this->crontab);
231
232 //Make sure it's really empty
233 $lastIndex = count($this->crontab) - 1;
234
235 while ($lastIndex >= 0 && !$this->crontab[$lastIndex]) {
236 unset($this->crontab[$lastIndex]);
237 $lastIndex--;
238 }
239 } else {
240 $this->crontab = [];
241 }
242
243 Log::debug('(Re)loaded crontab contents');
244
245 return $this;
246 }
247
248 /**
249 * Instantiates the crontab handler
250 *
251 * @author Art <a.molcanovas@gmail.com>
252 * @throws OS When the machine is running Windows
253 * @return Cron
254 */
255 static function cron() {
256 return new Cron();
257 }
258
259 /**
260 * Edits the cron job at index $index. Can be substituded with the full
261 * CRON expression (schedule + command) to perform a search -
262 * use with caution!
263 *
264 * @author Art <a.molcanovas@gmail.com>
265 *
266 * @param int $index The job index in the crontab array
267 * @param string $command The command to run
268 * @param int|string $minute The minute parameter
269 * @param int|string $hour The hour parameter
270 * @param int|string $day The day of the month parameter
271 * @param int|string $month The month parameter
272 * @param int|string $weekday The day of the week parameter
273 *
274 * @throws CE When the minute expression is invalid
275 * @throws CE When the parameters are invalid
276 * @throws CE When one or more parameters are non-scalar
277 * @return Cron
278 */
279 function editCronJob($index, $command, $minute = '*', $hour = '*', $day = '*', $month = '*',
280 $weekday = '*') {
281 if (!is_numeric($index)) {
282 $search = array_search($index, $this->crontab);
283
284 if ($search !== false) {
285 $index = $search;
286 }
287 }
288
289 return $this->editCrontab($index, $command, $minute, $hour, $day, $month, $weekday);
290 }
291
292 /**
293 * Performs modifiation on the crontab file
294 *
295 * @author Art <a.molcanovas@gmail.com>
296 *
297 * @param int $index The job index in the crontab array
298 * @param string $command The command to run
299 * @param int|string $minute The minute parameter
300 * @param int|string $hour The hour parameter
301 * @param int|string $day The day of the month parameter
302 * @param int|string $month The month parameter
303 * @param int|string $weekday The day of the week parameter
304 *
305 * @return Cron
306 * @throws CE When the minute expression is invalid
307 * @throws CE When the parameters are invalid
308 * @throws CE When one or more parameters are non-scalar
309 */
310 protected function editCrontab($index, $command, $minute = '*', $hour = '*', $day = '*', $month = '*',
311 $weekday = '*') {
312
313 if (!is_scalar($command) || !is_scalar($minute) || !is_scalar($hour) || !is_scalar($day) ||
314 !is_scalar($month) || !is_scalar($weekday)
315 ) {
316 throw new CE('All cron attributes must be scalar!', CE::E_ARGS_NONSCALAR);
317 } elseif (!self::formatOK($minute, $hour, $day, $month, $weekday)) {
318 throw new CE('Invalid schedule parameters: ' . json_encode(['minute/constant' => $minute,
319 'hour' => $hour,
320 'day' => $day,
321 'month' => $month,
322 'weekday' => $weekday,
323 'cmd' => $command,
324 'index' => $index]),
325 CE::E_INVALID_EXPR);
326 } else {
327 $add = $minute . ' ' . $hour . ' ' . $day . ' ' . $month . ' ' . $weekday . ' ' . $command;
328
329 if ($index === null) {
330 $this->crontab[] = $add;
331 Log::debug('Appended crontab with ' . $add);
332 } else {
333 $this->crontab[$index] = $add;
334 Log::debug('Edited crontab index ' . $index . ' with ' . $add);
335 }
336
337 if ($this->autocommit) {
338 $this->commit();
339 }
340 }
341
342 return $this;
343 }
344
345 /**
346 * Checks whether all the fields are formatted properly
347 *
348 * @author Art <a.molcanovas@gmail.com>
349 *
350 * @param int|string $min The minute parameter
351 * @param int|string $hour The hour parameter
352 * @param int|string $day The day of the month parameter
353 * @param int|string $month The month parameter
354 * @param int|string $weekday The day of the week parameter
355 *
356 * @return boolean
357 */
358 protected static function formatOK($min, $hour, $day, $month, $weekday) {
359 $patMinHMth = '/^(\*|[0-9]{1,2}|\*\/[0-9]{1,2}|[0-9,]+|[0-9\-]+)$/';
360 $patDay = '/^(\*|[0-9]{1,2}|[0-9]{1,2}(L|W)|\*\/[0-9]{1,2}|[0-9,]+|[0-9\-]+)$/';
361 $patWeekday = '/^(\*|[0-9]{1,2}|[0-9]{1,2}L|\*\/[0-9]{1,2}|[0-9,]+|[0-9\-]+)$/';
362
363 return preg_match($patMinHMth, $min) && preg_match($patMinHMth, $hour) &&
364 preg_match($patMinHMth, $month) && preg_match($patDay, $day) &&
365 preg_match($patWeekday, $weekday);
366 }
367
368 /**
369 * Saves any changes made
370 *
371 * @author Art <a.molcanovas@gmail.com>
372 * @return bool
373 */
374 function commit() {
375 $commit = trim(implode("\n", $this->crontab)) . "\n";
376 $path = DIR_TMP . 'crontab.txt';
377 file_put_contents($path, $commit);
378
379 if (file_exists($path)) {
380 $exec = shell_exec('crontab "' . $path . '"');
381 echo $exec;
382 unlink($path);
383 Log::debug('Crontab change output: ' . $exec);
384
385 return true;
386 } else {
387 trigger_error(Log::error('Failed to save crontab: temporary file could not be created.'),
388 E_USER_WARNING);
389 }
390
391 return false;
392 }
393
394 /**
395 * Appends the crontab file
396 *
397 * @author Art <a.molcanovas@gmail.com>
398 *
399 * @param string $command The command to run
400 * @param int|string $minuteConst The minute parameter
401 * @param int|string $hour The hour parameter
402 * @param int|string $day The day of the month parameter
403 * @param int|string $month The month parameter
404 * @param int|string $weekday The day of the week parameter
405 *
406 * @throws CE When the minute expression is invalid
407 * @throws CE When the parameters are invalid
408 * @throws CE When one or more parameters are non-scalar
409 * @return Cron
410 */
411 function appendCrontab($command, $minuteConst = '*', $hour = '*', $day = '*', $month = '*',
412 $weekday = '*') {
413 return $this->editCrontab(null, $command, $minuteConst, $hour, $day, $month, $weekday);
414 }
415
416 /**
417 * Removes the cron job @ index $index
418 *
419 * @author Art <a.molcanovas@gmail.com>
420 *
421 * @param int $index The cron job's index in the array
422 *
423 * @return Cron
424 */
425 function deleteJob($index) {
426 unset($this->crontab[$index]);
427 Log::debug('Deleted crontab entry @ index ' . $index);
428
429 if ($this->autocommit) {
430 $this->commit();
431 }
432
433 return $this;
434 }
435
436 /**
437 * Clears the crontab
438 *
439 * @author Art <a.molcanovas@gmail.com>
440 * @return Cron
441 */
442 function clearCrontab() {
443 $this->crontab = [];
444
445 if ($this->autocommit) {
446 $this->commit();
447 }
448
449 return $this;
450 }
451
452 /**
453 * If no parameter is passed or the parameter isn't TRUE/FALSE, returns the current autocommit setting, otherwise
454 * sets it. Use with caution!
455 *
456 * @author Art <a.molcanovas@gmail.com>
457 *
458 * @param bool|null $set The desired setting if changing
459 *
460 * @return Cron|bool $this if not changing the autocommit value or the value otherwise
461 */
462 function autocommit($set = null) {
463 if (is_bool($set)) {
464 $this->autocommit = $set;
465
466 return $this;
467 } else {
468 return $this->autocommit;
469 }
470 }
471
472 /**
473 * Returns crontab entry at index $i
474 *
475 * @author Art <a.molcanovas@gmail.com>
476 *
477 * @param int $i The index
478 *
479 * @return null|string
480 */
481 function getAtIndex($i) {
482 return \get($this->crontab[$i]);
483 }
484
485 /**
486 * Returns the crontab array
487 *
488 * @author Art <a.molcanovas@gmail.com>
489 * @return array
490 */
491 function getCrontab() {
492 return $this->crontab;
493 }
494
495 /**
496 * Returns a string representation of the object data
497 *
498 * @author Art <a.molcanovas@gmail.com>
499 * @return string
500 */
501 function __toString() {
502 return \debugLite($this);
503 }
504
505 }
506 }
507