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
 * Defines classes used for updates.
19
 *
20
 * @package    core
21
 * @copyright  2011 David Mudrak <david@moodle.com>
22
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
namespace core\update;
25
 
26
use html_writer, coding_exception, core_component;
27
 
28
defined('MOODLE_INTERNAL') || die();
29
 
30
/**
31
 * Singleton class that handles checking for available updates
32
 */
33
class checker {
34
 
35
    /** @var \core\update\checker holds the singleton instance */
36
    protected static $singletoninstance;
37
    /** @var null|int the timestamp of when the most recent response was fetched */
38
    protected $recentfetch = null;
39
    /** @var null|array the recent response from the update notification provider */
40
    protected $recentresponse = null;
41
    /** @var null|string the numerical version of the local Moodle code */
42
    protected $currentversion = null;
43
    /** @var null|string the release info of the local Moodle code */
44
    protected $currentrelease = null;
45
    /** @var null|string branch of the local Moodle code */
46
    protected $currentbranch = null;
47
    /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */
48
    protected $currentplugins = array();
49
 
50
    /**
51
     * Direct initiation not allowed, use the factory method {@link self::instance()}
52
     */
53
    protected function __construct() {
54
    }
55
 
56
    /**
57
     * Sorry, this is singleton
58
     */
59
    protected function __clone() {
60
    }
61
 
62
    /**
63
     * Factory method for this class
64
     *
65
     * @return \core\update\checker the singleton instance
66
     */
67
    public static function instance() {
68
        if (is_null(self::$singletoninstance)) {
69
            self::$singletoninstance = new self();
70
        }
71
        return self::$singletoninstance;
72
    }
73
 
74
    /**
75
     * Reset any caches
76
     * @param bool $phpunitreset
77
     */
78
    public static function reset_caches($phpunitreset = false) {
79
        if ($phpunitreset) {
80
            self::$singletoninstance = null;
81
        }
82
    }
83
 
84
    /**
85
     * Is checking for available updates enabled?
86
     *
87
     * The feature is enabled unless it is prohibited via config.php.
88
     * If enabled, the button for manual checking for available updates is
89
     * displayed at admin screens. To perform scheduled checks for updates
90
     * automatically, the admin setting $CFG->updateautocheck has to be enabled.
91
     *
92
     * @return bool
93
     */
94
    public function enabled() {
95
        global $CFG;
96
 
97
        return empty($CFG->disableupdatenotifications);
98
    }
99
 
100
    /**
101
     * Returns the timestamp of the last execution of {@link fetch()}
102
     *
103
     * @return int|null null if it has never been executed or we don't known
104
     */
105
    public function get_last_timefetched() {
106
 
107
        $this->restore_response();
108
 
109
        if (!empty($this->recentfetch)) {
110
            return $this->recentfetch;
111
 
112
        } else {
113
            return null;
114
        }
115
    }
116
 
117
    /**
118
     * Fetches the available update status from the remote site
119
     *
120
     * @throws checker_exception
121
     */
122
    public function fetch() {
123
 
124
        $response = $this->get_response();
125
        $this->validate_response($response);
126
        $this->store_response($response);
127
 
128
        // We need to reset plugin manager's caches - the currently existing
129
        // singleton is not aware of eventually available updates we just fetched.
130
        \core_plugin_manager::reset_caches();
131
    }
132
 
133
    /**
134
     * Returns the available update information for the given component
135
     *
136
     * This method returns null if the most recent response does not contain any information
137
     * about it. The returned structure is an array of available updates for the given
138
     * component. Each update info is an object with at least one property called
139
     * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'.
140
     *
141
     * For the 'core' component, the method returns real updates only (those with higher version).
142
     * For all other components, the list of all known remote updates is returned and the caller
143
     * (usually the {@link core_plugin_manager}) is supposed to make the actual comparison of versions.
144
     *
145
     * @param string $component frankenstyle
146
     * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds'
147
     * @return null|array null or array of \core\update\info objects
148
     */
149
    public function get_update_info($component, array $options = array()) {
150
 
151
        if (!isset($options['minmaturity'])) {
152
            $options['minmaturity'] = 0;
153
        }
154
 
155
        if (!isset($options['notifybuilds'])) {
156
            $options['notifybuilds'] = false;
157
        }
158
 
159
        if ($component === 'core') {
160
            $this->load_current_environment();
161
        }
162
 
163
        $this->restore_response();
164
 
165
        if (empty($this->recentresponse['updates'][$component])) {
166
            return null;
167
        }
168
 
169
        $updates = array();
170
        foreach ($this->recentresponse['updates'][$component] as $info) {
171
            $update = new info($component, $info);
172
            if (isset($update->maturity) and ($update->maturity < $options['minmaturity'])) {
173
                continue;
174
            }
175
            if ($component === 'core') {
176
                if ($update->version <= $this->currentversion) {
177
                    continue;
178
                }
179
                if (empty($options['notifybuilds']) and $this->is_same_release($update->release)) {
180
                    continue;
181
                }
182
            }
183
            $updates[] = $update;
184
        }
185
 
186
        if (empty($updates)) {
187
            return null;
188
        }
189
 
190
        return $updates;
191
    }
192
 
193
    /**
194
     * The method being run via cron.php
195
     */
196
    public function cron() {
197
        global $CFG;
198
 
199
        if (!$this->enabled() or !$this->cron_autocheck_enabled()) {
200
            $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
201
            return;
202
        }
203
 
204
        $now = $this->cron_current_timestamp();
205
 
206
        if ($this->cron_has_fresh_fetch($now)) {
207
            $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.');
208
            return;
209
        }
210
 
211
        if ($this->cron_has_outdated_fetch($now)) {
212
            $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', '');
213
            $this->cron_execute();
214
            return;
215
        }
216
 
217
        $offset = $this->cron_execution_offset();
218
        $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time
219
        if ($now > $start + $offset) {
220
            $this->cron_mtrace('Regular daily check for available updates ... ', '');
221
            $this->cron_execute();
222
            return;
223
        }
224
    }
225
 
226
    /* === End of public API === */
227
 
228
    /**
229
     * Makes cURL request to get data from the remote site
230
     *
231
     * @return string raw request result
232
     * @throws checker_exception
233
     */
234
    protected function get_response() {
235
        global $CFG;
236
        require_once($CFG->libdir.'/filelib.php');
237
 
238
        $curl = new \curl(array('proxy' => true));
239
        $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params(), $this->prepare_request_options());
240
        $curlerrno = $curl->get_errno();
241
        if (!empty($curlerrno)) {
242
            throw new checker_exception('err_response_curl', 'cURL error '.$curlerrno.': '.$curl->error);
243
        }
244
        $curlinfo = $curl->get_info();
245
        if ($curlinfo['http_code'] != 200) {
246
            throw new checker_exception('err_response_http_code', $curlinfo['http_code']);
247
        }
248
        return $response;
249
    }
250
 
251
    /**
252
     * Makes sure the response is valid, has correct API format etc.
253
     *
254
     * @param string $response raw response as returned by the {@link self::get_response()}
255
     * @throws checker_exception
256
     */
257
    protected function validate_response($response) {
258
 
259
        $response = $this->decode_response($response);
260
 
261
        if (empty($response)) {
262
            throw new checker_exception('err_response_empty');
263
        }
264
 
265
        if (empty($response['status']) or $response['status'] !== 'OK') {
266
            throw new checker_exception('err_response_status', $response['status']);
267
        }
268
 
269
        if (empty($response['apiver']) or $response['apiver'] !== '1.3') {
270
            throw new checker_exception('err_response_format_version', $response['apiver']);
271
        }
272
 
273
        if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
274
            throw new checker_exception('err_response_target_version', $response['forbranch']);
275
        }
276
    }
277
 
278
    /**
279
     * Decodes the raw string response from the update notifications provider
280
     *
281
     * @param string $response as returned by {@link self::get_response()}
282
     * @return array decoded response structure
283
     */
284
    protected function decode_response($response) {
285
        return json_decode($response, true);
286
    }
287
 
288
    /**
289
     * Stores the valid fetched response for later usage
290
     *
291
     * This implementation uses the config_plugins table as the permanent storage.
292
     *
293
     * @param string $response raw valid data returned by {@link self::get_response()}
294
     */
295
    protected function store_response($response) {
296
 
297
        set_config('recentfetch', time(), 'core_plugin');
298
        set_config('recentresponse', $response, 'core_plugin');
299
 
300
        if (defined('CACHE_DISABLE_ALL') and CACHE_DISABLE_ALL) {
301
            // Very nasty hack to work around cache coherency issues on admin/index.php?cache=0 page,
302
            // we definitely need to keep caches in sync when writing into DB at all times!
303
            \cache_helper::purge_all(true);
304
        }
305
 
306
        $this->restore_response(true);
307
    }
308
 
309
    /**
310
     * Loads the most recent raw response record we have fetched
311
     *
312
     * After this method is called, $this->recentresponse is set to an array. If the
313
     * array is empty, then either no data have been fetched yet or the fetched data
314
     * do not have expected format (and thence they are ignored and a debugging
315
     * message is displayed).
316
     *
317
     * This implementation uses the config_plugins table as the permanent storage.
318
     *
319
     * @param bool $forcereload reload even if it was already loaded
320
     */
321
    protected function restore_response($forcereload = false) {
322
 
323
        if (!$forcereload and !is_null($this->recentresponse)) {
324
            // We already have it, nothing to do.
325
            return;
326
        }
327
 
328
        $config = get_config('core_plugin');
329
 
330
        if (!empty($config->recentresponse) and !empty($config->recentfetch)) {
331
            try {
332
                $this->validate_response($config->recentresponse);
333
                $this->recentfetch = $config->recentfetch;
334
                $this->recentresponse = $this->decode_response($config->recentresponse);
335
            } catch (checker_exception $e) {
336
                // The server response is not valid. Behave as if no data were fetched yet.
337
                // This may happen when the most recent update info (cached locally) has been
338
                // fetched with the previous branch of Moodle (like during an upgrade from 2.x
339
                // to 2.y) or when the API of the response has changed.
340
                $this->recentresponse = array();
341
            }
342
 
343
        } else {
344
            $this->recentresponse = array();
345
        }
346
    }
347
 
348
    /**
349
     * Compares two raw {@link $recentresponse} records and returns the list of changed updates
350
     *
351
     * This method is used to populate potential update info to be sent to site admins.
352
     *
353
     * @param array $old
354
     * @param array $new
355
     * @throws checker_exception
356
     * @return array parts of $new['updates'] that have changed
357
     */
358
    protected function compare_responses(array $old, array $new) {
359
 
360
        if (empty($new)) {
361
            return array();
362
        }
363
 
364
        if (!array_key_exists('updates', $new)) {
365
            throw new checker_exception('err_response_format');
366
        }
367
 
368
        if (empty($old)) {
369
            return $new['updates'];
370
        }
371
 
372
        if (!array_key_exists('updates', $old)) {
373
            throw new checker_exception('err_response_format');
374
        }
375
 
376
        $changes = array();
377
 
378
        foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
379
            if (empty($old['updates'][$newcomponent])) {
380
                $changes[$newcomponent] = $newcomponentupdates;
381
                continue;
382
            }
383
            foreach ($newcomponentupdates as $newcomponentupdate) {
384
                $inold = false;
385
                foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
386
                    if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
387
                        $inold = true;
388
                    }
389
                }
390
                if (!$inold) {
391
                    if (!isset($changes[$newcomponent])) {
392
                        $changes[$newcomponent] = array();
393
                    }
394
                    $changes[$newcomponent][] = $newcomponentupdate;
395
                }
396
            }
397
        }
398
 
399
        return $changes;
400
    }
401
 
402
    /**
403
     * Returns the URL to send update requests to
404
     *
405
     * During the development or testing, you can set $CFG->alternativeupdateproviderurl
406
     * to a custom URL that will be used. Otherwise the standard URL will be returned.
407
     *
408
     * @return string URL
409
     */
410
    protected function prepare_request_url() {
411
        global $CFG;
412
 
413
        if (!empty($CFG->config_php_settings['alternativeupdateproviderurl'])) {
414
            return $CFG->config_php_settings['alternativeupdateproviderurl'];
415
        } else {
416
            return 'https://download.moodle.org/api/1.3/updates.php';
417
        }
418
    }
419
 
420
    /**
421
     * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
422
     *
423
     * @param bool $forcereload
424
     */
425
    protected function load_current_environment($forcereload=false) {
426
        global $CFG;
427
 
428
        if (!is_null($this->currentversion) and !$forcereload) {
429
            // Nothing to do.
430
            return;
431
        }
432
 
433
        $version = null;
434
        $release = null;
435
 
436
        require($CFG->dirroot.'/version.php');
437
        $this->currentversion = $version;
438
        $this->currentrelease = $release;
439
        $this->currentbranch = moodle_major_version(true);
440
 
441
        $pluginman = \core_plugin_manager::instance();
442
        foreach ($pluginman->get_plugins() as $type => $plugins) {
443
            // Iterate over installed plugins and determine which are non-standard and eligible for update checks. Note that we
444
            // disregard empty component names here, to ensure we only request valid data from the update site (in the case of an
445
            // improperly removed plugin containing sub-plugins, we would get an empty value here for each sub-plugin).
446
            foreach ($plugins as $plugin) {
447
                if ($plugin->component !== '' && !$plugin->is_standard()) {
448
                    $this->currentplugins[$plugin->component] = $plugin->versiondisk;
449
                }
450
            }
451
        }
452
    }
453
 
454
    /**
455
     * Returns the list of HTTP params to be sent to the updates provider URL
456
     *
457
     * @return array of (string)param => (string)value
458
     */
459
    protected function prepare_request_params() {
460
        global $CFG;
461
 
462
        $this->load_current_environment();
463
        $this->restore_response();
464
 
465
        $params = array();
466
        $params['format'] = 'json';
467
 
468
        if (isset($this->recentresponse['ticket'])) {
469
            $params['ticket'] = $this->recentresponse['ticket'];
470
        }
471
 
472
        if (isset($this->currentversion)) {
473
            $params['version'] = $this->currentversion;
474
        } else {
475
            throw new coding_exception('Main Moodle version must be already known here');
476
        }
477
 
478
        if (isset($this->currentbranch)) {
479
            $params['branch'] = $this->currentbranch;
480
        } else {
481
            throw new coding_exception('Moodle release must be already known here');
482
        }
483
 
484
        $plugins = array();
485
        foreach ($this->currentplugins as $plugin => $version) {
486
            $plugins[] = $plugin.'@'.$version;
487
        }
488
        if (!empty($plugins)) {
489
            $params['plugins'] = implode(',', $plugins);
490
        }
491
 
492
        return $params;
493
    }
494
 
495
    /**
496
     * Returns the list of cURL options to use when fetching available updates data
497
     *
498
     * @return array of (string)param => (string)value
499
     */
500
    protected function prepare_request_options() {
501
        $options = array(
502
            'CURLOPT_SSL_VERIFYHOST' => 2,      // This is the default in {@link curl} class but just in case.
503
            'CURLOPT_SSL_VERIFYPEER' => true,
504
        );
505
 
506
        return $options;
507
    }
508
 
509
    /**
510
     * Returns the current timestamp
511
     *
512
     * @return int the timestamp
513
     */
514
    protected function cron_current_timestamp() {
515
        return time();
516
    }
517
 
518
    /**
519
     * Output cron debugging info
520
     *
521
     * @see mtrace()
522
     * @param string $msg output message
523
     * @param string $eol end of line
524
     */
525
    protected function cron_mtrace($msg, $eol = PHP_EOL) {
526
        mtrace($msg, $eol);
527
    }
528
 
529
    /**
530
     * Decide if the autocheck feature is disabled in the server setting
531
     *
532
     * @return bool true if autocheck enabled, false if disabled
533
     */
534
    protected function cron_autocheck_enabled() {
535
        global $CFG;
536
 
537
        if (empty($CFG->updateautocheck)) {
538
            return false;
539
        } else {
540
            return true;
541
        }
542
    }
543
 
544
    /**
545
     * Decide if the recently fetched data are still fresh enough
546
     *
547
     * @param int $now current timestamp
548
     * @return bool true if no need to re-fetch, false otherwise
549
     */
550
    protected function cron_has_fresh_fetch($now) {
551
        $recent = $this->get_last_timefetched();
552
 
553
        if (empty($recent)) {
554
            return false;
555
        }
556
 
557
        if ($now < $recent) {
558
            $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
559
            return true;
560
        }
561
 
562
        if ($now - $recent > 24 * HOURSECS) {
563
            return false;
564
        }
565
 
566
        return true;
567
    }
568
 
569
    /**
570
     * Decide if the fetch is outadated or even missing
571
     *
572
     * @param int $now current timestamp
573
     * @return bool false if no need to re-fetch, true otherwise
574
     */
575
    protected function cron_has_outdated_fetch($now) {
576
        $recent = $this->get_last_timefetched();
577
 
578
        if (empty($recent)) {
579
            return true;
580
        }
581
 
582
        if ($now < $recent) {
583
            $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
584
            return false;
585
        }
586
 
587
        if ($now - $recent > 48 * HOURSECS) {
588
            return true;
589
        }
590
 
591
        return false;
592
    }
593
 
594
    /**
595
     * Returns the cron execution offset for this site
596
     *
597
     * The main {@link self::cron()} is supposed to run every night in some random time
598
     * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
599
     * execution offset, that is the amount of time after 01:00 AM. The offset value is
600
     * initially generated randomly and then used consistently at the site. This way, the
601
     * regular checks against the download.moodle.org server are spread in time.
602
     *
603
     * @return int the offset number of seconds from range 1 sec to 5 hours
604
     */
605
    protected function cron_execution_offset() {
606
        global $CFG;
607
 
608
        if (empty($CFG->updatecronoffset)) {
609
            set_config('updatecronoffset', rand(1, 5 * HOURSECS));
610
        }
611
 
612
        return $CFG->updatecronoffset;
613
    }
614
 
615
    /**
616
     * Fetch available updates info and eventually send notification to site admins
617
     */
618
    protected function cron_execute() {
619
 
620
        try {
621
            $this->restore_response();
622
            $previous = $this->recentresponse;
623
            $this->fetch();
624
            $this->restore_response(true);
625
            $current = $this->recentresponse;
626
            $changes = $this->compare_responses($previous, $current);
627
            $notifications = $this->cron_notifications($changes);
628
            $this->cron_notify($notifications);
629
            $this->cron_mtrace('done');
630
        } catch (checker_exception $e) {
631
            $this->cron_mtrace('FAILED!');
632
        }
633
    }
634
 
635
    /**
636
     * Given the list of changes in available updates, pick those to send to site admins
637
     *
638
     * @param array $changes as returned by {@link self::compare_responses()}
639
     * @return array of \core\update\info objects to send to site admins
640
     */
641
    protected function cron_notifications(array $changes) {
642
        global $CFG;
643
 
644
        if (empty($changes)) {
645
            return array();
646
        }
647
 
648
        $notifications = array();
649
        $pluginman = \core_plugin_manager::instance();
650
        $plugins = $pluginman->get_plugins();
651
 
652
        foreach ($changes as $component => $componentchanges) {
653
            if (empty($componentchanges)) {
654
                continue;
655
            }
656
            $componentupdates = $this->get_update_info($component,
657
                array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds));
658
            if (empty($componentupdates)) {
659
                continue;
660
            }
661
            // Notify only about those $componentchanges that are present in $componentupdates
662
            // to respect the preferences.
663
            foreach ($componentchanges as $componentchange) {
664
                foreach ($componentupdates as $componentupdate) {
665
                    if ($componentupdate->version == $componentchange['version']) {
666
                        if ($component == 'core') {
667
                            // In case of 'core', we already know that the $componentupdate
668
                            // is a real update with higher version ({@see self::get_update_info()}).
669
                            // We just perform additional check for the release property as there
670
                            // can be two Moodle releases having the same version (e.g. 2.4.0 and 2.5dev shortly
671
                            // after the release). We can do that because we have the release info
672
                            // always available for the core.
673
                            if ((string)$componentupdate->release === (string)$componentchange['release']) {
674
                                $notifications[] = $componentupdate;
675
                            }
676
                        } else {
677
                            // Use the core_plugin_manager to check if the detected $componentchange
678
                            // is a real update with higher version. That is, the $componentchange
679
                            // is present in the array of {@link \core\update\info} objects
680
                            // returned by the plugin's available_updates() method.
681
                            list($plugintype, $pluginname) = core_component::normalize_component($component);
682
                            if (!empty($plugins[$plugintype][$pluginname])) {
683
                                $availableupdates = $plugins[$plugintype][$pluginname]->available_updates();
684
                                if (!empty($availableupdates)) {
685
                                    foreach ($availableupdates as $availableupdate) {
686
                                        if ($availableupdate->version == $componentchange['version']) {
687
                                            $notifications[] = $componentupdate;
688
                                        }
689
                                    }
690
                                }
691
                            }
692
                        }
693
                    }
694
                }
695
            }
696
        }
697
 
698
        return $notifications;
699
    }
700
 
701
    /**
702
     * Sends the given notifications to site admins via messaging API
703
     *
704
     * @param array $notifications array of \core\update\info objects to send
705
     */
706
    protected function cron_notify(array $notifications) {
707
        global $CFG;
708
 
709
        if (empty($notifications)) {
710
            $this->cron_mtrace('nothing to notify about. ', '');
711
            return;
712
        }
713
 
714
        $admins = get_admins();
715
 
716
        if (empty($admins)) {
717
            return;
718
        }
719
 
720
        $this->cron_mtrace('sending notifications ... ', '');
721
 
722
        $text = get_string('updatenotifications', 'core_admin') . PHP_EOL;
723
        $html = html_writer::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL;
724
 
725
        $coreupdates = array();
726
        $pluginupdates = array();
727
 
728
        foreach ($notifications as $notification) {
729
            if ($notification->component == 'core') {
730
                $coreupdates[] = $notification;
731
            } else {
732
                $pluginupdates[] = $notification;
733
            }
734
        }
735
 
736
        if (!empty($coreupdates)) {
737
            $text .= PHP_EOL . get_string('updateavailable', 'core_admin') . PHP_EOL;
738
            $html .= html_writer::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL;
739
            $html .= html_writer::start_tag('ul') . PHP_EOL;
740
            foreach ($coreupdates as $coreupdate) {
741
                $html .= html_writer::start_tag('li');
742
                if (isset($coreupdate->release)) {
743
                    $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release);
744
                    $html .= html_writer::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release));
745
                }
746
                if (isset($coreupdate->version)) {
747
                    $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
748
                    $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
749
                }
750
                if (isset($coreupdate->maturity)) {
751
                    $text .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
752
                    $html .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
753
                }
754
                $text .= PHP_EOL;
755
                $html .= html_writer::end_tag('li') . PHP_EOL;
756
            }
757
            $text .= PHP_EOL;
758
            $html .= html_writer::end_tag('ul') . PHP_EOL;
759
 
760
            $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/index.php');
761
            $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
762
            $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/index.php', $CFG->wwwroot.'/'.$CFG->admin.'/index.php'));
763
            $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
764
 
765
            $text .= PHP_EOL . get_string('updateavailablerecommendation', 'core_admin') . PHP_EOL;
766
            $html .= html_writer::tag('p', get_string('updateavailablerecommendation', 'core_admin')) . PHP_EOL;
767
        }
768
 
769
        if (!empty($pluginupdates)) {
770
            $text .= PHP_EOL . get_string('updateavailableforplugin', 'core_admin') . PHP_EOL;
771
            $html .= html_writer::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL;
772
 
773
            $html .= html_writer::start_tag('ul') . PHP_EOL;
774
            foreach ($pluginupdates as $pluginupdate) {
775
                $html .= html_writer::start_tag('li');
776
                $text .= get_string('pluginname', $pluginupdate->component);
777
                $html .= html_writer::tag('strong', get_string('pluginname', $pluginupdate->component));
778
 
779
                $text .= ' ('.$pluginupdate->component.')';
780
                $html .= ' ('.$pluginupdate->component.')';
781
 
782
                $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
783
                $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
784
 
785
                $text .= PHP_EOL;
786
                $html .= html_writer::end_tag('li') . PHP_EOL;
787
            }
788
            $text .= PHP_EOL;
789
            $html .= html_writer::end_tag('ul') . PHP_EOL;
790
 
791
            $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php');
792
            $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
793
            $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/plugins.php', $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php'));
794
            $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
795
        }
796
 
797
        $a = array('siteurl' => $CFG->wwwroot);
798
        $text .= PHP_EOL . get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL;
799
        $a = array('siteurl' => html_writer::link($CFG->wwwroot, $CFG->wwwroot));
800
        $html .= html_writer::tag('footer', html_writer::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
801
            array('style' => 'font-size:smaller; color:#333;')));
802
 
803
        foreach ($admins as $admin) {
804
            $message = new \core\message\message();
805
            $message->courseid          = SITEID;
806
            $message->component         = 'moodle';
807
            $message->name              = 'availableupdate';
808
            $message->userfrom          = get_admin();
809
            $message->userto            = $admin;
810
            $message->subject           = get_string('updatenotificationsubject', 'core_admin', array('siteurl' => $CFG->wwwroot));
811
            $message->fullmessage       = $text;
812
            $message->fullmessageformat = FORMAT_PLAIN;
813
            $message->fullmessagehtml   = $html;
814
            $message->smallmessage      = get_string('updatenotifications', 'core_admin');
815
            $message->notification      = 1;
816
            message_send($message);
817
        }
818
    }
819
 
820
    /**
821
     * Compare two release labels and decide if they are the same
822
     *
823
     * @param string $remote release info of the available update
824
     * @param null|string $local release info of the local code, defaults to $release defined in version.php
825
     * @return boolean true if the releases declare the same minor+major version
826
     */
827
    protected function is_same_release($remote, $local=null) {
828
 
829
        if (is_null($local)) {
830
            $this->load_current_environment();
831
            $local = $this->currentrelease;
832
        }
833
 
834
        $pattern = '/^([0-9\.\+]+)([^(]*)/';
835
 
836
        preg_match($pattern, $remote, $remotematches);
837
        preg_match($pattern, $local, $localmatches);
838
 
839
        $remotematches[1] = str_replace('+', '', $remotematches[1]);
840
        $localmatches[1] = str_replace('+', '', $localmatches[1]);
841
 
842
        if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
843
            return true;
844
        } else {
845
            return false;
846
        }
847
    }
848
}