MDL-18734 - documentation and coding style in portfolio/*
[moodle.git] / lib / portfolio / plugin.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 base classes for portfolio plugins to inherit from:
27  * portfolio_plugin_pull_base and portfolio_plugin_push_base
28  * which both in turn inherit from portfolio_plugin_base.
29  * See http://docs.moodle.org/en/Development:Writing_a_Portfolio_Plugin
30  */
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 abstract class portfolio_plugin_base {
39     /**
40     * whether this object needs writing out to the database
41     * @var boolean $dirty
42     */
43     protected $dirty;
45     /**
46     * id of instance
47     * @var integer $id
48     */
49     protected $id;
51     /**
52     * name of instance
53     * @var string $name
54     */
55     protected $name;
57     /**
58     * plugin this instance belongs to
59     * @var string $plugin
60     */
61     protected $plugin;
63     /**
64     * whether this instance is visible or not
65     * @var boolean $visible
66     */
67     protected $visible;
69     /**
70     * named array
71     * admin configured config
72     * use {@link set_config} and {@get_config} to access
73     */
74     protected $config;
76     /**
77     *
78     * user config cache
79     * named array of named arrays
80     * keyed on userid and then on config field => value
81     * use {@link get_user_config} and {@link set_user_config} to access.
82     */
83     protected $userconfig;
85     /**
86     * named array
87     * export config during export
88     * use {@link get_export_config} and {@link set export_config} to access.
89     */
90     protected $exportconfig;
92     /**
93     * stdclass object
94     * user currently exporting data
95     */
96     protected $user;
98     /**
99     * a reference to the exporter object
100     */
101     protected $exporter;
103     /**
104     * array of formats this portfolio supports
105     * the intersection of what this function returns
106     * and what the caller supports will be used
107     * use the constants PORTFOLIO_FORMAT_*
108     *
109     * @return array list of formats
110     */
111     public static function supported_formats() {
112         return array(PORTFOLIO_FORMAT_FILE, PORTFOLIO_FORMAT_RICHHTML);
113     }
115     /**
116     * override this if you are supporting the 'file' type (or a subformat)
117     * but have restrictions on mimetypes (see googledocs plugin for more info)
118     *
119     * @return boolean
120     */
121     public static function file_mime_check($mimetype) {
122         return true;
123     }
126     /**
127     * how long does this reasonably expect to take..
128     * should we offer the user the option to wait..
129     * this is deliberately nonstatic so it can take filesize into account
130     *
131     * @param string $callertime - what the caller thinks
132     *                             the portfolio plugin instance
133     *                             is given the final say
134     *                             because it might be (for example) download.
135     * @return string (see PORTFOLIO_TIME_* constants)
136     */
137     public abstract function expected_time($callertime);
139     /**
140     * is this plugin push or pill.
141     * if push, cleanup will be called directly after send_package
142     * if not, cleanup will be called after portfolio/file.php is requested
143     *
144     * @return boolean
145     */
146     public abstract function is_push();
148     public static abstract function get_name();
150     /**
151     * check sanity of plugin
152     * if this function returns something non empty, ALL instances of your plugin
153     * will be set to invisble and not be able to be set back until it's fixed
154     *
155     * @return mixed - string = error string KEY (must be inside plugin_$yourplugin) or 0/false if you're ok
156     */
157     public static function plugin_sanity_check() {
158         return 0;
159     }
161     /**
162     * check sanity of instances
163     * if this function returns something non empty, the instance will be
164     * set to invislbe and not be able to be set back until it's fixed.
165     *
166     * @return mixed - string = error string KEY (must be inside plugin_$yourplugin) or 0/false if you're ok
167     */
168     public function instance_sanity_check() {
169         return 0;
170     }
172     /**
173     * does this plugin need any configuration by the administrator?
174     *
175     * if you override this to return true,
176     * you <b>must</b> implement {@see admin_config_form}
177     */
178     public static function has_admin_config() {
179         return false;
180     }
182     /**
183     * can this plugin be configured by the user in their profile?
184     *
185     * if you override this to return true,
186     * you <b>must</b> implement {@see user_config_form}
187     */
188     public function has_user_config() {
189         return false;
190     }
192     /**
193     * does this plugin need configuration during export time?
194     *
195     * if you override this to return true,
196     * you <b>must</b> implement {@see export_config_form}
197     */
198     public function has_export_config() {
199         return false;
200     }
202     /**
203     * just like the moodle form validation function
204     * this is passed in the data array from the form
205     * and if a non empty array is returned, form processing will stop.
206     *
207     * @param array $data data from form.
208     * @return array keyvalue pairs - form element => error string
209     */
210     public function export_config_validation() {}
212     /**
213     * just like the moodle form validation function
214     * this is passed in the data array from the form
215     * and if a non empty array is returned, form processing will stop.
216     *
217     * @param array $data data from form.
218     * @return array keyvalue pairs - form element => error string
219     */
220     public function user_config_validation() {}
222     /**
223     * sets the export time config from the moodle form.
224     * you can also use this to set export config that
225     * isn't actually controlled by the user
226     * eg things that your subclasses want to keep in state
227     * across the export.
228     * keys must be in {@see get_allowed_export_config}
229     *
230     * this is deliberately not final (see boxnet plugin)
231     *
232     * @param array $config named array of config items to set.
233     */
234     public function set_export_config($config) {
235         $allowed = array_merge(
236             array('wait', 'hidewait', 'format', 'hideformat'),
237             $this->get_allowed_export_config()
238         );
239         foreach ($config as $key => $value) {
240             if (!in_array($key, $allowed)) {
241                 $a = (object)array('property' => $key, 'class' => get_class($this));
242                 throw new portfolio_export_exception($this->get('exporter'), 'invalidexportproperty', 'portfolio', null, $a);
243             }
244             $this->exportconfig[$key] = $value;
245         }
246     }
248     /**
249     * gets an export time config value.
250     * subclasses should not override this.
251     *
252     * @param string key field to fetch
253     *
254     * @return string config value
255     *
256     */
257     public final function get_export_config($key) {
258         $allowed = array_merge(
259             array('hidewait', 'wait', 'format', 'hideformat'),
260             $this->get_allowed_export_config()
261         );
262         if (!in_array($key, $allowed)) {
263             $a = (object)array('property' => $key, 'class' => get_class($this));
264             throw new portfolio_export_exception($this->get('exporter'), 'invalidexportproperty', 'portfolio', null, $a);
265         }
266         if (!array_key_exists($key, $this->exportconfig)) {
267             return null;
268         }
269         return $this->exportconfig[$key];
270     }
272     /**
273     * after the user submits their config
274     * they're given a confirm screen
275     * summarising what they've chosen.
276     *
277     * this function should return a table of nice strings => values
278     * of what they've chosen
279     * to be displayed in a table.
280     *
281     * @return array array of config items.
282     */
283     public function get_export_summary() {
284         return false;
285     }
287     /**
288     * called after the caller has finished having control
289     * of its prepare_package function.
290     * this function should read all the files from the portfolio
291     * working file area and zip them and send them or whatever it wants.
292     * {@see get_tempfiles} to get the list of files.
293     *
294     */
295     public abstract function prepare_package();
297     /**
298     * this is the function that is responsible for sending
299     * the package to the remote system,
300     * or whatever request is necessary to initiate the transfer.
301     *
302     * @return boolean success
303     */
304     public abstract function send_package();
307     /**
308     * once everything is done and the user
309     * has the finish page displayed to them
310     * the base class takes care of printing them
311     * "return to where you are" or "continue to portfolio" links
312     * this function allows for exta finish options from the plugin
313     *
314     * @return array named array of links => titles
315     */
316     public function get_extra_finish_options() {
317         return false;
318     }
320     /**
321     * the url for the user to continue to their portfolio
322     *
323     * @return string url or false.
324     */
325     public abstract function get_continue_url();
327     /**
328     * mform to display to the user in their profile
329     * if your plugin can't be configured by the user,
330     * (see {@link has_user_config})
331     * don't bother overriding this function
332     *
333     * @param moodleform $mform passed by reference, add elements to it
334     */
335     public function user_config_form(&$mform) {}
337     /**
338     * mform to display to the admin configuring the plugin.
339     * if your plugin can't be configured by the admin,
340     * (see {@link} has_admin_config)
341     * don't bother overriding this function
342     *
343     * this function can be called statically or non statically,
344     * depending on whether it's creating a new instance (statically),
345     * or editing an existing one (non statically)
346     *
347     * @param moodleform $mform passed by reference, add elements to it.
348     */
349     public function admin_config_form(&$mform) {}
351     /**
352     * just like the moodle form validation function
353     * this is passed in the data array from the form
354     * and if a non empty array is returned, form processing will stop.
355     *
356     * @param array $data data from form.
357     * @return array keyvalue pairs - form element => error string
358     */
359     public function admin_config_validation($data) {}
360     /**
361     * mform to display to the user exporting data using this plugin.
362     * if your plugin doesn't need user input at this time,
363     * (see {@link has_export_config}
364     * don't bother overrideing this function
365     *
366     * @param moodleform $mform passed by reference, add elements to it.
367     */
368     public function export_config_form(&$mform) {}
370     /**
371     * override this if your plugin doesn't allow multiple instances
372     *
373     * @return boolean
374     */
375     public static function allows_multiple() {
376         return true;
377     }
379     /**
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 {@link post_control} is called before the rest of the processing
388     * for the stage is done
389     *
390     * @param int stage to steal control *before* (see constants PARAM_STAGE_*}
391     *
392     * @return boolean or string url
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     */
412     public function post_control($stage, $params) { }
414     /**
415     * this function creates a new instance of a plugin
416     * saves it in the database, saves the config
417     * and returns it.
418     * you shouldn't need to override it
419     * unless you're doing something really funky
420     *
421     * @param string $plugin portfolio plugin to create
422     * @param string $name name of new instance
423     * @param array $config what the admin config form returned
424     *
425     * @return object subclass of portfolio_plugin_base
426     */
427     public static function create_instance($plugin, $name, $config) {
428         global $DB, $CFG;
429         $new = (object)array(
430             'plugin' => $plugin,
431             'name'   => $name,
432         );
433         if (!portfolio_static_function($plugin, 'allows_multiple')) {
434             // check we don't have one already
435             if ($DB->record_exists('portfolio_instance', array('plugin' => $plugin))) {
436                 throw new portfolio_exception('multipledisallowed', 'portfolio', '', $plugin);
437             }
438         }
439         $newid = $DB->insert_record('portfolio_instance', $new);
440         require_once($CFG->dirroot . '/portfolio/type/' . $plugin . '/lib.php');
441         $classname = 'portfolio_plugin_'  . $plugin;
442         $obj = new $classname($newid);
443         $obj->set_config($config);
444         return $obj;
445     }
447     /**
448     * construct a plugin instance
449     * subclasses should not need  to override this unless they're doing something special
450     * and should call parent::__construct afterwards
451     *
452     * @param int $instanceid id of plugin instance to construct
453     * @param mixed $record stdclass object or named array - use this i you already have the record to avoid another query
454     *
455     * @return object subclass of portfolio_plugin_base
456     */
457     public function __construct($instanceid, $record=null) {
458         global $DB;
459         if (!$record) {
460             if (!$record = $DB->get_record('portfolio_instance', array('id' => $instanceid))) {
461                 throw new portfolio_exception('invalidinstance', 'portfolio');
462             }
463         }
464         foreach ((array)$record as $key =>$value) {
465             if (property_exists($this, $key)) {
466                 $this->{$key} = $value;
467             }
468         }
469         $this->config = new StdClass;
470         $this->userconfig = array();
471         $this->exportconfig = array();
472         foreach ($DB->get_records('portfolio_instance_config', array('instance' => $instanceid)) as $config) {
473             $this->config->{$config->name} = $config->value;
474         }
475         return $this;
476     }
478     /**
479     * a list of fields that can be configured per instance.
480     * this is used for the save handlers of the config form
481     * and as checks in set_config and get_config
482     *
483     * @return array array of strings (config item names)
484     */
485     public static function get_allowed_config() {
486         return array();
487     }
489     /**
490     * a list of fields that can be configured by the user.
491     * this is used for the save handlers in the config form
492     * and as checks in set_user_config and get_user_config.
493     *
494     * @return array array of strings (config field names)
495     */
496     public function get_allowed_user_config() {
497         return array();
498     }
500     /**
501     * a list of fields that can be configured by the user.
502     * this is used for the save handlers in the config form
503     * and as checks in set_export_config and get_export_config.
504     *
505     * @return array array of strings (config field names)
506     */
507     public function get_allowed_export_config() {
508         return array();
509     }
511     /**
512     * saves (or updates) the config stored in portfolio_instance_config.
513     * you shouldn't need to override this unless you're doing something funky.
514     *
515     * @param array $config array of config items.
516     */
517     public final function set_config($config) {
518         global $DB;
519         foreach ($config as $key => $value) {
520             // try set it in $this first
521             try {
522                 $this->set($key, $value);
523                 continue;
524             } catch (portfolio_exception $e) { }
525             if (!in_array($key, $this->get_allowed_config())) {
526                 $a = (object)array('property' => $key, 'class' => get_class($this));
527                 throw new portfolio_export_exception($this->get('exporter'), 'invalidconfigproperty', 'portfolio', null, $a);
528             }
529             if (!isset($this->config->{$key})) {
530                 $DB->insert_record('portfolio_instance_config', (object)array(
531                     'instance' => $this->id,
532                     'name' => $key,
533                     'value' => $value,
534                 ));
535             } else if ($this->config->{$key} != $value) {
536                 $DB->set_field('portfolio_instance_config', 'value', $value, array('name' => $key, 'instance' => $this->id));
537             }
538             $this->config->{$key} = $value;
539         }
540     }
542     /**
543     * gets the value of a particular config item
544     *
545     * @param string $key key to fetch
546     *
547     * @return string the corresponding value
548     */
549     public final function get_config($key) {
550         if (!in_array($key, $this->get_allowed_config())) {
551             $a = (object)array('property' => $key, 'class' => get_class($this));
552             throw new portfolio_export_exception($this->get('exporter'), 'invalidconfigproperty', 'portfolio', null, $a);
553         }
554         if (isset($this->config->{$key})) {
555             return $this->config->{$key};
556         }
557         return null;
558     }
560     /**
561     * get the value of a config item for a particular user
562     *
563     * @param string $key key to fetch
564     * @param integer $userid id of user (defaults to current)
565     *
566     * @return string the corresponding value
567     *
568     */
569     public final function get_user_config($key, $userid=0) {
570         global $DB;
572         if (empty($userid)) {
573             $userid = $this->user->id;
574         }
576         if ($key != 'visible') { // handled by the parent class
577             if (!in_array($key, $this->get_allowed_user_config())) {
578                 $a = (object)array('property' => $key, 'class' => get_class($this));
579                 throw new portfolio_export_exception($this->get('exporter'), 'invaliduserproperty', 'portfolio', null, $a);
580             }
581         }
582         if (!array_key_exists($userid, $this->userconfig)) {
583             $this->userconfig[$userid] = (object)array_fill_keys(array_merge(array('visible'), $this->get_allowed_user_config()), null);
584             foreach ($DB->get_records('portfolio_instance_user', array('instance' => $this->id, 'userid' => $userid)) as $config) {
585                 $this->userconfig[$userid]->{$config->name} = $config->value;
586             }
587         }
588         if ($this->userconfig[$userid]->visible === null) {
589             $this->set_user_config(array('visible' => 1), $userid);
590         }
591         return $this->userconfig[$userid]->{$key};
593     }
595     /**
596     *
597     * sets config options for a given user
598     *
599     * @param mixed $config array or stdclass containing key/value pairs to set
600     * @param integer $userid userid to set config for (defaults to current)
601     *
602     */
603     public final function set_user_config($config, $userid=0) {
604         global $DB;
606         if (empty($userid)) {
607             $userid = $this->user->id;
608         }
610         foreach ($config as $key => $value) {
611             if ($key != 'visible' && !in_array($key, $this->get_allowed_user_config())) {
612                 $a = (object)array('property' => $key, 'class' => get_class($this));
613                 throw new portfolio_export_exception($this->get('exporter'), 'invaliduserproperty', 'portfolio', null, $a);
614             }
615             if (!$existing = $DB->get_record('portfolio_instance_user', array('instance'=> $this->id, 'userid' => $userid, 'name' => $key))) {
616                 $DB->insert_record('portfolio_instance_user', (object)array(
617                     'instance' => $this->id,
618                     'name' => $key,
619                     'value' => $value,
620                     'userid' => $userid,
621                 ));
622             } else if ($existing->value != $value) {
623                 $DB->set_field('portfolio_instance_user', 'value', $value, array('name' => $key, 'instance' => $this->id, 'userid' => $userid));
624             }
625             $this->userconfig[$userid]->{$key} = $value;
626         }
628     }
630     /**
631     * generic getter for properties belonging to this instance
632     * <b>outside</b> the subclasses
633     * like name, visible etc.
634     *
635     */
636     public final function get($field) {
637         if (property_exists($this, $field)) {
638             return $this->{$field};
639         }
640         $a = (object)array('property' => $field, 'class' => get_class($this));
641         throw new portfolio_export_exception($this->get('exporter'), 'invalidproperty', 'portfolio', null, $a);
642     }
644     /**
645     * generic setter for properties belonging to this instance
646     * <b>outside</b> the subclass
647     * like name, visible, etc.
648     *
649     */
650     public final function set($field, $value) {
651         if (property_exists($this, $field)) {
652             $this->{$field} =& $value;
653             $this->dirty = true;
654             return true;
655         }
656         $a = (object)array('property' => $field, 'class' => get_class($this));
657         if ($this->get('exporter')) {
658             throw new portfolio_export_exception($this->get('exporter'), 'invalidproperty', 'portfolio', null, $a);
659         }
660         throw new portfolio_exception('invalidproperty', 'portfolio', null, $a); // this happens outside export (eg admin settings)
662     }
664     /**
665     * saves stuff that's been stored in the object to the database
666     * you shouldn't need to override this
667     * unless you're doing something really funky.
668     * and if so, call parent::save when you're done.
669     */
670     public function save() {
671         global $DB;
672         if (!$this->dirty) {
673             return true;
674         }
675         $fordb = new StdClass();
676         foreach (array('id', 'name', 'plugin', 'visible') as $field) {
677             $fordb->{$field} = $this->{$field};
678         }
679         $DB->update_record('portfolio_instance', $fordb);
680         $this->dirty = false;
681         return true;
682     }
684     /**
685     * deletes everything from the database about this plugin instance.
686     * you shouldn't need to override this unless you're storing stuff
687     * in your own tables.  and if so, call parent::delete when you're done.
688     */
689     public function delete() {
690         global $DB;
691         $DB->delete_records('portfolio_instance_config', array('instance' => $this->get('id')));
692         $DB->delete_records('portfolio_instance_user', array('instance' => $this->get('id')));
693         $DB->delete_records('portfolio_instance', array('id' => $this->get('id')));
694         $this->dirty = false;
695         return true;
696     }
698     /**
699     * perform any required cleanup functions
700     */
701     public function cleanup() {
702         return true;
703     }
705     public static function mnet_publishes() {
706         return array();
707     }
710 /**
711 * class to inherit from for 'push' type plugins
712 * eg those that send the file via a HTTP post or whatever
713 */
714 abstract class portfolio_plugin_push_base extends portfolio_plugin_base {
716     public function is_push() {
717         return true;
718     }
721 /**
722 * class to inherit from for 'pull' type plugins
723 * eg those that write a file and wait for the remote system to request it
724 * from portfolio/file.php
725 * if you're using this you must do $this->set('file', $file) so that it can be served.
726 */
727 abstract class portfolio_plugin_pull_base extends portfolio_plugin_base {
729     protected $file;
731     public function is_push() {
732         return false;
733     }
735     /**
736     * the base part of the download file url to pull files from
737     * your plugin might need to add &foo=bar on the end
738     * {@see verify_file_request_params}
739     *
740     * @return string the url
741     */
742     public function get_base_file_url() {
743         global $CFG;
744         return $CFG->wwwroot . '/portfolio/file.php?id=' . $this->exporter->get('id');
745     }
747     /**
748     * before sending the file when the pull is requested, verify the request parameters
749     * these might include a token of some sort of whatever
750     *
751     * @param array request parameters (POST wins over GET)
752     */
753     public abstract function verify_file_request_params($params);
755     /**
756     * called from portfolio/file.php
757     * this function sends the stored file out to the browser
758     * the default is to just use send_stored_file,
759     * but other implementations might do something different
760     * for example, send back the file base64 encoded and encrypted
761     * mahara does this but in the response to an xmlrpc request
762     * rather than through file.php
763     */
764     public function send_file() {
765         $file = $this->get('file');
766         if (!($file instanceof stored_file)) {
767             throw new portfolio_export_exception($this->get('exporter'), 'filenotfound', 'portfolio');
768         }
769         // the last 'true' on the end of this means don't die(); afterwards, so we can clean up.
770         send_stored_file($file, 0, 0, true, null, true);
771         $this->get('exporter')->log_transfer();
772     }
776 ?>