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