MDL-15666 - change all the portfolio plugins and callers to use exceptions
[moodle.git] / lib / portfolio / exporter.php
1 <?php
2 /**
3  * Moodle - Modular Object-Oriented Dynamic Learning Environment
4  *          http://moodle.org
5  * Copyright (C) 1999 onwards Martin Dougiamas  http://dougiamas.com
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation, either version 2 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19  *
20  * @package    moodle
21  * @subpackage portfolio
22  * @author     Penny Leach <penny@catalyst.net.nz>
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL
24  * @copyright  (C) 1999 onwards Martin Dougiamas  http://dougiamas.com
25  *
26  * This file contains the class definition for the exporter object.
27  */
29 /**
30 * The class that handles the various stages of the actual export
31 * and the communication between the caller and the portfolio plugin.
32 * this is stored in the database between page requests in serialized base64 encoded form
33 * also contains helper methods for the plugin and caller to use (at the end of the file)
34 * {@see get_base_filearea} - where to write files to
35 * {@see write_new_file} - write some content to a file in the export filearea
36 * {@see copy_existing_file} - copy an existing file into the export filearea
37 * {@see get_tempfiles} - return list of all files in the export filearea
38 */
39 class portfolio_exporter {
41     /**
42     * the caller object used during the export
43     */
44     private $caller;
46     /** the portfolio plugin instanced used during the export
47     */
48     private $instance;
50     /**
51     * if there has been no config form displayed to the user
52     */
53     private $noexportconfig;
55     /**
56     * the navigation to display on the wizard screens
57     * built from build_navigation
58     */
59     private $navigation;
61     /**
62     * the user currently exporting content
63     * always $USER, but more conveniently placed here
64     */
65     private $user;
67     /** the file to include that contains the class defintion
68     * of the portfolio instance plugin
69     * used to re-waken the object after sleep
70     */
71     public $instancefile;
73     /**
74     * the file to include that contains the class definition
75     * of the caller object
76     * used to re-waken the object after sleep
77     */
78     public $callerfile;
80     /**
81     * the current stage of the export
82     */
83     private $stage;
85     /**
86     * whether something (usually the portfolio plugin)
87     * has forced queuing
88     */
89     private $forcequeue;
91     /**
92     * id of this export
93     * matches record in portfolio_tempdata table
94     * and used for itemid for file storage.
95     */
96     private $id;
98     /**
99     * the session key during the export
100     * used to avoid hijacking transfers
101     */
102     private $sesskey;
104     /**
105     * array of stages that have had the portfolio plugin already steal control from them
106     */
107     private $alreadystolen;
109     /**
110     * files that the exporter has written to this temp area
111     * keep track of this in case of duplicates within one export
112     * see MDL-16390
113     */
114     private $newfilehashes;
116     /**
117     * construct a new exporter for use
118     *
119     * @param portfolio_plugin_base subclass $instance portfolio instance (passed by reference)
120     * @param portfolio_caller_base subclass $caller portfolio caller (passed by reference)
121     * @param string $callerfile path to callerfile (relative to dataroot)
122     * @param string $navigation result of build_navigation (passed to print_header)
123     */
124     public function __construct(&$instance, &$caller, $callerfile, $navigation) {
125         $this->instance =& $instance;
126         $this->caller =& $caller;
127         if ($instance) {
128             $this->instancefile = 'portfolio/type/' . $instance->get('plugin') . '/lib.php';
129             $this->instance->set('exporter', $this);
130         }
131         $this->callerfile = $callerfile;
132         $this->stage = PORTFOLIO_STAGE_CONFIG;
133         $this->navigation = $navigation;
134         $this->caller->set('exporter', $this);
135         $this->alreadystolen = array();
136         $this->newfilehashes = array();
137     }
139     /*
140     * generic getter for properties belonging to this instance
141     * <b>outside</b> the subclasses
142     * like name, visible etc.
143     */
144     public function get($field) {
145         if (property_exists($this, $field)) {
146             return $this->{$field};
147         }
148         $a = (object)array('property' => $field, 'class' => get_class($this));
149         throw new portfolio_export_exception($this, 'invalidproperty', 'portfolio', null, $a);
150     }
152     /**
153     * generic setter for properties belonging to this instance
154     * <b>outside</b> the subclass
155     * like name, visible, etc.
156     */
157     public function set($field, &$value) {
158         if (property_exists($this, $field)) {
159             $this->{$field} =& $value;
160             if ($field == 'instance') {
161                 $this->instancefile = 'portfolio/type/' . $this->instance->get('plugin') . '/lib.php';
162                 $this->instance->set('exporter', $this);
163             }
164             $this->dirty = true;
165             return true;
166         }
167         $a = (object)array('property' => $field, 'class' => get_class($this));
168         throw new portfolio_export_exception($this, 'invalidproperty', 'portfolio', null, $a);
170     }
172     /**
173     * process the given stage calling whatever functions are necessary
174     *
175     * @param int $stage (see PORTFOLIO_STAGE_* constants)
176     * @param boolean $alreadystolen used to avoid letting plugins steal control twice.
177     *
178     * @return boolean whether or not to process the next stage. this is important as the function is called recursively.
179     */
180     public function process_stage($stage, $alreadystolen=false) {
181         $this->set('stage', $stage);
182         if ($alreadystolen) {
183             $this->alreadystolen[$stage] = true;
184         } else {
185             if (!array_key_exists($stage, $this->alreadystolen)) {
186                 $this->alreadystolen[$stage] = false;
187             }
188         }
189         if (!$this->alreadystolen[$stage] && $url = $this->instance->steal_control($stage)) {
190             $this->save();
191             redirect($url);
192             break;
193         } else {
194             $this->save();
195         }
197         $waiting = $this->instance->get_export_config('wait');
198         if ($stage > PORTFOLIO_STAGE_QUEUEORWAIT && empty($waiting)) {
199             $stage = PORTFOLIO_STAGE_FINISHED;
200         }
201         $functionmap = array(
202             PORTFOLIO_STAGE_CONFIG        => 'config',
203             PORTFOLIO_STAGE_CONFIRM       => 'confirm',
204             PORTFOLIO_STAGE_QUEUEORWAIT   => 'queueorwait',
205             PORTFOLIO_STAGE_PACKAGE       => 'package',
206             PORTFOLIO_STAGE_CLEANUP       => 'cleanup',
207             PORTFOLIO_STAGE_SEND          => 'send',
208             PORTFOLIO_STAGE_FINISHED      => 'finished'
209         );
211         $function = 'process_stage_' . $functionmap[$stage];
212         try {
213             if ($this->$function()) {
214                 // if we get through here it means control was returned
215                 // as opposed to wanting to stop processing
216                 // eg to wait for user input.
217                 $this->save();
218                 $stage++;
219                 return $this->process_stage($stage);
220             } else {
221                 $this->save();
222                 return false;
223             }
224         } catch (portfolio_caller_exception $e) {
225             portfolio_export_rethrow_exception($this, $e);
226         } catch (portfolio_plugin_exception $e) {
227             portfolio_export_rethrow_exception($this, $e);
228         } catch (portfolio_export_exception $e) {
229             throw $e;
230         } catch (Exception $e) {
231             debugging(get_string('thirdpartyexception', 'portfolio', get_class($e)));
232             portfolio_export_rethrow_exception($this, $e);
233         }
234     }
236     /**
237     * helper function to return the portfolio instance
238     *
239     * @return  portfolio_plugin_base subclass
240     */
241     public function instance() {
242         return $this->instance;
243     }
245     /**
246     * helper function to return the caller object
247     *
248     * @return portfolio_caller_base subclass
249     */
250     public function caller() {
251         return $this->caller;
252     }
254     /**
255     * processes the 'config' stage of the export
256     *
257     * @return boolean whether or not to process the next stage. this is important as the control function is called recursively.
258     */
259     public function process_stage_config() {
261         $pluginobj = $callerobj = null;
262         if ($this->instance->has_export_config()) {
263             $pluginobj = $this->instance;
264         }
265         if ($this->caller->has_export_config()) {
266             $callerobj = $this->caller;
267         }
268         $formats = portfolio_supported_formats_intersect($this->caller->supported_formats($this->caller), $this->instance->supported_formats());
269         $expectedtime = $this->instance->expected_time($this->caller->expected_time());
270         if (count($formats) == 0) {
271             // something went wrong, we should not have gotten this far.
272             throw new portfolio_export_exception($this, 'nocommonformats', 'portfolio', null, get_class($this->caller));
273         }
274         // even if neither plugin or caller wants any config, we have to let the user choose their format, and decide to wait.
275         if ($pluginobj || $callerobj || count($formats) > 1 || ($expectedtime != PORTFOLIO_TIME_LOW && $expectedtime != PORTFOLIO_TIME_FORCEQUEUE)) {
276             $customdata = array(
277                 'instance' => $this->instance,
278                 'plugin' => $pluginobj,
279                 'caller' => $callerobj,
280                 'userid' => $this->user->id,
281                 'formats' => $formats,
282                 'expectedtime' => $expectedtime,
283             );
284             $mform = new portfolio_export_form('', $customdata);
285             if ($mform->is_cancelled()){
286                 $this->cancel_request();
287             } else if ($fromform = $mform->get_data()){
288                 if (!confirm_sesskey()) {
289                     throw new portfolio_export_exception($this, 'confirmsesskeybad');
290                 }
291                 $pluginbits = array();
292                 $callerbits = array();
293                 foreach ($fromform as $key => $value) {
294                     if (strpos($key, 'plugin_') === 0) {
295                         $pluginbits[substr($key, 7)]  = $value;
296                     } else if (strpos($key, 'caller_') === 0) {
297                         $callerbits[substr($key, 7)] = $value;
298                     }
299                 }
300                 $callerbits['format'] = $pluginbits['format'] = $fromform->format;
301                 $pluginbits['wait'] = $fromform->wait;
302                 if ($expectedtime == PORTFOLIO_TIME_LOW) {
303                     $pluginbits['wait'] = 1;
304                     $pluginbits['hidewait'] = 1;
305                 } else if ($expectedtime == PORTFOLIO_TIME_FORCEQUEUE) {
306                     $pluginbits['wait'] = 0;
307                     $pluginbits['hidewait'] = 1;
308                     $this->forcequeue = true;
309                 }
310                 $callerbits['hideformat'] = $pluginbits['hideformat'] = (count($formats) == 1);
311                 $this->caller->set_export_config($callerbits);
312                 $this->instance->set_export_config($pluginbits);
313                 return true;
314             } else {
315                 $this->print_header('configexport');
316                 print_simple_box_start();
317                 $mform->display();
318                 print_simple_box_end();
319                 print_footer();
320                 return false;;
321             }
322         } else {
323             $this->noexportconfig = true;
324             $format = array_shift($formats);
325             $config = array(
326                 'hidewait' => 1,
327                 'wait' => (($expectedtime == PORTFOLIO_TIME_LOW) ? 1 : 0),
328                 'format' => $format,
329                 'hideformat' => 1
330             );
331             $this->instance->set_export_config($config);
332             $this->caller->set_export_config(array('format' => $format, 'hideformat' => 1));
333             if ($expectedtime == PORTFOLIO_TIME_FORCEQUEUE) {
334                 $this->forcequeue = true;
335             }
336             return true;
337             // do not break - fall through to confirm
338         }
339     }
341     /**
342     * processes the 'confirm' stage of the export
343     *
344     * @return boolean whether or not to process the next stage. this is important as the control function is called recursively.
345     */
346     public function process_stage_confirm() {
347         global $CFG, $DB;
349         $previous = $DB->get_records(
350             'portfolio_log',
351             array(
352                 'userid'      => $this->user->id,
353                 'portfolio'   => $this->instance->get('id'),
354                 'caller_sha1' => $this->caller->get_sha1(),
355             )
356         );
357         if (isset($this->noexportconfig) && empty($previous)) {
358             return true;
359         }
360         $strconfirm = get_string('confirmexport', 'portfolio');
361         $yesurl = $CFG->wwwroot . '/portfolio/add.php?stage=' . PORTFOLIO_STAGE_QUEUEORWAIT;
362         $nourl  = $CFG->wwwroot . '/portfolio/add.php?cancel=1';
363         $this->print_header('confirmexport');
364         print_simple_box_start();
365         print_heading(get_string('confirmsummary', 'portfolio'), '', 4);
366         $mainsummary = array();
367         if (!$this->instance->get_export_config('hideformat')) {
368             $mainsummary[get_string('selectedformat', 'portfolio')] = get_string('format_' . $this->instance->get_export_config('format'), 'portfolio');
369         }
370         if (!$this->instance->get_export_config('hidewait')) {
371             $mainsummary[get_string('selectedwait', 'portfolio')] = get_string(($this->instance->get_export_config('wait') ? 'yes' : 'no'));
372         }
373         if ($previous) {
374             $previousstr = '';
375             foreach ($previous as $row) {
376                 $previousstr .= userdate($row->time);
377                 if ($row->caller_class != get_class($this->caller)) {
378                     require_once($CFG->dirroot . '/' . $row->caller_file);
379                     $previousstr .= ' (' . call_user_func(array($row->caller_class, 'display_name')) . ')';
380                 }
381                 $previousstr .= '<br />';
382             }
383             $mainsummary[get_string('exportedpreviously', 'portfolio')] = $previousstr;
384         }
385         if (!$csummary = $this->caller->get_export_summary()) {
386             $csummary = array();
387         }
388         if (!$isummary = $this->instance->get_export_summary()) {
389             $isummary = array();
390         }
391         $mainsummary = array_merge($mainsummary, $csummary, $isummary);
392         $table = new StdClass;
393         $table->data = array();
394         foreach ($mainsummary as $string => $value) {
395             $table->data[] = array($string, $value);
396         }
397         print_table($table);
398         notice_yesno($strconfirm, $yesurl, $nourl);
399         print_simple_box_end();
400         print_footer();
401         return false;
402     }
404     /**
405     * processes the 'queueornext' stage of the export
406     *
407     * @return boolean whether or not to process the next stage. this is important as the control function is called recursively.
408     */
409     public function process_stage_queueorwait() {
410         global $SESSION;
411         $wait = $this->instance->get_export_config('wait');
412         if (empty($wait)) {
413             events_trigger('portfolio_send', $this->id);
414             unset($SESSION->portfolioexport);
415             return $this->process_stage_finished(true);
416         }
417         return true;
418     }
420     /**
421     * processes the 'package' stage of the export
422     *
423     * @return boolean whether or not to process the next stage. this is important as the control function is called recursively.
424     */
425     public function process_stage_package() {
426         // now we've agreed on a format,
427         // the caller is given control to package it up however it wants
428         // and then the portfolio plugin is given control to do whatever it wants.
429         try {
430             $this->caller->prepare_package();
431         } catch (portfolio_exception $e) {
432             throw new portfolio_export_exception($this, 'callercouldnotpackage', 'portfolio', null, $e->getMessage());
433         }
434         catch (file_exception $e) {
435             throw new portfolio_export_exception($this, 'callercouldnotpackage', 'portfolio', null, $e->getMessage());
436         }
437         try {
438             $this->instance->prepare_package();
439         }
440         catch (portfolio_exception $e) {
441             throw new portfolio_export_exception($this, 'plugincouldnotpackage', 'portfolio', null, $e->getMessage());
442         }
443         catch (file_exception $e) {
444             throw new portfolio_export_exception($this, 'plugincouldnotpackage', 'portfolio', null, $e->getMessage());
445         }
446         return true;
447     }
449     /**
450     * processes the 'cleanup' stage of the export
451     *
452     * @return boolean whether or not to process the next stage. this is important as the control function is called recursively.
453     */
454     public function process_stage_cleanup($pullok=false) {
455         global $CFG, $DB, $SESSION;
457         if (!$pullok && $this->get('instance') && !$this->get('instance')->is_push()) {
458             unset($SESSION->portfolioexport);
459             return true;
460         }
461         if ($this->get('instance')) {
462             // might not be set - before export really starts
463             $this->get('instance')->cleanup();
464         }
465         $DB->delete_records('portfolio_tempdata', array('id' => $this->id));
466         $fs = get_file_storage();
467         $fs->delete_area_files(SYSCONTEXTID, 'portfolio_exporter', $this->id);
468         unset($SESSION->portfolioexport);
469         return true;
470     }
472     /**
473     * processes the 'send' stage of the export
474     *
475     * @return boolean whether or not to process the next stage. this is important as the control function is called recursively.
476     */
477     public function process_stage_send() {
478         // send the file
479         try {
480             $this->instance->send_package();
481         }
482         catch (portfolio_plugin_exception $e) {
483             // not catching anything more general here. plugins with dependencies on other libraries that throw exceptions should catch and rethrow.
484             // eg curl exception
485             throw new portfolio_export_exception($this, 'failedtosendpackage', 'portfolio', null, $e->getMessage());
486         }
487         // log the transfer
488         global $DB;
489         $l = array(
490             'userid'         => $this->user->id,
491             'portfolio'      => $this->instance->get('id'),
492             'caller_file'    => $this->callerfile,
493             'caller_sha1'    => $this->caller->get_sha1(),
494             'caller_class'   => get_class($this->caller),
495             'time'           => time(),
496         );
497         $DB->insert_record('portfolio_log', $l);
498         return true;
499     }
501     /**
502     * processes the 'finish' stage of the export
503     *
504     * @return boolean whether or not to process the next stage. this is important as the control function is called recursively.
505     */
506     public function process_stage_finished($queued=false) {
507         $returnurl = $this->caller->get_return_url();
508         $continueurl = $this->instance->get_continue_url();
509         $extras = $this->instance->get_extra_finish_options();
511         $key = 'exportcomplete';
512         if ($queued || $this->forcequeue) {
513             $key = 'exportqueued';
514             if ($this->forcequeue) {
515                 $key = 'exportqueuedforced';
516             }
517         }
518         $this->print_header($key, false);
519         if ($returnurl) {
520             echo '<a href="' . $returnurl . '">' . get_string('returntowhereyouwere', 'portfolio') . '</a><br />';
521         }
522         if ($continueurl) {
523             echo '<a href="' . $continueurl . '">' . get_string('continuetoportfolio', 'portfolio') . '</a><br />';
524         }
525         if (is_array($extras)) {
526             foreach ($extras as $link => $string) {
527                 echo '<a href="' . $link . '">' . $string . '</a><br />';
528             }
529         }
530         print_footer();
531         return false;
532     }
535     /**
536     * local print header function to be reused across the export
537     *
538     * @param string $titlestring key for a portfolio language string
539     * @param string $headerstring key for a portfolio language string
540     */
541     public function print_header($headingstr, $summary=true) {
542         $titlestr = get_string('exporting', 'portfolio');
543         $headerstr = get_string('exporting', 'portfolio');
545         print_header($titlestr, $headerstr, $this->navigation);
546         print_heading(get_string($headingstr, 'portfolio'));
548         if (!$summary) {
549             return;
550         }
552         print_simple_box_start();
553         echo $this->caller->heading_summary();
554         print_simple_box_end();
555     }
557     /**
558     * cancels a potfolio request and cleans up the tempdata
559     * and redirects the user back to where they started
560     */
561     public function cancel_request() {
562         if (!isset($this)) {
563             return;
564         }
565         $this->process_stage_cleanup(true);
566         redirect($this->caller->get_return_url());
567         exit;
568     }
570     /**
571     * writes out the contents of this object and all its data to the portfolio_tempdata table and sets the 'id' field.
572     */
573     public function save() {
574         global $DB;
575         if (empty($this->id)) {
576             $r = (object)array(
577                 'data' => base64_encode(serialize($this)),
578                 'expirytime' => time() + (60*60*24),
579                 'userid' => $this->user->id,
580             );
581             $this->id = $DB->insert_record('portfolio_tempdata', $r);
582             $this->save(); // call again so that id gets added to the save data.
583         } else {
584             $DB->set_field('portfolio_tempdata', 'data', base64_encode(serialize($this)), array('id' => $this->id));
585         }
586     }
588     /**
589     * rewakens the data from the database given the id
590     * makes sure to load the required files with the class definitions
591     *
592     * @param int $id id of data
593     *
594     * @return portfolio_exporter
595     */
596     public static function rewaken_object($id) {
597         global $DB, $CFG;
598         require_once($CFG->libdir . '/filelib.php');
599         if (!$data = $DB->get_record('portfolio_tempdata', array('id' => $id))) {
600             throw new portfolio_exception('invalidtempid', 'portfolio');
601         }
602         $exporter = unserialize(base64_decode($data->data));
603         if ($exporter->instancefile) {
604             require_once($CFG->dirroot . '/' . $exporter->instancefile);
605         }
606         require_once($CFG->dirroot . '/' . $exporter->callerfile);
607         $exporter = unserialize(serialize($exporter));
608         return $exporter;
609     }
611     /**
612     * helper function to create the beginnings of a file_record object
613     * to create a new file in the portfolio_temporary working directory
614     * use {@see write_new_file} or {@see copy_existing_file} externally
615     *
616     * @param string $name filename of new record
617     */
618     private function new_file_record_base($name) {
619         return (object)array_merge($this->get_base_filearea(), array(
620             'filepath' => '/',
621             'filename' => $name,
622         ));
623     }
625     /**
626     * verifies a rewoken object
627     *
628     * checks to make sure it belongs to the same user and session as is currently in use.
629     *
630     * @throws portfolio_exception
631     */
632     public function verify_rewaken() {
633         global $USER;
634         if ($this->get('user')->id != $USER->id) {
635             throw new portfolio_exception('notyours', 'portfolio');
636         }
637         if (!confirm_sesskey($this->get('sesskey'))) {
638             throw new portfolio_exception('confirmsesskeybad');
639         }
640     }
641     /**
642     * copies a file from somewhere else in moodle
643     * to the portfolio temporary working directory
644     * associated with this export
645     *
646     * @param $oldfile stored_file object
647     * @return new stored_file object
648     */
649     public function copy_existing_file($oldfile) {
650         if (array_key_exists($oldfile->get_contenthash(), $this->newfilehashes)) {
651             return $this->newfilehashes[$oldfile->get_contenthash()];
652         }
653         $fs = get_file_storage();
654         $file_record = $this->new_file_record_base($oldfile->get_filename());
655         try {
656             $newfile = $fs->create_file_from_storedfile($file_record, $oldfile->get_id());
657             $this->newfilehashes[$newfile->get_contenthash()] = $newfile;
658             return $newfile;
659         } catch (file_exception $e) {
660             return false;
661         }
662     }
664     /**
665     * writes out some content to a file in the
666     * portfolio temporary working directory
667     * associated with this export
668     *
669     * @param string $content content to write
670     * @param string $name filename to use
671     * @return new stored_file object
672     */
673     public function write_new_file($content, $name) {
674         $fs = get_file_storage();
675         $file_record = $this->new_file_record_base($name);
676         return $fs->create_file_from_string($file_record, $content);
677     }
679     public function zip_tempfiles($filename='portfolio-export.zip', $filepath='/final/') {
680         $zipper = new zip_packer();
682         list ($contextid, $filearea, $itemid) = array_values($this->get_base_filearea());
683         if ($newfile = $zipper->archive_to_storage($files, $contextid, $filearea, $itemid, $filepath, $filename, $this->user->id)) {
684             return $newfile;
685         }
686         return false;
688     }
690     /**
691     * returns an arary of files in the temporary working directory
692     * for this export
693     * always use this instead of the files api directly
694     *
695     * @return array of stored_file objects keyed by name
696     */
697     public function get_tempfiles() {
698         $fs = get_file_storage();
699         $files = $fs->get_area_files(SYSCONTEXTID, 'portfolio_exporter', $this->id, '', false);
700         if (empty($files)) {
701             return array();
702         }
703         $returnfiles = array();
704         foreach ($files as $f) {
705             $returnfiles[$f->get_filename()] = $f;
706         }
707         return $returnfiles;
708     }
710     /**
711     * returns the context, filearea, and itemid
712     * parts of a filearea (not filepath) to be used by
713     * plugins if they want to do things like zip up the contents of
714     * the temp area to here, or something that can't be done just using
715     * write_new_file,  copy_existing_file or get_tempfiles
716     *
717     * @return array contextid, filearea, itemid are the keys.
718     */
719     public function get_base_filearea() {
720         return array(
721             'contextid' => SYSCONTEXTID,
722             'filearea' => 'portfolio_exporter',
723             'itemid'   => $this->id,
724         );
725     }
729 ?>