2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * This file contains the base classes for portfolio plugins to inherit from:
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}
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
30 defined('MOODLE_INTERNAL') ||
die();
33 * The base class for portfolio plugins.
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
40 * @package core_portfolio
42 * @copyright 2008 Penny Leach <penny@catalyst.net.nz>
43 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
45 abstract class portfolio_plugin_base
{
47 /** @var bool whether this object needs writing out to the database */
50 /** @var integer id of instance */
53 /** @var string name of instance */
56 /** @var string plugin this instance belongs to */
59 /** @var bool whether this instance is visible or not */
62 /** @var array admin configured config use {@link set_config} and {@get_config} to access */
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 */
74 /** @var stdClass a reference to the exporter object */
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_*
83 * @return array list of formats
85 public function supported_formats() {
86 return array(PORTFOLIO_FORMAT_FILE
, PORTFOLIO_FORMAT_RICH
);
90 * Override this if you are supporting the 'file' type (or a subformat)
91 * but have restrictions on mimetypes
93 * @param string $mimetype file type or subformat
96 public static function file_mime_check($mimetype) {
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
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.
111 public abstract function expected_time($callertime);
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
118 public abstract function is_push();
121 * Returns the user-friendly name for this plugin.
122 * Usually just get_string('pluginname', 'portfolio_something')
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');
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
133 * @return string|int|bool - string = error string KEY (must be inside portfolio_$yourplugin) or 0/false if you're ok
135 public static function plugin_sanity_check() {
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.
144 * @return int|string|bool - string = error string KEY (must be inside portfolio_$yourplugin) or 0/false if you're ok
146 public function instance_sanity_check() {
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
158 public static function has_admin_config() {
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
170 public function has_user_config() {
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
182 public function has_export_config() {
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.
191 * @param array $data data from form.
193 public function export_config_validation(array $data) {}
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.
200 * @param array $data data from form.
202 public function user_config_validation(array $data) {}
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
210 * Keys must be in get_allowed_export_config
211 * This is deliberately not final (see googledocs plugin)
212 * @see get_allowed_export_config
214 * @param array $config named array of config items to set.
216 public function set_export_config($config) {
217 $allowed = array_merge(
218 array('wait', 'hidewait', 'format', 'hideformat'),
219 $this->get_allowed_export_config()
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);
226 $this->exportconfig
[$key] = $value;
231 * Gets an export time config value.
232 * Subclasses should not override this.
234 * @param string $key field to fetch
235 * @return null|string config value
237 public final function get_export_config($key) {
238 $allowed = array_merge(
239 array('hidewait', 'wait', 'format', 'hideformat'),
240 $this->get_allowed_export_config()
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);
246 if (!array_key_exists($key, $this->exportconfig
)) {
249 return $this->exportconfig
[$key];
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.
262 public function get_export_summary() {
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.
275 public abstract function prepare_package();
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.
282 * @return bool success
284 public abstract function send_package();
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
296 public function get_extra_finish_options() {
301 * The url for the user to continue to their portfolio
302 * during the lifecycle of the request
304 public abstract function get_interactive_continue_url();
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.
313 public function get_static_continue_url() {
314 return $this->get_interactive_continue_url();
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.
322 * @param string $url static continue url
325 public function resolve_static_continue_url($url) {
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
335 * @param moodleform $mform passed by reference, add elements to it
337 public function user_config_form(&$mform) {}
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)
348 * @param moodleform $mform passed by reference, add elements to it.
350 public static function admin_config_form(&$mform) {}
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.
357 * @param array $data data from form.
359 public static function admin_config_validation($data) {}
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
367 * @param moodleform $mform passed by reference, add elements to it.
369 public function export_config_form(&$mform) {}
372 * Override this if your plugin doesn't allow multiple instances
376 public static function allows_multiple_instances() {
381 * If at any point the caller wants to steal control,
382 * it can, by returning something that isn't false
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,
391 * @param int $stage to steal control *before* (see constants PARAM_STAGE_*}
394 public function steal_control($stage) {
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 googledocs portfolio plugin.
407 * @param int $stage the stage before control was stolen
408 * @param array $params a merge of $_GET and $_POST
410 public function post_control($stage, $params) { }
413 * This function creates a new instance of a plugin
414 * saves it in the database, saves the config
416 * You shouldn't need to override it
417 * unless you're doing something really funky
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
424 public static function create_instance($plugin, $name, $config) {
426 $new = (object)array(
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);
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);
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.
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
454 public function __construct($instanceid, $record=null) {
457 if (!$record = $DB->get_record('portfolio_instance', array('id' => $instanceid))) {
458 throw new portfolio_exception('invalidinstance', 'portfolio');
461 foreach ((array)$record as $key =>$value) {
462 if (property_exists($this, $key)) {
463 $this->{$key} = $value;
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
;
477 * Called after __construct - allows plugins to perform initialisation tasks
478 * without having to override the constructor.
480 protected function init() { }
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.
487 * @return array array of strings (config item names)
489 public static function get_allowed_config() {
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.
498 * @return array array of strings (config field names)
500 public function get_allowed_user_config() {
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.
509 * @return array array of strings (config field names)
511 public function get_allowed_export_config() {
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.
519 * @param array $config array of config items.
521 public final function set_config($config) {
523 foreach ($config as $key => $value) {
524 // try set it in $this first
526 $this->set($key, $value);
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);
533 if (!isset($this->config
->{$key})) {
534 $DB->insert_record('portfolio_instance_config', (object)array(
535 'instance' => $this->id
,
539 } else if ($this->config
->{$key} != $value) {
540 $DB->set_field('portfolio_instance_config', 'value', $value, array('name' => $key, 'instance' => $this->id
));
542 $this->config
->{$key} = $value;
547 * Gets the value of a particular config item
549 * @param string $key key to fetch
550 * @return null|mixed the corresponding value
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);
557 if (isset($this->config
->{$key})) {
558 return $this->config
->{$key};
564 * Get the value of a config item for a particular user.
566 * @param string $key key to fetch
567 * @param int $userid id of user (defaults to current)
568 * @return string the corresponding value
571 public final function get_user_config($key, $userid=0) {
574 if (empty($userid)) {
575 $userid = $this->user
->id
;
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);
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
;
590 if ($this->userconfig
[$userid]->visible
=== null) {
591 $this->set_user_config(array('visible' => 1), $userid);
593 return $this->userconfig
[$userid]->{$key};
598 * Sets config options for a given user.
600 * @param array $config array containing key/value pairs to set
601 * @param int $userid userid to set config for (defaults to current)
604 public final function set_user_config($config, $userid=0) {
607 if (empty($userid)) {
608 $userid = $this->user
->id
;
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);
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
,
623 } else if ($existing->value
!= $value) {
624 $DB->set_field('portfolio_instance_user', 'value', $value, array('name' => $key, 'instance' => $this->id
, 'userid' => $userid));
626 $this->userconfig
[$userid]->{$key} = $value;
632 * Generic getter for properties belonging to this instance
633 * <b>outside</b> the subclasses
634 * like name, visible etc.
636 * @param string $field property name
637 * @return array|string|int|boolean value of the field
639 public final function get($field) {
640 // This is a legacy change to the way files are get/set.
641 // We now only set $this->file to the id of the \stored_file. So, we need to convert that id back to a \stored_file here.
642 if ($field === 'file') {
643 return $this->get_file();
645 if (property_exists($this, $field)) {
646 return $this->{$field};
648 $a = (object)array('property' => $field, 'class' => get_class($this));
649 throw new portfolio_export_exception($this->get('exporter'), 'invalidproperty', 'portfolio', null, $a);
653 * Generic setter for properties belonging to this instance
654 * <b>outside</b> the subclass
655 * like name, visible, etc.
657 * @param string $field property's name
658 * @param string $value property's value
661 public final function set($field, $value) {
662 // This is a legacy change to the way files are get/set.
663 // Make sure we never save the \stored_file object. Instead, use the id from $file->get_id() - set_file() does this for us.
664 if ($field === 'file') {
665 $this->set_file($value);
668 if (property_exists($this, $field)) {
669 $this->{$field} =& $value;
673 $a = (object)array('property' => $field, 'class' => get_class($this));
674 if ($this->get('exporter')) {
675 throw new portfolio_export_exception($this->get('exporter'), 'invalidproperty', 'portfolio', null, $a);
677 throw new portfolio_exception('invalidproperty', 'portfolio', null, $a); // this happens outside export (eg admin settings)
682 * Saves stuff that's been stored in the object to the database.
683 * You shouldn't need to override this
684 * unless you're doing something really funky.
685 * and if so, call parent::save when you're done.
689 public function save() {
694 $fordb = new StdClass();
695 foreach (array('id', 'name', 'plugin', 'visible') as $field) {
696 $fordb->{$field} = $this->{$field};
698 $DB->update_record('portfolio_instance', $fordb);
699 $this->dirty
= false;
704 * Deletes everything from the database about this plugin instance.
705 * You shouldn't need to override this unless you're storing stuff
706 * in your own tables. and if so, call parent::delete when you're done.
710 public function delete() {
712 $DB->delete_records('portfolio_instance_config', array('instance' => $this->get('id')));
713 $DB->delete_records('portfolio_instance_user', array('instance' => $this->get('id')));
714 $DB->delete_records('portfolio_tempdata', array('instance' => $this->get('id')));
715 $DB->delete_records('portfolio_instance', array('id' => $this->get('id')));
716 $this->dirty
= false;
721 * Perform any required cleanup functions
725 public function cleanup() {
730 * Whether this plugin supports multiple exports in the same session
731 * most plugins should handle this, but some that require a redirect for authentication
732 * and then don't support dynamically constructed urls to return to (eg box.net)
733 * need to override this to return false.
734 * This means that moodle will prevent multiple exports of this *type* of plugin
735 * occurring in the same session.
739 public static function allows_multiple_exports() {
744 * Return a string to put at the header summarising this export
745 * by default, just the plugin instance name
749 public function heading_summary() {
750 return get_string('exportingcontentto', 'portfolio', $this->name
);
755 * Class to inherit from for 'push' type plugins
757 * Eg: those that send the file via a HTTP post or whatever
759 * @package core_portfolio
760 * @category portfolio
761 * @copyright 2008 Penny Leach <penny@catalyst.net.nz>
762 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
764 abstract class portfolio_plugin_push_base
extends portfolio_plugin_base
{
767 * Get the capability to push
771 public function is_push() {
777 * Class to inherit from for 'pull' type plugins.
779 * Eg: those that write a file and wait for the remote system to request it
780 * from portfolio/file.php.
781 * If you're using this you must do $this->set('file', $file) so that it can be served.
783 * @package core_portfolio
784 * @category portfolio
785 * @copyright 2008 Penny Leach <penny@catalyst.net.nz>
786 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
788 abstract class portfolio_plugin_pull_base
extends portfolio_plugin_base
{
790 /** @var int $file the id of a single file */
794 * return the enablelity to push
798 public function is_push() {
803 * The base part of the download file url to pull files from
804 * your plugin might need to add &foo=bar on the end
805 * @see verify_file_request_params
807 * @return string the url
809 public function get_base_file_url() {
811 return $CFG->wwwroot
. '/portfolio/file.php?id=' . $this->exporter
->get('id');
815 * Before sending the file when the pull is requested, verify the request parameters.
816 * These might include a token of some sort of whatever
818 * @param array $params request parameters (POST wins over GET)
820 public abstract function verify_file_request_params($params);
823 * Called from portfolio/file.php.
824 * This function sends the stored file out to the browser.
825 * The default is to just use send_stored_file,
826 * but other implementations might do something different,
827 * for example, send back the file base64 encoded and encrypted
828 * mahara does this but in the response to an xmlrpc request
829 * rather than through file.php
831 public function send_file() {
832 $file = $this->get('file');
833 if (!($file instanceof stored_file
)) {
834 throw new portfolio_export_exception($this->get('exporter'), 'filenotfound', 'portfolio');
836 // don't die(); afterwards, so we can clean up.
837 send_stored_file($file, 0, 0, true, array('dontdie' => true));
838 $this->get('exporter')->log_transfer();
842 * Sets the $file instance var to the id of the supplied \stored_file.
844 * This helper allows the $this->get('file') call to return a \stored_file, but means that we only ever record an id reference
845 * in the $file instance var.
847 * @param \stored_file $file The stored_file instance.
850 protected function set_file(\stored_file
$file) {
851 $fileid = $file->get_id();
852 if (empty($fileid)) {
853 debugging('stored_file->id should not be empty');
856 $this->file
= $fileid;
861 * Gets the \stored_file object from the file id in the $file instance var.
863 * @return stored_file|null the \stored_file object if it exists, null otherwise.
865 protected function get_file() {
869 // The get_file_by_id call can return false, so normalise to null.
870 $file = get_file_storage()->get_file_by_id($this->file
);
871 return ($file) ?
$file : null;