1 |
efrain |
1 |
<?php
|
|
|
2 |
|
|
|
3 |
// This file is part of Moodle - http://moodle.org/
|
|
|
4 |
//
|
|
|
5 |
// Moodle is free software: you can redistribute it and/or modify
|
|
|
6 |
// it under the terms of the GNU General Public License as published by
|
|
|
7 |
// the Free Software Foundation, either version 3 of the License, or
|
|
|
8 |
// (at your option) any later version.
|
|
|
9 |
//
|
|
|
10 |
// Moodle is distributed in the hope that it will be useful,
|
|
|
11 |
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
12 |
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
13 |
// GNU General Public License for more details.
|
|
|
14 |
//
|
|
|
15 |
// You should have received a copy of the GNU General Public License
|
|
|
16 |
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
|
|
17 |
|
|
|
18 |
/**
|
|
|
19 |
* This library includes all the necessary stuff to use the one-click
|
|
|
20 |
* download and install feature of Moodle, used to keep updated some
|
|
|
21 |
* items like languages, pear, enviroment... i.e, components.
|
|
|
22 |
*
|
|
|
23 |
* It has been developed harcoding some important limits that are
|
|
|
24 |
* explained below:
|
|
|
25 |
* - It only can check, download and install items under moodledata.
|
|
|
26 |
* - Every downloadeable item must be one zip file.
|
|
|
27 |
* - The zip file root content must be 1 directory, i.e, everything
|
|
|
28 |
* is stored under 1 directory.
|
|
|
29 |
* - Zip file name and root directory must have the same name (but
|
|
|
30 |
* the .zip extension, of course).
|
|
|
31 |
* - Every .zip file must be defined in one .md5 file that will be
|
|
|
32 |
* stored in the same remote directory than the .zip file.
|
|
|
33 |
* - The name of such .md5 file is free, although it's recommended
|
|
|
34 |
* to use the same name than the .zip (that's the default
|
|
|
35 |
* assumption if no specified).
|
|
|
36 |
* - Every remote .md5 file will be a comma separated (CVS) file where each
|
|
|
37 |
* line will follow this format:
|
|
|
38 |
* - Field 1: name of the zip file (without extension). Mandatory.
|
|
|
39 |
* - Field 2: md5 of the zip file. Mandatory.
|
|
|
40 |
* - Field 3: whatever you want (or need). Optional.
|
|
|
41 |
* -Every local .md5 file will:
|
|
|
42 |
* - Have the zip file name (without the extension) plus -md5
|
|
|
43 |
* - Will reside inside the expanded zip file dir
|
|
|
44 |
* - Will contain the md5 od the latest installed component
|
|
|
45 |
* With all these details present, the process will perform this tasks:
|
|
|
46 |
* - Perform security checks. Only admins are allowed to use this for now.
|
|
|
47 |
* - Read the .md5 file from source (1).
|
|
|
48 |
* - Extract the correct line for the .zip being requested.
|
|
|
49 |
* - Compare it with the local .md5 file (2).
|
|
|
50 |
* - If different:
|
|
|
51 |
* - Download the newer .zip file from source.
|
|
|
52 |
* - Calculate its md5 (3).
|
|
|
53 |
* - Compare (1) and (3).
|
|
|
54 |
* - If equal:
|
|
|
55 |
* - Delete old directory.
|
|
|
56 |
* - Uunzip the newer .zip file.
|
|
|
57 |
* - Create the new local .md5 file.
|
|
|
58 |
* - Delete the .zip file.
|
|
|
59 |
* - If different:
|
|
|
60 |
* - ERROR. Old package won't be modified. We shouldn't
|
|
|
61 |
* reach here ever.
|
|
|
62 |
* - If component download is not possible, a message text about how to do
|
|
|
63 |
* the process manually (remotedownloaderror) must be displayed to explain it.
|
|
|
64 |
*
|
|
|
65 |
* General Usage:
|
|
|
66 |
*
|
|
|
67 |
* To install one component:
|
|
|
68 |
* <code>
|
|
|
69 |
* require_once($CFG->libdir.'/componentlib.class.php');
|
|
|
70 |
* if ($cd = new component_installer('https://download.moodle.org', 'langpack/2.0',
|
|
|
71 |
* 'es.zip', 'languages.md5', 'lang')) {
|
|
|
72 |
* $status = $cd->install(); //returns COMPONENT_(ERROR | UPTODATE | INSTALLED)
|
|
|
73 |
* switch ($status) {
|
|
|
74 |
* case COMPONENT_ERROR:
|
|
|
75 |
* if ($cd->get_error() == 'remotedownloaderror') {
|
|
|
76 |
* $a = new stdClass();
|
|
|
77 |
* $a->url = 'https://download.moodle.org/langpack/2.0/es.zip';
|
|
|
78 |
* $a->dest= $CFG->dataroot.'/lang';
|
|
|
79 |
* throw new \moodle_exception($cd->get_error(), 'error', '', $a);
|
|
|
80 |
* } else {
|
|
|
81 |
* throw new \moodle_exception($cd->get_error(), 'error');
|
|
|
82 |
* }
|
|
|
83 |
* break;
|
|
|
84 |
* case COMPONENT_UPTODATE:
|
|
|
85 |
* //Print error string or whatever you want to do
|
|
|
86 |
* break;
|
|
|
87 |
* case COMPONENT_INSTALLED:
|
|
|
88 |
* //Print/do whatever you want
|
|
|
89 |
* break;
|
|
|
90 |
* default:
|
|
|
91 |
* //We shouldn't reach this point
|
|
|
92 |
* }
|
|
|
93 |
* } else {
|
|
|
94 |
* //We shouldn't reach this point
|
|
|
95 |
* }
|
|
|
96 |
* </code>
|
|
|
97 |
*
|
|
|
98 |
* To switch of component (maintaining the rest of settings):
|
|
|
99 |
* <code>
|
|
|
100 |
* $status = $cd->change_zip_file('en.zip'); //returns boolean false on error
|
|
|
101 |
* </code>
|
|
|
102 |
*
|
|
|
103 |
* To retrieve all the components in one remote md5 file
|
|
|
104 |
* <code>
|
|
|
105 |
* $components = $cd->get_all_components_md5(); //returns boolean false on error, array instead
|
|
|
106 |
* </code>
|
|
|
107 |
*
|
|
|
108 |
* To check if current component needs to be updated
|
|
|
109 |
* <code>
|
|
|
110 |
* $status = $cd->need_upgrade(); //returns COMPONENT_(ERROR | UPTODATE | NEEDUPDATE)
|
|
|
111 |
* </code>
|
|
|
112 |
*
|
|
|
113 |
* To get the 3rd field of the md5 file (optional)
|
|
|
114 |
* <code>
|
|
|
115 |
* $field = $cd->get_extra_md5_field(); //returns string (empty if not exists)
|
|
|
116 |
* </code>
|
|
|
117 |
*
|
|
|
118 |
* For all the error situations the $cd->get_error() method should return always the key of the
|
|
|
119 |
* error to be retrieved by one standard get_string() call against the error.php lang file.
|
|
|
120 |
*
|
|
|
121 |
* That's all!
|
|
|
122 |
*
|
|
|
123 |
* @package core
|
|
|
124 |
* @copyright (C) 2001-3001 Eloy Lafuente (stronk7) {@link http://contiento.com}
|
|
|
125 |
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
|
126 |
*/
|
|
|
127 |
|
|
|
128 |
defined('MOODLE_INTERNAL') || die();
|
|
|
129 |
|
|
|
130 |
/**
|
|
|
131 |
* @global object $CFG
|
|
|
132 |
* @name $CFG
|
|
|
133 |
*/
|
|
|
134 |
global $CFG;
|
|
|
135 |
require_once($CFG->libdir.'/filelib.php');
|
|
|
136 |
|
|
|
137 |
// Some needed constants
|
|
|
138 |
define('COMPONENT_ERROR', 0);
|
|
|
139 |
define('COMPONENT_UPTODATE', 1);
|
|
|
140 |
define('COMPONENT_NEEDUPDATE', 2);
|
|
|
141 |
define('COMPONENT_INSTALLED', 3);
|
|
|
142 |
|
|
|
143 |
/**
|
|
|
144 |
* This class is used to check, download and install items from
|
|
|
145 |
* download.moodle.org to the moodledata directory.
|
|
|
146 |
*
|
|
|
147 |
* It always return true/false in all their public methods to say if
|
|
|
148 |
* execution has ended succesfuly or not. If there is any problem
|
|
|
149 |
* its getError() method can be called, returning one error string
|
|
|
150 |
* to be used with the standard get/print_string() functions.
|
|
|
151 |
*
|
|
|
152 |
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
|
153 |
* @package moodlecore
|
|
|
154 |
*/
|
|
|
155 |
class component_installer {
|
|
|
156 |
/**
|
|
|
157 |
* @var string
|
|
|
158 |
*/
|
|
|
159 |
var $sourcebase; /// Full http URL, base for downloadable items
|
|
|
160 |
var $zippath; /// Relative path (from sourcebase) where the
|
|
|
161 |
/// downloadeable item resides.
|
|
|
162 |
var $zipfilename; /// Name of the .zip file to be downloaded
|
|
|
163 |
var $md5filename; /// Name of the .md5 file to be read
|
|
|
164 |
var $componentname;/// Name of the component. Must be the zip name without
|
|
|
165 |
/// the extension. And it defines a lot of things:
|
|
|
166 |
/// the md5 line to search for, the default m5 file name
|
|
|
167 |
/// and the name of the root dir stored inside the zip file
|
|
|
168 |
var $destpath; /// Relative path (from moodledata) where the .zip
|
|
|
169 |
/// file will be expanded.
|
|
|
170 |
var $errorstring; /// Latest error produced. It will contain one lang string key.
|
|
|
171 |
var $extramd5info; /// Contents of the optional third field in the .md5 file.
|
|
|
172 |
var $requisitesok; /// Flag to see if requisites check has been passed ok.
|
|
|
173 |
/**
|
|
|
174 |
* @var array
|
|
|
175 |
*/
|
|
|
176 |
var $cachedmd5components; /// Array of cached components to avoid to
|
|
|
177 |
/// download the same md5 file more than once per request.
|
|
|
178 |
|
|
|
179 |
/**
|
|
|
180 |
* Standard constructor of the class. It will initialize all attributes.
|
|
|
181 |
* without performing any check at all.
|
|
|
182 |
*
|
|
|
183 |
* @param string $sourcebase Full http URL, base for downloadeable items
|
|
|
184 |
* @param string $zippath Relative path (from sourcebase) where the
|
|
|
185 |
* downloadeable item resides
|
|
|
186 |
* @param string $zipfilename Name of the .zip file to be downloaded
|
|
|
187 |
* @param string $md5filename Name of the .md5 file to be read (default '' = same
|
|
|
188 |
* than zipfilename)
|
|
|
189 |
* @param string $destpath Relative path (from moodledata) where the .zip file will
|
|
|
190 |
* be expanded (default='' = moodledataitself)
|
|
|
191 |
* @return object
|
|
|
192 |
*/
|
|
|
193 |
public function __construct($sourcebase, $zippath, $zipfilename, $md5filename='', $destpath='') {
|
|
|
194 |
|
|
|
195 |
$this->sourcebase = $sourcebase;
|
|
|
196 |
$this->zippath = $zippath;
|
|
|
197 |
$this->zipfilename = $zipfilename;
|
|
|
198 |
$this->md5filename = $md5filename;
|
|
|
199 |
$this->componentname= '';
|
|
|
200 |
$this->destpath = $destpath;
|
|
|
201 |
$this->errorstring = '';
|
|
|
202 |
$this->extramd5info = '';
|
|
|
203 |
$this->requisitesok = false;
|
|
|
204 |
$this->cachedmd5components = array();
|
|
|
205 |
|
|
|
206 |
$this->check_requisites();
|
|
|
207 |
}
|
|
|
208 |
|
|
|
209 |
/**
|
|
|
210 |
* Old syntax of class constructor. Deprecated in PHP7.
|
|
|
211 |
*
|
|
|
212 |
* @deprecated since Moodle 3.1
|
|
|
213 |
*/
|
|
|
214 |
public function component_installer($sourcebase, $zippath, $zipfilename, $md5filename='', $destpath='') {
|
|
|
215 |
debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
|
|
|
216 |
self::__construct($sourcebase, $zippath, $zipfilename, $md5filename, $destpath);
|
|
|
217 |
}
|
|
|
218 |
|
|
|
219 |
/**
|
|
|
220 |
* This function will check if everything is properly set to begin
|
|
|
221 |
* one installation. Also, it will check for required settings
|
|
|
222 |
* and will fill everything as needed.
|
|
|
223 |
*
|
|
|
224 |
* @global object
|
|
|
225 |
* @return boolean true/false (plus detailed error in errorstring)
|
|
|
226 |
*/
|
|
|
227 |
function check_requisites() {
|
|
|
228 |
global $CFG;
|
|
|
229 |
|
|
|
230 |
$this->requisitesok = false;
|
|
|
231 |
|
|
|
232 |
/// Check that everything we need is present
|
|
|
233 |
if (empty($this->sourcebase) || empty($this->zipfilename)) {
|
|
|
234 |
$this->errorstring='missingrequiredfield';
|
|
|
235 |
return false;
|
|
|
236 |
}
|
|
|
237 |
/// Check for correct sourcebase (this will be out in the future)
|
|
|
238 |
if (!PHPUNIT_TEST and $this->sourcebase != 'https://download.moodle.org') {
|
|
|
239 |
$this->errorstring='wrongsourcebase';
|
|
|
240 |
return false;
|
|
|
241 |
}
|
|
|
242 |
/// Check the zip file is a correct one (by extension)
|
|
|
243 |
if (stripos($this->zipfilename, '.zip') === false) {
|
|
|
244 |
$this->errorstring='wrongzipfilename';
|
|
|
245 |
return false;
|
|
|
246 |
}
|
|
|
247 |
/// Check that exists under dataroot
|
|
|
248 |
if (!empty($this->destpath)) {
|
|
|
249 |
if (!file_exists($CFG->dataroot.'/'.$this->destpath)) {
|
|
|
250 |
$this->errorstring='wrongdestpath';
|
|
|
251 |
return false;
|
|
|
252 |
}
|
|
|
253 |
}
|
|
|
254 |
/// Calculate the componentname
|
|
|
255 |
$pos = stripos($this->zipfilename, '.zip');
|
|
|
256 |
$this->componentname = substr($this->zipfilename, 0, $pos);
|
|
|
257 |
/// Calculate md5filename if it's empty
|
|
|
258 |
if (empty($this->md5filename)) {
|
|
|
259 |
$this->md5filename = $this->componentname.'.md5';
|
|
|
260 |
}
|
|
|
261 |
/// Set the requisites passed flag
|
|
|
262 |
$this->requisitesok = true;
|
|
|
263 |
return true;
|
|
|
264 |
}
|
|
|
265 |
|
|
|
266 |
/**
|
|
|
267 |
* This function will perform the full installation if needed, i.e.
|
|
|
268 |
* compare md5 values, download, unzip, install and regenerate
|
|
|
269 |
* local md5 file
|
|
|
270 |
*
|
|
|
271 |
* @uses COMPONENT_ERROR
|
|
|
272 |
* @uses COMPONENT_UPTODATE
|
|
|
273 |
* @uses COMPONENT_ERROR
|
|
|
274 |
* @uses COMPONENT_INSTALLED
|
|
|
275 |
* @return int COMPONENT_(ERROR | UPTODATE | INSTALLED)
|
|
|
276 |
*/
|
|
|
277 |
public function install() {
|
|
|
278 |
global $CFG;
|
|
|
279 |
|
|
|
280 |
/// Check requisites are passed
|
|
|
281 |
if (!$this->requisitesok) {
|
|
|
282 |
return COMPONENT_ERROR;
|
|
|
283 |
}
|
|
|
284 |
/// Confirm we need upgrade
|
|
|
285 |
if ($this->need_upgrade() === COMPONENT_ERROR) {
|
|
|
286 |
return COMPONENT_ERROR;
|
|
|
287 |
} else if ($this->need_upgrade() === COMPONENT_UPTODATE) {
|
|
|
288 |
$this->errorstring='componentisuptodate';
|
|
|
289 |
return COMPONENT_UPTODATE;
|
|
|
290 |
}
|
|
|
291 |
/// Create temp directory if necesary
|
|
|
292 |
if (!make_temp_directory('', false)) {
|
|
|
293 |
$this->errorstring='cannotcreatetempdir';
|
|
|
294 |
return COMPONENT_ERROR;
|
|
|
295 |
}
|
|
|
296 |
/// Download zip file and save it to temp
|
|
|
297 |
if ($this->zippath) {
|
|
|
298 |
$source = $this->sourcebase.'/'.$this->zippath.'/'.$this->zipfilename;
|
|
|
299 |
} else {
|
|
|
300 |
$source = $this->sourcebase.'/'.$this->zipfilename;
|
|
|
301 |
}
|
|
|
302 |
|
|
|
303 |
$zipfile= $CFG->tempdir.'/'.$this->zipfilename;
|
|
|
304 |
|
|
|
305 |
$contents = download_file_content($source, null, null, true);
|
|
|
306 |
if ($contents->results && (int) $contents->status === 200) {
|
|
|
307 |
if ($file = fopen($zipfile, 'w')) {
|
|
|
308 |
if (!fwrite($file, $contents->results)) {
|
|
|
309 |
fclose($file);
|
|
|
310 |
$this->errorstring='cannotsavezipfile';
|
|
|
311 |
return COMPONENT_ERROR;
|
|
|
312 |
}
|
|
|
313 |
} else {
|
|
|
314 |
$this->errorstring='cannotsavezipfile';
|
|
|
315 |
return COMPONENT_ERROR;
|
|
|
316 |
}
|
|
|
317 |
fclose($file);
|
|
|
318 |
} else {
|
|
|
319 |
$this->errorstring='cannotdownloadzipfile';
|
|
|
320 |
return COMPONENT_ERROR;
|
|
|
321 |
}
|
|
|
322 |
/// Calculate its md5
|
|
|
323 |
$new_md5 = md5($contents->results);
|
|
|
324 |
/// Compare it with the remote md5 to check if we have the correct zip file
|
|
|
325 |
if (!$remote_md5 = $this->get_component_md5()) {
|
|
|
326 |
return COMPONENT_ERROR;
|
|
|
327 |
}
|
|
|
328 |
if ($new_md5 != $remote_md5) {
|
|
|
329 |
$this->errorstring='downloadedfilecheckfailed';
|
|
|
330 |
return COMPONENT_ERROR;
|
|
|
331 |
}
|
|
|
332 |
|
|
|
333 |
// Move current revision to a safe place.
|
|
|
334 |
$destinationdir = $CFG->dataroot . '/' . $this->destpath;
|
|
|
335 |
$destinationcomponent = $destinationdir . '/' . $this->componentname;
|
|
|
336 |
$destinationcomponentold = $destinationcomponent . '_old';
|
|
|
337 |
@remove_dir($destinationcomponentold); // Deleting a possible old version.
|
|
|
338 |
|
|
|
339 |
// Moving to a safe place.
|
|
|
340 |
@rename($destinationcomponent, $destinationcomponentold);
|
|
|
341 |
|
|
|
342 |
// Unzip new version.
|
|
|
343 |
$packer = get_file_packer('application/zip');
|
|
|
344 |
$unzipsuccess = $packer->extract_to_pathname($zipfile, $destinationdir, null, null, true);
|
|
|
345 |
if (!$unzipsuccess) {
|
|
|
346 |
@remove_dir($destinationcomponent);
|
|
|
347 |
@rename($destinationcomponentold, $destinationcomponent);
|
|
|
348 |
$this->errorstring = 'cannotunzipfile';
|
|
|
349 |
return COMPONENT_ERROR;
|
|
|
350 |
}
|
|
|
351 |
|
|
|
352 |
// Delete old component version.
|
|
|
353 |
@remove_dir($destinationcomponentold);
|
|
|
354 |
|
|
|
355 |
// Create local md5.
|
|
|
356 |
if ($file = fopen($destinationcomponent.'/'.$this->componentname.'.md5', 'w')) {
|
|
|
357 |
if (!fwrite($file, $new_md5)) {
|
|
|
358 |
fclose($file);
|
|
|
359 |
$this->errorstring='cannotsavemd5file';
|
|
|
360 |
return COMPONENT_ERROR;
|
|
|
361 |
}
|
|
|
362 |
} else {
|
|
|
363 |
$this->errorstring='cannotsavemd5file';
|
|
|
364 |
return COMPONENT_ERROR;
|
|
|
365 |
}
|
|
|
366 |
fclose($file);
|
|
|
367 |
/// Delete temp zip file
|
|
|
368 |
@unlink($zipfile);
|
|
|
369 |
|
|
|
370 |
return COMPONENT_INSTALLED;
|
|
|
371 |
}
|
|
|
372 |
|
|
|
373 |
/**
|
|
|
374 |
* This function will detect if remote component needs to be installed
|
|
|
375 |
* because it's different from the local one
|
|
|
376 |
*
|
|
|
377 |
* @uses COMPONENT_ERROR
|
|
|
378 |
* @uses COMPONENT_UPTODATE
|
|
|
379 |
* @uses COMPONENT_NEEDUPDATE
|
|
|
380 |
* @return int COMPONENT_(ERROR | UPTODATE | NEEDUPDATE)
|
|
|
381 |
*/
|
|
|
382 |
function need_upgrade() {
|
|
|
383 |
|
|
|
384 |
/// Check requisites are passed
|
|
|
385 |
if (!$this->requisitesok) {
|
|
|
386 |
return COMPONENT_ERROR;
|
|
|
387 |
}
|
|
|
388 |
/// Get local md5
|
|
|
389 |
$local_md5 = $this->get_local_md5();
|
|
|
390 |
/// Get remote md5
|
|
|
391 |
if (!$remote_md5 = $this->get_component_md5()) {
|
|
|
392 |
return COMPONENT_ERROR;
|
|
|
393 |
}
|
|
|
394 |
/// Return result
|
|
|
395 |
if ($local_md5 == $remote_md5) {
|
|
|
396 |
return COMPONENT_UPTODATE;
|
|
|
397 |
} else {
|
|
|
398 |
return COMPONENT_NEEDUPDATE;
|
|
|
399 |
}
|
|
|
400 |
}
|
|
|
401 |
|
|
|
402 |
/**
|
|
|
403 |
* This function will change the zip file to install on the fly
|
|
|
404 |
* to allow the class to process different components of the
|
|
|
405 |
* same md5 file without intantiating more objects.
|
|
|
406 |
*
|
|
|
407 |
* @param string $newzipfilename New zip filename to process
|
|
|
408 |
* @return boolean true/false
|
|
|
409 |
*/
|
|
|
410 |
function change_zip_file($newzipfilename) {
|
|
|
411 |
|
|
|
412 |
$this->zipfilename = $newzipfilename;
|
|
|
413 |
return $this->check_requisites();
|
|
|
414 |
}
|
|
|
415 |
|
|
|
416 |
/**
|
|
|
417 |
* This function will get the local md5 value of the installed
|
|
|
418 |
* component.
|
|
|
419 |
*
|
|
|
420 |
* @global object
|
|
|
421 |
* @return bool|string md5 of the local component (false on error)
|
|
|
422 |
*/
|
|
|
423 |
function get_local_md5() {
|
|
|
424 |
global $CFG;
|
|
|
425 |
|
|
|
426 |
/// Check requisites are passed
|
|
|
427 |
if (!$this->requisitesok) {
|
|
|
428 |
return false;
|
|
|
429 |
}
|
|
|
430 |
|
|
|
431 |
$return_value = 'needtobeinstalled'; /// Fake value to force new installation
|
|
|
432 |
|
|
|
433 |
/// Calculate source to read
|
|
|
434 |
$source = $CFG->dataroot.'/'.$this->destpath.'/'.$this->componentname.'/'.$this->componentname.'.md5';
|
|
|
435 |
/// Read md5 value stored (if exists)
|
|
|
436 |
if (file_exists($source)) {
|
|
|
437 |
if ($temp = file_get_contents($source)) {
|
|
|
438 |
$return_value = $temp;
|
|
|
439 |
}
|
|
|
440 |
}
|
|
|
441 |
return $return_value;
|
|
|
442 |
}
|
|
|
443 |
|
|
|
444 |
/**
|
|
|
445 |
* This function will download the specified md5 file, looking for the
|
|
|
446 |
* current componentname, returning its md5 field and storing extramd5info
|
|
|
447 |
* if present. Also it caches results to cachedmd5components for better
|
|
|
448 |
* performance in the same request.
|
|
|
449 |
*
|
|
|
450 |
* @return mixed md5 present in server (or false if error)
|
|
|
451 |
*/
|
|
|
452 |
function get_component_md5() {
|
|
|
453 |
|
|
|
454 |
/// Check requisites are passed
|
|
|
455 |
if (!$this->requisitesok) {
|
|
|
456 |
return false;
|
|
|
457 |
}
|
|
|
458 |
/// Get all components of md5 file
|
|
|
459 |
if (!$comp_arr = $this->get_all_components_md5()) {
|
|
|
460 |
if (empty($this->errorstring)) {
|
|
|
461 |
$this->errorstring='cannotdownloadcomponents';
|
|
|
462 |
}
|
|
|
463 |
return false;
|
|
|
464 |
}
|
|
|
465 |
/// Search for the componentname component
|
|
|
466 |
if (empty($comp_arr[$this->componentname]) || !$component = $comp_arr[$this->componentname]) {
|
|
|
467 |
$this->errorstring='cannotfindcomponent';
|
|
|
468 |
return false;
|
|
|
469 |
}
|
|
|
470 |
/// Check we have a valid md5
|
|
|
471 |
if (empty($component[1]) || strlen($component[1]) != 32) {
|
|
|
472 |
$this->errorstring='invalidmd5';
|
|
|
473 |
return false;
|
|
|
474 |
}
|
|
|
475 |
/// Set the extramd5info field
|
|
|
476 |
if (!empty($component[2])) {
|
|
|
477 |
$this->extramd5info = $component[2];
|
|
|
478 |
}
|
|
|
479 |
return $component[1];
|
|
|
480 |
}
|
|
|
481 |
|
|
|
482 |
/**
|
|
|
483 |
* This function allows you to retrieve the complete array of components found in
|
|
|
484 |
* the md5filename
|
|
|
485 |
*
|
|
|
486 |
* @return bool|array array of components in md5 file or false if error
|
|
|
487 |
*/
|
|
|
488 |
function get_all_components_md5() {
|
|
|
489 |
|
|
|
490 |
/// Check requisites are passed
|
|
|
491 |
if (!$this->requisitesok) {
|
|
|
492 |
return false;
|
|
|
493 |
}
|
|
|
494 |
|
|
|
495 |
/// Initialize components array
|
|
|
496 |
$comp_arr = array();
|
|
|
497 |
|
|
|
498 |
/// Define and retrieve the full md5 file
|
|
|
499 |
if ($this->zippath) {
|
|
|
500 |
$source = $this->sourcebase.'/'.$this->zippath.'/'.$this->md5filename;
|
|
|
501 |
} else {
|
|
|
502 |
$source = $this->sourcebase.'/'.$this->md5filename;
|
|
|
503 |
}
|
|
|
504 |
|
|
|
505 |
/// Check if we have downloaded the md5 file before (per request cache)
|
|
|
506 |
if (!empty($this->cachedmd5components[$source])) {
|
|
|
507 |
$comp_arr = $this->cachedmd5components[$source];
|
|
|
508 |
} else {
|
|
|
509 |
/// Not downloaded, let's do it now
|
|
|
510 |
$availablecomponents = array();
|
|
|
511 |
|
|
|
512 |
$contents = download_file_content($source, null, null, true);
|
|
|
513 |
if ($contents->results && (int) $contents->status === 200) {
|
|
|
514 |
/// Split text into lines
|
|
|
515 |
$lines = preg_split('/\r?\n/', $contents->results);
|
|
|
516 |
/// Each line will be one component
|
|
|
517 |
foreach($lines as $line) {
|
|
|
518 |
$availablecomponents[] = explode(',', $line);
|
|
|
519 |
}
|
|
|
520 |
/// If no components have been found, return error
|
|
|
521 |
if (empty($availablecomponents)) {
|
|
|
522 |
$this->errorstring='cannotdownloadcomponents';
|
|
|
523 |
return false;
|
|
|
524 |
}
|
|
|
525 |
/// Build an associative array of components for easily search
|
|
|
526 |
/// applying trim to avoid linefeeds and other...
|
|
|
527 |
$comp_arr = array();
|
|
|
528 |
foreach ($availablecomponents as $component) {
|
|
|
529 |
/// Avoid sometimes empty lines
|
|
|
530 |
if (empty($component[0])) {
|
|
|
531 |
continue;
|
|
|
532 |
}
|
|
|
533 |
$component[0]=trim($component[0]);
|
|
|
534 |
if (!empty($component[1])) {
|
|
|
535 |
$component[1]=trim($component[1]);
|
|
|
536 |
}
|
|
|
537 |
if (!empty($component[2])) {
|
|
|
538 |
$component[2]=trim($component[2]);
|
|
|
539 |
}
|
|
|
540 |
$comp_arr[$component[0]] = $component;
|
|
|
541 |
}
|
|
|
542 |
/// Cache components
|
|
|
543 |
$this->cachedmd5components[$source] = $comp_arr;
|
|
|
544 |
} else {
|
|
|
545 |
/// Return error
|
|
|
546 |
$this->errorstring='remotedownloaderror';
|
|
|
547 |
return false;
|
|
|
548 |
}
|
|
|
549 |
}
|
|
|
550 |
/// If there is no commponents or erros found, error
|
|
|
551 |
if (!empty($this->errorstring)) {
|
|
|
552 |
return false;
|
|
|
553 |
|
|
|
554 |
} else if (empty($comp_arr)) {
|
|
|
555 |
$this->errorstring='cannotdownloadcomponents';
|
|
|
556 |
return false;
|
|
|
557 |
}
|
|
|
558 |
return $comp_arr;
|
|
|
559 |
}
|
|
|
560 |
|
|
|
561 |
/**
|
|
|
562 |
* This function returns the errorstring
|
|
|
563 |
*
|
|
|
564 |
* @return string the error string
|
|
|
565 |
*/
|
|
|
566 |
function get_error() {
|
|
|
567 |
return $this->errorstring;
|
|
|
568 |
}
|
|
|
569 |
|
|
|
570 |
/** This function returns the extramd5 field (optional in md5 file)
|
|
|
571 |
*
|
|
|
572 |
* @return string the extramd5 field
|
|
|
573 |
*/
|
|
|
574 |
function get_extra_md5_field() {
|
|
|
575 |
return $this->extramd5info;
|
|
|
576 |
}
|
|
|
577 |
|
|
|
578 |
} /// End of component_installer class
|
|
|
579 |
|
|
|
580 |
|
|
|
581 |
/**
|
|
|
582 |
* Language packs installer
|
|
|
583 |
*
|
|
|
584 |
* This class wraps the functionality provided by {@link component_installer}
|
|
|
585 |
* and adds support for installing a set of language packs.
|
|
|
586 |
*
|
|
|
587 |
* Given an array of required language packs, this class fetches them all
|
|
|
588 |
* and installs them. It detects eventual dependencies and installs
|
|
|
589 |
* all parent languages, too.
|
|
|
590 |
*
|
|
|
591 |
* @copyright 2011 David Mudrak <david@moodle.com>
|
|
|
592 |
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
|
593 |
*/
|
|
|
594 |
class lang_installer {
|
|
|
595 |
|
|
|
596 |
/** lang pack was successfully downloaded and deployed */
|
|
|
597 |
const RESULT_INSTALLED = 'installed';
|
|
|
598 |
/** lang pack was up-to-date so no download was needed */
|
|
|
599 |
const RESULT_UPTODATE = 'uptodate';
|
|
|
600 |
/** there was a problem with downloading the lang pack */
|
|
|
601 |
const RESULT_DOWNLOADERROR = 'downloaderror';
|
|
|
602 |
|
|
|
603 |
/** @var array of languages to install */
|
|
|
604 |
protected $queue = array();
|
|
|
605 |
/** @var string the code of language being currently installed */
|
|
|
606 |
protected $current;
|
|
|
607 |
/** @var array of languages already installed by this instance */
|
|
|
608 |
protected $done = array();
|
|
|
609 |
/** @var string this Moodle major version */
|
|
|
610 |
protected $version;
|
|
|
611 |
|
|
|
612 |
/**
|
|
|
613 |
* Prepare the installer
|
|
|
614 |
*
|
|
|
615 |
* @param string|array $langcode a code of the language to install
|
|
|
616 |
*/
|
|
|
617 |
public function __construct($langcode = '') {
|
|
|
618 |
global $CFG;
|
|
|
619 |
|
|
|
620 |
$this->set_queue($langcode);
|
|
|
621 |
$this->version = moodle_major_version(true);
|
|
|
622 |
|
|
|
623 |
if (!empty($CFG->langotherroot) and $CFG->langotherroot !== $CFG->dataroot . '/lang') {
|
|
|
624 |
debugging('The in-built language pack installer does not support alternative location ' .
|
|
|
625 |
'of languages root directory. You are supposed to install and update your language '.
|
|
|
626 |
'packs on your own.');
|
|
|
627 |
}
|
|
|
628 |
}
|
|
|
629 |
|
|
|
630 |
/**
|
|
|
631 |
* Sets the queue of language packs to be installed
|
|
|
632 |
*
|
|
|
633 |
* @param string|array $langcodes language code like 'cs' or a list of them
|
|
|
634 |
*/
|
|
|
635 |
public function set_queue($langcodes) {
|
|
|
636 |
if (is_array($langcodes)) {
|
|
|
637 |
$this->queue = $langcodes;
|
|
|
638 |
} else if (!empty($langcodes)) {
|
|
|
639 |
$this->queue = array($langcodes);
|
|
|
640 |
}
|
|
|
641 |
}
|
|
|
642 |
|
|
|
643 |
/**
|
|
|
644 |
* Runs the installer
|
|
|
645 |
*
|
|
|
646 |
* This method calls {@link self::install_language_pack} for every language in the
|
|
|
647 |
* queue. If a dependency is detected, the parent language is added to the queue.
|
|
|
648 |
*
|
|
|
649 |
* @return array results, array of self::RESULT_xxx constants indexed by language code
|
|
|
650 |
*/
|
|
|
651 |
public function run() {
|
|
|
652 |
|
|
|
653 |
$results = array();
|
|
|
654 |
|
|
|
655 |
while ($this->current = array_shift($this->queue)) {
|
|
|
656 |
|
|
|
657 |
if ($this->was_processed($this->current)) {
|
|
|
658 |
// do not repeat yourself
|
|
|
659 |
continue;
|
|
|
660 |
}
|
|
|
661 |
|
|
|
662 |
if ($this->current === 'en') {
|
|
|
663 |
$this->mark_processed($this->current);
|
|
|
664 |
continue;
|
|
|
665 |
}
|
|
|
666 |
|
|
|
667 |
$results[$this->current] = $this->install_language_pack($this->current);
|
|
|
668 |
|
|
|
669 |
if (in_array($results[$this->current], array(self::RESULT_INSTALLED, self::RESULT_UPTODATE))) {
|
|
|
670 |
if ($parentlang = $this->get_parent_language($this->current)) {
|
|
|
671 |
if (!$this->is_queued($parentlang) and !$this->was_processed($parentlang)) {
|
|
|
672 |
$this->add_to_queue($parentlang);
|
|
|
673 |
}
|
|
|
674 |
}
|
|
|
675 |
}
|
|
|
676 |
|
|
|
677 |
$this->mark_processed($this->current);
|
|
|
678 |
}
|
|
|
679 |
|
|
|
680 |
return $results;
|
|
|
681 |
}
|
|
|
682 |
|
|
|
683 |
/**
|
|
|
684 |
* Returns the URL where a given language pack can be downloaded
|
|
|
685 |
*
|
|
|
686 |
* Alternatively, if the parameter is empty, returns URL of the page with the
|
|
|
687 |
* list of all available language packs.
|
|
|
688 |
*
|
|
|
689 |
* @param string $langcode language code like 'cs' or empty for unknown
|
|
|
690 |
* @return string URL
|
|
|
691 |
*/
|
|
|
692 |
public function lang_pack_url($langcode = '') {
|
|
|
693 |
|
|
|
694 |
if (empty($langcode)) {
|
|
|
695 |
return 'https://download.moodle.org/langpack/'.$this->version.'/';
|
|
|
696 |
} else {
|
|
|
697 |
return 'https://download.moodle.org/download.php/langpack/'.$this->version.'/'.$langcode.'.zip';
|
|
|
698 |
}
|
|
|
699 |
}
|
|
|
700 |
|
|
|
701 |
/**
|
|
|
702 |
* Returns the list of available language packs from download.moodle.org
|
|
|
703 |
*
|
|
|
704 |
* @return array|bool false if can not download
|
|
|
705 |
*/
|
|
|
706 |
public function get_remote_list_of_languages() {
|
|
|
707 |
$source = 'https://download.moodle.org/langpack/' . $this->version . '/languages.md5';
|
|
|
708 |
$availablelangs = array();
|
|
|
709 |
|
|
|
710 |
$contents = download_file_content($source, null, null, true);
|
|
|
711 |
if ($contents->results && (int) $contents->status === 200) {
|
|
|
712 |
$alllines = explode("\n", $contents->results);
|
|
|
713 |
foreach($alllines as $line) {
|
|
|
714 |
if (!empty($line)){
|
|
|
715 |
$availablelangs[] = explode(',', $line);
|
|
|
716 |
}
|
|
|
717 |
}
|
|
|
718 |
return $availablelangs;
|
|
|
719 |
|
|
|
720 |
} else {
|
|
|
721 |
return false;
|
|
|
722 |
}
|
|
|
723 |
}
|
|
|
724 |
|
|
|
725 |
// Internal implementation /////////////////////////////////////////////////
|
|
|
726 |
|
|
|
727 |
/**
|
|
|
728 |
* Adds a language pack (or a list of them) to the queue
|
|
|
729 |
*
|
|
|
730 |
* @param string|array $langcodes code of the language to install or a list of them
|
|
|
731 |
*/
|
|
|
732 |
protected function add_to_queue($langcodes) {
|
|
|
733 |
if (is_array($langcodes)) {
|
|
|
734 |
$this->queue = array_merge($this->queue, $langcodes);
|
|
|
735 |
} else if (!empty($langcodes)) {
|
|
|
736 |
$this->queue[] = $langcodes;
|
|
|
737 |
}
|
|
|
738 |
}
|
|
|
739 |
|
|
|
740 |
/**
|
|
|
741 |
* Checks if the given language is queued or if the queue is empty
|
|
|
742 |
*
|
|
|
743 |
* @example $installer->is_queued('es'); // is Spanish going to be installed?
|
|
|
744 |
* @example $installer->is_queued(); // is there a language queued?
|
|
|
745 |
*
|
|
|
746 |
* @param string $langcode language code or empty string for "any"
|
|
|
747 |
* @return boolean
|
|
|
748 |
*/
|
|
|
749 |
protected function is_queued($langcode = '') {
|
|
|
750 |
|
|
|
751 |
if (empty($langcode)) {
|
|
|
752 |
return !empty($this->queue);
|
|
|
753 |
|
|
|
754 |
} else {
|
|
|
755 |
return in_array($langcode, $this->queue);
|
|
|
756 |
}
|
|
|
757 |
}
|
|
|
758 |
|
|
|
759 |
/**
|
|
|
760 |
* Checks if the given language has already been processed by this instance
|
|
|
761 |
*
|
|
|
762 |
* @see self::mark_processed()
|
|
|
763 |
* @param string $langcode
|
|
|
764 |
* @return boolean
|
|
|
765 |
*/
|
|
|
766 |
protected function was_processed($langcode) {
|
|
|
767 |
return isset($this->done[$langcode]);
|
|
|
768 |
}
|
|
|
769 |
|
|
|
770 |
/**
|
|
|
771 |
* Mark the given language pack as processed
|
|
|
772 |
*
|
|
|
773 |
* @see self::was_processed()
|
|
|
774 |
* @param string $langcode
|
|
|
775 |
*/
|
|
|
776 |
protected function mark_processed($langcode) {
|
|
|
777 |
$this->done[$langcode] = 1;
|
|
|
778 |
}
|
|
|
779 |
|
|
|
780 |
/**
|
|
|
781 |
* Returns a parent language of the given installed language
|
|
|
782 |
*
|
|
|
783 |
* @param string $langcode
|
|
|
784 |
* @return string parent language's code
|
|
|
785 |
*/
|
|
|
786 |
protected function get_parent_language($langcode) {
|
|
|
787 |
return get_parent_language($langcode);
|
|
|
788 |
}
|
|
|
789 |
|
|
|
790 |
/**
|
|
|
791 |
* Perform the actual language pack installation
|
|
|
792 |
*
|
|
|
793 |
* @uses component_installer
|
|
|
794 |
* @param string $langcode
|
|
|
795 |
* @return string return status
|
|
|
796 |
*/
|
|
|
797 |
protected function install_language_pack($langcode) {
|
|
|
798 |
|
|
|
799 |
// initialise new component installer to process this language
|
|
|
800 |
$installer = new component_installer('https://download.moodle.org', 'download.php/direct/langpack/' . $this->version,
|
|
|
801 |
$langcode . '.zip', 'languages.md5', 'lang');
|
|
|
802 |
|
|
|
803 |
if (!$installer->requisitesok) {
|
|
|
804 |
throw new lang_installer_exception('installer_requisites_check_failed');
|
|
|
805 |
}
|
|
|
806 |
|
|
|
807 |
$status = $installer->install();
|
|
|
808 |
|
|
|
809 |
if ($status == COMPONENT_ERROR) {
|
|
|
810 |
if ($installer->get_error() === 'remotedownloaderror') {
|
|
|
811 |
return self::RESULT_DOWNLOADERROR;
|
|
|
812 |
} else {
|
|
|
813 |
throw new lang_installer_exception($installer->get_error(), $langcode);
|
|
|
814 |
}
|
|
|
815 |
|
|
|
816 |
} else if ($status == COMPONENT_UPTODATE) {
|
|
|
817 |
return self::RESULT_UPTODATE;
|
|
|
818 |
|
|
|
819 |
} else if ($status == COMPONENT_INSTALLED) {
|
|
|
820 |
return self::RESULT_INSTALLED;
|
|
|
821 |
|
|
|
822 |
} else {
|
|
|
823 |
throw new lang_installer_exception('unexpected_installer_result', $status);
|
|
|
824 |
}
|
|
|
825 |
}
|
|
|
826 |
}
|
|
|
827 |
|
|
|
828 |
|
|
|
829 |
/**
|
|
|
830 |
* Exception thrown by {@link lang_installer}
|
|
|
831 |
*
|
|
|
832 |
* @copyright 2011 David Mudrak <david@moodle.com>
|
|
|
833 |
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
|
834 |
*/
|
|
|
835 |
class lang_installer_exception extends moodle_exception {
|
|
|
836 |
|
|
|
837 |
public function __construct($errorcode, $debuginfo = null) {
|
|
|
838 |
parent::__construct($errorcode, 'error', '', null, $debuginfo);
|
|
|
839 |
}
|
|
|
840 |
}
|