Commit | Line | Data |
---|---|---|
754a102b EL |
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 | * @package moodlecore | |
20 | * @subpackage backup-controller | |
21 | * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} | |
22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
23 | */ | |
24 | ||
25 | /** | |
26 | * Class implementing the controller of any restore process | |
27 | * | |
28 | * This final class is in charge of controlling all the restore architecture, for any | |
29 | * type of backup. | |
30 | * | |
31 | * TODO: Finish phpdocs | |
32 | */ | |
77b3d9df | 33 | class restore_controller extends base_controller { |
754a102b | 34 | |
7aa06e6d | 35 | protected $tempdir; // Directory under tempdir/backup awaiting restore |
754a102b EL |
36 | protected $restoreid; // Unique identificator for this restore |
37 | ||
38 | protected $courseid; // courseid where restore is going to happen | |
39 | ||
40 | protected $type; // Type of backup (activity, section, course) | |
41 | protected $format; // Format of backup (moodle, imscc) | |
42 | protected $interactive; // yes/no | |
43 | protected $mode; // Purpose of the backup (default settings) | |
44 | protected $userid; // user id executing the restore | |
45 | protected $operation; // Type of operation (backup/restore) | |
46 | protected $target; // Restoring to new/existing/current_adding/_deleting | |
482aac65 | 47 | protected $samesite; // Are we restoring to the same site where the backup was generated |
754a102b EL |
48 | |
49 | protected $status; // Current status of the controller (created, planned, configured...) | |
c3ea499d | 50 | protected $precheck; // Results of the execution of restore prechecks |
754a102b EL |
51 | |
52 | protected $info; // Information retrieved from backup contents | |
53 | protected $plan; // Restore execution plan | |
54 | ||
55 | protected $execution; // inmediate/delayed | |
56 | protected $executiontime; // epoch time when we want the restore to be executed (requires cron to run) | |
57 | ||
754a102b EL |
58 | protected $checksum; // Cache @checksumable results for lighter @is_checksum_correct() uses |
59 | ||
ddf368a2 | 60 | /** @var int Number of restore_controllers that are currently executing */ |
61 | protected static $executing = 0; | |
62 | ||
785d6603 | 63 | /** |
bf2a039e | 64 | * Constructor. |
65 | * | |
66 | * If you specify a progress monitor, this will be used to report progress | |
67 | * while loading the plan, as well as for future use. (You can change it | |
68 | * for a different one later using set_progress.) | |
785d6603 | 69 | * |
7aa06e6d | 70 | * @param string $tempdir Directory under tempdir/backup awaiting restore |
785d6603 SH |
71 | * @param int $courseid Course id where restore is going to happen |
72 | * @param bool $interactive backup::INTERACTIVE_YES[true] or backup::INTERACTIVE_NO[false] | |
73 | * @param int $mode backup::MODE_[ GENERAL | HUB | IMPORT | SAMESITE ] | |
74 | * @param int $userid | |
75 | * @param int $target backup::TARGET_[ NEW_COURSE | CURRENT_ADDING | CURRENT_DELETING | EXISTING_ADDING | EXISTING_DELETING ] | |
809fdb83 | 76 | * @param \core\progress\base $progress Optional progress monitor |
785d6603 | 77 | */ |
bf2a039e | 78 | public function __construct($tempdir, $courseid, $interactive, $mode, $userid, $target, |
809fdb83 | 79 | \core\progress\base $progress = null) { |
754a102b EL |
80 | $this->tempdir = $tempdir; |
81 | $this->courseid = $courseid; | |
82 | $this->interactive = $interactive; | |
83 | $this->mode = $mode; | |
84 | $this->userid = $userid; | |
85 | $this->target = $target; | |
86 | ||
87 | // Apply some defaults | |
88 | $this->type = ''; | |
89 | $this->format = backup::FORMAT_UNKNOWN; | |
90 | $this->execution = backup::EXECUTION_INMEDIATE; | |
91 | $this->operation = backup::OPERATION_RESTORE; | |
92 | $this->executiontime = 0; | |
482aac65 | 93 | $this->samesite = false; |
754a102b | 94 | $this->checksum = ''; |
c3ea499d | 95 | $this->precheck = null; |
754a102b EL |
96 | |
97 | // Apply current backup version and release if necessary | |
98 | backup_controller_dbops::apply_version_and_release(); | |
99 | ||
100 | // Check courseid is correct | |
101 | restore_check::check_courseid($this->courseid); | |
102 | ||
103 | // Check user is correct | |
104 | restore_check::check_user($this->userid); | |
105 | ||
106 | // Calculate unique $restoreid | |
107 | $this->calculate_restoreid(); | |
108 | ||
109 | // Default logger chain (based on interactive/execution) | |
110 | $this->logger = backup_factory::get_logger_chain($this->interactive, $this->execution, $this->restoreid); | |
111 | ||
bf2a039e | 112 | // By default there is no progress reporter unless you specify one so it |
113 | // can be used during loading of the plan. | |
114 | if ($progress) { | |
115 | $this->progress = $progress; | |
116 | } else { | |
303936aa | 117 | $this->progress = new \core\progress\none(); |
bf2a039e | 118 | } |
119 | $this->progress->start_progress('Constructing restore_controller'); | |
16cd7088 | 120 | |
754a102b EL |
121 | // Instantiate the output_controller singleton and active it if interactive and inmediate |
122 | $oc = output_controller::get_instance(); | |
123 | if ($this->interactive == backup::INTERACTIVE_YES && $this->execution == backup::EXECUTION_INMEDIATE) { | |
124 | $oc->set_active(true); | |
125 | } | |
126 | ||
127 | $this->log('instantiating restore controller', backup::LOG_INFO, $this->restoreid); | |
128 | ||
129 | // Set initial status | |
130 | $this->set_status(backup::STATUS_CREATED); | |
131 | ||
132 | // Calculate original restore format | |
133 | $this->format = backup_general_helper::detect_backup_format($tempdir); | |
134 | ||
135 | // If format is not moodle2, set to conversion needed | |
136 | if ($this->format !== backup::FORMAT_MOODLE) { | |
137 | $this->set_status(backup::STATUS_REQUIRE_CONV); | |
138 | ||
139 | // Else, format is moodle2, load plan, apply security and set status based on interactivity | |
140 | } else { | |
141 | // Load plan | |
142 | $this->load_plan(); | |
143 | ||
144 | // Perform all initial security checks and apply (2nd param) them to settings automatically | |
145 | restore_check::check_security($this, true); | |
146 | ||
147 | if ($this->interactive == backup::INTERACTIVE_YES) { | |
148 | $this->set_status(backup::STATUS_SETTING_UI); | |
149 | } else { | |
150 | $this->set_status(backup::STATUS_NEED_PRECHECK); | |
151 | } | |
152 | } | |
bf2a039e | 153 | |
154 | // Tell progress monitor that we finished loading. | |
155 | $this->progress->end_progress(); | |
754a102b EL |
156 | } |
157 | ||
8231bb15 EL |
158 | /** |
159 | * Clean structures used by the restore_controller | |
160 | * | |
161 | * This method clean various structures used by the restore_controller, | |
162 | * destroying them in an ordered way, so their memory will be gc properly | |
163 | * by PHP (mainly circular references). | |
164 | * | |
165 | * Note that, while it's not mandatory to execute this method, it's highly | |
166 | * recommended to do so, specially in scripts performing multiple operations | |
167 | * (like the automated backups) or the system will run out of memory after | |
168 | * a few dozens of backups) | |
169 | */ | |
170 | public function destroy() { | |
171 | // Only need to destroy circulars under the plan. Delegate to it. | |
172 | $this->plan->destroy(); | |
53f95c99 EL |
173 | // Loggers may have also chained references, destroy them. Also closing resources when needed. |
174 | $this->logger->destroy(); | |
8231bb15 EL |
175 | } |
176 | ||
754a102b EL |
177 | public function finish_ui() { |
178 | if ($this->status != backup::STATUS_SETTING_UI) { | |
785d6603 | 179 | throw new restore_controller_exception('cannot_finish_ui_if_not_setting_ui'); |
754a102b EL |
180 | } |
181 | $this->set_status(backup::STATUS_NEED_PRECHECK); | |
182 | } | |
183 | ||
184 | public function process_ui_event() { | |
185 | ||
186 | // Perform security checks throwing exceptions (2nd param) if something is wrong | |
187 | restore_check::check_security($this, false); | |
188 | } | |
189 | ||
190 | public function set_status($status) { | |
749fd68c EL |
191 | // Note: never save_controller() with the object info after STATUS_EXECUTING or the whole controller, |
192 | // containing all the steps will be sent to DB. 100% (monster) useless. | |
754a102b | 193 | $this->log('setting controller status to', backup::LOG_DEBUG, $status); |
749fd68c | 194 | // TODO: Check it's a correct status. |
754a102b | 195 | $this->status = $status; |
749fd68c | 196 | // Ensure that, once set to backup::STATUS_AWAITING | STATUS_NEED_PRECHECK, controller is stored in DB. |
754a102b EL |
197 | if ($status == backup::STATUS_AWAITING || $status == backup::STATUS_NEED_PRECHECK) { |
198 | $this->save_controller(); | |
8231bb15 EL |
199 | $tbc = self::load_controller($this->restoreid); |
200 | $this->logger = $tbc->logger; // wakeup loggers | |
53f95c99 | 201 | $tbc->plan->destroy(); // Clean plan controller structures, keeping logger alive. |
749fd68c EL |
202 | |
203 | } else if ($status == backup::STATUS_FINISHED_OK) { | |
204 | // If the operation has ended without error (backup::STATUS_FINISHED_OK) | |
205 | // proceed by cleaning the object from database. MDL-29262. | |
206 | $this->save_controller(false, true); | |
754a102b EL |
207 | } |
208 | } | |
209 | ||
210 | public function set_execution($execution, $executiontime = 0) { | |
211 | $this->log('setting controller execution', backup::LOG_DEBUG); | |
212 | // TODO: Check valid execution mode | |
213 | // TODO: Check time in future | |
214 | // TODO: Check time = 0 if inmediate | |
215 | $this->execution = $execution; | |
216 | $this->executiontime = $executiontime; | |
217 | ||
218 | // Default logger chain (based on interactive/execution) | |
219 | $this->logger = backup_factory::get_logger_chain($this->interactive, $this->execution, $this->restoreid); | |
220 | } | |
221 | ||
222 | // checksumable interface methods | |
223 | ||
224 | public function calculate_checksum() { | |
225 | // Reset current checksum to take it out from calculations! | |
226 | $this->checksum = ''; | |
227 | // Init checksum | |
228 | $tempchecksum = md5('tempdir-' . $this->tempdir . | |
229 | 'restoreid-' . $this->restoreid . | |
230 | 'courseid-' . $this->courseid . | |
231 | 'type-' . $this->type . | |
232 | 'format-' . $this->format . | |
233 | 'interactive-'. $this->interactive . | |
234 | 'mode-' . $this->mode . | |
235 | 'userid-' . $this->userid . | |
236 | 'target-' . $this->target . | |
482aac65 | 237 | 'samesite-' . $this->samesite . |
754a102b EL |
238 | 'operation-' . $this->operation . |
239 | 'status-' . $this->status . | |
c3ea499d | 240 | 'precheck-' . backup_general_helper::array_checksum_recursive(array($this->precheck)) . |
754a102b EL |
241 | 'execution-' . $this->execution . |
242 | 'plan-' . backup_general_helper::array_checksum_recursive(array($this->plan)) . | |
243 | 'info-' . backup_general_helper::array_checksum_recursive(array($this->info)) . | |
244 | 'logger-' . backup_general_helper::array_checksum_recursive(array($this->logger))); | |
245 | $this->log('calculating controller checksum', backup::LOG_DEBUG, $tempchecksum); | |
246 | return $tempchecksum; | |
247 | } | |
248 | ||
249 | public function is_checksum_correct($checksum) { | |
250 | return $this->checksum === $checksum; | |
251 | } | |
252 | ||
253 | public function get_tempdir() { | |
254 | return $this->tempdir; | |
255 | } | |
256 | ||
257 | public function get_restoreid() { | |
258 | return $this->restoreid; | |
259 | } | |
260 | ||
261 | public function get_type() { | |
262 | return $this->type; | |
263 | } | |
264 | ||
265 | public function get_operation() { | |
266 | return $this->operation; | |
267 | } | |
268 | ||
269 | public function get_courseid() { | |
270 | return $this->courseid; | |
271 | } | |
272 | ||
273 | public function get_format() { | |
274 | return $this->format; | |
275 | } | |
276 | ||
277 | public function get_interactive() { | |
278 | return $this->interactive; | |
279 | } | |
280 | ||
281 | public function get_mode() { | |
282 | return $this->mode; | |
283 | } | |
284 | ||
285 | public function get_userid() { | |
286 | return $this->userid; | |
287 | } | |
288 | ||
289 | public function get_target() { | |
290 | return $this->target; | |
291 | } | |
292 | ||
482aac65 EL |
293 | public function is_samesite() { |
294 | return $this->samesite; | |
295 | } | |
296 | ||
754a102b EL |
297 | public function get_status() { |
298 | return $this->status; | |
299 | } | |
300 | ||
301 | public function get_execution() { | |
302 | return $this->execution; | |
303 | } | |
304 | ||
305 | public function get_executiontime() { | |
306 | return $this->executiontime; | |
307 | } | |
308 | ||
785d6603 SH |
309 | /** |
310 | * Returns the restore plan | |
311 | * @return restore_plan | |
312 | */ | |
754a102b EL |
313 | public function get_plan() { |
314 | return $this->plan; | |
315 | } | |
316 | ||
317 | public function get_info() { | |
318 | return $this->info; | |
319 | } | |
320 | ||
754a102b | 321 | public function execute_plan() { |
b7a45fa7 | 322 | // Basic/initial prevention against time/memory limits |
3ef7279f | 323 | core_php_time_limit::raise(1 * 60 * 60); // 1 hour for 1 course initially granted |
b7a45fa7 | 324 | raise_memory_limit(MEMORY_EXTRA); |
184acafa EL |
325 | // If this is not a course restore, inform the plan we are not |
326 | // including all the activities for sure. This will affect any | |
327 | // task/step executed conditionally to stop processing information | |
328 | // for section and activity restore. MDL-28180. | |
329 | if ($this->get_type() !== backup::TYPE_1COURSE) { | |
330 | $this->log('notifying plan about excluded activities by type', backup::LOG_DEBUG); | |
331 | $this->plan->set_excluding_activities(); | |
332 | } | |
ddf368a2 | 333 | self::$executing++; |
334 | try { | |
335 | $this->plan->execute(); | |
336 | } catch (Exception $e) { | |
337 | self::$executing--; | |
338 | throw $e; | |
339 | } | |
340 | self::$executing--; | |
341 | } | |
342 | ||
343 | /** | |
344 | * Checks whether restore is currently executing. Certain parts of code that | |
345 | * is called during restore, but not directly part of the restore system, may | |
346 | * need to behave differently during restore (e.g. do not bother resetting a | |
347 | * cache because we know it will be reset at end of operation). | |
348 | * | |
349 | * @return bool True if any restore is currently executing | |
350 | */ | |
351 | public static function is_executing() { | |
352 | return self::$executing > 0; | |
754a102b EL |
353 | } |
354 | ||
8d4e41f4 EL |
355 | /** |
356 | * Execute the restore prechecks to detect any problem before proceed with restore | |
357 | * | |
358 | * This function checks various parts of the restore (versions, users, roles...) | |
359 | * returning true if everything was ok or false if any warning/error was detected. | |
360 | * Any warning/error is returned by the get_precheck_results() method. | |
361 | * Note: if any problem is found it will, automatically, drop all the restore temp | |
362 | * tables as far as the next step is to inform about the warning/errors. If no problem | |
363 | * is found, then default behaviour is to keep the temp tables so, in the same request | |
364 | * restore will be executed, saving a lot of checks to be executed again. | |
365 | * Note: If for any reason (UI to show after prechecks...) you want to force temp tables | |
366 | * to be dropped always, you can pass true to the $droptemptablesafter parameter | |
367 | */ | |
368 | public function execute_precheck($droptemptablesafter = false) { | |
c3ea499d EL |
369 | if (is_array($this->precheck)) { |
370 | throw new restore_controller_exception('precheck_alredy_executed', $this->status); | |
371 | } | |
372 | if ($this->status != backup::STATUS_NEED_PRECHECK) { | |
373 | throw new restore_controller_exception('cannot_precheck_wrong_status', $this->status); | |
374 | } | |
91dbb244 | 375 | // Basic/initial prevention against time/memory limits |
3ef7279f | 376 | core_php_time_limit::raise(1 * 60 * 60); // 1 hour for 1 course initially granted |
91dbb244 | 377 | raise_memory_limit(MEMORY_EXTRA); |
8d4e41f4 | 378 | $this->precheck = restore_prechecks_helper::execute_prechecks($this, $droptemptablesafter); |
c3ea499d EL |
379 | if (!array_key_exists('errors', $this->precheck)) { // No errors, can be executed |
380 | $this->set_status(backup::STATUS_AWAITING); | |
381 | } | |
382 | if (empty($this->precheck)) { // No errors nor warnings, return true | |
383 | return true; | |
384 | } | |
385 | return false; | |
754a102b EL |
386 | } |
387 | ||
388 | public function get_results() { | |
389 | return $this->plan->get_results(); | |
390 | } | |
391 | ||
3acc9b81 SH |
392 | /** |
393 | * Returns true if the prechecks have been executed | |
394 | * @return bool | |
395 | */ | |
396 | public function precheck_executed() { | |
397 | return (is_array($this->precheck)); | |
398 | } | |
399 | ||
754a102b | 400 | public function get_precheck_results() { |
c3ea499d EL |
401 | if (!is_array($this->precheck)) { |
402 | throw new restore_controller_exception('precheck_not_executed'); | |
403 | } | |
404 | return $this->precheck; | |
754a102b EL |
405 | } |
406 | ||
749fd68c EL |
407 | /** |
408 | * Save controller information | |
409 | * | |
410 | * @param bool $includeobj to decide if the object itself must be updated (true) or no (false) | |
411 | * @param bool $cleanobj to decide if the object itself must be cleaned (true) or no (false) | |
412 | */ | |
413 | public function save_controller($includeobj = true, $cleanobj = false) { | |
754a102b EL |
414 | // Going to save controller to persistent storage, calculate checksum for later checks and save it |
415 | // TODO: flag the controller as NA. Any operation on it should be forbidden util loaded back | |
416 | $this->log('saving controller to db', backup::LOG_DEBUG); | |
749fd68c EL |
417 | if ($includeobj ) { // Only calculate checksum if we are going to include the object. |
418 | $this->checksum = $this->calculate_checksum(); | |
419 | } | |
420 | restore_controller_dbops::save_controller($this, $this->checksum, $includeobj, $cleanobj); | |
754a102b EL |
421 | } |
422 | ||
423 | public static function load_controller($restoreid) { | |
424 | // Load controller from persistent storage | |
425 | // TODO: flag the controller as available. Operations on it can continue | |
426 | $controller = restore_controller_dbops::load_controller($restoreid); | |
427 | $controller->log('loading controller from db', backup::LOG_DEBUG); | |
428 | return $controller; | |
429 | } | |
430 | ||
431 | /** | |
432 | * class method to provide pseudo random unique "correct" tempdir names | |
433 | */ | |
434 | public static function get_tempdir_name($courseid = 0, $userid = 0) { | |
435 | // Current epoch time + courseid + userid + random bits | |
436 | return md5(time() . '-' . $courseid . '-'. $userid . '-'. random_string(20)); | |
437 | } | |
438 | ||
439 | /** | |
0164592b | 440 | * Converts from current format to backup::MOODLE format |
754a102b EL |
441 | */ |
442 | public function convert() { | |
1e8c265c | 443 | global $CFG; |
1e2c7351 | 444 | require_once($CFG->dirroot . '/backup/util/helper/convert_helper.class.php'); |
1e8c265c | 445 | |
b7a45fa7 | 446 | // Basic/initial prevention against time/memory limits |
3ef7279f | 447 | core_php_time_limit::raise(1 * 60 * 60); // 1 hour for 1 course initially granted |
b7a45fa7 | 448 | raise_memory_limit(MEMORY_EXTRA); |
5d5c4c0b | 449 | $this->progress->start_progress('Backup format conversion'); |
b7a45fa7 | 450 | |
754a102b EL |
451 | if ($this->status != backup::STATUS_REQUIRE_CONV) { |
452 | throw new restore_controller_exception('cannot_convert_not_required_status'); | |
453 | } | |
1e8c265c | 454 | |
fe50f530 DM |
455 | $this->log('backup format conversion required', backup::LOG_INFO); |
456 | ||
0164592b | 457 | // Run conversion to the proper format |
fe50f530 | 458 | if (!convert_helper::to_moodle2_format($this->get_tempdir(), $this->format, $this->get_logger())) { |
1e2c7351 DM |
459 | // todo - unable to find the conversion path, what to do now? |
460 | // throwing the exception as a temporary solution | |
461 | throw new restore_controller_exception('unable_to_find_conversion_path'); | |
462 | } | |
1e8c265c | 463 | |
fe50f530 DM |
464 | $this->log('backup format conversion successful', backup::LOG_INFO); |
465 | ||
c5c8b350 MN |
466 | // If no exceptions were thrown, then we are in the proper format |
467 | $this->format = backup::FORMAT_MOODLE; | |
754a102b | 468 | |
c5c8b350 MN |
469 | // Load plan, apply security and set status based on interactivity |
470 | $this->load_plan(); | |
754a102b | 471 | |
c5c8b350 MN |
472 | // Perform all initial security checks and apply (2nd param) them to settings automatically |
473 | restore_check::check_security($this, true); | |
754a102b | 474 | |
c5c8b350 MN |
475 | if ($this->interactive == backup::INTERACTIVE_YES) { |
476 | $this->set_status(backup::STATUS_SETTING_UI); | |
754a102b | 477 | } else { |
c5c8b350 | 478 | $this->set_status(backup::STATUS_NEED_PRECHECK); |
754a102b | 479 | } |
5d5c4c0b | 480 | $this->progress->end_progress(); |
754a102b EL |
481 | } |
482 | ||
483 | // Protected API starts here | |
484 | ||
485 | protected function calculate_restoreid() { | |
486 | // Current epoch time + tempdir + courseid + interactive + mode + userid + target + operation + random bits | |
487 | $this->restoreid = md5(time() . '-' . $this->tempdir . '-' . $this->courseid . '-'. $this->interactive . '-' . | |
488 | $this->mode . '-' . $this->userid . '-'. $this->target . '-' . $this->operation . '-' . | |
489 | random_string(20)); | |
490 | } | |
491 | ||
492 | protected function load_plan() { | |
493 | // First of all, we need to introspect the moodle_backup.xml file | |
494 | // in order to detect all the required stuff. So, create the | |
495 | // monster $info structure where everything will be defined | |
496 | $this->log('loading backup info', backup::LOG_DEBUG); | |
497 | $this->info = backup_general_helper::get_backup_information($this->tempdir); | |
498 | ||
499 | // Set the controller type to the one found in the information | |
500 | $this->type = $this->info->type; | |
501 | ||
482aac65 EL |
502 | // Set the controller samesite flag as needed |
503 | $this->samesite = backup_general_helper::backup_is_samesite($this->info); | |
504 | ||
754a102b EL |
505 | // Now we load the plan that will be configured following the |
506 | // information provided by the $info | |
507 | $this->log('loading controller plan', backup::LOG_DEBUG); | |
508 | $this->plan = new restore_plan($this); | |
509 | $this->plan->build(); // Build plan for this controller | |
510 | $this->set_status(backup::STATUS_PLANNED); | |
511 | } | |
512 | } | |
513 | ||
514 | /* | |
515 | * Exception class used by all the @restore_controller stuff | |
516 | */ | |
517 | class restore_controller_exception extends backup_exception { | |
518 | ||
519 | public function __construct($errorcode, $a=NULL, $debuginfo=null) { | |
520 | parent::__construct($errorcode, $a, $debuginfo); | |
521 | } | |
522 | } |