Fixes to the PaintWeb cron task.
[moodle/mihaisucan.git] / lib / componentlib.class.php
blobe44115f1edf1c77ddb311d7d494fdd9e88a8d9f6
1 <?php //$Id$
3 ///////////////////////////////////////////////////////////////////////////
4 // //
5 // NOTICE OF COPYRIGHT //
6 // //
7 // Moodle - Modular Object-Oriented Dynamic Learning Environment //
8 // http://moodle.com //
9 // //
10 // Copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com //
11 // (C) 2001-3001 Eloy Lafuente (stronk7) http://contiento.com //
12 // //
13 // This program is free software; you can redistribute it and/or modify //
14 // it under the terms of the GNU General Public License as published by //
15 // the Free Software Foundation; either version 2 of the License, or //
16 // (at your option) any later version. //
17 // //
18 // This program is distributed in the hope that it will be useful, //
19 // but WITHOUT ANY WARRANTY; without even the implied warranty of //
20 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
21 // GNU General Public License for more details: //
22 // //
23 // http://www.gnu.org/copyleft/gpl.html //
24 // //
25 ///////////////////////////////////////////////////////////////////////////
27 // This library includes all the necessary stuff to use the one-click
28 // download and install feature of Moodle, used to keep updated some
29 // items like languages, pear, enviroment... i.e, components.
31 // It has been developed harcoding some important limits that are
32 // explained below:
33 // - It only can check, download and install items under moodledata.
34 // - Every downloadeable item must be one zip file.
35 // - The zip file root content must be 1 directory, i.e, everything
36 // is stored under 1 directory.
37 // - Zip file name and root directory must have the same name (but
38 // the .zip extension, of course).
39 // - Every .zip file must be defined in one .md5 file that will be
40 // stored in the same remote directory than the .zip file.
41 // - The name of such .md5 file is free, although it's recommended
42 // to use the same name than the .zip (that's the default
43 // assumption if no specified).
44 // - Every remote .md5 file will be a comma separated (CVS) file where each
45 // line will follow this format:
46 // - Field 1: name of the zip file (without extension). Mandatory.
47 // - Field 2: md5 of the zip file. Mandatory.
48 // - Field 3: whatever you want (or need). Optional.
49 // -Every local .md5 file will:
50 // - Have the zip file name (without the extension) plus -md5
51 // - Will reside inside the expanded zip file dir
52 // - Will contain the md5 od the latest installed component
53 // With all these details present, the process will perform this tasks:
54 // - Perform security checks. Only admins are allowed to use this for now.
55 // - Read the .md5 file from source (1).
56 // - Extract the correct line for the .zip being requested.
57 // - Compare it with the local .md5 file (2).
58 // - If different:
59 // - Download the newer .zip file from source.
60 // - Calculate its md5 (3).
61 // - Compare (1) and (3).
62 // - If equal:
63 // - Delete old directory.
64 // - Uunzip the newer .zip file.
65 // - Create the new local .md5 file.
66 // - Delete the .zip file.
67 // - If different:
68 // - ERROR. Old package won't be modified. We shouldn't
69 // reach here ever.
70 // - If component download is not possible, a message text about how to do
71 // the process manually (remotedownloaderror) must be displayed to explain it.
73 // General Usage:
75 // To install one component:
77 // require_once($CFG->libdir.'/componentlib.class.php');
78 // if ($cd = new component_installer('http://download.moodle.org', 'lang16',
79 // 'es_utf8.zip', 'languages.md5', 'lang')) {
80 // $status = $cd->install(); //returns COMPONENT_(ERROR | UPTODATE | INSTALLED)
81 // switch ($status) {
82 // case COMPONENT_ERROR:
83 // if ($cd->get_error() == 'remotedownloaderror') {
84 // $a = new stdClass();
85 // $a->url = 'http://download.moodle.org/lang16/es_utf8.zip';
86 // $a->dest= $CFG->dataroot.'/lang';
87 // print_error($cd->get_error(), 'error', '', $a);
88 // } else {
89 // print_error($cd->get_error(), 'error');
90 // }
91 // break;
92 // case COMPONENT_UPTODATE:
93 // //Print error string or whatever you want to do
94 // break;
95 // case COMPONENT_INSTALLED:
96 // //Print/do whatever you want
97 // break;
98 // default:
99 // //We shouldn't reach this point
100 // }
101 // } else {
102 // //We shouldn't reach this point
103 // }
105 // To switch of component (maintaining the rest of settings):
107 // $status = $cd->change_zip_file('en_utf8.zip'); //returns boolean false on error
109 // To retrieve all the components in one remote md5 file
111 // $components = $cd->get_all_components_md5(); //returns boolean false on error, array instead
113 // To check if current component needs to be updated
115 // $status = $cd->need_upgrade(); //returns COMPONENT_(ERROR | UPTODATE | NEEDUPDATE)
117 // To get the 3rd field of the md5 file (optional)
119 // $field = $cd->get_extra_md5_field(); //returns string (empty if not exists)
121 // For all the error situations the $cd->get_error() method should return always the key of the
122 // error to be retrieved by one standard get_string() call against the error.php lang file.
124 // That's all!
125 global $CFG;
126 require_once($CFG->libdir.'/filelib.php');
128 // Some needed constants
129 define('COMPONENT_ERROR', 0);
130 define('COMPONENT_UPTODATE', 1);
131 define('COMPONENT_NEEDUPDATE', 2);
132 define('COMPONENT_INSTALLED', 3);
135 * This class is used to check, download and install items from
136 * download.moodle.org to the moodledata directory. It always
137 * return true/false in all their public methods to say if
138 * execution has ended succesfuly or not. If there is any problem
139 * its getError() method can be called, returning one error string
140 * to be used with the standard get/print_string() functions.
142 class component_installer {
144 var $sourcebase; /// Full http URL, base for downloadable items
145 var $zippath; /// Relative path (from sourcebase) where the
146 /// downloadeable item resides.
147 var $zipfilename; /// Name of the .zip file to be downloaded
148 var $md5filename; /// Name of the .md5 file to be read
149 var $componentname;/// Name of the component. Must be the zip name without
150 /// the extension. And it defines a lot of things:
151 /// the md5 line to search for, the default m5 file name
152 /// and the name of the root dir stored inside the zip file
153 var $destpath; /// Relative path (from moodledata) where the .zip
154 /// file will be expanded.
155 var $errorstring; /// Latest error produced. It will contain one lang string key.
156 var $extramd5info; /// Contents of the optional third field in the .md5 file.
157 var $requisitesok; /// Flag to see if requisites check has been passed ok.
159 var $cachedmd5components; /// Array of cached components to avoid to
160 /// download the same md5 file more than once per request.
163 * Standard constructor of the class. It will initialize all attributes.
164 * without performing any check at all.
166 * @param string Full http URL, base for downloadeable items
167 * @param string Relative path (from sourcebase) where the
168 * downloadeable item resides
169 * @param string Name of the .zip file to be downloaded
170 * @param string Name of the .md5 file to be read (default '' = same
171 * than zipfilename)
172 * @param string Relative path (from moodledata) where the .zip file will
173 * be expanded (default='' = moodledataitself)
174 * @return object
176 function component_installer ($sourcebase, $zippath, $zipfilename, $md5filename='', $destpath='') {
178 $this->sourcebase = $sourcebase;
179 $this->zippath = $zippath;
180 $this->zipfilename = $zipfilename;
181 $this->md5filename = $md5filename;
182 $this->componentname= '';
183 $this->destpath = $destpath;
184 $this->errorstring = '';
185 $this->extramd5info = '';
186 $this->requisitesok = false;
187 $this->cachedmd5components = array();
189 $this->check_requisites();
193 * This function will check if everything is properly set to begin
194 * one installation. Also, it will check for required settings
195 * and will fill everything as needed.
197 * @return boolean true/false (plus detailed error in errorstring)
199 function check_requisites() {
200 global $CFG;
202 $this->requisitesok = false;
204 /// Check that everything we need is present
205 if (empty($this->sourcebase) || empty($this->zippath) || empty($this->zipfilename)) {
206 $this->errorstring='missingrequiredfield';
207 return false;
209 /// Check for correct sourcebase (this will be out in the future)
210 if ($this->sourcebase != 'http://download.moodle.org') {
211 $this->errorstring='wrongsourcebase';
212 return false;
214 /// Check the zip file is a correct one (by extension)
215 if (stripos($this->zipfilename, '.zip') === false) {
216 $this->errorstring='wrongzipfilename';
217 return false;
219 /// Check that exists under dataroot
220 if (!empty($this->destpath)) {
221 if (!file_exists($CFG->dataroot.'/'.$this->destpath)) {
222 $this->errorstring='wrongdestpath';
223 return false;
226 /// Calculate the componentname
227 $pos = stripos($this->zipfilename, '.zip');
228 $this->componentname = substr($this->zipfilename, 0, $pos);
229 /// Calculate md5filename if it's empty
230 if (empty($this->md5filename)) {
231 $this->md5filename = $this->componentname.'.md5';
233 /// Set the requisites passed flag
234 $this->requisitesok = true;
235 return true;
239 * This function will perform the full installation if needed, i.e.
240 * compare md5 values, download, unzip, install and regenerate
241 * local md5 file
243 * @return int COMPONENT_(ERROR | UPTODATE | INSTALLED)
245 function install() {
247 global $CFG;
249 /// Check requisites are passed
250 if (!$this->requisitesok) {
251 return COMPONENT_ERROR;
253 /// Confirm we need upgrade
254 if ($this->need_upgrade() === COMPONENT_ERROR) {
255 return COMPONENT_ERROR;
256 } else if ($this->need_upgrade() === COMPONENT_UPTODATE) {
257 $this->errorstring='componentisuptodate';
258 return COMPONENT_UPTODATE;
260 /// Create temp directory if necesary
261 if (!make_upload_directory('temp', false)) {
262 $this->errorstring='cannotcreatetempdir';
263 return COMPONENT_ERROR;
265 /// Download zip file and save it to temp
266 $source = $this->sourcebase.'/'.$this->zippath.'/'.$this->zipfilename;
267 $zipfile= $CFG->dataroot.'/temp/'.$this->zipfilename;
269 if($contents = download_file_content($source)) {
270 if ($file = fopen($zipfile, 'w')) {
271 if (!fwrite($file, $contents)) {
272 fclose($file);
273 $this->errorstring='cannotsavezipfile';
274 return COMPONENT_ERROR;
276 } else {
277 $this->errorstring='cannotsavezipfile';
278 return COMPONENT_ERROR;
280 fclose($file);
281 } else {
282 $this->errorstring='cannotdownloadzipfile';
283 return COMPONENT_ERROR;
285 /// Calculate its md5
286 $new_md5 = md5($contents);
287 /// Compare it with the remote md5 to check if we have the correct zip file
288 if (!$remote_md5 = $this->get_component_md5()) {
289 return COMPONENT_ERROR;
291 if ($new_md5 != $remote_md5) {
292 $this->errorstring='downloadedfilecheckfailed';
293 return COMPONENT_ERROR;
295 /// Move current revision to a safe place
296 $destinationdir = $CFG->dataroot.'/'.$this->destpath;
297 $destinationcomponent = $destinationdir.'/'.$this->componentname;
298 @remove_dir($destinationcomponent.'_old'); //Deleting possible old components before
299 @rename ($destinationcomponent, $destinationcomponent.'_old'); //Moving to a safe place
300 /// Unzip new version
301 if (!unzip_file($zipfile, $destinationdir, false)) {
302 /// Error so, go back to the older
303 @remove_dir($destinationcomponent);
304 @rename ($destinationcomponent.'_old', $destinationcomponent);
305 $this->errorstring='cannotunzipfile';
306 return COMPONENT_ERROR;
308 /// Delete old component version
309 @remove_dir($destinationcomponent.'_old');
310 /// Create local md5
311 if ($file = fopen($destinationcomponent.'/'.$this->componentname.'.md5', 'w')) {
312 if (!fwrite($file, $new_md5)) {
313 fclose($file);
314 $this->errorstring='cannotsavemd5file';
315 return COMPONENT_ERROR;
317 } else {
318 $this->errorstring='cannotsavemd5file';
319 return COMPONENT_ERROR;
321 fclose($file);
322 /// Delete temp zip file
323 @unlink($zipfile);
325 return COMPONENT_INSTALLED;
329 * This function will detect if remote component needs to be installed
330 * because it's different from the local one
332 * @return int COMPONENT_(ERROR | UPTODATE | NEEDUPDATE)
334 function need_upgrade() {
336 /// Check requisites are passed
337 if (!$this->requisitesok) {
338 return COMPONENT_ERROR;
340 /// Get local md5
341 $local_md5 = $this->get_local_md5();
342 /// Get remote md5
343 if (!$remote_md5 = $this->get_component_md5()) {
344 return COMPONENT_ERROR;
346 /// Return result
347 if ($local_md5 == $remote_md5) {
348 return COMPONENT_UPTODATE;
349 } else {
350 return COMPONENT_NEEDUPDATE;
355 * This function will change the zip file to install on the fly
356 * to allow the class to process different components of the
357 * same md5 file without intantiating more objects.
359 * @param string New zip filename to process
360 * @return boolean true/false
362 function change_zip_file($newzipfilename) {
364 $this->zipfilename = $newzipfilename;
365 return $this->check_requisites();
369 * This function will get the local md5 value of the installed
370 * component.
372 * @return string md5 of the local component (false on error)
374 function get_local_md5() {
375 global $CFG;
377 /// Check requisites are passed
378 if (!$this->requisitesok) {
379 return false;
382 $return_value = 'needtobeinstalled'; /// Fake value to force new installation
384 /// Calculate source to read
385 $source = $CFG->dataroot.'/'.$this->destpath.'/'.$this->componentname.'/'.$this->componentname.'.md5';
386 /// Read md5 value stored (if exists)
387 if (file_exists($source)) {
388 if ($temp = file_get_contents($source)) {
389 $return_value = $temp;
392 return $return_value;
396 * This function will download the specified md5 file, looking for the
397 * current componentname, returning its md5 field and storing extramd5info
398 * if present. Also it caches results to cachedmd5components for better
399 * performance in the same request.
401 * @return mixed md5 present in server (or false if error)
403 function get_component_md5() {
405 /// Check requisites are passed
406 if (!$this->requisitesok) {
407 return false;
409 /// Get all components of md5 file
410 if (!$comp_arr = $this->get_all_components_md5()) {
411 if (empty($this->errorstring)) {
412 $this->errorstring='cannotdownloadcomponents';
414 return false;
416 /// Search for the componentname component
417 if (empty($comp_arr[$this->componentname]) || !$component = $comp_arr[$this->componentname]) {
418 $this->errorstring='cannotfindcomponent';
419 return false;
421 /// Check we have a valid md5
422 if (empty($component[1]) || strlen($component[1]) != 32) {
423 $this->errorstring='invalidmd5';
424 return false;
426 /// Set the extramd5info field
427 if (!empty($component[2])) {
428 $this->extramd5info = $component[2];
430 return $component[1];
434 * This function allows you to retrieve the complete array of components found in
435 * the md5filename
437 * @return array array of components in md5 file or false if error
439 function get_all_components_md5() {
441 /// Check requisites are passed
442 if (!$this->requisitesok) {
443 return false;
446 /// Initialize components array
447 $comp_arr = array();
449 /// Define and retrieve the full md5 file
450 $source = $this->sourcebase.'/'.$this->zippath.'/'.$this->md5filename;
452 /// Check if we have downloaded the md5 file before (per request cache)
453 if (!empty($this->cachedmd5components[$source])) {
454 $comp_arr = $this->cachedmd5components[$source];
455 } else {
456 /// Not downloaded, let's do it now
457 $availablecomponents = array();
459 if ($contents = download_file_content($source)) {
460 /// Split text into lines
461 $lines=preg_split('/\r?\n/',$contents);
462 /// Each line will be one component
463 foreach($lines as $line) {
464 $availablecomponents[] = split(',', $line);
466 /// If no components have been found, return error
467 if (empty($availablecomponents)) {
468 $this->errorstring='cannotdownloadcomponents';
469 return false;
471 /// Build an associative array of components for easily search
472 /// applying trim to avoid linefeeds and other...
473 $comp_arr = array();
474 foreach ($availablecomponents as $component) {
475 /// Avoid sometimes empty lines
476 if (empty($component[0])) {
477 continue;
479 $component[0]=trim($component[0]);
480 $component[1]=trim($component[1]);
481 if (!empty($component[2])) {
482 $component[2]=trim($component[2]);
484 $comp_arr[$component[0]] = $component;
486 /// Cache components
487 $this->cachedmd5components[$source] = $comp_arr;
488 } else {
489 /// Return error
490 $this->errorstring='remotedownloaderror';
491 return false;
494 /// If there is no commponents or erros found, error
495 if (!empty($this->errorstring)) {
496 return false;
498 } else if (empty($comp_arr)) {
499 $this->errorstring='cannotdownloadcomponents';
500 return false;
502 return $comp_arr;
506 * This function returns the errorstring
508 * @return string the error string
510 function get_error() {
511 return $this->errorstring;
514 /** This function returns the extramd5 field (optional in md5 file)
516 * @return string the extramd5 field
518 function get_extra_md5_field() {
519 return $this->extramd5info;
522 } /// End of component_installer class