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