/lib/classes/update/checker.php

https://bitbucket.org/synergylearning/campusconnect · PHP · 827 lines · 472 code · 122 blank · 233 comment · 82 complexity · e570455b9fd50bbb54aaccf6b9b4b994 MD5 · raw file

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