Merge branch 'MDL-29201_20' of git://github.com/timhunt/moodle into MOODLE_20_STABLE
[moodle.git] / lib / componentlib.class.php
blobb064161c51fa86854bba155f45413d85ce57e1b8
1 <?php
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.
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/>.
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.
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.
65 * General Usage:
67 * To install one component:
68 * <code>
69 * require_once($CFG->libdir.'/componentlib.class.php');
70 * if ($cd = new component_installer('http://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 = 'http://download.moodle.org/langpack/2.0/es.zip';
78 * $a->dest= $CFG->dataroot.'/lang';
79 * print_error($cd->get_error(), 'error', '', $a);
80 * } else {
81 * print_error($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>
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>
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>
108 * To check if current component needs to be updated
109 * <code>
110 * $status = $cd->need_upgrade(); //returns COMPONENT_(ERROR | UPTODATE | NEEDUPDATE)
111 * </code>
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>
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.
121 * That's all!
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
128 defined('MOODLE_INTERNAL') || die();
131 * @global object $CFG
132 * @name $CFG
134 global $CFG;
135 require_once($CFG->libdir.'/filelib.php');
137 // Some needed constants
138 define('COMPONENT_ERROR', 0);
139 define('COMPONENT_UPTODATE', 1);
140 define('COMPONENT_NEEDUPDATE', 2);
141 define('COMPONENT_INSTALLED', 3);
144 * This class is used to check, download and install items from
145 * download.moodle.org to the moodledata directory.
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.
152 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
153 * @package moodlecore
155 class component_installer {
157 * @var string
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.
174 * @var array
176 var $cachedmd5components; /// Array of cached components to avoid to
177 /// download the same md5 file more than once per request.
180 * Standard constructor of the class. It will initialize all attributes.
181 * without performing any check at all.
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
193 function component_installer ($sourcebase, $zippath, $zipfilename, $md5filename='', $destpath='') {
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();
206 $this->check_requisites();
210 * This function will check if everything is properly set to begin
211 * one installation. Also, it will check for required settings
212 * and will fill everything as needed.
214 * @global object
215 * @return boolean true/false (plus detailed error in errorstring)
217 function check_requisites() {
218 global $CFG;
220 $this->requisitesok = false;
222 /// Check that everything we need is present
223 if (empty($this->sourcebase) || empty($this->zippath) || empty($this->zipfilename)) {
224 $this->errorstring='missingrequiredfield';
225 return false;
227 /// Check for correct sourcebase (this will be out in the future)
228 if ($this->sourcebase != 'http://download.moodle.org') {
229 $this->errorstring='wrongsourcebase';
230 return false;
232 /// Check the zip file is a correct one (by extension)
233 if (stripos($this->zipfilename, '.zip') === false) {
234 $this->errorstring='wrongzipfilename';
235 return false;
237 /// Check that exists under dataroot
238 if (!empty($this->destpath)) {
239 if (!file_exists($CFG->dataroot.'/'.$this->destpath)) {
240 $this->errorstring='wrongdestpath';
241 return false;
244 /// Calculate the componentname
245 $pos = stripos($this->zipfilename, '.zip');
246 $this->componentname = substr($this->zipfilename, 0, $pos);
247 /// Calculate md5filename if it's empty
248 if (empty($this->md5filename)) {
249 $this->md5filename = $this->componentname.'.md5';
251 /// Set the requisites passed flag
252 $this->requisitesok = true;
253 return true;
257 * This function will perform the full installation if needed, i.e.
258 * compare md5 values, download, unzip, install and regenerate
259 * local md5 file
261 * @global object
262 * @uses COMPONENT_ERROR
263 * @uses COMPONENT_UPTODATE
264 * @uses COMPONENT_ERROR
265 * @uses COMPONENT_INSTALLED
266 * @return int COMPONENT_(ERROR | UPTODATE | INSTALLED)
268 function install() {
270 global $CFG;
272 /// Check requisites are passed
273 if (!$this->requisitesok) {
274 return COMPONENT_ERROR;
276 /// Confirm we need upgrade
277 if ($this->need_upgrade() === COMPONENT_ERROR) {
278 return COMPONENT_ERROR;
279 } else if ($this->need_upgrade() === COMPONENT_UPTODATE) {
280 $this->errorstring='componentisuptodate';
281 return COMPONENT_UPTODATE;
283 /// Create temp directory if necesary
284 if (!make_upload_directory('temp', false)) {
285 $this->errorstring='cannotcreatetempdir';
286 return COMPONENT_ERROR;
288 /// Download zip file and save it to temp
289 $source = $this->sourcebase.'/'.$this->zippath.'/'.$this->zipfilename;
290 $zipfile= $CFG->dataroot.'/temp/'.$this->zipfilename;
292 if($contents = download_file_content($source)) {
293 if ($file = fopen($zipfile, 'w')) {
294 if (!fwrite($file, $contents)) {
295 fclose($file);
296 $this->errorstring='cannotsavezipfile';
297 return COMPONENT_ERROR;
299 } else {
300 $this->errorstring='cannotsavezipfile';
301 return COMPONENT_ERROR;
303 fclose($file);
304 } else {
305 $this->errorstring='cannotdownloadzipfile';
306 return COMPONENT_ERROR;
308 /// Calculate its md5
309 $new_md5 = md5($contents);
310 /// Compare it with the remote md5 to check if we have the correct zip file
311 if (!$remote_md5 = $this->get_component_md5()) {
312 return COMPONENT_ERROR;
314 if ($new_md5 != $remote_md5) {
315 $this->errorstring='downloadedfilecheckfailed';
316 return COMPONENT_ERROR;
318 /// Move current revision to a safe place
319 $destinationdir = $CFG->dataroot.'/'.$this->destpath;
320 $destinationcomponent = $destinationdir.'/'.$this->componentname;
321 @remove_dir($destinationcomponent.'_old'); //Deleting possible old components before
322 @rename ($destinationcomponent, $destinationcomponent.'_old'); //Moving to a safe place
323 /// Unzip new version
324 if (!unzip_file($zipfile, $destinationdir, false)) {
325 /// Error so, go back to the older
326 @remove_dir($destinationcomponent);
327 @rename ($destinationcomponent.'_old', $destinationcomponent);
328 $this->errorstring='cannotunzipfile';
329 return COMPONENT_ERROR;
331 /// Delete old component version
332 @remove_dir($destinationcomponent.'_old');
333 /// Create local md5
334 if ($file = fopen($destinationcomponent.'/'.$this->componentname.'.md5', 'w')) {
335 if (!fwrite($file, $new_md5)) {
336 fclose($file);
337 $this->errorstring='cannotsavemd5file';
338 return COMPONENT_ERROR;
340 } else {
341 $this->errorstring='cannotsavemd5file';
342 return COMPONENT_ERROR;
344 fclose($file);
345 /// Delete temp zip file
346 @unlink($zipfile);
348 return COMPONENT_INSTALLED;
352 * This function will detect if remote component needs to be installed
353 * because it's different from the local one
355 * @uses COMPONENT_ERROR
356 * @uses COMPONENT_UPTODATE
357 * @uses COMPONENT_NEEDUPDATE
358 * @return int COMPONENT_(ERROR | UPTODATE | NEEDUPDATE)
360 function need_upgrade() {
362 /// Check requisites are passed
363 if (!$this->requisitesok) {
364 return COMPONENT_ERROR;
366 /// Get local md5
367 $local_md5 = $this->get_local_md5();
368 /// Get remote md5
369 if (!$remote_md5 = $this->get_component_md5()) {
370 return COMPONENT_ERROR;
372 /// Return result
373 if ($local_md5 == $remote_md5) {
374 return COMPONENT_UPTODATE;
375 } else {
376 return COMPONENT_NEEDUPDATE;
381 * This function will change the zip file to install on the fly
382 * to allow the class to process different components of the
383 * same md5 file without intantiating more objects.
385 * @param string $newzipfilename New zip filename to process
386 * @return boolean true/false
388 function change_zip_file($newzipfilename) {
390 $this->zipfilename = $newzipfilename;
391 return $this->check_requisites();
395 * This function will get the local md5 value of the installed
396 * component.
398 * @global object
399 * @return bool|string md5 of the local component (false on error)
401 function get_local_md5() {
402 global $CFG;
404 /// Check requisites are passed
405 if (!$this->requisitesok) {
406 return false;
409 $return_value = 'needtobeinstalled'; /// Fake value to force new installation
411 /// Calculate source to read
412 $source = $CFG->dataroot.'/'.$this->destpath.'/'.$this->componentname.'/'.$this->componentname.'.md5';
413 /// Read md5 value stored (if exists)
414 if (file_exists($source)) {
415 if ($temp = file_get_contents($source)) {
416 $return_value = $temp;
419 return $return_value;
423 * This function will download the specified md5 file, looking for the
424 * current componentname, returning its md5 field and storing extramd5info
425 * if present. Also it caches results to cachedmd5components for better
426 * performance in the same request.
428 * @return mixed md5 present in server (or false if error)
430 function get_component_md5() {
432 /// Check requisites are passed
433 if (!$this->requisitesok) {
434 return false;
436 /// Get all components of md5 file
437 if (!$comp_arr = $this->get_all_components_md5()) {
438 if (empty($this->errorstring)) {
439 $this->errorstring='cannotdownloadcomponents';
441 return false;
443 /// Search for the componentname component
444 if (empty($comp_arr[$this->componentname]) || !$component = $comp_arr[$this->componentname]) {
445 $this->errorstring='cannotfindcomponent';
446 return false;
448 /// Check we have a valid md5
449 if (empty($component[1]) || strlen($component[1]) != 32) {
450 $this->errorstring='invalidmd5';
451 return false;
453 /// Set the extramd5info field
454 if (!empty($component[2])) {
455 $this->extramd5info = $component[2];
457 return $component[1];
461 * This function allows you to retrieve the complete array of components found in
462 * the md5filename
464 * @return bool|array array of components in md5 file or false if error
466 function get_all_components_md5() {
468 /// Check requisites are passed
469 if (!$this->requisitesok) {
470 return false;
473 /// Initialize components array
474 $comp_arr = array();
476 /// Define and retrieve the full md5 file
477 $source = $this->sourcebase.'/'.$this->zippath.'/'.$this->md5filename;
479 /// Check if we have downloaded the md5 file before (per request cache)
480 if (!empty($this->cachedmd5components[$source])) {
481 $comp_arr = $this->cachedmd5components[$source];
482 } else {
483 /// Not downloaded, let's do it now
484 $availablecomponents = array();
486 if ($contents = download_file_content($source)) {
487 /// Split text into lines
488 $lines=preg_split('/\r?\n/',$contents);
489 /// Each line will be one component
490 foreach($lines as $line) {
491 $availablecomponents[] = explode(',', $line);
493 /// If no components have been found, return error
494 if (empty($availablecomponents)) {
495 $this->errorstring='cannotdownloadcomponents';
496 return false;
498 /// Build an associative array of components for easily search
499 /// applying trim to avoid linefeeds and other...
500 $comp_arr = array();
501 foreach ($availablecomponents as $component) {
502 /// Avoid sometimes empty lines
503 if (empty($component[0])) {
504 continue;
506 $component[0]=trim($component[0]);
507 if (!empty($component[1])) {
508 $component[1]=trim($component[1]);
510 if (!empty($component[2])) {
511 $component[2]=trim($component[2]);
513 $comp_arr[$component[0]] = $component;
515 /// Cache components
516 $this->cachedmd5components[$source] = $comp_arr;
517 } else {
518 /// Return error
519 $this->errorstring='remotedownloaderror';
520 return false;
523 /// If there is no commponents or erros found, error
524 if (!empty($this->errorstring)) {
525 return false;
527 } else if (empty($comp_arr)) {
528 $this->errorstring='cannotdownloadcomponents';
529 return false;
531 return $comp_arr;
535 * This function returns the errorstring
537 * @return string the error string
539 function get_error() {
540 return $this->errorstring;
543 /** This function returns the extramd5 field (optional in md5 file)
545 * @return string the extramd5 field
547 function get_extra_md5_field() {
548 return $this->extramd5info;
551 } /// End of component_installer class