MDL-49329 admin: Get rid of mdeploy and \core\update\deployer class
[moodle.git] / admin / tool / installaddon / classes / installer.php
CommitLineData
0056f2a3
DM
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/**
af96f120 19 * Provides tool_installaddon_installer related classes
0056f2a3
DM
20 *
21 * @package tool_installaddon
22 * @subpackage classes
23 * @copyright 2013 David Mudrak <david@moodle.com>
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 */
26
27defined('MOODLE_INTERNAL') || die();
28
29/**
30 * Implements main plugin features.
31 *
32 * @copyright 2013 David Mudrak <david@moodle.com>
33 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34 */
35class tool_installaddon_installer {
36
e835ef58 37 /** @var tool_installaddon_installfromzip_form */
ddab904b
DM
38 protected $installfromzipform = null;
39
0056f2a3
DM
40 /**
41 * Factory method returning an instance of this class.
42 *
43 * @return tool_installaddon_installer
44 */
45 public static function instance() {
07083b23 46 return new static();
0056f2a3
DM
47 }
48
af96f120
DM
49 /**
50 * Returns the URL to the main page of this admin tool
51 *
52 * @param array optional parameters
53 * @return moodle_url
54 */
55 public function index_url(array $params = null) {
56 return new moodle_url('/admin/tool/installaddon/index.php', $params);
57 }
58
0056f2a3
DM
59 /**
60 * Returns URL to the repository that addons can be searched in and installed from
61 *
62 * @return moodle_url
63 */
64 public function get_addons_repository_url() {
65 global $CFG;
66
67 if (!empty($CFG->config_php_settings['alternativeaddonsrepositoryurl'])) {
68 $url = $CFG->config_php_settings['alternativeaddonsrepositoryurl'];
69 } else {
70 $url = 'https://moodle.org/plugins/get.php';
71 }
72
73 if (!$this->should_send_site_info()) {
74 return new moodle_url($url);
75 }
76
77 // Append the basic information about our site.
78 $site = array(
79 'fullname' => $this->get_site_fullname(),
80 'url' => $this->get_site_url(),
b7f64426 81 'majorversion' => $this->get_site_major_version(),
0056f2a3
DM
82 );
83
84 $site = $this->encode_site_information($site);
85
86 return new moodle_url($url, array('site' => $site));
87 }
88
89 /**
e835ef58 90 * @return tool_installaddon_installfromzip_form
0056f2a3
DM
91 */
92 public function get_installfromzip_form() {
ddab904b
DM
93 if (!is_null($this->installfromzipform)) {
94 return $this->installfromzipform;
95 }
96
af96f120 97 $action = $this->index_url();
0056f2a3
DM
98 $customdata = array('installer' => $this);
99
e835ef58 100 $this->installfromzipform = new tool_installaddon_installfromzip_form($action, $customdata);
ddab904b
DM
101
102 return $this->installfromzipform;
0056f2a3
DM
103 }
104
585b64a6 105 /**
e835ef58 106 * Saves the ZIP file from the {@link tool_installaddon_installfromzip_form} form
585b64a6
DM
107 *
108 * The file is saved into the given temporary location for inspection and eventual
109 * deployment. The form is expected to be submitted and validated.
110 *
e835ef58 111 * @param tool_installaddon_installfromzip_form $form
585b64a6
DM
112 * @param string $targetdir full path to the directory where the ZIP should be stored to
113 * @return string filename of the saved file relative to the given target
114 */
e835ef58 115 public function save_installfromzip_file(tool_installaddon_installfromzip_form $form, $targetdir) {
585b64a6
DM
116
117 $filename = clean_param($form->get_new_filename('zipfile'), PARAM_FILE);
118 $form->save_file('zipfile', $targetdir.'/'.$filename);
119
120 return $filename;
121 }
122
07083b23
DM
123 /**
124 * Extracts the saved file previously saved by {self::save_installfromzip_file()}
125 *
126 * The list of files found in the ZIP is returned via $zipcontentfiles parameter
127 * by reference. The format of that list is array of (string)filerelpath => (bool|string)
128 * where the array value is either true or a string describing the problematic file.
129 *
130 * @see zip_packer::extract_to_pathname()
131 * @param string $zipfilepath full path to the saved ZIP file
132 * @param string $targetdir full path to the directory to extract the ZIP file to
133 * @param string $rootdir explicitly rename the root directory of the ZIP into this non-empty value
134 * @param array list of extracted files as returned by {@link zip_packer::extract_to_pathname()}
135 */
136 public function extract_installfromzip_file($zipfilepath, $targetdir, $rootdir = '') {
137 global $CFG;
138 require_once($CFG->libdir.'/filelib.php');
139
140 $fp = get_file_packer('application/zip');
141 $files = $fp->extract_to_pathname($zipfilepath, $targetdir);
142
99456a55 143 if (!$files) {
07083b23
DM
144 return array();
145 }
99456a55
PS
146
147 if (!empty($rootdir)) {
148 $files = $this->rename_extracted_rootdir($targetdir, $rootdir, $files);
149 }
150
151 // Sometimes zip may not contain all parent directories, add them to make it consistent.
152 foreach ($files as $path => $status) {
153 if ($status !== true) {
154 continue;
155 }
156 $parts = explode('/', trim($path, '/'));
157 while (array_pop($parts)) {
158 if (empty($parts)) {
159 break;
160 }
161 $dir = implode('/', $parts).'/';
162 if (!isset($files[$dir])) {
163 $files[$dir] = true;
164 }
165 }
166 }
167
168 return $files;
07083b23
DM
169 }
170
0056f2a3
DM
171 /**
172 * Returns localised list of available plugin types
173 *
174 * @return array (string)plugintype => (string)plugin name
175 */
176 public function get_plugin_types_menu() {
177 global $CFG;
0056f2a3 178
e87214bd 179 $pluginman = core_plugin_manager::instance();
0056f2a3
DM
180
181 $menu = array('' => get_string('choosedots'));
182 foreach (array_keys($pluginman->get_plugin_types()) as $plugintype) {
183 $menu[$plugintype] = $pluginman->plugintype_name($plugintype).' ('.$plugintype.')';
184 }
185
186 return $menu;
187 }
188
ddab904b 189 /**
30bec5ba
DM
190 * Returns the full path of the root of the given plugin type
191 *
192 * Null is returned if the plugin type is not known. False is returned if the plugin type
193 * root is expected but not found. Otherwise, string is returned.
ddab904b 194 *
ddab904b 195 * @param string $plugintype
30bec5ba 196 * @return string|bool|null
ddab904b 197 */
30bec5ba 198 public function get_plugintype_root($plugintype) {
ddab904b
DM
199
200 $plugintypepath = null;
46f6f7f2 201 foreach (core_component::get_plugin_types() as $type => $fullpath) {
ddab904b
DM
202 if ($type === $plugintype) {
203 $plugintypepath = $fullpath;
204 break;
205 }
206 }
207 if (is_null($plugintypepath)) {
30bec5ba 208 return null;
ddab904b
DM
209 }
210
211 if (!is_dir($plugintypepath)) {
30bec5ba
DM
212 return false;
213 }
214
215 return $plugintypepath;
216 }
217
218 /**
219 * Is it possible to create a new plugin directory for the given plugin type?
220 *
221 * @throws coding_exception for invalid plugin types or non-existing plugin type locations
222 * @param string $plugintype
223 * @return boolean
224 */
225 public function is_plugintype_writable($plugintype) {
226
227 $plugintypepath = $this->get_plugintype_root($plugintype);
228
229 if (is_null($plugintypepath)) {
230 throw new coding_exception('Unknown plugin type!');
231 }
232
233 if ($plugintypepath === false) {
ddab904b
DM
234 throw new coding_exception('Plugin type location does not exist!');
235 }
236
237 return is_writable($plugintypepath);
238 }
239
af96f120
DM
240 /**
241 * Hook method to handle the remote request to install an add-on
242 *
243 * This is used as a callback when the admin picks a plugin version in the
244 * Moodle Plugins directory and is redirected back to their site to install
245 * it.
246 *
247 * This hook is called early from admin/tool/installaddon/index.php page so that
248 * it has opportunity to take over the UI.
249 *
250 * @param tool_installaddon_renderer $output
251 * @param string|null $request
252 * @param bool $confirmed
253 */
254 public function handle_remote_request(tool_installaddon_renderer $output, $request, $confirmed = false) {
af96f120
DM
255
256 if (is_null($request)) {
257 return;
258 }
259
260 $data = $this->decode_remote_request($request);
261
262 if ($data === false) {
263 echo $output->remote_request_invalid_page($this->index_url());
264 exit();
265 }
266
56da374e 267 list($plugintype, $pluginname) = core_component::normalize_component($data->component);
af96f120
DM
268
269 $plugintypepath = $this->get_plugintype_root($plugintype);
270
271 if (file_exists($plugintypepath.'/'.$pluginname)) {
272 echo $output->remote_request_alreadyinstalled_page($data, $this->index_url());
273 exit();
274 }
275
276 if (!$this->is_plugintype_writable($plugintype)) {
277 $continueurl = $this->index_url(array('installaddonrequest' => $request));
278 echo $output->remote_request_permcheck_page($data, $plugintypepath, $continueurl, $this->index_url());
279 exit();
280 }
281
282 $continueurl = $this->index_url(array(
283 'installaddonrequest' => $request,
284 'confirm' => 1,
285 'sesskey' => sesskey()));
286
287 if (!$confirmed) {
288 echo $output->remote_request_confirm_page($data, $continueurl, $this->index_url());
289 exit();
290 }
291
292 // The admin has confirmed their intention to install the add-on.
293 require_sesskey();
294
295 // Fetch the plugin info. The essential information is the URL to download the ZIP
296 // and the MD5 hash of the ZIP, obtained via HTTPS.
48900324
DM
297 $client = \core\update\api::client();
298 $pluginfo = $client->get_plugin_info($data->component, $data->version);
af96f120 299
48900324
DM
300 if (empty($pluginfo) or empty($pluginfo->version)) {
301 echo $output->remote_request_pluginfo_failure($data, $this->index_url());
302 exit();
af96f120
DM
303 }
304
305 // Fetch the ZIP with the plugin version
306 $jobid = md5(rand().uniqid('', true));
307 $sourcedir = make_temp_directory('tool_installaddon/'.$jobid.'/source');
308 $zipfilename = 'downloaded.zip';
309
310 try {
48900324 311 $this->download_file($pluginfo->version->downloadurl, $sourcedir.'/'.$zipfilename);
af96f120
DM
312
313 } catch (tool_installaddon_installer_exception $e) {
314 if (debugging()) {
315 throw $e;
316 } else {
317 echo $output->installer_exception($e, $this->index_url());
318 exit();
319 }
320 }
321
322 // Check the MD5 checksum
48900324 323 $md5expected = $pluginfo->version->downloadmd5;
af96f120
DM
324 $md5actual = md5_file($sourcedir.'/'.$zipfilename);
325 if ($md5expected !== $md5actual) {
326 $e = new tool_installaddon_installer_exception('err_zip_md5', array('expected' => $md5expected, 'actual' => $md5actual));
327 if (debugging()) {
328 throw $e;
329 } else {
330 echo $output->installer_exception($e, $this->index_url());
331 exit();
332 }
333 }
334
335 // Redirect to the validation page.
336 $nexturl = new moodle_url('/admin/tool/installaddon/validate.php', array(
337 'sesskey' => sesskey(),
338 'jobid' => $jobid,
339 'zip' => $zipfilename,
340 'type' => $plugintype));
341 redirect($nexturl);
342 }
343
344 /**
345 * Download the given file into the given destination.
346 *
347 * This is basically a simplified version of {@link download_file_content()} from
348 * Moodle itself, tuned for fetching files from moodle.org servers. Same code is used
349 * in mdeploy.php for fetching available updates.
350 *
3bca7dbf
DM
351 * TODO This all will be rewritten to use new plugin manager features.
352 *
af96f120
DM
353 * @param string $source file url starting with http(s)://
354 * @param string $target store the downloaded content to this file (full path)
355 * @throws tool_installaddon_installer_exception
356 */
357 public function download_file($source, $target) {
358 global $CFG;
3e6a8aeb 359 require_once($CFG->libdir.'/filelib.php');
af96f120 360
3e6a8aeb
DM
361 $targetfile = fopen($target, 'w');
362
363 if (!$targetfile) {
364 throw new tool_installaddon_installer_exception('err_download_write_file', $target);
af96f120
DM
365 }
366
3e6a8aeb
DM
367 $options = array(
368 'file' => $targetfile,
369 'timeout' => 300,
370 'followlocation' => true,
371 'maxredirs' => 3,
372 'ssl_verifypeer' => true,
373 'ssl_verifyhost' => 2,
374 );
af96f120 375
3e6a8aeb 376 $curl = new curl(array('proxy' => true));
af96f120 377
3e6a8aeb 378 $result = $curl->download_one($source, null, $options);
af96f120 379
3e6a8aeb 380 $curlinfo = $curl->get_info();
af96f120
DM
381
382 fclose($targetfile);
383
3e6a8aeb
DM
384 if ($result !== true) {
385 throw new tool_installaddon_installer_exception('err_curl_exec', array(
386 'url' => $source, 'errorno' => $curl->get_errno(), 'error' => $result));
af96f120 387
3e6a8aeb
DM
388 } else if (empty($curlinfo['http_code']) or $curlinfo['http_code'] != 200) {
389 throw new tool_installaddon_installer_exception('err_curl_http_code', array(
390 'url' => $source, 'http_code' => $curlinfo['http_code']));
af96f120 391
3e6a8aeb
DM
392 } else if (isset($curlinfo['ssl_verify_result']) and $curlinfo['ssl_verify_result'] != 0) {
393 throw new tool_installaddon_installer_exception('err_curl_ssl_verify', array(
394 'url' => $source, 'ssl_verify_result' => $curlinfo['ssl_verify_result']));
af96f120
DM
395 }
396 }
397
ed70c74b
DM
398 /**
399 * Moves the given source into a new location recursively
400 *
401 * This is cross-device safe implementation to be used instead of the native rename() function.
402 * See https://bugs.php.net/bug.php?id=54097 for more details.
403 *
404 * @param string $source full path to the existing directory
405 * @param string $target full path to the new location of the directory
a635424f
PS
406 * @param int $dirpermissions
407 * @param int $filepermissions
ed70c74b 408 */
a635424f 409 public function move_directory($source, $target, $dirpermissions, $filepermissions) {
ed70c74b
DM
410
411 if (file_exists($target)) {
412 throw new tool_installaddon_installer_exception('err_folder_already_exists', array('path' => $target));
413 }
414
415 if (is_dir($source)) {
416 $handle = opendir($source);
417 } else {
418 throw new tool_installaddon_installer_exception('err_no_such_folder', array('path' => $source));
419 }
420
a635424f
PS
421 if (!file_exists($target)) {
422 // Do not use make_writable_directory() here - it is intended for dataroot only.
423 mkdir($target, true);
424 @chmod($target, $dirpermissions);
425 }
426
427 if (!is_writable($target)) {
428 closedir($handle);
429 throw new tool_installaddon_installer_exception('err_folder_not_writable', array('path' => $target));
430 }
ed70c74b
DM
431
432 while ($filename = readdir($handle)) {
433 $sourcepath = $source.'/'.$filename;
434 $targetpath = $target.'/'.$filename;
435
436 if ($filename === '.' or $filename === '..') {
437 continue;
438 }
439
440 if (is_dir($sourcepath)) {
a635424f 441 $this->move_directory($sourcepath, $targetpath, $dirpermissions, $filepermissions);
ed70c74b
DM
442
443 } else {
444 rename($sourcepath, $targetpath);
a635424f 445 @chmod($targetpath, $filepermissions);
ed70c74b
DM
446 }
447 }
448
449 closedir($handle);
450
451 rmdir($source);
452
453 clearstatcache();
454 }
455
bbf3cd4e
DM
456 /**
457 * Detect the given plugin's component name
458 *
459 * Only plugins that declare valid $plugin->component value in the version.php
460 * are supported.
461 *
462 * @param string $zipfilepath full path to the saved ZIP file
463 * @param string $workdir full path to the directory we can use for extracting required bits from the archive
464 * @return string|bool declared component name or false if unable to detect
465 */
466 public function detect_plugin_component($zipfilepath, $workdir) {
467
468 $versionphp = $this->extract_versionphp_file($zipfilepath, $workdir);
469
470 if (empty($versionphp)) {
471 return false;
472 }
473
474 return $this->detect_plugin_component_from_versionphp(file_get_contents($workdir.'/'.$versionphp));
475 }
476
0056f2a3
DM
477 //// End of external API ///////////////////////////////////////////////////
478
07083b23
DM
479 /**
480 * @see self::instance()
481 */
482 protected function __construct() {
483 }
484
0056f2a3
DM
485 /**
486 * @return string this site full name
487 */
488 protected function get_site_fullname() {
489 global $SITE;
490
b7f64426 491 return strip_tags($SITE->fullname);
0056f2a3
DM
492 }
493
494 /**
495 * @return string this site URL
496 */
497 protected function get_site_url() {
498 global $CFG;
499
500 return $CFG->wwwroot;
501 }
502
503 /**
504 * @return string major version like 2.5, 2.6 etc.
505 */
506 protected function get_site_major_version() {
507 return moodle_major_version();
508 }
509
510 /**
511 * Encodes the given array in a way that can be safely appended as HTTP GET param
512 *
513 * Be ware! The recipient may rely on the exact way how the site information is encoded.
514 * Do not change anything here unless you know what you are doing and understand all
515 * consequences! (Don't you love warnings like that, too? :-p)
516 *
517 * @param array $info
518 * @return string
519 */
520 protected function encode_site_information(array $info) {
521 return base64_encode(json_encode($info));
522 }
523
524 /**
525 * Decide if the encoded site information should be sent to the add-ons repository site
526 *
527 * For now, we just return true. In the future, we may want to implement some
528 * privacy aware logic (based on site/user preferences for example).
529 *
530 * @return bool
531 */
532 protected function should_send_site_info() {
533 return true;
534 }
07083b23
DM
535
536 /**
537 * Renames the root directory of the extracted ZIP package.
538 *
539 * This method does not validate the presence of the single root directory
540 * (the validator does it later). It just searches for the first directory
541 * under the given location and renames it.
542 *
543 * The method will not rename the root if the requested location already
544 * exists.
545 *
546 * @param string $dirname the location of the extracted ZIP package
547 * @param string $rootdir the requested name of the root directory
548 * @param array $files list of extracted files
549 * @return array eventually amended list of extracted files
550 */
551 protected function rename_extracted_rootdir($dirname, $rootdir, array $files) {
552
553 if (!is_dir($dirname)) {
554 debugging('Unable to rename rootdir of non-existing content', DEBUG_DEVELOPER);
555 return $files;
556 }
557
558 if (file_exists($dirname.'/'.$rootdir)) {
559 debugging('Unable to rename rootdir to already existing folder', DEBUG_DEVELOPER);
560 return $files;
561 }
562
563 $found = null; // The name of the first subdirectory under the $dirname.
564 foreach (scandir($dirname) as $item) {
565 if (substr($item, 0, 1) === '.') {
566 continue;
567 }
568 if (is_dir($dirname.'/'.$item)) {
569 $found = $item;
570 break;
571 }
572 }
573
574 if (!is_null($found)) {
575 if (rename($dirname.'/'.$found, $dirname.'/'.$rootdir)) {
576 $newfiles = array();
577 foreach ($files as $filepath => $status) {
578 $newpath = preg_replace('~^'.preg_quote($found.'/').'~', preg_quote($rootdir.'/'), $filepath);
579 $newfiles[$newpath] = $status;
580 }
581 return $newfiles;
582 }
583 }
584
585 return $files;
586 }
af96f120
DM
587
588 /**
589 * Decode the request from the Moodle Plugins directory
590 *
591 * @param string $request submitted via 'installaddonrequest' HTTP parameter
592 * @return stdClass|bool false on error, object otherwise
593 */
594 protected function decode_remote_request($request) {
595
596 $data = base64_decode($request, true);
597
598 if ($data === false) {
599 return false;
600 }
601
602 $data = json_decode($data);
603
604 if (is_null($data)) {
605 return false;
606 }
607
608 if (!isset($data->name) or !isset($data->component) or !isset($data->version)) {
609 return false;
610 }
611
612 $data->name = s(strip_tags($data->name));
613
614 if ($data->component !== clean_param($data->component, PARAM_COMPONENT)) {
615 return false;
616 }
617
9e19a0f0 618 list($plugintype, $pluginname) = core_component::normalize_component($data->component);
af96f120
DM
619
620 if ($plugintype === 'core') {
621 return false;
622 }
623
624 if ($data->component !== $plugintype.'_'.$pluginname) {
625 return false;
626 }
627
9e19a0f0
PS
628 if (!core_component::is_valid_plugin_name($plugintype, $pluginname)) {
629 return false;
630 }
631
632 $plugintypes = core_component::get_plugin_types();
633 if (!isset($plugintypes[$plugintype])) {
634 return false;
635 }
636
af96f120
DM
637 // Keep this regex in sync with the one used by the download.moodle.org/api/x.y/pluginfo.php
638 if (!preg_match('/^[0-9]+$/', $data->version)) {
639 return false;
640 }
641
642 return $data;
643 }
bbf3cd4e
DM
644
645 /**
646 * Extracts the version.php from the given plugin ZIP file into the target directory
647 *
648 * @param string $zipfilepath full path to the saved ZIP file
649 * @param string $targetdir full path to extract the file to
650 * @return string|bool path to the version.php within the $targetpath; false on error (e.g. not found)
651 */
652 protected function extract_versionphp_file($zipfilepath, $targetdir) {
653 global $CFG;
654 require_once($CFG->libdir.'/filelib.php');
655
656 $fp = get_file_packer('application/zip');
657 $files = $fp->list_files($zipfilepath);
658
659 if (empty($files)) {
660 return false;
661 }
662
663 $rootdirname = null;
664 $found = null;
665
666 foreach ($files as $file) {
667 // Valid plugin ZIP package has just one root directory with all
668 // files in it.
669 $pathnameitems = explode('/', $file->pathname);
670
671 if (empty($pathnameitems)) {
672 return false;
673 }
674
675 // Set the expected name of the root directory in the first
676 // iteration of the loop.
677 if ($rootdirname === null) {
678 $rootdirname = $pathnameitems[0];
679 }
680
681 // Require the same root directory for all files in the ZIP
682 // package.
683 if ($rootdirname !== $pathnameitems[0]) {
684 return false;
685 }
686
687 // If we reached the valid version.php file, remember it.
688 if ($pathnameitems[1] === 'version.php' and !$file->is_directory and $file->size > 0) {
689 $found = $file->pathname;
690 }
691 }
692
693 if (empty($found)) {
694 return false;
695 }
696
697 $extracted = $fp->extract_to_pathname($zipfilepath, $targetdir, array($found));
698
699 if (empty($extracted)) {
700 return false;
701 }
702
703 // The following syntax uses function array dereferencing, added in PHP 5.4.0.
704 return array_keys($extracted)[0];
705 }
706
707 /**
708 * Return the plugin component declared in its version.php file
709 *
710 * @param string $code the contents of the version.php file
711 * @return string|bool declared plugin component or false if unable to detect
712 */
713 protected function detect_plugin_component_from_versionphp($code) {
714
715 $result = preg_match_all('#^\s*\$plugin\->component\s*=\s*([\'"])(.+?_.+?)\1\s*;#m', $code, $matches);
716
717 // Return if and only if the single match was detected.
718 if ($result === 1 and !empty($matches[2][0])) {
719 return $matches[2][0];
720 }
721
722 return false;
723 }
af96f120
DM
724}
725
726
727/**
728 * General exception thrown by {@link tool_installaddon_installer} class
729 *
730 * @copyright 2013 David Mudrak <david@moodle.com>
731 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
732 */
733class tool_installaddon_installer_exception extends moodle_exception {
734
735 /**
736 * @param string $errorcode exception description identifier
737 * @param mixed $debuginfo debugging data to display
738 */
739 public function __construct($errorcode, $a=null, $debuginfo=null) {
740 parent::__construct($errorcode, 'tool_installaddon', '', $a, print_r($debuginfo, true));
741 }
0056f2a3 742}