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 foreach ($plugins as $plugin) { 444 if (!$plugin->is_standard()) { 445 $this->currentplugins[$plugin->component] = $plugin->versiondisk; 446 } 447 } 448 } 449 } 450 451 /** 452 * Returns the list of HTTP params to be sent to the updates provider URL 453 * 454 * @return array of (string)param => (string)value 455 */ 456 protected function prepare_request_params() { 457 global $CFG; 458 459 $this->load_current_environment(); 460 $this->restore_response(); 461 462 $params = array(); 463 $params['format'] = 'json'; 464 465 if (isset($this->recentresponse['ticket'])) { 466 $params['ticket'] = $this->recentresponse['ticket']; 467 } 468 469 if (isset($this->currentversion)) { 470 $params['version'] = $this->currentversion; 471 } else { 472 throw new coding_exception('Main Moodle version must be already known here'); 473 } 474 475 if (isset($this->currentbranch)) { 476 $params['branch'] = $this->currentbranch; 477 } else { 478 throw new coding_exception('Moodle release must be already known here'); 479 } 480 481 $plugins = array(); 482 foreach ($this->currentplugins as $plugin => $version) { 483 $plugins[] = $plugin.'@'.$version; 484 } 485 if (!empty($plugins)) { 486 $params['plugins'] = implode(',', $plugins); 487 } 488 489 return $params; 490 } 491 492 /** 493 * Returns the list of cURL options to use when fetching available updates data 494 * 495 * @return array of (string)param => (string)value 496 */ 497 protected function prepare_request_options() { 498 $options = array( 499 'CURLOPT_SSL_VERIFYHOST' => 2, // This is the default in {@link curl} class but just in case. 500 'CURLOPT_SSL_VERIFYPEER' => true, 501 ); 502 503 return $options; 504 } 505 506 /** 507 * Returns the current timestamp 508 * 509 * @return int the timestamp 510 */ 511 protected function cron_current_timestamp() { 512 return time(); 513 } 514 515 /** 516 * Output cron debugging info 517 * 518 * @see mtrace() 519 * @param string $msg output message 520 * @param string $eol end of line 521 */ 522 protected function cron_mtrace($msg, $eol = PHP_EOL) { 523 mtrace($msg, $eol); 524 } 525 526 /** 527 * Decide if the autocheck feature is disabled in the server setting 528 * 529 * @return bool true if autocheck enabled, false if disabled 530 */ 531 protected function cron_autocheck_enabled() { 532 global $CFG; 533 534 if (empty($CFG->updateautocheck)) { 535 return false; 536 } else { 537 return true; 538 } 539 } 540 541 /** 542 * Decide if the recently fetched data are still fresh enough 543 * 544 * @param int $now current timestamp 545 * @return bool true if no need to re-fetch, false otherwise 546 */ 547 protected function cron_has_fresh_fetch($now) { 548 $recent = $this->get_last_timefetched(); 549 550 if (empty($recent)) { 551 return false; 552 } 553 554 if ($now < $recent) { 555 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!'); 556 return true; 557 } 558 559 if ($now - $recent > 24 * HOURSECS) { 560 return false; 561 } 562 563 return true; 564 } 565 566 /** 567 * Decide if the fetch is outadated or even missing 568 * 569 * @param int $now current timestamp 570 * @return bool false if no need to re-fetch, true otherwise 571 */ 572 protected function cron_has_outdated_fetch($now) { 573 $recent = $this->get_last_timefetched(); 574 575 if (empty($recent)) { 576 return true; 577 } 578 579 if ($now < $recent) { 580 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!'); 581 return false; 582 } 583 584 if ($now - $recent > 48 * HOURSECS) { 585 return true; 586 } 587 588 return false; 589 } 590 591 /** 592 * Returns the cron execution offset for this site 593 * 594 * The main {@link self::cron()} is supposed to run every night in some random time 595 * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called 596 * execution offset, that is the amount of time after 01:00 AM. The offset value is 597 * initially generated randomly and then used consistently at the site. This way, the 598 * regular checks against the download.moodle.org server are spread in time. 599 * 600 * @return int the offset number of seconds from range 1 sec to 5 hours 601 */ 602 protected function cron_execution_offset() { 603 global $CFG; 604 605 if (empty($CFG->updatecronoffset)) { 606 set_config('updatecronoffset', rand(1, 5 * HOURSECS)); 607 } 608 609 return $CFG->updatecronoffset; 610 } 611 612 /** 613 * Fetch available updates info and eventually send notification to site admins 614 */ 615 protected function cron_execute() { 616 617 try { 618 $this->restore_response(); 619 $previous = $this->recentresponse; 620 $this->fetch(); 621 $this->restore_response(true); 622 $current = $this->recentresponse; 623 $changes = $this->compare_responses($previous, $current); 624 $notifications = $this->cron_notifications($changes); 625 $this->cron_notify($notifications); 626 $this->cron_mtrace('done'); 627 } catch (checker_exception $e) { 628 $this->cron_mtrace('FAILED!'); 629 } 630 } 631 632 /** 633 * Given the list of changes in available updates, pick those to send to site admins 634 * 635 * @param array $changes as returned by {@link self::compare_responses()} 636 * @return array of \core\update\info objects to send to site admins 637 */ 638 protected function cron_notifications(array $changes) { 639 global $CFG; 640 641 if (empty($changes)) { 642 return array(); 643 } 644 645 $notifications = array(); 646 $pluginman = \core_plugin_manager::instance(); 647 $plugins = $pluginman->get_plugins(); 648 649 foreach ($changes as $component => $componentchanges) { 650 if (empty($componentchanges)) { 651 continue; 652 } 653 $componentupdates = $this->get_update_info($component, 654 array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds)); 655 if (empty($componentupdates)) { 656 continue; 657 } 658 // Notify only about those $componentchanges that are present in $componentupdates 659 // to respect the preferences. 660 foreach ($componentchanges as $componentchange) { 661 foreach ($componentupdates as $componentupdate) { 662 if ($componentupdate->version == $componentchange['version']) { 663 if ($component == 'core') { 664 // In case of 'core', we already know that the $componentupdate 665 // is a real update with higher version ({@see self::get_update_info()}). 666 // We just perform additional check for the release property as there 667 // can be two Moodle releases having the same version (e.g. 2.4.0 and 2.5dev shortly 668 // after the release). We can do that because we have the release info 669 // always available for the core. 670 if ((string)$componentupdate->release === (string)$componentchange['release']) { 671 $notifications[] = $componentupdate; 672 } 673 } else { 674 // Use the core_plugin_manager to check if the detected $componentchange 675 // is a real update with higher version. That is, the $componentchange 676 // is present in the array of {@link \core\update\info} objects 677 // returned by the plugin's available_updates() method. 678 list($plugintype, $pluginname) = core_component::normalize_component($component); 679 if (!empty($plugins[$plugintype][$pluginname])) { 680 $availableupdates = $plugins[$plugintype][$pluginname]->available_updates(); 681 if (!empty($availableupdates)) { 682 foreach ($availableupdates as $availableupdate) { 683 if ($availableupdate->version == $componentchange['version']) { 684 $notifications[] = $componentupdate; 685 } 686 } 687 } 688 } 689 } 690 } 691 } 692 } 693 } 694 695 return $notifications; 696 } 697 698 /** 699 * Sends the given notifications to site admins via messaging API 700 * 701 * @param array $notifications array of \core\update\info objects to send 702 */ 703 protected function cron_notify(array $notifications) { 704 global $CFG; 705 706 if (empty($notifications)) { 707 $this->cron_mtrace('nothing to notify about. ', ''); 708 return; 709 } 710 711 $admins = get_admins(); 712 713 if (empty($admins)) { 714 return; 715 } 716 717 $this->cron_mtrace('sending notifications ... ', ''); 718 719 $text = get_string('updatenotifications', 'core_admin') . PHP_EOL; 720 $html = html_writer::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL; 721 722 $coreupdates = array(); 723 $pluginupdates = array(); 724 725 foreach ($notifications as $notification) { 726 if ($notification->component == 'core') { 727 $coreupdates[] = $notification; 728 } else { 729 $pluginupdates[] = $notification; 730 } 731 } 732 733 if (!empty($coreupdates)) { 734 $text .= PHP_EOL . get_string('updateavailable', 'core_admin') . PHP_EOL; 735 $html .= html_writer::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL; 736 $html .= html_writer::start_tag('ul') . PHP_EOL; 737 foreach ($coreupdates as $coreupdate) { 738 $html .= html_writer::start_tag('li'); 739 if (isset($coreupdate->release)) { 740 $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release); 741 $html .= html_writer::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release)); 742 } 743 if (isset($coreupdate->version)) { 744 $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version); 745 $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version); 746 } 747 if (isset($coreupdate->maturity)) { 748 $text .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')'; 749 $html .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')'; 750 } 751 $text .= PHP_EOL; 752 $html .= html_writer::end_tag('li') . PHP_EOL; 753 } 754 $text .= PHP_EOL; 755 $html .= html_writer::end_tag('ul') . PHP_EOL; 756 757 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/index.php'); 758 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL; 759 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/index.php', $CFG->wwwroot.'/'.$CFG->admin.'/index.php')); 760 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL; 761 762 $text .= PHP_EOL . get_string('updateavailablerecommendation', 'core_admin') . PHP_EOL; 763 $html .= html_writer::tag('p', get_string('updateavailablerecommendation', 'core_admin')) . PHP_EOL; 764 } 765 766 if (!empty($pluginupdates)) { 767 $text .= PHP_EOL . get_string('updateavailableforplugin', 'core_admin') . PHP_EOL; 768 $html .= html_writer::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL; 769 770 $html .= html_writer::start_tag('ul') . PHP_EOL; 771 foreach ($pluginupdates as $pluginupdate) { 772 $html .= html_writer::start_tag('li'); 773 $text .= get_string('pluginname', $pluginupdate->component); 774 $html .= html_writer::tag('strong', get_string('pluginname', $pluginupdate->component)); 775 776 $text .= ' ('.$pluginupdate->component.')'; 777 $html .= ' ('.$pluginupdate->component.')'; 778 779 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version); 780 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version); 781 782 $text .= PHP_EOL; 783 $html .= html_writer::end_tag('li') . PHP_EOL; 784 } 785 $text .= PHP_EOL; 786 $html .= html_writer::end_tag('ul') . PHP_EOL; 787 788 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php'); 789 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL; 790 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/plugins.php', $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php')); 791 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL; 792 } 793 794 $a = array('siteurl' => $CFG->wwwroot); 795 $text .= PHP_EOL . get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL; 796 $a = array('siteurl' => html_writer::link($CFG->wwwroot, $CFG->wwwroot)); 797 $html .= html_writer::tag('footer', html_writer::tag('p', get_string('updatenotificationfooter', 'core_admin', $a), 798 array('style' => 'font-size:smaller; color:#333;'))); 799 800 foreach ($admins as $admin) { 801 $message = new \core\message\message(); 802 $message->courseid = SITEID; 803 $message->component = 'moodle'; 804 $message->name = 'availableupdate'; 805 $message->userfrom = get_admin(); 806 $message->userto = $admin; 807 $message->subject = get_string('updatenotificationsubject', 'core_admin', array('siteurl' => $CFG->wwwroot)); 808 $message->fullmessage = $text; 809 $message->fullmessageformat = FORMAT_PLAIN; 810 $message->fullmessagehtml = $html; 811 $message->smallmessage = get_string('updatenotifications', 'core_admin'); 812 $message->notification = 1; 813 message_send($message); 814 } 815 } 816 817 /** 818 * Compare two release labels and decide if they are the same 819 * 820 * @param string $remote release info of the available update 821 * @param null|string $local release info of the local code, defaults to $release defined in version.php 822 * @return boolean true if the releases declare the same minor+major version 823 */ 824 protected function is_same_release($remote, $local=null) { 825 826 if (is_null($local)) { 827 $this->load_current_environment(); 828 $local = $this->currentrelease; 829 } 830 831 $pattern = '/^([0-9\.\+]+)([^(]*)/'; 832 833 preg_match($pattern, $remote, $remotematches); 834 preg_match($pattern, $local, $localmatches); 835 836 $remotematches[1] = str_replace('+', '', $remotematches[1]); 837 $localmatches[1] = str_replace('+', '', $localmatches[1]); 838 839 if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) { 840 return true; 841 } else { 842 return false; 843 } 844 } 845 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body