Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * Scheduled task abstract class.
19
 *
20
 * @package    core
21
 * @category   task
22
 * @copyright  2013 Damyon Wiese
23
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24
 */
25
namespace core\task;
26
 
27
/**
28
 * Abstract class defining a scheduled task.
29
 * @copyright  2013 Damyon Wiese
30
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
31
 */
32
abstract class scheduled_task extends task_base {
33
 
34
    /** Minimum minute value. */
35
    const MINUTEMIN = 0;
36
    /** Maximum minute value. */
37
    const MINUTEMAX = 59;
38
 
39
    /** Minimum hour value. */
40
    const HOURMIN = 0;
41
    /** Maximum hour value. */
42
    const HOURMAX = 23;
43
 
44
    /** Minimum day of month value. */
45
    const DAYMIN = 1;
46
    /** Maximum day of month value. */
47
    const DAYMAX = 31;
48
 
49
    /** Minimum month value. */
50
    const MONTHMIN = 1;
51
    /** Maximum month value. */
52
    const MONTHMAX = 12;
53
 
54
    /** Minimum dayofweek value. */
55
    const DAYOFWEEKMIN = 0;
56
    /** Maximum dayofweek value. */
57
    const DAYOFWEEKMAX = 6;
58
    /** Maximum dayofweek value allowed in input (7 = 0). */
59
    const DAYOFWEEKMAXINPUT = 7;
60
 
61
    /**
62
     * Minute field identifier.
63
     */
64
    const FIELD_MINUTE = 'minute';
65
    /**
66
     * Hour field identifier.
67
     */
68
    const FIELD_HOUR = 'hour';
69
    /**
70
     * Day-of-month field identifier.
71
     */
72
    const FIELD_DAY = 'day';
73
    /**
74
     * Month field identifier.
75
     */
76
    const FIELD_MONTH = 'month';
77
    /**
78
     * Day-of-week field identifier.
79
     */
80
    const FIELD_DAYOFWEEK = 'dayofweek';
81
 
82
    /**
83
     * Time used for the next scheduled time when a task should never run. This is 2222-01-01 00:00 GMT
84
     * which is a large time that still fits in 10 digits.
85
     */
86
    const NEVER_RUN_TIME = 7952342400;
87
 
88
    /** @var string $hour - Pattern to work out the valid hours */
89
    private $hour = '*';
90
 
91
    /** @var string $minute - Pattern to work out the valid minutes */
92
    private $minute = '*';
93
 
94
    /** @var string $day - Pattern to work out the valid days */
95
    private $day = '*';
96
 
97
    /** @var string $month - Pattern to work out the valid months */
98
    private $month = '*';
99
 
100
    /** @var string $dayofweek - Pattern to work out the valid dayofweek */
101
    private $dayofweek = '*';
102
 
103
    /** @var int $lastruntime - When this task was last run */
104
    private $lastruntime = 0;
105
 
106
    /** @var boolean $customised - Has this task been changed from it's default schedule? */
107
    private $customised = false;
108
 
109
    /** @var boolean $overridden - Does the task have values set VIA config? */
110
    private $overridden = false;
111
 
112
    /** @var int $disabled - Is this task disabled in cron? */
113
    private $disabled = false;
114
 
115
    /**
116
     * Get the last run time for this scheduled task.
117
     *
118
     * @return int
119
     */
120
    public function get_last_run_time() {
121
        return $this->lastruntime;
122
    }
123
 
124
    /**
125
     * Set the last run time for this scheduled task.
126
     *
127
     * @param int $lastruntime
128
     */
129
    public function set_last_run_time($lastruntime) {
130
        $this->lastruntime = $lastruntime;
131
    }
132
 
133
    /**
134
     * Has this task been changed from it's default config?
135
     *
136
     * @return bool
137
     */
138
    public function is_customised() {
139
        return $this->customised;
140
    }
141
 
142
    /**
143
     * Set customised for this scheduled task.
144
     *
145
     * @param bool
146
     */
147
    public function set_customised($customised) {
148
        $this->customised = $customised;
149
    }
150
 
151
    /**
152
     * Determine if this task is using its default configuration changed from the default. Returns true
153
     * if it is and false otherwise. Does not rely on the customised field.
154
     *
155
     * @return bool
156
     */
157
    public function has_default_configuration(): bool {
158
        $defaulttask = \core\task\manager::get_default_scheduled_task($this::class);
159
        if ($defaulttask->get_minute() !== $this->get_minute()) {
160
            return false;
161
        }
162
        if ($defaulttask->get_hour() != $this->get_hour()) {
163
            return false;
164
        }
165
        if ($defaulttask->get_month() != $this->get_month()) {
166
            return false;
167
        }
168
        if ($defaulttask->get_day_of_week() != $this->get_day_of_week()) {
169
            return false;
170
        }
171
        if ($defaulttask->get_day() != $this->get_day()) {
172
            return false;
173
        }
174
        if ($defaulttask->get_disabled() != $this->get_disabled()) {
175
            return false;
176
        }
177
        return true;
178
    }
179
 
180
    /**
181
     * Disable the task.
182
     */
183
    public function disable(): void {
184
        $this->set_disabled(true);
185
        $this->set_customised(!$this->has_default_configuration());
186
        \core\task\manager::configure_scheduled_task($this);
187
    }
188
 
189
    /**
190
     * Enable the task.
191
     */
192
    public function enable(): void {
193
        $this->set_disabled(false);
194
        $this->set_customised(!$this->has_default_configuration());
195
        \core\task\manager::configure_scheduled_task($this);
196
    }
197
 
198
    /**
199
     * Has this task been changed from it's default config?
200
     *
201
     * @return bool
202
     */
203
    public function is_overridden(): bool {
204
        return $this->overridden;
205
    }
206
 
207
    /**
208
     * Set the overridden value.
209
     *
210
     * @param bool $overridden
211
     */
212
    public function set_overridden(bool $overridden): void {
213
        $this->overridden = $overridden;
214
    }
215
 
216
    /**
217
     * Setter for $minute. Accepts a special 'R' value
218
     * which will be translated to a random minute.
219
     *
220
     * @param string $minute
221
     * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
222
     *      If false, they are left as 'R'
223
     */
224
    public function set_minute($minute, $expandr = true) {
225
        if ($minute === 'R' && $expandr) {
226
            $minute = mt_rand(self::MINUTEMIN, self::MINUTEMAX);
227
        }
228
        $this->minute = $minute;
229
    }
230
 
231
    /**
232
     * Getter for $minute.
233
     *
234
     * @return string
235
     */
236
    public function get_minute() {
237
        return $this->minute;
238
    }
239
 
240
    /**
241
     * Setter for $hour. Accepts a special 'R' value
242
     * which will be translated to a random hour.
243
     *
244
     * @param string $hour
245
     * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
246
     *      If false, they are left as 'R'
247
     */
248
    public function set_hour($hour, $expandr = true) {
249
        if ($hour === 'R' && $expandr) {
250
            $hour = mt_rand(self::HOURMIN, self::HOURMAX);
251
        }
252
        $this->hour = $hour;
253
    }
254
 
255
    /**
256
     * Getter for $hour.
257
     *
258
     * @return string
259
     */
260
    public function get_hour() {
261
        return $this->hour;
262
    }
263
 
264
    /**
265
     * Setter for $month.
266
     *
267
     * @param string $month
268
     */
269
    public function set_month($month) {
270
        $this->month = $month;
271
    }
272
 
273
    /**
274
     * Getter for $month.
275
     *
276
     * @return string
277
     */
278
    public function get_month() {
279
        return $this->month;
280
    }
281
 
282
    /**
283
     * Setter for $day.
284
     *
285
     * @param string $day
286
     */
287
    public function set_day($day) {
288
        $this->day = $day;
289
    }
290
 
291
    /**
292
     * Getter for $day.
293
     *
294
     * @return string
295
     */
296
    public function get_day() {
297
        return $this->day;
298
    }
299
 
300
    /**
301
     * Setter for $dayofweek.
302
     *
303
     * @param string $dayofweek
304
     * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
305
     *      If false, they are left as 'R'
306
     */
307
    public function set_day_of_week($dayofweek, $expandr = true) {
308
        if ($dayofweek === 'R' && $expandr) {
309
            $dayofweek = mt_rand(self::DAYOFWEEKMIN, self::DAYOFWEEKMAX);
310
        }
311
        $this->dayofweek = $dayofweek;
312
    }
313
 
314
    /**
315
     * Getter for $dayofweek.
316
     *
317
     * @return string
318
     */
319
    public function get_day_of_week() {
320
        return $this->dayofweek;
321
    }
322
 
323
    /**
324
     * Setter for $disabled.
325
     *
326
     * @param bool $disabled
327
     */
328
    public function set_disabled($disabled) {
329
        $this->disabled = (bool)$disabled;
330
    }
331
 
332
    /**
333
     * Getter for $disabled.
334
     * @return bool
335
     */
336
    public function get_disabled() {
337
        return $this->disabled;
338
    }
339
 
340
    /**
341
     * Override this function if you want this scheduled task to run, even if the component is disabled.
342
     *
343
     * @return bool
344
     */
345
    public function get_run_if_component_disabled() {
346
        return false;
347
    }
348
 
349
    /**
350
     * Informs whether the given field is valid.
351
     * Use the constants FIELD_* to identify the field.
352
     * Have to be called after the method set_{field}(string).
353
     *
354
     * @param string $field field identifier; expected values from constants FIELD_*.
355
     *
356
     * @return bool true if given field is valid. false otherwise.
357
     */
358
    public function is_valid(string $field): bool {
359
        return !empty($this->get_valid($field));
360
    }
361
 
362
    /**
363
     * Calculates the list of valid values according to the given field and stored expression.
364
     *
365
     * @param string $field field identifier. Must be one of those FIELD_*.
366
     *
367
     * @return array(int) list of matching values.
368
     *
369
     * @throws \coding_exception when passed an invalid field identifier.
370
     */
371
    private function get_valid(string $field): array {
372
        switch($field) {
373
            case self::FIELD_MINUTE:
374
                $min = self::MINUTEMIN;
375
                $max = self::MINUTEMAX;
376
                break;
377
            case self::FIELD_HOUR:
378
                $min = self::HOURMIN;
379
                $max = self::HOURMAX;
380
                break;
381
            case self::FIELD_DAY:
382
                $min = self::DAYMIN;
383
                $max = self::DAYMAX;
384
                break;
385
            case self::FIELD_MONTH:
386
                $min = self::MONTHMIN;
387
                $max = self::MONTHMAX;
388
                break;
389
            case self::FIELD_DAYOFWEEK:
390
                $min = self::DAYOFWEEKMIN;
391
                $max = self::DAYOFWEEKMAXINPUT;
392
                break;
393
            default:
394
                throw new \coding_exception("Field '$field' is not a valid crontab identifier.");
395
        }
396
 
397
        $result = $this->eval_cron_field($this->{$field}, $min, $max);
398
        if ($field === self::FIELD_DAYOFWEEK) {
399
            // For day of week, 0 and 7 both mean Sunday; if there is a 7 we set 0. The result array is sorted.
400
            if (end($result) === 7) {
401
                // Remove last element.
402
                array_pop($result);
403
                // Insert 0 as first element if it's not there already.
404
                if (reset($result) !== 0) {
405
                    array_unshift($result, 0);
406
                }
407
            }
408
        }
409
        return $result;
410
    }
411
 
412
    /**
413
     * Take a cron field definition and return an array of valid numbers with the range min-max.
414
     *
415
     * @param string $field - The field definition.
416
     * @param int $min - The minimum allowable value.
417
     * @param int $max - The maximum allowable value.
418
     * @return array(int)
419
     */
420
    public function eval_cron_field($field, $min, $max) {
421
        // Cleanse the input.
422
        $field = trim($field);
423
 
424
        // Format for a field is:
425
        // <fieldlist> := <range>(/<step>)(,<fieldlist>)
426
        // <step>  := int
427
        // <range> := <any>|<int>|<min-max>
428
        // <any>   := *
429
        // <min-max> := int-int
430
        // End of format BNF.
431
 
432
        // This function is complicated but is covered by unit tests.
433
        $range = array();
434
 
435
        $matches = array();
436
        preg_match_all('@[0-9]+|\*|,|/|-@', $field, $matches);
437
 
438
        $last = 0;
439
        $inrange = false;
440
        $instep = false;
441
        foreach ($matches[0] as $match) {
442
            if ($match == '*') {
443
                array_push($range, range($min, $max));
444
            } else if ($match == '/') {
445
                $instep = true;
446
            } else if ($match == '-') {
447
                $inrange = true;
448
            } else if (is_numeric($match)) {
449
                if ($min > $match || $match > $max) {
450
                    // This is a value error: The value lays out of the expected range of values.
451
                    return [];
452
                }
453
                if ($instep) {
454
                    // Normalise range property, account for "5/10".
455
                    $insteprange = $range[count($range) - 1];
456
                    if (!is_array($insteprange)) {
457
                        $range[count($range) - 1] = range($insteprange, $max);
458
                    }
459
                    for ($i = 0; $i < count($range[count($range) - 1]); $i++) {
460
                        if (($i) % $match != 0) {
461
                            $range[count($range) - 1][$i] = -1;
462
                        }
463
                    }
464
                    $instep = false;
465
                } else if ($inrange) {
466
                    if (count($range)) {
467
                        $range[count($range) - 1] = range($last, $match);
468
                    }
469
                    $inrange = false;
470
                } else {
471
                    array_push($range, $match);
472
                    $last = $match;
473
                }
474
            }
475
        }
476
 
477
        // If inrange or instep were not processed, there is a syntax error.
478
        // Cleanup any existing values to show up the error.
479
        if ($inrange || $instep) {
480
            return [];
481
        }
482
 
483
        // Flatten the result.
484
        $result = array();
485
        foreach ($range as $r) {
486
            if (is_array($r)) {
487
                foreach ($r as $rr) {
488
                    if ($rr >= $min && $rr <= $max) {
489
                        $result[$rr] = 1;
490
                    }
491
                }
492
            } else if (is_numeric($r)) {
493
                if ($r >= $min && $r <= $max) {
494
                    $result[$r] = 1;
495
                }
496
            }
497
        }
498
        $result = array_keys($result);
499
        sort($result, SORT_NUMERIC);
500
        return $result;
501
    }
502
 
503
    /**
504
     * Assuming $list is an ordered list of items, this function returns the item
505
     * in the list that is greater than or equal to the current value (or 0). If
506
     * no value is greater than or equal, this will return the first valid item in the list.
507
     * If list is empty, this function will return 0.
508
     *
509
     * @param int $current The current value
510
     * @param int[] $list The list of valid items.
511
     * @return int $next.
512
     */
513
    private function next_in_list($current, $list) {
514
        foreach ($list as $l) {
515
            if ($l >= $current) {
516
                return $l;
517
            }
518
        }
519
        if (count($list)) {
520
            return $list[0];
521
        }
522
 
523
        return 0;
524
    }
525
 
526
    /**
527
     * Calculate when this task should next be run based on the schedule.
528
     *
529
     * @param int $now Current time, for testing (leave 0 to use default time)
530
     * @return int $nextruntime.
531
     */
532
    public function get_next_scheduled_time(int $now = 0): int {
533
        if (!$now) {
534
            $now = time();
535
        }
536
 
537
        // We need to change to the server timezone before using php date() functions.
538
        \core_date::set_default_server_timezone();
539
 
540
        $validminutes = $this->get_valid(self::FIELD_MINUTE);
541
        $validhours = $this->get_valid(self::FIELD_HOUR);
542
        $validdays = $this->get_valid(self::FIELD_DAY);
543
        $validdaysofweek = $this->get_valid(self::FIELD_DAYOFWEEK);
544
        $validmonths = $this->get_valid(self::FIELD_MONTH);
545
 
546
        // If any of the fields contain no valid data then the task will never run.
547
        if (!$validminutes || !$validhours || !$validdays || !$validdaysofweek || !$validmonths) {
548
            return self::NEVER_RUN_TIME;
549
        }
550
 
551
        $result = self::get_next_scheduled_time_inner($now, $validminutes, $validhours, $validdays, $validdaysofweek, $validmonths);
552
        return $result;
553
    }
554
 
555
    /**
556
     * Recursively calculate the next valid time for this task.
557
     *
558
     * @param int $now Start time
559
     * @param array $validminutes Valid minutes
560
     * @param array $validhours Valid hours
561
     * @param array $validdays Valid days
562
     * @param array $validdaysofweek Valid days of week
563
     * @param array $validmonths Valid months
564
     * @param int $originalyear Zero for first call, original year for recursive calls
565
     * @return int Next run time
566
     */
567
    protected function get_next_scheduled_time_inner(int $now, array $validminutes, array $validhours,
568
            array $validdays, array $validdaysofweek, array $validmonths, int $originalyear = 0) {
569
        $currentyear = (int)date('Y', $now);
570
        if ($originalyear) {
571
            // In recursive calls, check we didn't go more than 8 years ahead, that indicates the
572
            // user has chosen an impossible date. 8 years is the maximum time, considering a task
573
            // set to run on 29 February over a century boundary when a leap year is skipped.
574
            if ($currentyear - $originalyear > 8) {
575
                // Use this time if it's never going to happen.
576
                return self::NEVER_RUN_TIME;
577
            }
578
            $firstyear = $originalyear;
579
        } else {
580
            $firstyear = $currentyear;
581
        }
582
        $currentmonth = (int)date('n', $now);
583
 
584
        // Evaluate month first.
585
        $nextvalidmonth = $this->next_in_list($currentmonth, $validmonths);
586
        if ($nextvalidmonth < $currentmonth) {
587
            $currentyear += 1;
588
        }
589
        // If we moved to another month, set the current time to start of month, and restart calculations.
590
        if ($nextvalidmonth !== $currentmonth) {
591
            $newtime = strtotime($currentyear . '-' . $nextvalidmonth . '-01 00:00');
592
            return $this->get_next_scheduled_time_inner($newtime, $validminutes, $validhours, $validdays,
593
                    $validdaysofweek, $validmonths, $firstyear);
594
        }
595
 
596
        // Special handling for dayofmonth vs dayofweek (see man 5 cron). If both are specified, then
597
        // it is ok to continue when either matches. If only one is specified then it must match.
598
        $currentday = (int)date("j", $now);
599
        $currentdayofweek = (int)date("w", $now);
600
        $nextvaliddayofmonth = self::next_in_list($currentday, $validdays);
601
        $nextvaliddayofweek = self::next_in_list($currentdayofweek, $validdaysofweek);
602
        $daysincrementbymonth = $nextvaliddayofmonth - $currentday;
603
        $daysinmonth = (int)date('t', $now);
604
        if ($nextvaliddayofmonth < $currentday) {
605
            $daysincrementbymonth += $daysinmonth;
606
        }
607
 
608
        $daysincrementbyweek = $nextvaliddayofweek - $currentdayofweek;
609
        if ($nextvaliddayofweek < $currentdayofweek) {
610
            $daysincrementbyweek += 7;
611
        }
612
 
613
        if ($this->dayofweek == '*') {
614
            $daysincrement = $daysincrementbymonth;
615
        } else if ($this->day == '*') {
616
            $daysincrement = $daysincrementbyweek;
617
        } else {
618
            // Take the smaller increment of days by month or week.
619
            $daysincrement = min($daysincrementbymonth, $daysincrementbyweek);
620
        }
621
 
622
        // If we moved day, recurse using new start time.
623
        if ($daysincrement != 0) {
624
            $newtime = strtotime($currentyear . '-' . $currentmonth . '-' . $currentday .
625
                    ' 00:00 +' . $daysincrement . ' days');
626
            return $this->get_next_scheduled_time_inner($newtime, $validminutes, $validhours, $validdays,
627
                    $validdaysofweek, $validmonths, $firstyear);
628
        }
629
 
630
        $currenthour = (int)date('H', $now);
631
        $nextvalidhour = $this->next_in_list($currenthour, $validhours);
632
        if ($nextvalidhour != $currenthour) {
633
            if ($nextvalidhour < $currenthour) {
634
                $offset = ' +1 day';
635
            } else {
636
                $offset = '';
637
            }
638
            $newtime = strtotime($currentyear . '-' . $currentmonth . '-' . $currentday . ' ' . $nextvalidhour .
639
                    ':00' . $offset);
640
            return $this->get_next_scheduled_time_inner($newtime, $validminutes, $validhours, $validdays,
641
                $validdaysofweek, $validmonths, $firstyear);
642
        }
643
 
644
        // Round time down to an exact minute because we need to use numeric calculations on it now.
645
        // If we construct times based on all the components, it will mess up around DST changes
646
        // (because there are two times with the same representation).
647
        $now = intdiv($now, 60) * 60;
648
 
649
        $currentminute = (int)date('i', $now);
650
        $nextvalidminute = $this->next_in_list($currentminute, $validminutes);
651
        if ($nextvalidminute == $currentminute && !$originalyear) {
652
            // This is not a recursive call so time has not moved on at all yet. We can't use the
653
            // same minute as now because it has already happened, it has to be at least one minute
654
            // later, so update time and retry.
655
            $newtime = $now + 60;
656
            return $this->get_next_scheduled_time_inner($newtime, $validminutes, $validhours, $validdays,
657
                $validdaysofweek, $validmonths, $firstyear);
658
        }
659
 
660
        if ($nextvalidminute < $currentminute) {
661
            // The time is in the next hour so we need to recurse. Don't use strtotime at this
662
            // point because it will mess up around DST changes.
663
            $minutesforward = $nextvalidminute + 60 - $currentminute;
664
            $newtime = $now + $minutesforward * 60;
665
            return $this->get_next_scheduled_time_inner($newtime, $validminutes, $validhours, $validdays,
666
                $validdaysofweek, $validmonths, $firstyear);
667
        }
668
 
669
        // The next valid minute is in the same hour so it must be valid according to all other
670
        // checks and we can finally return it.
671
        return $now + ($nextvalidminute - $currentminute) * 60;
672
    }
673
 
674
    /**
675
     * Informs whether this task can be run.
676
     *
677
     * @return bool true when this task can be run. false otherwise.
678
     */
679
    public function can_run(): bool {
680
        return $this->is_component_enabled() || $this->get_run_if_component_disabled();
681
    }
682
 
683
    /**
684
     * Checks whether the component and the task disabled flag enables to run this task.
685
     * This do not checks whether the task manager allows running them or if the
686
     * site allows tasks to "run now".
687
     *
688
     * @return bool true if task is enabled. false otherwise.
689
     */
690
    public function is_enabled(): bool {
691
        return $this->can_run() && !$this->get_disabled();
692
    }
693
 
694
    /**
695
     * Produces a valid id string to use as id attribute based on the given FQCN class name.
696
     *
697
     * @param string $classname FQCN of a task.
698
     * @return string valid string to be used as id attribute.
699
     */
700
    public static function get_html_id(string $classname): string {
701
        return str_replace('\\', '-', ltrim($classname, '\\'));
702
    }
703
}