MDL-15777 more fixes for portfolio to use file api
[moodle.git] / lib / portfoliolib.php
1 <?php
2 /**
3 * this file contains:
4 * {@link portfolio_add_button} -entry point for callers
5 * {@link class portfolio_plugin_base} - class plugins extend
6 * {@link class portfolio_caller_base} - class callers extend
7 * {@link class portfolio_admin_form} - base moodleform class for plugin administration
8 * {@link class portfolio_user_form} - base moodleform class for plugin instance user config
9 * {@link class portfolio_export_form} - base moodleform class for during-export configuration (eg metadata)
10 * {@link class portfolio_exporter} - class used during export process
11 *
12 * and some helper functions:
13 * {@link portfolio_instances - returns an array of all configured instances
14 * {@link portfolio_instance - returns an instance of the right class given an id
15 * {@link portfolio_instance_select} - returns a drop menu of available instances
16 * {@link portfolio_static_function - requires the file, and calls a static function on the given class
17 " {@link portfolio_plugin_sanity_check - polls given (or all) portfolio_plugins for sanity and disables insane ones
18 " {@link portfolio_instance_sanity_check - polls given (or all) portfolio instances for sanity and disables insane ones
19 * {@link portfolio_report_instane} - returns a table of insane plugins and the reasons (used for plugins or instances thereof)
20 * {@link portfolio_supported_formats - returns array of all available formats for plugins and callers to use
21 * {@link portfolio_handle_event} - event handler for queued transfers that get triggered on cron
22 *
23 */
24 require_once ($CFG->libdir.'/formslib.php');
26 // **** EXPORT STAGE CONSTANTS **** //
28 /**
29 * display a form to the user
30 * this one might not be used if neither
31 * the plugin, or the caller has any config.
32 */
33 define('PORTFOLIO_STAGE_CONFIG', 1);
35 /**
36 * summarise the form and ask for confirmation
37 * if we skipped PORTFOLIO_STAGE_CONFIG,
38 * just confirm the send.
39 */
40 define('PORTFOLIO_STAGE_CONFIRM', 2);
42 /**
43 * either queue the event and skip to PORTFOLIO_STAGE_FINISHED
44 * or continue to PORTFOLIO_STAGE_PACKAGE
45 */
47 define('PORTFOLIO_STAGE_QUEUEORWAIT', 3);
49 /**
50 * package up the various bits
51 * during this stage both the caller
52 * and the plugin get their package methods called
53 */
54 define('PORTFOLIO_STAGE_PACKAGE', 4);
56 /*
57 * the portfolio plugin must send the file
58 */
59 define('PORTFOLIO_STAGE_SEND', 5);
61 /**
62 * cleanup the temporary area
63 */
64 define('PORTFOLIO_STAGE_CLEANUP', 6);
66 /**
67 * display the "finished notification"
68 */
69 define('PORTFOLIO_STAGE_FINISHED', 7);
73 // **** EXPORT FORMAT CONSTANTS **** //
74 // these should always correspond to a string
75 // in the portfolio module, called format_{$value}
76 // ****                         **** //
79 /**
80 * file - the most basic fallback format.
81 * this should always be supported
82 * in remote system.s
83 */
84 define('PORTFOLIO_FORMAT_FILE', 'file');
86 /**
87 * moodle backup - the plugin needs to be able to write a complete backup
88 * the caller need to be able to export the particular XML bits to insert
89 * into moodle.xml (?and the file bits if necessary)
90 */
91 define('PORTFOLIO_FORMAT_MBKP', 'mbkp');
94 // **** EXPORT TIME LEVELS  **** //
95 // these should correspond to a string
96 // in the portfolio module, called time_{$value}
98 /**
99 * no delay. don't even offer the user the option
100 * of not waiting for the transfer
101 */
102 define('PORTFOLIO_TIME_LOW', 'low');
104 /**
105 * a small delay. user can still easily opt to
106 * watch this transfer and wait.
107 */
108 define('PORTFOLIO_TIME_MODERATE', 'moderate');
110 /**
111 * slow. the user really should not be given the option
112 * to choose this.
113 */
114 define('PORTFOLIO_TIME_HIGH', 'high');
117 /**
118 * entry point to add an 'add to portfolio' button somewhere in moodle
119 * this function does not check permissions. the caller must check permissions first.
120 * later, during the export process, the caller class is instantiated and the check_permissions method is called
121 * but not in this function.
123 * @param string $callbackclass           name of the class containing the callback functions
124 *                                        activity modules should ALWAYS use their name_portfolio_caller
125 *                                        other locations must use something unique
126 * @param mixed $callbackargs             this can be an array or hash of arguments to pass
127 *                                        back to the callback functions (passed by reference)
128 *                                        these MUST be primatives to be added as hidden form fields.
129 *                                        and the values get cleaned to PARAM_ALPHAEXT or PARAM_NUMBER or PARAM_PATH
130 * @param string $callbackfile            this can be autodetected if it's in the same file as your caller,
131 *                                        but more often, the caller is a script.php and the class in a lib.php
132 *                                        so you can pass it here if necessary.
133 *                                        this path should be relative (ie, not include) dirroot
134 * @param boolean $fullform               either display the fullform with the dropmenu of available instances
135 *                                        or just a small icon (which will trigger instance selection in a new screen)
136 *                                        optional, defaults to true.
137 * @param boolean $return                 whether to echo or return content (optional defaults to false (echo)
138 */
139 function portfolio_add_button($callbackclass, $callbackargs, $callbackfile=null, $fullform=true, $return=false) {
141     global $SESSION, $CFG, $COURSE, $USER;
143     if (empty($CFG->portfolioenabled)) {
144         return;
145     }
147     if (!$instances = portfolio_instances()) {
148         return;
149     }
151     if (defined('PORTFOLIO_INTERNAL')) {
152         // something somewhere has detected a risk of this being called during inside the preparation
153         // eg forum_print_attachments
154         return;
155     }
157     if (isset($SESSION->portfolioexport)) {
158         print_error('alreadyexporting', 'portfolio', null, $CFG->wwwroot . '/portfolio/add.php?cancel=1');
159     }
161     if (empty($callbackfile)) {
162         $backtrace = debug_backtrace();
163         if (!array_key_exists(0, $backtrace) || !array_key_exists('file', $backtrace[0]) || !is_readable($backtrace[0]['file'])) {
164             debugging(get_string('nocallbackfile', 'portfolio'));
165             return;
166         }
168         $callbackfile = substr($backtrace[0]['file'], strlen($CFG->dirroot));
169     } else {
170         if (!is_readable($CFG->dirroot . $callbackfile)) {
171             debugging(get_string('nocallbackfile', 'portfolio'));
172             return;
173         }
174     }
176     require_once($CFG->dirroot . $callbackfile);
178     $callersupports = call_user_func(array($callbackclass, 'supported_formats'));
180     $output = '<form method="post" action="' . $CFG->wwwroot . '/portfolio/add.php" id="portfolio-add-button">' . "\n";
181     foreach ($callbackargs as $key => $value) {
182         if (!empty($value) && !is_string($value) && !is_numeric($value)) {
183             $a->key = $key;
184             $a->value = print_r($value, true);
185             debugging(get_string('nonprimative', 'portfolio', $a));
186             return;
187         }
188         $output .= "\n" . '<input type="hidden" name="ca_' . $key . '" value="' . $value . '" />';
189     }
190     $output .= "\n" . '<input type="hidden" name="callbackfile" value="' . $callbackfile . '" />';
191     $output .= "\n" . '<input type="hidden" name="callbackclass" value="' . $callbackclass . '" />';
192     $output .= "\n" . '<input type="hidden" name="course" value="' . (!empty($COURSE) ? $COURSE->id : 0) . '" />';
193     $selectoutput = '';
194     if (count($instances) == 1) {
195         $instance = array_shift($instances);
196         if (count(array_intersect($callersupports,  $instance->supported_formats())) == 0) {
197             // bail. no common formats.
198             debugging(get_string('nocommonformats', 'portfolio', $callbackclass));
199             return;
200         }
201         if ($error = portfolio_instance_sanity_check($instance)) {
202             // bail, plugin is misconfigured
203             debugging(get_string('instancemisconfigured', 'portfolio', get_string($error[$instance->get('id')], 'portfolio_' . $instance->get('plugin'))));
204             return;
205         }
206         $output .= "\n" . '<input type="hidden" name="instance" value="' . $instance->get('id') . '" />';
207     }
208     else {
209         $selectoutput = portfolio_instance_select($instances, $callersupports, $callbackclass, 'instance', true);
210     }
212     if ($fullform) {
213         $output .= $selectoutput;
214         $output .= "\n" . '<input type="submit" value="' . get_string('addtoportfolio', 'portfolio') .'" />';
215     } else {
216         $output .= "\n" . '<input type="image" src="' . $CFG->pixpath . '/t/portfolio.gif" alt=' . get_string('addtoportfolio', 'portfolio') .'" />';
217         //@todo replace this with a little icon
218     }
220     $output .= "\n" . '</form>';
222     if ($return) {
223         return $output;
224     } else {
225         echo $output;
226     }
227     return true;
230 /**
231 * returns a drop menu with a list of available instances.
233 * @param array $instances     the instances to put in the menu
234 * @param array $callerformats the formats the caller supports
235                               (this is used to filter plugins)
236 * @param array $callbackclass the callback class name
238 * @return string the html, from <select> to </select> inclusive.
239 */
240 function portfolio_instance_select($instances, $callerformats, $callbackclass, $selectname='instance', $return=false, $returnarray=false) {
241     global $CFG;
243     if (empty($CFG->portfolioenabled)) {
244         return;
245     }
247     $insane = portfolio_instance_sanity_check();
248     $count = 0;
249     $selectoutput = "\n" . '<select name="' . $selectname . '">' . "\n";
250     foreach ($instances as $instance) {
251         if (count(array_intersect($callerformats,  $instance->supported_formats())) == 0) {
252             // bail. no common formats.
253             continue;
254         }
255         if (array_key_exists($instance->get('id'), $insane)) {
256             // bail, plugin is misconfigured
257             debugging(get_string('instancemisconfigured', 'portfolio', get_string($insane[$instance->get('id')], 'portfolio_' . $instance->get('plugin'))));
258             continue;
259         }
260         $count++;
261         $selectoutput .= "\n" . '<option value="' . $instance->get('id') . '">' . $instance->get('name') . '</option>' . "\n";
262         $options[$instance->get('id')] = $instance->get('name');
263     }
264     if (empty($count)) {
265         // bail. no common formats.
266         debugging(get_string('nocommonformats', 'portfolio', $callbackclass));
267         return;
268     }
269     $selectoutput .= "\n" . "</select>\n";
270     if (!empty($returnarray)) {
271         return $options;
272     }
273     if (!empty($return)) {
274         return $selectoutput;
275     }
276     echo $selectoutput;
279 /**
280 * return all portfolio instances
282 * @param boolean visibleonly don't include hidden instances (defaults to true and will be overridden to true if the next parameter is true)
283 * @param boolean useronly check the visibility preferences and permissions of the logged in user
284 * @return array of portfolio instances (full objects, not just database records)
285 */
286 function portfolio_instances($visibleonly=true, $useronly=true) {
288     global $DB, $USER;
290     $values = array();
291     $sql = 'SELECT * FROM {portfolio_instance}';
293     if ($visibleonly || $useronly) {
294         $values[] = 1;
295         $sql .= ' WHERE visible = ?';
296     }
297     if ($useronly) {
298         $sql .= ' AND id NOT IN (
299                 SELECT instance FROM {portfolio_instance_user}
300                 WHERE userid = ? AND name = ? AND value = ?
301             )';
302         $values = array_merge($values, array($USER->id, 'visible', 0));
303     }
304     $sql .= ' ORDER BY name';
306     $instances = array();
307     foreach ($DB->get_records_sql($sql, $values) as $instance) {
308         $instances[$instance->id] = portfolio_instance($instance->id, $instance);
309     }
310     // @todo check capabilities here - see MDL-15768
311     return $instances;
314 /**
315 * supported formats that portfolio plugins and callers
316 * can use for exporting content
318 * @return array of all the available export formats
319 */
320 function portfolio_supported_formats() {
321     return array(
322         PORTFOLIO_FORMAT_FILE,
323         /*PORTFOLIO_FORMAT_MBKP, */ // later
324         /*PORTFOLIO_FORMAT_PIOP, */ // also later
325     );
328 /**
329 * helper function to return an instance of a plugin (with config loaded)
331 * @param int $instance id of instance
332 * @param array $record database row that corresponds to this instance
333 *                      this is passed to avoid unnecessary lookups
335 * @return subclass of portfolio_plugin_base
336 */
337 function portfolio_instance($instanceid, $record=null) {
338     global $DB, $CFG;
340     if ($record) {
341         $instance  = $record;
342     } else {
343         if (!$instance = $DB->get_record('portfolio_instance', array('id' => $instanceid))) {
344             return false; // @todo throw exception?
345         }
346     }
347     require_once($CFG->dirroot . '/portfolio/type/'. $instance->plugin . '/lib.php');
348     $classname = 'portfolio_plugin_' . $instance->plugin;
349     return new $classname($instanceid, $instance);
352 /**
353 * helper function to call a static function on a portfolio plugin class
354 * this will figure out the classname and require the right file and call the function.
355 * you can send a variable number of arguments to this function after the first two
356 * and they will be passed on to the function you wish to call.
358 * @param string $plugin name of plugin
359 * @param string $function function to call
360 */
361 function portfolio_static_function($plugin, $function) {
362     global $CFG;
364     $pname = null;
365     if (is_object($plugin) || is_array($plugin)) {
366         $plugin = (object)$plugin;
367         $pname = $plugin->name;
368     } else {
369         $pname = $plugin;
370     }
372     $args = func_get_args();
373     if (count($args) <= 2) {
374         $args = array();
375     }
376     else {
377         array_shift($args);
378         array_shift($args);
379     }
381     require_once($CFG->dirroot . '/portfolio/type/' . $plugin .  '/lib.php');
382     return call_user_func_array(array('portfolio_plugin_' . $plugin, $function), $args);
385 /**
386 * helper function to check all the plugins for sanity and set any insane ones to invisible.
388 * @param array $plugins to check (if null, defaults to all)
389 *               one string will work too for a single plugin.
391 * @return array array of insane instances (keys= id, values = reasons (keys for plugin lang)
392 */
393 function portfolio_plugin_sanity_check($plugins=null) {
394     global $DB;
395     if (is_string($plugins)) {
396        $plugins = array($plugins);
397     } else if (empty($plugins)) {
398         $plugins = get_list_of_plugins('portfolio/type');
399     }
401     $insane = array();
402     foreach ($plugins as $plugin) {
403         if ($result = portfolio_static_function($plugin, 'plugin_sanity_check')) {
404             $insane[$plugin] = $result;
405         }
406     }
407     if (empty($insane)) {
408         return array();
409     }
410     list($where, $params) = $DB->get_in_or_equal(array_keys($insane));
411     $where = ' plugin ' . $where;
412     $DB->set_field_select('portfolio_instance', 'visible', 0, $where, $params);
413     return $insane;
416 /**
417 * helper function to check all the instances for sanity and set any insane ones to invisible.
419 * @param array $instances to check (if null, defaults to all)
420 *              one instance or id will work too
422 * @return array array of insane instances (keys= id, values = reasons (keys for plugin lang)
423 */
424 function portfolio_instance_sanity_check($instances=null) {
425     global $DB;
426     if (empty($instances)) {
427         $instances = portfolio_instances(false);
428     } else if (!is_array($instances)) {
429         $instances = array($instances);
430     }
432     $insane = array();
433     foreach ($instances as $instance) {
434         if (is_object($instance) && !($instance instanceof portfolio_plugin_base)) {
435             $instance = portfolio_instance($instance->id, $instance);
436         } else if (is_numeric($instance)) {
437             $instance = portfolio_instance($instance);
438         }
439         if (!($instance instanceof portfolio_plugin_base)) {
440             debugging('something weird passed to portfolio_instance_sanity_check, not subclass or id');
441             continue;
442         }
443         if ($result = $instance->instance_sanity_check()) {
444             $insane[$instance->get('id')] = $result;
445         }
446     }
447     if (empty($insane)) {
448         return array();
449     }
450     list ($where, $params) = $DB->get_in_or_equal(array_keys($insane));
451     $where = ' id ' . $where;
452     $DB->set_field_select('portfolio_instance', 'visible', 0, $where, $params);
453     return $insane;
456 /**
457 * helper function to display a table of plugins (or instances) and reasons for disabling
459 * @param array $insane array of insane plugins (key = plugin (or instance id), value = reason)
460 * @param array $instances if reporting instances rather than whole plugins, pass the array (key = id, value = object) here
462 */
463 function portfolio_report_insane($insane, $instances=false, $return=false) {
464     if (empty($insane)) {
465         return;
466     }
468     static $pluginstr;
469     if (empty($pluginstr)) {
470         $pluginstr = get_string('plugin', 'portfolio');
471     }
472     if ($instances) {
473         $headerstr = get_string('someinstancesdisabled', 'portfolio');
474     } else {
475         $headerstr = get_string('somepluginsdisabled', 'portfolio');
476     }
478     $output = notify($headerstr, 'notifyproblem', 'center', true);
479     $table = new StdClass;
480     $table->head = array($pluginstr, '');
481     $table->data = array();
482     foreach ($insane as $plugin => $reason) {
483         if ($instances) {
484             // @todo this isn't working
485             // because it seems the new recordset object
486             // doesn't implement the key correctly.
487             // see MDL-15798
488             $instance = $instances[$plugin];
489             $plugin   = $instance->get('plugin');
490             $name     = $instance->get('name');
491         } else {
492             $name = $plugin;
493         }
494         $table->data[] = array($name, get_string($reason, 'portfolio_' . $plugin));
495     }
496     $output .= print_table($table, true);
497     $output .= '<br /><br /><br />';
499     if ($return) {
500         return $output;
501     }
502     echo $output;
505 /**
506 * fake the url to portfolio/add.php from data from somewhere else
507 * you should use portfolio_add_button instead 99% of the time
509 * @param int $instanceid instanceid (optional, will force a new screen if not specified)
510 * @param string $classname callback classname
511 * @param string $classfile file containing the callback class definition
512 * @param array $callbackargs arguments to pass to the callback class
513 */
514 function portfolio_fake_add_url($instanceid, $classname, $classfile, $callbackargs) {
515     global $CFG;
516     $url = $CFG->wwwroot . '/portfolio/add.php?instance=' . $instanceid . '&amp;callbackclass=' . $classname . '&amp;callbackfile=' . $classfile;
518     if (is_object($callbackargs)) {
519         $callbackargs = (array)$callbackargs;
520     }
521     if (!is_array($callbackargs) || empty($callbackargs)) {
522         return $url;
523     }
524     foreach ($callbackargs as $key => $value) {
525         $url .= '&amp;ca_' . $key . '=' . urlencode($value);
526     }
527     return $url;
530 /**
531 * base class for the caller
532 * places in moodle that want to display
533 * the add form should subclass this for their callback.
534 */
535 abstract class portfolio_caller_base {
537     /**
538     * stdclass object
539     * course that was active during the caller
540     */
541     protected $course;
543     /**
544     * named array of export config
545     * use{@link  set_export_config} and {@link get_export_config} to access
546     */
547     protected $exportconfig;
549     /**
550     * stdclass object
551     * user currently exporting content
552     */
553     protected $user;
555     /**
556     * a reference to the exporter object
557     */
558     protected $exporter;
560     /**
561     *
562     */
563     private $stage;
565     /**
566     * if this caller wants any additional config items
567     * they should be defined here.
568     *
569     * @param array $mform moodleform object (passed by reference) to add elements to
570     * @param object $instance subclass of portfolio_plugin_base
571     * @param integer $userid id of user exporting content
572     */
573     public function export_config_form(&$mform, $instance) {}
576     /**
577     * whether this caller wants any additional
578     * config during export (eg options or metadata)
579     *
580     * @return boolean
581     */
582     public function has_export_config() {
583         return false;
584     }
586     /**
587     * just like the moodle form validation function
588     * this is passed in the data array from the form
589     * and if a non empty array is returned, form processing will stop.
590     *
591     * @param array $data data from form.
592     * @return array keyvalue pairs - form element => error string
593     */
594     public function export_config_validation($data) {}
596     /**
597     * how long does this reasonably expect to take..
598     * should we offer the user the option to wait..
599     * this is deliberately nonstatic so it can take filesize into account
600     * the portfolio plugin can override this.
601     * (so for exmaple even if a huge file is being sent,
602     * the download portfolio plugin doesn't care )
603     *
604     * @return string (see PORTFOLIO_TIME_* constants)
605     */
606     public abstract function expected_time();
608     /**
609     * used for displaying the navigation during the export screens.
610     *
611     * this function must be implemented, but can really return anything.
612     * an Exporting.. string will be added on the end.
613     * @return array of $extranav and $cm
614     *
615     * to pass to build_navigation
616     *
617     */
618     public abstract function get_navigation();
620     /**
621     *
622     */
623     public abstract function get_sha1();
625     /*
626     * generic getter for properties belonging to this instance
627     * <b>outside</b> the subclasses
628     * like name, visible etc.
629     *
630     * @todo  determine what to return in the error case
631     */
632     public function get($field) {
633         if (property_exists($this, $field)) {
634             return $this->{$field};
635         }
636         return false; // @todo throw exception?
637     }
639     /**
640     * generic setter for properties belonging to this instance
641     * <b>outside</b> the subclass
642     * like name, visible, etc.
643     *
644     * @todo  determine what to return in the error case
645     */
646     public final function set($field, &$value) {
647         if (property_exists($this, $field)) {
648             $this->{$field} =& $value;
649             $this->dirty = true;
650             return true;
651         }
652         return false; // @todo throw exception?
654     }
656     /**
657     * stores the config generated at export time.
658     * subclasses can retrieve values using
659     * {@link get_export_config}
660     *
661     * @param array $config formdata
662     */
663     public final function set_export_config($config) {
664         $allowed = array_merge(
665             array('wait', 'hidewait', 'format', 'hideformat'),
666             $this->get_allowed_export_config()
667         );
668         foreach ($config as $key => $value) {
669             if (!in_array($key, $allowed)) {
670                 continue; // @ todo throw exception
671             }
672             $this->exportconfig[$key] = $value;
673         }
674     }
676     /**
677     * returns a particular export config value.
678     * subclasses shouldn't need to override this
679     *
680     * @param string key the config item to fetch
681     * @todo figure out the error cases (item not found or not allowed)
682     */
683     public final function get_export_config($key) {
684         $allowed = array_merge(
685             array('wait', 'hidewait', 'format', 'hideformat'),
686             $this->get_allowed_export_config()
687         );
688         if (!in_array($key, $allowed)) {
689             return false; // @todo throw exception?
690         }
691         if (!array_key_exists($key, $this->exportconfig)) {
692             return null; // @todo what to return|
693         }
694         return $this->exportconfig[$key];
695     }
699     /**
700     * Similar to the other allowed_config functions
701     * if you need export config, you must provide
702     * a list of what the fields are.
703     *
704     * even if you want to store stuff during export
705     * without displaying a form to the user,
706     * you can use this.
707     *
708     * @return array array of allowed keys
709     */
710     public function get_allowed_export_config() {
711         return array();
712     }
714     /**
715     * after the user submits their config
716     * they're given a confirm screen
717     * summarising what they've chosen.
718     *
719     * this function should return a table of nice strings => values
720     * of what they've chosen
721     * to be displayed in a table.
722     *
723     * @return array array of config items.
724     */
725     public function get_export_summary() {
726         return false;
727     }
729     /**
730     * called before the portfolio plugin gets control
731     * this function should copy all the files it wants to
732     * the temporary directory, using {@see copy_existing_file}
733     * or {@see write_new_file}
734     */
735     public abstract function prepare_package();
737     /**
738     * array of formats this caller supports
739     * the intersection of what this function returns
740     * and what the selected portfolio plugin supports
741     * will be used
742     * use the constants PORTFOLIO_FORMAT_*
743     *
744     * @return array list of formats
745     */
746     public static function supported_formats() {
747         return array(PORTFOLIO_FORMAT_FILE);
748     }
750     /**
751     * this is the "return to where you were" url
752     *
753     * @return string url
754     */
755     public abstract function get_return_url();
757     /**
758     * callback to do whatever capability checks required
759     * in the caller (called during the export process
760     */
761     public abstract function check_permissions();
763     /**
764     * nice name to display to the user about this caller location
765     */
766     public abstract static function display_name();
769 abstract class portfolio_module_caller_base extends portfolio_caller_base {
771     protected $cm;
772     protected $course;
774     public function get_navigation() {
775         $extranav = array('name' => $this->cm->name, 'link' => $this->get_return_url());
776         return array($extranav, $this->cm);
777     }
779     public function get_return_url() {
780         global $CFG;
781         return $CFG->wwwroot . '/mod/' . $this->cm->modname . '/view.php?id=' . $this->cm->id;
782     }
784     public function get($key) {
785         if ($key != 'course') {
786             return parent::get($key);
787         }
788         global $DB;
789         if (empty($this->course)) {
790             $this->course = $DB->get_record('course', array('id' => $this->cm->course));
791         }
792         return $this->course;
793     }
796 /**
797 * the base class for portfolio plguins
798 * all plugins must subclass this.
799 */
800 abstract class portfolio_plugin_base {
802     /**
803     * boolean
804     * whether this object needs writing out to the database
805     */
806     protected $dirty;
808     /**
809     * integer
810     * id of instance
811     */
812     protected $id;
814     /**
815     * string
816     * name of instance
817     */
818     protected $name;
820     /**
821     * string
822     * plugin this instance belongs to
823     */
824     protected $plugin;
826     /**
827     * boolean
828     * whether this instance is visible or not
829     */
830     protected $visible;
832     /**
833     * named array
834     * admin configured config
835     * use {@link set_config} and {@get_config} to access
836     */
837     protected $config;
839     /**
840     *
841     * user config cache
842     * named array of named arrays
843     * keyed on userid and then on config field => value
844     * use {@link get_user_config} and {@link set_user_config} to access.
845     */
846     protected $userconfig;
848     /**
849     * named array
850     * export config during export
851     * use {@link get_export_config} and {@link set export_config} to access.
852     */
853     protected $exportconfig;
855     /**
856     * stdclass object
857     * user currently exporting data
858     */
859     protected $user;
861     /**
862     * a reference to the exporter object
863     */
864     protected $exporter;
866     /**
867     * array of formats this portfolio supports
868     * the intersection of what this function returns
869     * and what the caller supports will be used
870     * use the constants PORTFOLIO_FORMAT_*
871     *
872     * @return array list of formats
873     */
874     public static function supported_formats() {
875         return array(PORTFOLIO_FORMAT_FILE);
876     }
879     /**
880     * how long does this reasonably expect to take..
881     * should we offer the user the option to wait..
882     * this is deliberately nonstatic so it can take filesize into account
883     *
884     * @param string $callertime - what the caller thinks
885     *                             the portfolio plugin instance
886     *                             is given the final say
887     *                             because it might be (for example) download.
888     * @return string (see PORTFOLIO_TIME_* constants)
889     */
890     public abstract function expected_time($callertime);
892     /**
893     * is this plugin push or pill.
894     * if push, cleanup will be called directly after send_package
895     * if not, cleanup will be called after portfolio/file.php is requested
896     *
897     * @return boolean
898     */
899     public abstract function is_push();
901     /**
902     * check sanity of plugin
903     * if this function returns something non empty, ALL instances of your plugin
904     * will be set to invisble and not be able to be set back until it's fixed
905     *
906     * @return mixed - string = error string KEY (must be inside plugin_$yourplugin) or 0/false if you're ok
907     */
908     public static function plugin_sanity_check() {
909         return 0;
910     }
912     /**
913     * check sanity of instances
914     * if this function returns something non empty, the instance will be
915     * set to invislbe and not be able to be set back until it's fixed.
916     *
917     * @return mixed - string = error string KEY (must be inside plugin_$yourplugin) or 0/false if you're ok
918     */
919     public function instance_sanity_check() {
920         return 0;
921     }
923     /**
924     * does this plugin need any configuration by the administrator?
925     *
926     * if you override this to return true,
927     * you <b>must</b> implement {@link} admin_config_form
928     */
929     public static function has_admin_config() {
930         return false;
931     }
933     /**
934     * can this plugin be configured by the user in their profile?
935     *
936     * if you override this to return true,
937     * you <b>must</b> implement {@link user_config_form
938     */
939     public function has_user_config() {
940         return false;
941     }
943     /**
944     * does this plugin need configuration during export time?
945     *
946     * if you override this to return true,
947     * you <b>must</b> implement {@link export_config_form}
948     */
949     public function has_export_config() {
950         return false;
951     }
953     /**
954     * just like the moodle form validation function
955     * this is passed in the data array from the form
956     * and if a non empty array is returned, form processing will stop.
957     *
958     * @param array $data data from form.
959     * @return array keyvalue pairs - form element => error string
960     */
961     public function export_config_validation() {}
963     /**
964     * just like the moodle form validation function
965     * this is passed in the data array from the form
966     * and if a non empty array is returned, form processing will stop.
967     *
968     * @param array $data data from form.
969     * @return array keyvalue pairs - form element => error string
970     */
971     public function user_config_validation() {}
973     /**
974     * sets the export time config from the moodle form.
975     * you can also use this to set export config that
976     * isn't actually controlled by the user
977     * eg things that your subclasses want to keep in state
978     * across the export.
979     * keys must be in {@link get_allowed_export_config}
980     *
981     * this is deliberately not final (see boxnet plugin)
982     *
983     * @param array $config named array of config items to set.
984     */
985     public function set_export_config($config) {
986         $allowed = array_merge(
987             array('wait', 'hidewait', 'format', 'hideformat'),
988             $this->get_allowed_export_config()
989         );
990         foreach ($config as $key => $value) {
991             if (!in_array($key, $allowed)) {
992                 continue; // @ todo throw exception
993             }
994             $this->exportconfig[$key] = $value;
995         }
996     }
998     /**
999     * gets an export time config value.
1000     * subclasses should not override this.
1001     *
1002     * @param string key field to fetch
1003     *
1004     * @return string config value
1005     *
1006     * @todo figure out the error cases
1007     */
1008     public final function get_export_config($key) {
1009         $allowed = array_merge(
1010             array('hidewait', 'wait', 'format', 'hideformat'),
1011             $this->get_allowed_export_config()
1012         );
1013         if (!in_array($key, $allowed)) {
1014             return false; // @todo throw exception?
1015         }
1016         if (!array_key_exists($key, $this->exportconfig)) {
1017             return null; // @todo what to return|
1018         }
1019         return $this->exportconfig[$key];
1020     }
1022     /**
1023     * after the user submits their config
1024     * they're given a confirm screen
1025     * summarising what they've chosen.
1026     *
1027     * this function should return a table of nice strings => values
1028     * of what they've chosen
1029     * to be displayed in a table.
1030     *
1031     * @return array array of config items.
1032     */
1033     public function get_export_summary() {
1034         return false;
1035     }
1037     /**
1038     * called after the caller has finished having control
1039     * of its prepare_package function.
1040     * this function should read all the files from the portfolio
1041     * working file area and zip them and send them or whatever it wants.
1042     * {@see get_tempfiles} to get the list of files.
1043     *
1044     */
1045     public abstract function prepare_package();
1047     /**
1048     * this is the function that is responsible for sending
1049     * the package to the remote system,
1050     * or whatever request is necessary to initiate the transfer.
1051     *
1052     * @return boolean success
1053     */
1054     public abstract function send_package();
1057     /**
1058     * once everything is done and the user
1059     * has the finish page displayed to them
1060     * the base class takes care of printing them
1061     * "return to where you are" or "continue to portfolio" links
1062     * this function allows for exta finish options from the plugin
1063     *
1064     * @return array named array of links => titles
1065     */
1066     public function get_extra_finish_options() {
1067         return false;
1068     }
1070     /**
1071     * the url for the user to continue to their portfolio
1072     *
1073     * @return string url or false.
1074     */
1075     public abstract function get_continue_url();
1077     /**
1078     * mform to display to the user in their profile
1079     * if your plugin can't be configured by the user,
1080     * (see {@link has_user_config})
1081     * don't bother overriding this function
1082     *
1083     * @param moodleform $mform passed by reference, add elements to it
1084     */
1085     public function user_config_form(&$mform) {}
1087     /**
1088     * mform to display to the admin configuring the plugin.
1089     * if your plugin can't be configured by the admin,
1090     * (see {@link} has_admin_config)
1091     * don't bother overriding this function
1092     *
1093     * this function can be called statically or non statically,
1094     * depending on whether it's creating a new instance (statically),
1095     * or editing an existing one (non statically)
1096     *
1097     * @param moodleform $mform passed by reference, add elements to it.
1098     * @return mixed - if a string is returned, it means the plugin cannot create an instance
1099     *                 and the string is an error code
1100     */
1101     public function admin_config_form(&$mform) {}
1103     /**
1104     * just like the moodle form validation function
1105     * this is passed in the data array from the form
1106     * and if a non empty array is returned, form processing will stop.
1107     *
1108     * @param array $data data from form.
1109     * @return array keyvalue pairs - form element => error string
1110     */
1111     public function admin_config_validation($data) {}
1112     /**
1113     * mform to display to the user exporting data using this plugin.
1114     * if your plugin doesn't need user input at this time,
1115     * (see {@link has_export_config}
1116     * don't bother overrideing this function
1117     *
1118     * @param moodleform $mform passed by reference, add elements to it.
1119     */
1120     public function export_config_form(&$mform) {}
1122     /**
1123     * override this if your plugin doesn't allow multiple instances
1124     *
1125     * @return boolean
1126     */
1127     public static function allows_multiple() {
1128         return true;
1129     }
1131     /**
1132     *
1133     * If at any point the caller wants to steal control
1134     * it can, by returning something that isn't false
1135     * in this function
1136     * The controller will redirect to whatever url
1137     * this function returns.
1138     * Afterwards, you can redirect back to portfolio/add.php?postcontrol=1
1139     * and {@link post_control} is called before the rest of the processing
1140     * for the stage is done
1141     *
1142     * @param int stage to steal control *before* (see constants PARAM_STAGE_*}
1143     *
1144     * @return boolean or string url
1145     */
1146     public function steal_control($stage) {
1147         return false;
1148     }
1150     /**
1151     * after a plugin has elected to steal control,
1152     * and control returns to portfolio/add.php|postcontrol=1,
1153     * this function is called, and passed the stage that was stolen control from
1154     * and the request (get and post but not cookie) parameters
1155     * this is useful for external systems that need to redirect the user back
1156     * with some extra data in the url (like auth tokens etc)
1157     * for an example implementation, see boxnet portfolio plugin.
1158     *
1159     * @param int $stage the stage before control was stolen
1160     * @param array $params a merge of $_GET and $_POST
1161     *
1162     */
1164     public function post_control($stage, $params) { }
1166     /**
1167     * this function creates a new instance of a plugin
1168     * saves it in the database, saves the config
1169     * and returns it.
1170     * you shouldn't need to override it
1171     * unless you're doing something really funky
1172     *
1173     * @return object subclass of portfolio_plugin_base
1174     */
1175     public static function create_instance($plugin, $name, $config) {
1176         global $DB, $CFG;
1177         $new = (object)array(
1178             'plugin' => $plugin,
1179             'name'   => $name,
1180         );
1181         $newid = $DB->insert_record('portfolio_instance', $new);
1182         require_once($CFG->dirroot . '/portfolio/type/' . $plugin . '/lib.php');
1183         $classname = 'portfolio_plugin_'  . $plugin;
1184         $obj = new $classname($newid);
1185         $obj->set_config($config);
1186         return $obj;
1187     }
1189     /**
1190     * construct a plugin instance
1191     * subclasses should not need  to override this unless they're doing something special
1192     * and should call parent::__construct afterwards
1193     *
1194     * @param int $instanceid id of plugin instance to construct
1195     * @param mixed $record stdclass object or named array - use this is you already have the record to avoid another query
1196     *
1197     * @return object subclass of portfolio_plugin_base
1198     */
1199     public function __construct($instanceid, $record=null) {
1200         global $DB;
1201         if (!$record) {
1202             if (!$record = $DB->get_record('portfolio_instance', array('id' => $instanceid))) {
1203                 return false; // @todo throw exception?
1204             }
1205         }
1206         foreach ((array)$record as $key =>$value) {
1207             if (property_exists($this, $key)) {
1208                 $this->{$key} = $value;
1209             }
1210         }
1211         $this->config = new StdClass;
1212         $this->userconfig = array();
1213         $this->exportconfig = array();
1214         foreach ($DB->get_records('portfolio_instance_config', array('instance' => $instanceid)) as $config) {
1215             $this->config->{$config->name} = $config->value;
1216         }
1217         return $this;
1218     }
1220     /**
1221     * a list of fields that can be configured per instance.
1222     * this is used for the save handlers of the config form
1223     * and as checks in set_config and get_config
1224     *
1225     * @return array array of strings (config item names)
1226     */
1227     public static function get_allowed_config() {
1228         return array();
1229     }
1231     /**
1232     * a list of fields that can be configured by the user.
1233     * this is used for the save handlers in the config form
1234     * and as checks in set_user_config and get_user_config.
1235     *
1236     * @return array array of strings (config field names)
1237     */
1238     public function get_allowed_user_config() {
1239         return array();
1240     }
1242     /**
1243     * a list of fields that can be configured by the user.
1244     * this is used for the save handlers in the config form
1245     * and as checks in set_export_config and get_export_config.
1246     *
1247     * @return array array of strings (config field names)
1248     */
1249     public function get_allowed_export_config() {
1250         return array();
1251     }
1253     /**
1254     * saves (or updates) the config stored in portfolio_instance_config.
1255     * you shouldn't need to override this unless you're doing something funky.
1256     *
1257     * @param array $config array of config items.
1258     */
1259     public final function set_config($config) {
1260         global $DB;
1261         foreach ($config as $key => $value) {
1262             // try set it in $this first
1263             if ($this->set($key, $value)) {
1264                 continue;
1265             }
1266             if (!in_array($key, $this->get_allowed_config())) {
1267                 continue; // @todo throw exception?
1268             }
1269             if (!isset($this->config->{$key})) {
1270                 $DB->insert_record('portfolio_instance_config', (object)array(
1271                     'instance' => $this->id,
1272                     'name' => $key,
1273                     'value' => $value,
1274                 ));
1275             } else if ($this->config->{$key} != $value) {
1276                 $DB->set_field('portfolio_instance_config', 'value', $value, array('name' => $key, 'instance' => $this->id));
1277             }
1278             $this->config->{$key} = $value;
1279         }
1280         return true; // @todo - if we're going to change here to throw exceptions, this can change
1281     }
1283     /**
1284     * gets the value of a particular config item
1285     *
1286     * @param string $key key to fetch
1287     *
1288     * @return string the corresponding value
1289     *
1290     * @todo determine what to return in the error case.
1291     */
1292     public final function get_config($key) {
1293         if (!in_array($key, $this->get_allowed_config())) {
1294             return false; // @todo throw exception?
1295         }
1296         if (isset($this->config->{$key})) {
1297             return $this->config->{$key};
1298         }
1299         return false; // @todo null?
1300     }
1302     /**
1303     * get the value of a config item for a particular user
1304     *
1305     * @param string $key key to fetch
1306     * @param integer $userid id of user (defaults to current)
1307     *
1308     * @return string the corresponding value
1309     *
1310     * @todo determine what to return in the error case
1311     */
1312     public final function get_user_config($key, $userid=0) {
1313         global $DB;
1315         if (empty($userid)) {
1316             $userid = $this->user->id;
1317         }
1319         if ($key != 'visible') { // handled by the parent class
1320             if (!in_array($key, $this->get_allowed_user_config())) {
1321                 return false; // @todo throw exception?
1322             }
1323         }
1324         if (!array_key_exists($userid, $this->userconfig)) {
1325             $this->userconfig[$userid] = (object)array_fill_keys(array_merge(array('visible'), $this->get_allowed_user_config()), null);
1326             foreach ($DB->get_records('portfolio_instance_user', array('instance' => $this->id, 'userid' => $userid)) as $config) {
1327                 $this->userconfig[$userid]->{$config->name} = $config->value;
1328             }
1329         }
1330         if ($this->userconfig[$userid]->visible === null) {
1331             $this->set_user_config(array('visible' => 1), $userid);
1332         }
1333         return $this->userconfig[$userid]->{$key};
1335     }
1337     /**
1338     *
1339     * sets config options for a given user
1340     *
1341     * @param mixed $config array or stdclass containing key/value pairs to set
1342     * @param integer $userid userid to set config for (defaults to current)
1343     *
1344     * @todo determine what to return in the error case
1345     */
1346     public final function set_user_config($config, $userid=0) {
1347         global $DB;
1349         if (empty($userid)) {
1350             $userid = $this->user->id;
1351         }
1353         foreach ($config as $key => $value) {
1354             if ($key != 'visible' && !in_array($key, $this->get_allowed_user_config())) {
1355                 continue; // @todo throw exception?
1356             }
1357             if (!$existing = $DB->get_record('portfolio_instance_user', array('instance'=> $this->id, 'userid' => $userid, 'name' => $key))) {
1358                 $DB->insert_record('portfolio_instance_user', (object)array(
1359                     'instance' => $this->id,
1360                     'name' => $key,
1361                     'value' => $value,
1362                     'userid' => $userid,
1363                 ));
1364             } else if ($existing->value != $value) {
1365                 $DB->set_field('portfolio_instance_user', 'value', $value, array('name' => $key, 'instance' => $this->id, 'userid' => $userid));
1366             }
1367             $this->userconfig[$userid]->{$key} = $value;
1368         }
1369         return true; // @todo
1371     }
1373     /**
1374     * generic getter for properties belonging to this instance
1375     * <b>outside</b> the subclasses
1376     * like name, visible etc.
1377     *
1378     * @todo  determine what to return in the error case
1379     */
1380     public final function get($field) {
1381         if (property_exists($this, $field)) {
1382             return $this->{$field};
1383         }
1384         return false; // @todo throw exception?
1385     }
1387     /**
1388     * generic setter for properties belonging to this instance
1389     * <b>outside</b> the subclass
1390     * like name, visible, etc.
1391     *
1392     * @todo  determine what to return in the error case
1393     */
1394     public final function set($field, $value) {
1395         if (property_exists($this, $field)) {
1396             $this->{$field} =& $value;
1397             $this->dirty = true;
1398             return true;
1399         }
1400         return false; // @todo throw exception?
1402     }
1404     /**
1405     * saves stuff that's been stored in the object to the database
1406     * you shouldn't need to override this
1407     * unless you're doing something really funky.
1408     * and if so, call parent::save when you're done.
1409     */
1410     public function save() {
1411         global $DB;
1412         if (!$this->dirty) {
1413             return true;
1414         }
1415         $fordb = new StdClass();
1416         foreach (array('id', 'name', 'plugin', 'visible') as $field) {
1417             $fordb->{$field} = $this->{$field};
1418         }
1419         $DB->update_record('portfolio_instance', $fordb);
1420         $this->dirty = false;
1421         return true;
1422     }
1424     /**
1425     * deletes everything from the database about this plugin instance.
1426     * you shouldn't need to override this unless you're storing stuff
1427     * in your own tables.  and if so, call parent::delete when you're done.
1428     */
1429     public function delete() {
1430         global $DB;
1431         $DB->delete_records('portfolio_instance_config', array('instance' => $this->get('id')));
1432         $DB->delete_records('portfolio_instance_user', array('instance' => $this->get('id')));
1433         $DB->delete_records('portfolio_instance', array('id' => $this->get('id')));
1434         $this->dirty = false;
1435         return true;
1436     }
1439 /**
1440 * class to inherit from for 'push' type plugins
1441 */
1442 abstract class portfolio_plugin_push_base extends portfolio_plugin_base {
1444     public function is_push() {
1445         return true;
1446     }
1449 /**
1450 * class to inherit from for 'pull' type plugins
1451 */
1452 abstract class portfolio_plugin_pull_base extends portfolio_plugin_base {
1454     private $file;
1456     public function is_push() {
1457         return false;
1458     }
1461     /**
1462     * before sending the file when the pull is requested, verify the request parameters
1463     * these might include a token of some sort of whatever
1464     *
1465     * @param array request parameters (POST wins over GET)
1466     */
1467     public abstract function verify_file_request_params($params);
1471 /**
1472 * this is the form that is actually used while exporting.
1473 * plugins and callers don't get to define their own class
1474 * as we have to handle form elements from both places
1475 * see the docs for portfolio_plugin_base and portfolio_caller for more information
1476 */
1477 final class portfolio_export_form extends moodleform {
1479     public function definition() {
1481         $mform =& $this->_form;
1482         $mform->addElement('hidden', 'stage', PORTFOLIO_STAGE_CONFIG);
1483         $mform->addElement('hidden', 'instance', $this->_customdata['instance']->get('id'));
1485         if (array_key_exists('formats', $this->_customdata) && is_array($this->_customdata['formats'])) {
1486             if (count($this->_customdata['formats']) > 1) {
1487                 $options = array();
1488                 foreach ($this->_customdata['formats'] as $key) {
1489                     $options[$key] = get_string('format_' . $key, 'portfolio');
1490                 }
1491                 $mform->addElement('select', 'format', get_string('availableformats', 'portfolio'), $options);
1492             } else {
1493                 $f = array_shift($this->_customdata['formats']);
1494                 $mform->addElement('hidden', 'format', $f);
1495             }
1496         }
1498         if (array_key_exists('expectedtime', $this->_customdata) && $this->_customdata['expectedtime'] != PORTFOLIO_TIME_LOW) {
1499             //$mform->addElement('select', 'wait', get_string('waitlevel_' . $this->_customdata['expectedtime'], 'portfolio'), $options);
1502             $radioarray = array();
1503             $radioarray[] = &MoodleQuickForm::createElement('radio', 'wait', '', get_string('wait', 'portfolio'), 1);
1504             $radioarray[] = &MoodleQuickForm::createElement('radio', 'wait', '', get_string('dontwait', 'portfolio'),  0);
1505             $mform->addGroup($radioarray, 'radioar', get_string('wanttowait_' . $this->_customdata['expectedtime'], 'portfolio') , array(' '), false);
1507             $mform->setDefault('wait', 0);
1508         }
1509         else {
1510             $mform->addElement('hidden', 'wait', 1);
1511         }
1513         if (array_key_exists('plugin', $this->_customdata) && is_object($this->_customdata['plugin'])) {
1514             $this->_customdata['plugin']->export_config_form($mform, $this->_customdata['userid']);
1515         }
1517         if (array_key_exists('caller', $this->_customdata) && is_object($this->_customdata['caller'])) {
1518             $this->_customdata['caller']->export_config_form($mform, $this->_customdata['instance'], $this->_customdata['userid']);
1519         }
1521         $this->add_action_buttons(true, get_string('next'));
1522     }
1524     public function validation($data) {
1526         $errors = array();
1528         if (array_key_exists('plugin', $this->_customdata) && is_object($this->_customdata['plugin'])) {
1529             $pluginerrors = $this->_customdata['plugin']->export_config_validation($data);
1530             if (is_array($pluginerrors)) {
1531                 $errors = $pluginerrors;
1532             }
1533         }
1534         if (array_key_exists('caller', $this->_customdata) && is_object($this->_customdata['caller'])) {
1535             $callererrors = $this->_customdata['caller']->export_config_validation($data);
1536             if (is_array($callererrors)) {
1537                 $errors = array_merge($errors, $callererrors);
1538             }
1539         }
1540         return $errors;
1541     }
1544 /**
1545 * this form is extendable by plugins
1546 * who want the admin to be able to configure
1547 * more than just the name of the instance.
1548 * this is NOT done by subclassing this class,
1549 * see the docs for portfolio_plugin_base for more information
1550 */
1551 final class portfolio_admin_form extends moodleform {
1553     protected $instance;
1554     protected $plugin;
1556     public function definition() {
1557         global $CFG;
1558         $this->plugin = $this->_customdata['plugin'];
1559         $this->instance = (isset($this->_customdata['instance'])
1560                 && is_subclass_of($this->_customdata['instance'], 'portfolio_plugin_base'))
1561             ? $this->_customdata['instance'] : null;
1563         $mform =& $this->_form;
1564         $strrequired = get_string('required');
1566         $mform->addElement('hidden', 'edit',  ($this->instance) ? $this->instance->get('id') : 0);
1567         $mform->addElement('hidden', 'new',   $this->plugin);
1568         $mform->addElement('hidden', 'plugin', $this->plugin);
1570         $mform->addElement('text', 'name', get_string('name'), 'maxlength="100" size="30"');
1571         $mform->addRule('name', $strrequired, 'required', null, 'client');
1573         // let the plugin add the fields they want (either statically or not)
1574         if (portfolio_static_function($this->plugin, 'has_admin_config')) {
1575             if (!$this->instance) {
1576                 $result = portfolio_static_function($this->plugin, 'admin_config_form', $mform);
1577             } else {
1578                 $result = $this->instance->admin_config_form($mform);
1579             }
1580         }
1582         if (isset($result) && is_string($result)) { // something went wrong, stop
1583             return $this->raise_error($result, 'portfolio_' . $this->plugin, $CFG->wwwroot . '/' . $CFG->admin . '/portfolio.php');
1584         }
1586         // and set the data if we have some.
1587         if ($this->instance) {
1588             $data = array('name' => $this->instance->get('name'));
1589             foreach ($this->instance->get_allowed_config() as $config) {
1590                 $data[$config] = $this->instance->get_config($config);
1591             }
1592             $this->set_data($data);
1593         }
1594         $this->add_action_buttons(true, get_string('save', 'portfolio'));
1595     }
1597     public function validation($data) {
1598         global $DB;
1600         $errors = array();
1601         if ($DB->count_records('portfolio_instance', array('name' => $data['name'], 'plugin' => $data['plugin'])) > 1) {
1602             $errors = array('name' => get_string('err_uniquename', 'portfolio'));
1603         }
1605         $pluginerrors = array();
1606         if ($this->instance) {
1607             $pluginerrors = $this->instance->admin_config_validation($data);
1608         }
1609         else {
1610             $pluginerrors = portfolio_static_function($this->plugin, 'admin_config_validation', $data);
1611         }
1612         if (is_array($pluginerrors)) {
1613             $errors = array_merge($errors, $pluginerrors);
1614         }
1615         return $errors;
1616     }
1619 /**
1620 * this is the form for letting the user configure an instance of a plugin.
1621 * in order to extend this, you don't subclass this in the plugin..
1622 * see the docs in portfolio_plugin_base for more information
1623 */
1624 final class portfolio_user_form extends moodleform {
1626     protected $instance;
1627     protected $userid;
1629     public function definition() {
1630         $this->instance = $this->_customdata['instance'];
1631         $this->userid = $this->_customdata['userid'];
1633         $this->_form->addElement('hidden', 'config', $this->instance->get('id'));
1635         $this->instance->user_config_form($this->_form, $this->userid);
1637         $data = array();
1638         foreach ($this->instance->get_allowed_user_config() as $config) {
1639             $data[$config] = $this->instance->get_user_config($config, $this->userid);
1640         }
1641         $this->set_data($data);
1642         $this->add_action_buttons(true, get_string('save', 'portfolio'));
1643     }
1645     public function validation($data) {
1647         $errors = $this->instance->user_config_validation($data);
1649     }
1652 /**
1654 * Class that handles the various stages of the actual export
1655 */
1656 final class portfolio_exporter {
1658     private $currentstage;
1659     private $caller;
1660     private $instance;
1661     private $noconfig;
1662     private $navigation;
1663     private $user;
1665     public $instancefile;
1666     public $callerfile;
1668     /**
1669     * id of this export
1670     * matches record in portfolio_tempdata table
1671     * and used for itemid for file storage.
1672     */
1673     private $id;
1675     /**
1676     * construct a new exporter for use
1677     *
1678     * @param portfolio_plugin_base subclass $instance portfolio instance (passed by reference)
1679     * @param portfolio_caller_base subclass $caller portfolio caller (passed by reference)
1680     * @param string $navigation result of build_navigation (passed to print_header)
1681     */
1682     public function __construct(&$instance, &$caller, $callerfile, $navigation) {
1683         $this->instance =& $instance;
1684         $this->caller =& $caller;
1685         if ($instance) {
1686             $this->instancefile = 'portfolio/type/' . $instance->get('plugin') . '/lib.php';
1687             $this->instance->set('exporter', $this);
1688         }
1689         $this->callerfile = $callerfile;
1690         $this->stage = PORTFOLIO_STAGE_CONFIG;
1691         $this->navigation = $navigation;
1692         $this->caller->set('exporter', $this);
1693     }
1695     /*
1696     * generic getter for properties belonging to this instance
1697     * <b>outside</b> the subclasses
1698     * like name, visible etc.
1699     *
1700     * @todo  determine what to return in the error case
1701     */
1702     public function get($field) {
1703         if (property_exists($this, $field)) {
1704             return $this->{$field};
1705         }
1706         return false; // @todo throw exception?
1707     }
1709     /**
1710     * generic setter for properties belonging to this instance
1711     * <b>outside</b> the subclass
1712     * like name, visible, etc.
1713     *
1714     * @todo  determine what to return in the error case
1715     */
1717     public function set($field, &$value) {
1718         if (property_exists($this, $field)) {
1719             $this->{$field} =& $value;
1720             if ($field == 'instance') {
1721                 $this->instancefile = 'portfolio/type/' . $this->instance->get('plugin') . '/lib.php';
1722                 $this->instance->set('exporter', $this);
1723             }
1724             $this->dirty = true;
1725             return true;
1726         }
1727         return false; // @todo throw exception?
1729     }
1730     /**
1731     * process the given stage calling whatever functions are necessary
1732     *
1733     * @param int $stage (see PORTFOLIO_STAGE_* constants)
1734     * @param boolean $alreadystolen used to avoid letting plugins steal control twice.
1735     *
1736     * @return boolean whether or not to process the next stage. this is important as the function is called recursively.
1737     */
1738     public function process_stage($stage, $alreadystolen=false) {
1739         if (!$alreadystolen && $url = $this->instance->steal_control($stage)) {
1740             $this->set('stage', $stage);
1741             $this->save();
1742             redirect($url);
1743             break;
1744         }
1746         $waiting = $this->instance->get_export_config('wait');
1747         if ($stage > PORTFOLIO_STAGE_QUEUEORWAIT && empty($waiting)) {
1748             $stage = PORTFOLIO_STAGE_FINISHED;
1749         }
1750         $functionmap = array(
1751             PORTFOLIO_STAGE_CONFIG        => 'config',
1752             PORTFOLIO_STAGE_CONFIRM       => 'confirm',
1753             PORTFOLIO_STAGE_QUEUEORWAIT   => 'queueorwait',
1754             PORTFOLIO_STAGE_PACKAGE       => 'package',
1755             PORTFOLIO_STAGE_CLEANUP       => 'cleanup',
1756             PORTFOLIO_STAGE_SEND          => 'send',
1757             PORTFOLIO_STAGE_FINISHED      => 'finished'
1758         );
1760         $function = 'process_stage_' . $functionmap[$stage];
1761         if ($this->$function()) {
1762             // if we get through here it means control was returned
1763             // as opposed to wanting to stop processing
1764             // eg to wait for user input.
1765             $stage++;
1766             return $this->process_stage($stage);
1767         }
1768         $this->save();
1769         return false;
1770     }
1772     /**
1773     * helper function to return the portfolio instance
1774     *
1775     * @return  portfolio_plugin_base subclass
1776     */
1777     public function instance() {
1778         return $this->instance;
1779     }
1781     /**
1782     * helper function to return the caller object
1783     *
1784     * @return portfolio_caller_base subclass
1785     */
1786     public function caller() {
1787         return $this->caller;
1788     }
1790     /**
1791     * processes the 'config' stage of the export
1792     *
1793     * @return boolean whether or not to process the next stage. this is important as the control function is called recursively.
1794     */
1795     public function process_stage_config() {
1797         $pluginobj = $callerobj = null;
1798         if ($this->instance->has_export_config()) {
1799             $pluginobj = $this->instance;
1800         }
1801         if ($this->caller->has_export_config()) {
1802             $callerobj = $this->caller;
1803         }
1804         $formats = array_intersect($this->instance->supported_formats(), $this->caller->supported_formats());
1805         $allsupported = portfolio_supported_formats();
1806         foreach ($formats as $key => $format) {
1807             if (!in_array($format, $allsupported)) {
1808                 debugging(get_string('invalidformat', 'portfolio', $format));
1809                 unset($formats[$key]);
1810             }
1811         }
1812         $expectedtime = $this->instance->expected_time($this->caller->expected_time());
1813         if (count($formats) == 0) {
1814             // something went wrong, we should not have gotten this far.
1815             return $this->raise_error('nocommonformats', 'portfolio', get_class($caller));
1816         }
1817         // even if neither plugin or caller wants any config, we have to let the user choose their format, and decide to wait.
1818         if ($pluginobj || $callerobj || count($formats) > 1 || $expectedtime != PORTFOLIO_TIME_LOW) {
1819             $customdata = array(
1820                 'instance' => $this->instance,
1821                 'plugin' => $pluginobj,
1822                 'caller' => $callerobj,
1823                 'userid' => $this->user->id,
1824                 'formats' => $formats,
1825                 'expectedtime' => $expectedtime,
1826             );
1827             $mform = new portfolio_export_form('', $customdata);
1828             if ($mform->is_cancelled()){
1829                 $this->cancel_request();
1830             } else if ($fromform = $mform->get_data()){
1831                 if (!confirm_sesskey()) {
1832                     return $this->raise_error('confirmsesskeybad', '', $caller->get_return_url());
1833                 }
1834                 $pluginbits = array();
1835                 $callerbits = array();
1836                 foreach ($fromform as $key => $value) {
1837                     if (strpos($key, 'plugin_') === 0) {
1838                         $pluginbits[substr($key, 7)]  = $value;
1839                     } else if (strpos($key, 'caller_') === 0) {
1840                         $callerbits[substr($key, 7)] = $value;
1841                     }
1842                 }
1843                 $callerbits['format'] = $pluginbits['format'] = $fromform->format;
1844                 $pluginbits['wait'] = $fromform->wait;
1845                 if ($expectedtime == PORTFOLIO_TIME_LOW) {
1846                     $pluginbits['wait'] = 1;
1847                     $pluginbits['hidewait'] = 1;
1848                 }
1849                 $callerbits['hideformat'] = $pluginbits['hideformat'] = (count($formats) == 1);
1850                 $this->caller->set_export_config($callerbits);
1851                 $this->instance->set_export_config($pluginbits);
1852                 return true;
1853             } else {
1854                 $this->print_header();
1855                 print_heading(get_string('configexport' ,'portfolio'));
1856                 print_simple_box_start();
1857                 $mform->display();
1858                 print_simple_box_end();
1859                 print_footer();
1860                 return false;;
1861             }
1862         } else {
1863             $this->noexportconfig = true;
1864             $format = array_shift($formats);
1865             $this->instance->set_export_config(array('hidewait' => 1, 'wait' => 1, 'format' => $format, 'hideformat' => 1));
1866             $this->caller->set_export_config(array('format' => $format, 'hideformat' => 1));
1867             return true;
1868             // do not break - fall through to confirm
1869         }
1870     }
1872     /**
1873     * processes the 'confirm' stage of the export
1874     *
1875     * @return boolean whether or not to process the next stage. this is important as the control function is called recursively.
1876     */
1877     public function process_stage_confirm() {
1878         global $CFG, $DB;
1880         $previous = $DB->get_records(
1881             'portfolio_log',
1882             array(
1883                 'userid'      => $this->user->id,
1884                 'portfolio'   => $this->instance->get('id'),
1885                 'caller_sha1' => $this->caller->get_sha1(),
1886             )
1887         );
1888         if (isset($this->noexportconfig) && empty($previous)) {
1889             return true;
1890         }
1891         $strconfirm = get_string('confirmexport', 'portfolio');
1892         $yesurl = $CFG->wwwroot . '/portfolio/add.php?stage=' . PORTFOLIO_STAGE_QUEUEORWAIT;
1893         $nourl  = $CFG->wwwroot . '/portfolio/add.php?cancel=1';
1894         $this->print_header();
1895         print_heading($strconfirm);
1896         print_simple_box_start();
1897         print_heading(get_string('confirmsummary', 'portfolio'), '', 4);
1898         $mainsummary = array();
1899         if (!$this->instance->get_export_config('hideformat')) {
1900             $mainsummary[get_string('selectedformat', 'portfolio')] = get_string('format_' . $this->instance->get_export_config('format'), 'portfolio');
1901         }
1902         if (!$this->instance->get_export_config('hidewait')) {
1903             $mainsummary[get_string('selectedwait', 'portfolio')] = get_string(($this->instance->get_export_config('wait') ? 'yes' : 'no'));
1904         }
1905         if ($previous) {
1906             $previousstr = '';
1907             foreach ($previous as $row) {
1908                 $previousstr .= userdate($row->time);
1909                 if ($row->caller_class != get_class($this->caller)) {
1910                     require_once($CFG->dirroot . '/' . $row->caller_file);
1911                     $previousstr .= ' (' . call_user_func(array($row->caller_class, 'display_name')) . ')';
1912                 }
1913                 $previousstr .= '<br />';
1914             }
1915             $mainsummary[get_string('exportedpreviously', 'portfolio')] = $previousstr;
1916         }
1917         if (!$csummary = $this->caller->get_export_summary()) {
1918             $csummary = array();
1919         }
1920         if (!$isummary = $this->instance->get_export_summary()) {
1921             $isummary = array();
1922         }
1923         $mainsummary = array_merge($mainsummary, $csummary, $isummary);
1924         $table = new StdClass;
1925         $table->data = array();
1926         foreach ($mainsummary as $string => $value) {
1927             $table->data[] = array($string, $value);
1928         }
1929         print_table($table);
1930         notice_yesno($strconfirm, $yesurl, $nourl);
1931         print_simple_box_end();
1932         print_footer();
1933         return false;
1934     }
1936     /**
1937     * processes the 'queueornext' stage of the export
1938     *
1939     * @return boolean whether or not to process the next stage. this is important as the control function is called recursively.
1940     */
1941     public function process_stage_queueorwait() {
1942         global $SESSION;
1943         $wait = $this->instance->get_export_config('wait');
1944         if (empty($wait)) {
1945             events_trigger('portfolio_send', $this->id);
1946             unset($SESSION->portfolioexport);
1947             return $this->process_stage_finished(true);
1948         }
1949         return true;
1950     }
1952     /**
1953     * processes the 'package' stage of the export
1954     *
1955     * @return boolean whether or not to process the next stage. this is important as the control function is called recursively.
1956     */
1957     public function process_stage_package() {
1958         // now we've agreed on a format,
1959         // the caller is given control to package it up however it wants
1960         // and then the portfolio plugin is given control to do whatever it wants.
1961         if (!$this->caller->prepare_package()) {
1962             return $this->raise_error('callercouldnotpackage', 'portfolio', $this->caller->get_return_url());
1963         }
1964         if (!$package = $this->instance->prepare_package()) {
1965             return $this->raise_error('plugincouldnotpackage', 'portfolio', $this->caller->get_return_url());
1966         }
1967         return true;
1968     }
1970     /**
1971     * processes the 'cleanup' stage of the export
1972     *
1973     * @return boolean whether or not to process the next stage. this is important as the control function is called recursively.
1974     */
1975     public function process_stage_cleanup($pullok=false) {
1976         global $CFG, $DB, $SESSION;
1978         if (!$pullok && !$this->get('instance')->is_push()) {
1979             unset($SESSION->portfolioexport);
1980             return true;
1981         }
1982         // @todo maybe add a hook in the plugin(s)
1983         $DB->delete_records('portfolio_tempdata', array('id' => $this->id));
1984         $fs = get_file_storage();
1985         $fs->delete_area_files(SYSCONTEXTID, 'portfolio_exporter', $this->id);
1986         unset($SESSION->portfolioexport);
1987         return true;
1988     }
1990     /**
1991     * processes the 'send' stage of the export
1992     *
1993     * @return boolean whether or not to process the next stage. this is important as the control function is called recursively.
1994     */
1995     public function process_stage_send() {
1996         // send the file
1997         if (!$this->instance->send_package()) {
1998             return $this->raise_error('failedtosendpackage', 'portfolio');
1999         }
2000         // log the transfer
2001         global $DB;
2002         $l = array(
2003             'userid'         => $this->user->id,
2004             'portfolio'      => $this->instance->get('id'),
2005             'caller_file'    => $this->callerfile,
2006             'caller_sha1'    => $this->caller->get_sha1(),
2007             'caller_class'   => get_class($this->caller),
2008             'time'           => time(),
2009         );
2010         $DB->insert_record('portfolio_log', $l);
2011         return true;
2012     }
2014     /**
2015     * processes the 'finish' stage of the export
2016     *
2017     * @return boolean whether or not to process the next stage. this is important as the control function is called recursively.
2018     */
2019     public function process_stage_finished($queued=false) {
2020         $returnurl = $this->caller->get_return_url();
2021         $continueurl = $this->instance->get_continue_url();
2022         $extras = $this->instance->get_extra_finish_options();
2024         $this->print_header();
2025         if ($queued) {
2026             print_heading(get_string('exportqueued', 'portfolio'));
2027         } else {
2028             print_heading(get_string('exportcomplete', 'portfolio'));
2029         }
2030         if ($returnurl) {
2031             echo '<a href="' . $returnurl . '">' . get_string('returntowhereyouwere', 'portfolio') . '</a><br />';
2032         }
2033         if ($continueurl) {
2034             echo '<a href="' . $continueurl . '">' . get_string('continuetoportfolio', 'portfolio') . '</a><br />';
2035         }
2036         if (is_array($extras)) {
2037             foreach ($extras as $link => $string) {
2038                 echo '<a href="' . $link . '">' . $string . '</a><br />';
2039             }
2040         }
2041         print_footer();
2042         return false;
2043     }
2046     /**
2047     * local print header function to be reused across the export
2048     *
2049     * @param string $titlestring key for a portfolio language string
2050     * @param string $headerstring key for a portfolio language string
2051     */
2052     public function print_header($titlestr='exporting', $headerstr='exporting') {
2053         $titlestr = get_string($titlestr, 'portfolio');
2054         $headerstr = get_string($headerstr, 'portfolio');
2056         print_header($titlestr, $headerstr, $this->navigation);
2057     }
2059     /**
2060     * error handler - decides whether we're running interactively or not
2061     * and behaves accordingly
2062     */
2063     public function raise_error($string, $module='moodle', $continue=null, $a=null) {
2064         if (defined('FULLME') && FULLME == 'cron') {
2065             debugging(get_string($string, $module));
2066             return false;
2067         }
2068         if (isset($this)) {
2069             $this->process_stage_cleanup(true);
2070         }
2071         print_error($string, $module, $continue, $a);
2072     }
2074     public function cancel_request() {
2075         if (!isset($this)) {
2076             return;
2077         }
2078         $this->process_stage_cleanup(true);
2079         redirect($this->caller->get_return_url());
2080         exit;
2081     }
2083     /**
2084     * writes out the contents of this object and all its data to the portfolio_tempdata table and sets the 'id' field.
2085     */
2086     public function save() {
2087         global $DB;
2088         if (empty($this->id)) {
2089             $r = (object)array(
2090                 'data' => base64_encode(serialize($this)),
2091                 'expirytime' => time() + (60*60*24),
2092             );
2093             $this->id = $DB->insert_record('portfolio_tempdata', $r);
2094             $this->save(); // call again so that id gets added to the save data.
2095         } else {
2096             $DB->set_field('portfolio_tempdata', 'data', base64_encode(serialize($this)), array('id' => $this->id));
2097         }
2098     }
2100     public static function rewaken_object($id) {
2101         global $DB, $CFG;
2102         if (!$data = $DB->get_record('portfolio_tempdata', array('id' => $id))) {
2103             portfolio_exporter::raise_error('invalidtempid', 'portfolio');
2104         }
2105         $exporter = unserialize(base64_decode($data->data));
2106         if ($exporter->instancefile) {
2107             require_once($CFG->dirroot . '/' . $exporter->instancefile);
2108         }
2109         require_once($CFG->dirroot . '/' . $exporter->callerfile);
2110         $exporter = unserialize(serialize($exporter));
2111         return $exporter;
2112     }
2114     /**
2115     * copies a file from somewhere else in moodle
2116     * to the portfolio temporary working directory
2117     * associated with this export
2118     *
2119     * @param $oldfile stored_file object
2120     */
2121     public function copy_existing_file($oldfile) {
2122         $fs = get_file_storage();
2123         $file_record = $this->new_file_record_base($oldfile->get_filename());
2124         try {
2125             return $fs->create_file_from_storedfile($file_record, $oldfile->get_id());
2126         } catch (file_exception $e) {
2127             return false;
2128         }
2129     }
2131     /**
2132     * writes out some content to a file in the
2133     * portfolio temporary working directory
2134     * associated with this export
2135     *
2136     * @param string $content content to write
2137     * @param string $name filename to use
2138     */
2139     public function write_new_file($content, $name) {
2140         $fs = get_file_storage();
2141         $file_record = $this->new_file_record_base($name);
2142         return $fs->create_file_from_string($file_record, $content);
2143     }
2145     /**
2146     * returns an arary of files in the temporary working directory
2147     * for this export
2148     * always use this instead of the files api directly
2149     *
2150     * @return arary
2151     */
2152     public function get_tempfiles() {
2153         $fs = get_file_storage();
2154         $files = $fs->get_area_files(SYSCONTEXTID, 'portfolio_exporter', $this->id, '', false);
2155         if (empty($files)) {
2156             return array();
2157         }
2158         $returnfiles = array();
2159         foreach ($files as $f) {
2160             $returnfiles[$f->get_filename()] = $f;
2161         }
2162         return $returnfiles;
2163     }
2165     /**
2166     * helper function to create the beginnings of a file_record object
2167     * to create a new file in the portfolio_temporary working directory
2168     * use {@see write_new_file} or {@see copy_existing_file} externally
2169     *
2170     * @param string $name filename of new record
2171     */
2172     private function new_file_record_base($name) {
2173         return (object)array(
2174             'contextid' => SYSCONTEXTID,
2175             'filearea' => 'portfolio_exporter',
2176             'itemid'   => $this->id,
2177             'filepath' => '/',
2178             'filename' => $name,
2179         );
2180     }
2184 class portfolio_instance_select extends moodleform {
2186     private $caller;
2188     function definition() {
2189         $this->caller = $this->_customdata['caller'];
2190         $options = portfolio_instance_select(
2191             portfolio_instances(),
2192             $this->caller->supported_formats(),
2193             get_class($this->caller),
2194             'instance',
2195             true,
2196             true
2197         );
2198         if (empty($options)) {
2199             portfolio_exporter::raise_error('noavailableplugins', 'portfolio');
2200         }
2201         $mform =& $this->_form;
2202         $mform->addElement('select', 'instance', get_string('selectplugin', 'portfolio'), $options);
2203         $this->add_action_buttons(true, get_string('next'));
2204     }
2207 /**
2208 * event handler for the portfolio_send event
2209 */
2210 function portfolio_handle_event($eventdata) {
2211     global $CFG;
2212     $exporter = portfolio_exporter::rewaken_object($eventdata);
2213     $exporter->process_stage_package();
2214     $exporter->process_stage_send();
2215     $exporter->process_stage_cleanup();
2216     return true;
2219 /**
2220 * main portfolio cronjob
2222 */
2223 function portfolio_cron() {
2224     global $DB;
2226     if ($expired = $DB->get_records_select('portfolio_tempdata', 'expirytime < ?', array(time()))) {
2227         foreach ($expired as $d) {
2228             $DB->delete_records('portfolio_tempdata', array('id' => $d->id));
2229             $fs = get_file_storage();
2230             $fs->delete_area_files(SYSCONTEXTID, 'portfolio_exporter', $d->id);
2231         }
2232     }
2234     // @todo add hooks in the plugins
2238 ?>