6c32ffc64204c172deda77d25e000a9de667017e
[moodle.git] / lib / portfolio / plugin.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * This file contains the base classes for portfolio plugins to inherit from:
19  *
20  * portfolio_plugin_pull_base and portfolio_plugin_push_base
21  * which both in turn inherit from portfolio_plugin_base.
22  * {@link http://docs.moodle.org/dev/Writing_a_Portfolio_Plugin}
23  *
24  * @package    core_portfolio
25  * @copyright  2008 Penny Leach <penny@catalyst.net.nz>,
26  *             Martin Dougiamas <http://dougiamas.com>
27  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28  */
30 defined('MOODLE_INTERNAL') || die();
32 /**
33  * The base class for portfolio plugins.
34  *
35  * All plugins must subclass this
36  * either via portfolio_plugin_pull_base or portfolio_plugin_push_base
37  * @see portfolio_plugin_pull_base
38  * @see portfolio_plugin_push_base
39  *
40  * @package core_portfolio
41  * @category portfolio
42  * @copyright 2008 Penny Leach <penny@catalyst.net.nz>
43  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
44  */
45 abstract class portfolio_plugin_base {
47     /** @var bool whether this object needs writing out to the database */
48     protected $dirty;
50     /** @var integer id of instance */
51     protected $id;
53     /** @var string name of instance */
54     protected $name;
56     /** @var string plugin this instance belongs to */
57     protected $plugin;
59     /** @var bool whether this instance is visible or not */
60     protected $visible;
62     /** @var array admin configured config use {@link set_config} and {@get_config} to access */
63     protected $config;
65     /** @var array user config cache. keyed on userid and then on config field => value use {@link get_user_config} and {@link set_user_config} to access. */
66     protected $userconfig;
68     /** @var array export config during export use {@link get_export_config} and {@link set export_config} to access. */
69     protected $exportconfig;
71     /** @var stdClass user currently exporting data */
72     protected $user;
74     /** @var stdClass a reference to the exporter object */
75     protected $exporter;
77     /**
78      * Array of formats this portfolio supports
79      * the intersection of what this function returns
80      * and what the caller supports will be used.
81      * Use the constants PORTFOLIO_FORMAT_*
82      *
83      * @return array list of formats
84      */
85     public function supported_formats() {
86         return array(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_RICH);
87     }
89     /**
90      * Override this if you are supporting the 'file' type (or a subformat)
91      * but have restrictions on mimetypes
92      *
93      * @param string $mimetype file type or subformat
94      * @return bool
95      */
96     public static function file_mime_check($mimetype) {
97         return true;
98     }
101     /**
102      * How long does this reasonably expect to take..
103      * Should we offer the user the option to wait..
104      * This is deliberately nonstatic so it can take filesize into account
105      *
106      * @param string $callertime - what the caller thinks
107      *                             the portfolio plugin instance
108      *                             is given the final say
109      *                             because it might be (for example) download.
110      */
111     public abstract function expected_time($callertime);
113     /**
114      * Is this plugin push or pull.
115      * If push, cleanup will be called directly after send_package
116      * If not, cleanup will be called after portfolio/file.php is requested
117      */
118     public abstract function is_push();
120     /**
121      * Returns the user-friendly name for this plugin.
122      * Usually just get_string('pluginname', 'portfolio_something')
123      */
124     public static function get_name() {
125         throw new coding_exception('get_name() method needs to be overridden in each subclass of portfolio_plugin_base');
126     }
128     /**
129      * Check sanity of plugin.
130      * If this function returns something non empty, ALL instances of your plugin
131      * will be set to invisble and not be able to be set back until it's fixed
132      *
133      * @return string|int|bool - string = error string KEY (must be inside portfolio_$yourplugin) or 0/false if you're ok
134      */
135     public static function plugin_sanity_check() {
136         return 0;
137     }
139     /**
140      * Check sanity of instances.
141      * If this function returns something non empty, the instance will be
142      * set to invislbe and not be able to be set back until it's fixed.
143      *
144      * @return int|string|bool - string = error string KEY (must be inside portfolio_$yourplugin) or 0/false if you're ok
145      */
146     public function instance_sanity_check() {
147         return 0;
148     }
150     /**
151      * Does this plugin need any configuration by the administrator?
152      * If you override this to return true,
153      * you <b>must</b> implement admin_config_form.
154      * @see admin_config_form
155      *
156      * @return bool
157      */
158     public static function has_admin_config() {
159         return false;
160     }
162     /**
163      * Can this plugin be configured by the user in their profile?
164      * If you override this to return true,
165      * you <b>must</b> implement user_config_form
166      * @see user_config_form
167      *
168      * @return bool
169      */
170     public function has_user_config() {
171         return false;
172     }
174     /**
175      * Does this plugin need configuration during export time?
176      * If you override this to return true,
177      * you <b>must</b> implement export_config_form.
178      * @see export_config_form
179      *
180      * @return bool
181      */
182     public function has_export_config() {
183         return false;
184     }
186     /**
187      * Just like the moodle form validation function.
188      * This is passed in the data array from the form
189      * and if a non empty array is returned, form processing will stop.
190      *
191      * @param array $data data from form.
192      */
193     public function export_config_validation(array $data) {}
195     /**
196      * Just like the moodle form validation function.
197      * This is passed in the data array from the form
198      * and if a non empty array is returned, form processing will stop.
199      *
200      * @param array $data data from form.
201      */
202     public function user_config_validation(array $data) {}
204     /**
205      * Sets the export time config from the moodle form.
206      * You can also use this to set export config that
207      * isn't actually controlled by the user.
208      * Eg: things that your subclasses want to keep in state
209      * across the export.
210      * Keys must be in get_allowed_export_config
211      * This is deliberately not final (see boxnet plugin)
212      * @see get_allowed_export_config
213      *
214      * @param array $config named array of config items to set.
215      */
216     public function set_export_config($config) {
217         $allowed = array_merge(
218             array('wait', 'hidewait', 'format', 'hideformat'),
219             $this->get_allowed_export_config()
220         );
221         foreach ($config as $key => $value) {
222             if (!in_array($key, $allowed)) {
223                 $a = (object)array('property' => $key, 'class' => get_class($this));
224                 throw new portfolio_export_exception($this->get('exporter'), 'invalidexportproperty', 'portfolio', null, $a);
225             }
226             $this->exportconfig[$key] = $value;
227         }
228     }
230     /**
231      * Gets an export time config value.
232      * Subclasses should not override this.
233      *
234      * @param string $key field to fetch
235      * @return null|string config value
236      */
237     public final function get_export_config($key) {
238         $allowed = array_merge(
239             array('hidewait', 'wait', 'format', 'hideformat'),
240             $this->get_allowed_export_config()
241         );
242         if (!in_array($key, $allowed)) {
243             $a = (object)array('property' => $key, 'class' => get_class($this));
244             throw new portfolio_export_exception($this->get('exporter'), 'invalidexportproperty', 'portfolio', null, $a);
245         }
246         if (!array_key_exists($key, $this->exportconfig)) {
247             return null;
248         }
249         return $this->exportconfig[$key];
250     }
252     /**
253      * After the user submits their config,
254      * they're given a confirm screen
255      * summarising what they've chosen.
256      * This function should return a table of nice strings => values
257      * of what they've chosen
258      * to be displayed in a table.
259      *
260      * @return bool
261      */
262     public function get_export_summary() {
263         return false;
264     }
266     /**
267      * Called after the caller has finished having control
268      * of its prepare_package function.
269      * This function should read all the files from the portfolio
270      * working file area and zip them and send them or whatever it wants.
271      * get_tempfiles to get the list of files.
272      * @see get_tempfiles
273      *
274      */
275     public abstract function prepare_package();
277     /**
278      * This is the function that is responsible for sending
279      * the package to the remote system,
280      * or whatever request is necessary to initiate the transfer.
281      *
282      * @return bool success
283      */
284     public abstract function send_package();
287     /**
288      * Once everything is done and the user
289      * has the finish page displayed to them.
290      * The base class takes care of printing them
291      * "return to where you are" or "continue to portfolio" links.
292      * This function allows for exta finish options from the plugin
293      *
294      * @return bool
295      */
296     public function get_extra_finish_options() {
297         return false;
298     }
300     /**
301      * The url for the user to continue to their portfolio
302      * during the lifecycle of the request
303      */
304     public abstract function get_interactive_continue_url();
306     /**
307      * The url to save in the log as the continue url.
308      * This is passed through resolve_static_continue_url()
309      * at display time to the user.
310      *
311      * @return string
312      */
313     public function get_static_continue_url() {
314         return $this->get_interactive_continue_url();
315     }
317     /**
318      * Override this function if you need to add something on to the url
319      * for post-export continues (eg from the log page).
320      * Mahara does this, for example, to start a jump session.
321      *
322      * @param string $url static continue url
323      * @return string
324      */
325     public function resolve_static_continue_url($url) {
326         return $url;
327     }
329     /**
330      * mform to display to the user in their profile
331      * if your plugin can't be configured by the user,
332      * @see has_user_config.
333      * Don't bother overriding this function
334      *
335      * @param moodleform $mform passed by reference, add elements to it
336      */
337     public function user_config_form(&$mform) {}
339     /**
340      * mform to display to the admin configuring the plugin.
341      * If your plugin can't be configured by the admin,
342      * @see has_admin_config
343      * Don't bother overriding this function.
344      * This function can be called statically or non statically,
345      * depending on whether it's creating a new instance (statically),
346      * or editing an existing one (non statically)
347      *
348      * @param moodleform $mform passed by reference, add elements to it.
349      */
350     public function admin_config_form(&$mform) {}
352     /**
353      * Just like the moodle form validation function,
354      * this is passed in the data array from the form
355      * and if a non empty array is returned, form processing will stop.
356      *
357      * @param array $data data from form.
358      */
359     public function admin_config_validation($data) {}
361     /**
362      * mform to display to the user exporting data using this plugin.
363      * If your plugin doesn't need user input at this time,
364      * @see has_export_config.
365      * Don't bother overrideing this function
366      *
367      * @param moodleform $mform passed by reference, add elements to it.
368      */
369     public function export_config_form(&$mform) {}
371     /**
372      * Override this if your plugin doesn't allow multiple instances
373      *
374      * @return bool
375      */
376     public static function allows_multiple_instances() {
377         return true;
378     }
380     /**
381      * If at any point the caller wants to steal control,
382      * it can, by returning something that isn't false
383      * in this function
384      * The controller will redirect to whatever url
385      * this function returns.
386      * Afterwards, you can redirect back to portfolio/add.php?postcontrol=1
387      * and post_control is called before the rest of the processing
388      * for the stage is done,
389      * @see post_control
390      *
391      * @param int $stage to steal control *before* (see constants PARAM_STAGE_*}
392      * @return bool
393      */
394     public function steal_control($stage) {
395         return false;
396     }
398     /**
399      * After a plugin has elected to steal control,
400      * and control returns to portfolio/add.php|postcontrol=1,
401      * this function is called, and passed the stage that was stolen control from
402      * and the request (get and post but not cookie) parameters.
403      * This is useful for external systems that need to redirect the user back
404      * with some extra data in the url (like auth tokens etc)
405      * for an example implementation, see boxnet portfolio plugin.
406      *
407      * @param int $stage the stage before control was stolen
408      * @param array $params a merge of $_GET and $_POST
409      */
410     public function post_control($stage, $params) { }
412     /**
413      * This function creates a new instance of a plugin
414      * saves it in the database, saves the config
415      * and returns it.
416      * You shouldn't need to override it
417      * unless you're doing something really funky
418      *
419      * @param string $plugin portfolio plugin to create
420      * @param string $name name of new instance
421      * @param array $config what the admin config form returned
422      * @return object subclass of portfolio_plugin_base
423      */
424     public static function create_instance($plugin, $name, $config) {
425         global $DB, $CFG;
426         $new = (object)array(
427             'plugin' => $plugin,
428             'name'   => $name,
429         );
430         if (!portfolio_static_function($plugin, 'allows_multiple_instances')) {
431             // check we don't have one already
432             if ($DB->record_exists('portfolio_instance', array('plugin' => $plugin))) {
433                 throw new portfolio_exception('multipleinstancesdisallowed', 'portfolio', '', $plugin);
434             }
435         }
436         $newid = $DB->insert_record('portfolio_instance', $new);
437         require_once($CFG->dirroot . '/portfolio/' . $plugin . '/lib.php');
438         $classname = 'portfolio_plugin_'  . $plugin;
439         $obj = new $classname($newid);
440         $obj->set_config($config);
441         $obj->save();
442         return $obj;
443     }
445     /**
446      * Construct a plugin instance.
447      * Subclasses should not need to override this unless they're doing something special
448      * and should call parent::__construct afterwards.
449      *
450      * @param int $instanceid id of plugin instance to construct
451      * @param mixed $record stdclass object or named array - use this if you already have the record to avoid another query
452      * @return portfolio_plugin_base
453      */
454     public function __construct($instanceid, $record=null) {
455         global $DB;
456         if (!$record) {
457             if (!$record = $DB->get_record('portfolio_instance', array('id' => $instanceid))) {
458                 throw new portfolio_exception('invalidinstance', 'portfolio');
459             }
460         }
461         foreach ((array)$record as $key =>$value) {
462             if (property_exists($this, $key)) {
463                 $this->{$key} = $value;
464             }
465         }
466         $this->config = new StdClass;
467         $this->userconfig = array();
468         $this->exportconfig = array();
469         foreach ($DB->get_records('portfolio_instance_config', array('instance' => $instanceid)) as $config) {
470             $this->config->{$config->name} = $config->value;
471         }
472         $this->init();
473         return $this;
474     }
476     /**
477      * Called after __construct - allows plugins to perform initialisation tasks
478      * without having to override the constructor.
479      */
480     protected function init() { }
482     /**
483      * A list of fields that can be configured per instance.
484      * This is used for the save handlers of the config form
485      * and as checks in set_config and get_config.
486      *
487      * @return array array of strings (config item names)
488      */
489     public static function get_allowed_config() {
490         return array();
491     }
493     /**
494      * A list of fields that can be configured by the user.
495      * This is used for the save handlers in the config form
496      * and as checks in set_user_config and get_user_config.
497      *
498      * @return array array of strings (config field names)
499      */
500     public function get_allowed_user_config() {
501         return array();
502     }
504     /**
505      * A list of fields that can be configured by the user.
506      * This is used for the save handlers in the config form
507      * and as checks in set_export_config and get_export_config.
508      *
509      * @return array array of strings (config field names)
510      */
511     public function get_allowed_export_config() {
512         return array();
513     }
515     /**
516      * Saves (or updates) the config stored in portfolio_instance_config.
517      * You shouldn't need to override this unless you're doing something funky.
518      *
519      * @param array $config array of config items.
520      */
521     public final function set_config($config) {
522         global $DB;
523         foreach ($config as $key => $value) {
524             // try set it in $this first
525             try {
526                 $this->set($key, $value);
527                 continue;
528             } catch (portfolio_exception $e) { }
529             if (!in_array($key, $this->get_allowed_config())) {
530                 $a = (object)array('property' => $key, 'class' => get_class($this));
531                 throw new portfolio_export_exception($this->get('exporter'), 'invalidconfigproperty', 'portfolio', null, $a);
532             }
533             if (!isset($this->config->{$key})) {
534                 $DB->insert_record('portfolio_instance_config', (object)array(
535                     'instance' => $this->id,
536                     'name' => $key,
537                     'value' => $value,
538                 ));
539             } else if ($this->config->{$key} != $value) {
540                 $DB->set_field('portfolio_instance_config', 'value', $value, array('name' => $key, 'instance' => $this->id));
541             }
542             $this->config->{$key} = $value;
543         }
544     }
546     /**
547      * Gets the value of a particular config item
548      *
549      * @param string $key key to fetch
550      * @return null|mixed the corresponding value
551      */
552     public final function get_config($key) {
553         if (!in_array($key, $this->get_allowed_config())) {
554             $a = (object)array('property' => $key, 'class' => get_class($this));
555             throw new portfolio_export_exception($this->get('exporter'), 'invalidconfigproperty', 'portfolio', null, $a);
556         }
557         if (isset($this->config->{$key})) {
558             return $this->config->{$key};
559         }
560         return null;
561     }
563     /**
564      * Get the value of a config item for a particular user.
565      *
566      * @param string $key key to fetch
567      * @param int $userid id of user (defaults to current)
568      * @return string the corresponding value
569      *
570      */
571     public final function get_user_config($key, $userid=0) {
572         global $DB;
574         if (empty($userid)) {
575             $userid = $this->user->id;
576         }
578         if ($key != 'visible') { // handled by the parent class
579             if (!in_array($key, $this->get_allowed_user_config())) {
580                 $a = (object)array('property' => $key, 'class' => get_class($this));
581                 throw new portfolio_export_exception($this->get('exporter'), 'invaliduserproperty', 'portfolio', null, $a);
582             }
583         }
584         if (!array_key_exists($userid, $this->userconfig)) {
585             $this->userconfig[$userid] = (object)array_fill_keys(array_merge(array('visible'), $this->get_allowed_user_config()), null);
586             foreach ($DB->get_records('portfolio_instance_user', array('instance' => $this->id, 'userid' => $userid)) as $config) {
587                 $this->userconfig[$userid]->{$config->name} = $config->value;
588             }
589         }
590         if ($this->userconfig[$userid]->visible === null) {
591             $this->set_user_config(array('visible' => 1), $userid);
592         }
593         return $this->userconfig[$userid]->{$key};
595     }
597     /**
598      * Sets config options for a given user.
599      *
600      * @param array $config array containing key/value pairs to set
601      * @param int $userid userid to set config for (defaults to current)
602      *
603      */
604     public final function set_user_config($config, $userid=0) {
605         global $DB;
607         if (empty($userid)) {
608             $userid = $this->user->id;
609         }
611         foreach ($config as $key => $value) {
612             if ($key != 'visible' && !in_array($key, $this->get_allowed_user_config())) {
613                 $a = (object)array('property' => $key, 'class' => get_class($this));
614                 throw new portfolio_export_exception($this->get('exporter'), 'invaliduserproperty', 'portfolio', null, $a);
615             }
616             if (!$existing = $DB->get_record('portfolio_instance_user', array('instance'=> $this->id, 'userid' => $userid, 'name' => $key))) {
617                 $DB->insert_record('portfolio_instance_user', (object)array(
618                     'instance' => $this->id,
619                     'name' => $key,
620                     'value' => $value,
621                     'userid' => $userid,
622                 ));
623             } else if ($existing->value != $value) {
624                 $DB->set_field('portfolio_instance_user', 'value', $value, array('name' => $key, 'instance' => $this->id, 'userid' => $userid));
625             }
626             $this->userconfig[$userid]->{$key} = $value;
627         }
629     }
631     /**
632      * Generic getter for properties belonging to this instance
633      * <b>outside</b> the subclasses
634      * like name, visible etc.
635      *
636      * @param string $field property name
637      * @return array|string|int|boolean value of the field
638      */
639     public final function get($field) {
640         if (property_exists($this, $field)) {
641             return $this->{$field};
642         }
643         $a = (object)array('property' => $field, 'class' => get_class($this));
644         throw new portfolio_export_exception($this->get('exporter'), 'invalidproperty', 'portfolio', null, $a);
645     }
647     /**
648      * Generic setter for properties belonging to this instance
649      * <b>outside</b> the subclass
650      * like name, visible, etc.
651      *
652      * @param string $field property's name
653      * @param string $value property's value
654      * @return bool
655      */
656     public final function set($field, $value) {
657         if (property_exists($this, $field)) {
658             $this->{$field} =& $value;
659             $this->dirty = true;
660             return true;
661         }
662         $a = (object)array('property' => $field, 'class' => get_class($this));
663         if ($this->get('exporter')) {
664             throw new portfolio_export_exception($this->get('exporter'), 'invalidproperty', 'portfolio', null, $a);
665         }
666         throw new portfolio_exception('invalidproperty', 'portfolio', null, $a); // this happens outside export (eg admin settings)
668     }
670     /**
671      * Saves stuff that's been stored in the object to the database.
672      * You shouldn't need to override this
673      * unless you're doing something really funky.
674      * and if so, call parent::save when you're done.
675      *
676      * @return bool
677      */
678     public function save() {
679         global $DB;
680         if (!$this->dirty) {
681             return true;
682         }
683         $fordb = new StdClass();
684         foreach (array('id', 'name', 'plugin', 'visible') as $field) {
685             $fordb->{$field} = $this->{$field};
686         }
687         $DB->update_record('portfolio_instance', $fordb);
688         $this->dirty = false;
689         return true;
690     }
692     /**
693      * Deletes everything from the database about this plugin instance.
694      * You shouldn't need to override this unless you're storing stuff
695      * in your own tables.  and if so, call parent::delete when you're done.
696      *
697      * @return bool
698      */
699     public function delete() {
700         global $DB;
701         $DB->delete_records('portfolio_instance_config', array('instance' => $this->get('id')));
702         $DB->delete_records('portfolio_instance_user', array('instance' => $this->get('id')));
703         $DB->delete_records('portfolio_tempdata', array('instance' => $this->get('id')));
704         $DB->delete_records('portfolio_instance', array('id' => $this->get('id')));
705         $this->dirty = false;
706         return true;
707     }
709     /**
710      * Perform any required cleanup functions
711      *
712      * @return bool
713      */
714     public function cleanup() {
715         return true;
716     }
718     /**
719      * Whether this plugin supports multiple exports in the same session
720      * most plugins should handle this, but some that require a redirect for authentication
721      * and then don't support dynamically constructed urls to return to (eg box.net)
722      * need to override this to return false.
723      * This means that moodle will prevent multiple exports of this *type* of plugin
724      * occurring in the same session.
725      *
726      * @return bool
727      */
728     public static function allows_multiple_exports() {
729         return true;
730     }
732     /**
733      * Return a string to put at the header summarising this export
734      * by default, just the plugin instance name
735      *
736      * @return string
737      */
738     public function heading_summary() {
739         return get_string('exportingcontentto', 'portfolio', $this->name);
740     }
743 /**
744  * Class to inherit from for 'push' type plugins
745  *
746  * Eg: those that send the file via a HTTP post or whatever
747  *
748  * @package core_portfolio
749  * @category portfolio
750  * @copyright 2008 Penny Leach <penny@catalyst.net.nz>
751  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
752  */
753 abstract class portfolio_plugin_push_base extends portfolio_plugin_base {
755     /**
756      * Get the capability to push
757      *
758      * @return bool
759      */
760     public function is_push() {
761         return true;
762     }
765 /**
766  * Class to inherit from for 'pull' type plugins.
767  *
768  * Eg: those that write a file and wait for the remote system to request it
769  * from portfolio/file.php.
770  * If you're using this you must do $this->set('file', $file) so that it can be served.
771  *
772  * @package core_portfolio
773  * @category portfolio
774  * @copyright 2008 Penny Leach <penny@catalyst.net.nz>
775  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
776  */
777 abstract class portfolio_plugin_pull_base extends portfolio_plugin_base {
779     /** @var stdclass single file */
780     protected $file;
782     /**
783      * return the enablelity to push
784      *
785      * @return bool
786      */
787     public function is_push() {
788         return false;
789     }
791     /**
792      * The base part of the download file url to pull files from
793      * your plugin might need to add &foo=bar on the end
794      * @see verify_file_request_params
795      *
796      * @return string the url
797      */
798     public function get_base_file_url() {
799         global $CFG;
800         return $CFG->wwwroot . '/portfolio/file.php?id=' . $this->exporter->get('id');
801     }
803     /**
804      * Before sending the file when the pull is requested, verify the request parameters.
805      * These might include a token of some sort of whatever
806      *
807      * @param array $params request parameters (POST wins over GET)
808      */
809     public abstract function verify_file_request_params($params);
811     /**
812      * Called from portfolio/file.php.
813      * This function sends the stored file out to the browser.
814      * The default is to just use send_stored_file,
815      * but other implementations might do something different,
816      * for example, send back the file base64 encoded and encrypted
817      * mahara does this but in the response to an xmlrpc request
818      * rather than through file.php
819      */
820     public function send_file() {
821         $file = $this->get('file');
822         if (!($file instanceof stored_file)) {
823             throw new portfolio_export_exception($this->get('exporter'), 'filenotfound', 'portfolio');
824         }
825         // don't die(); afterwards, so we can clean up.
826         send_stored_file($file, 0, 0, true, array('dontdie' => true));
827         $this->get('exporter')->log_transfer();
828     }