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 |
}
|