1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * This file contains the base classes that are extended to create portfolio export functionality.
19 *
20 * For places in moodle that want to
21 * add export functionality to subclass from {@link http://docs.moodle.org/dev/Adding_a_Portfolio_Button_to_a_page}
22 *
23 * @package core_portfolio
24 * @copyright 2008 Penny Leach <penny@catalyst.net.nz>, Martin Dougiamas
25 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26 */
27
28defined('MOODLE_INTERNAL') || die();
29
30/**
31 * Base class for callers
32 *
33 * @link See http://docs.moodle.org/dev/Adding_a_Portfolio_Button_to_a_page
34 * @see also portfolio_module_caller_base
35 *
36 * @package core_portfolio
37 * @category portfolio
38 * @copyright 2008 Penny Leach <penny@catalyst.net.nz>
39 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40 */
41abstract class portfolio_caller_base {
42
43    /** @var stdClass course active during the call */
44    protected $course;
45
46    /** @var array configuration used for export. Use set_export_config and get_export_config to access */
47    protected $exportconfig = array();
48
49    /** @var stdclass user currently exporting content */
50    protected $user;
51
52    /** @var stdClass a reference to the exporter object */
53    protected $exporter;
54
55    /** @var array can be optionally overridden by subclass constructors */
56    protected $supportedformats;
57
58    /** @var stored_file single file exports configuration*/
59    protected $singlefile;
60
61    /** @var stored_file|object set this for multi file exports */
62    protected $multifiles;
63
64    /** @var string set this for generated-file exports */
65    protected $intendedmimetype;
66
67    /**
68     * Create portfolio_caller object
69     *
70     * @param array $callbackargs argument properties
71     */
72    public function __construct($callbackargs) {
73        $expected = call_user_func(array(get_class($this), 'expected_callbackargs'));
74        foreach ($expected as $key => $required) {
75            if (!array_key_exists($key, $callbackargs)) {
76                if ($required) {
77                    $a = (object)array('arg' => $key, 'class' => get_class($this));
78                    throw new portfolio_caller_exception('missingcallbackarg', 'portfolio', null, $a);
79                }
80                continue;
81            }
82            $this->{$key} = $callbackargs[$key];
83        }
84    }
85
86    /**
87     * If this caller wants any additional config items,
88     * they should be defined here.
89     *
90     * @param moodleform $mform passed by reference, add elements to it.
91     * @param portfolio_plugin_base $instance subclass of portfolio_plugin_base
92     */
93    public function export_config_form(&$mform, $instance) {}
94
95
96    /**
97     * Whether this caller wants any additional
98     * config during export (eg options or metadata)
99     *
100     * @return bool
101     */
102    public function has_export_config() {
103        return false;
104    }
105
106    /**
107     * Just like the moodle form validation function,
108     * this is passed in the data array from the form
109     * and if a non empty array is returned, form processing will stop.
110     *
111     * @param array $data data from form.
112     */
113    public function export_config_validation($data) {}
114
115    /**
116     * How long does this reasonably expect to take..
117     * Should we offer the user the option to wait..?
118     * This is deliberately nonstatic so it can take filesize into account
119     * the portfolio plugin can override this.
120     * (so for example even if a huge file is being sent,
121     * the download portfolio plugin doesn't care )
122     */
123    public abstract function expected_time();
124
125    /**
126     * Helper method to calculate expected time for multi or single file exports
127     *
128     * @return string file time expectation
129     */
130    public function expected_time_file() {
131        if ($this->multifiles) {
132            return portfolio_expected_time_file($this->multifiles);
133        }
134        else if ($this->singlefile) {
135            return portfolio_expected_time_file($this->singlefile);
136        }
137        return PORTFOLIO_TIME_LOW;
138    }
139
140    /**
141     * Function to build navigation
142     */
143    public abstract function get_navigation();
144
145    /**
146     * Helper function to get sha1
147     */
148    public abstract function get_sha1();
149
150    /**
151     * Helper function to calculate the sha1 for multi or single file exports
152     *
153     * @return string sha1 file exports
154     */
155    public function get_sha1_file() {
156        if (empty($this->singlefile) && empty($this->multifiles)) {
157            throw new portfolio_caller_exception('invalidsha1file', 'portfolio', $this->get_return_url());
158        }
159        if ($this->singlefile) {
160            return $this->singlefile->get_contenthash();
161        }
162        $sha1s = array();
163        foreach ($this->multifiles as $file) {
164            $sha1s[] = $file->get_contenthash();
165        }
166        asort($sha1s);
167        return sha1(implode('', $sha1s));
168    }
169
170    /**
171     * Generic getter for properties belonging to this instance
172     * <b>outside</b> the subclasses
173     * like name, visible etc.
174     *
175     * @param string $field property's name
176     * @return mixed
177     * @throws portfolio_export_exception
178     */
179    public function get($field) {
180        if (property_exists($this, $field)) {
181            return $this->{$field};
182        }
183        $a = (object)array('property' => $field, 'class' => get_class($this));
184        throw new portfolio_export_exception($this->get('exporter'), 'invalidproperty', 'portfolio', $this->get_return_url(), $a);
185    }
186
187    /**
188     * Generic setter for properties belonging to this instance
189     * <b>outside</b> the subclass
190     * like name, visible, etc.
191     *
192     * @param string $field property's name
193     * @param mixed $value property's value
194     * @return bool
195     * @throws moodle_exception
196     */
197    public final function set($field, &$value) {
198        if (property_exists($this, $field)) {
199            $this->{$field} =& $value;
200            $this->dirty = true;
201            return true;
202        }
203        $a = (object)array('property' => $field, 'class' => get_class($this));
204        throw new portfolio_export_exception($this->get('exporter'), 'invalidproperty', 'portfolio', $this->get_return_url(), $a);
205    }
206
207    /**
208     * Stores the config generated at export time.
209     * Subclasses can retrieve values using
210     * @see get_export_config
211     *
212     * @param array $config formdata
213     */
214    public final function set_export_config($config) {
215        $allowed = array_merge(
216            array('wait', 'hidewait', 'format', 'hideformat'),
217            $this->get_allowed_export_config()
218        );
219        foreach ($config as $key => $value) {
220            if (!in_array($key, $allowed)) {
221                $a = (object)array('property' => $key, 'class' => get_class($this));
222                throw new portfolio_export_exception($this->get('exporter'), 'invalidexportproperty', 'portfolio', $this->get_return_url(), $a);
223            }
224            $this->exportconfig[$key] = $value;
225        }
226    }
227
228    /**
229     * Returns a particular export config value.
230     * Subclasses shouldn't need to override this
231     *
232     * @param string $key the config item to fetch
233     * @return null|mixed of export configuration
234     */
235    public final function get_export_config($key) {
236        $allowed = array_merge(
237            array('wait', 'hidewait', 'format', 'hideformat'),
238            $this->get_allowed_export_config()
239        );
240        if (!in_array($key, $allowed)) {
241            $a = (object)array('property' => $key, 'class' => get_class($this));
242            throw new portfolio_export_exception($this->get('exporter'), 'invalidexportproperty', 'portfolio', $this->get_return_url(), $a);
243        }
244        if (!array_key_exists($key, $this->exportconfig)) {
245            return null;
246        }
247        return $this->exportconfig[$key];
248    }
249
250    /**
251     * Similar to the other allowed_config functions
252     * if you need export config, you must provide
253     * a list of what the fields are.
254     * Even if you want to store stuff during export
255     * without displaying a form to the user,
256     * you can use this.
257     *
258     * @return array array of allowed keys
259     */
260    public function get_allowed_export_config() {
261        return array();
262    }
263
264    /**
265     * After the user submits their config,
266     * they're given a confirm screen
267     * summarising what they've chosen.
268     * This function should return a table of nice strings => values
269     * of what they've chosen
270     * to be displayed in a table.
271     *
272     * @return bool
273     */
274    public function get_export_summary() {
275        return false;
276    }
277
278    /**
279     * Called before the portfolio plugin gets control.
280     * This function should copy all the files it wants to
281     * the temporary directory, using copy_existing_file
282     * or write_new_file
283     *
284     * @see copy_existing_file()
285     * @see write_new_file()
286     */
287    public abstract function prepare_package();
288
289    /**
290     * Helper function to copy files into the temp area
291     * for single or multi file exports.
292     *
293     * @return stored_file|bool
294     */
295    public function prepare_package_file() {
296        if (empty($this->singlefile) && empty($this->multifiles)) {
297            throw new portfolio_caller_exception('invalidpreparepackagefile', 'portfolio', $this->get_return_url());
298        }
299        if ($this->singlefile) {
300            return $this->exporter->copy_existing_file($this->singlefile);
301        }
302        foreach ($this->multifiles as $file) {
303            $this->exporter->copy_existing_file($file);
304        }
305    }
306
307    /**
308     * Array of formats this caller supports.
309     *
310     * @return array list of formats
311     */
312    public final function supported_formats() {
313        $basic = $this->base_supported_formats();
314        if (empty($this->supportedformats)) {
315            $specific = array();
316        } else if (!is_array($this->supportedformats)) {
317            debugging(get_class($this) . ' has set a non array value of member variable supported formats - working around but should be fixed in code');
318            $specific = array($this->supportedformats);
319        } else {
320            $specific = $this->supportedformats;
321        }
322        return portfolio_most_specific_formats($specific, $basic);
323    }
324
325    /**
326     * Base supported formats
327     *
328     * @throws coding_exception
329     */
330    public static function base_supported_formats() {
331        throw new coding_exception('base_supported_formats() method needs to be overridden in each subclass of portfolio_caller_base');
332    }
333
334    /**
335     * This is the "return to where you were" url
336     */
337    public abstract function get_return_url();
338
339    /**
340     * Callback to do whatever capability checks required
341     * in the caller (called during the export process
342     */
343    public abstract function check_permissions();
344
345    /**
346     * Clean name to display to the user about this caller location
347     */
348    public static function display_name() {
349        throw new coding_exception('display_name() method needs to be overridden in each subclass of portfolio_caller_base');
350    }
351
352    /**
353     * Return a string to put at the header summarising this export.
354     * By default, it just display the name (usually just 'assignment' or something unhelpful
355     *
356     * @return string
357     */
358    public function heading_summary() {
359        return get_string('exportingcontentfrom', 'portfolio', $this->display_name());
360    }
361
362    /**
363     * Load data
364     */
365    public abstract function load_data();
366
367    /**
368     * Set up the required files for this export.
369     * This supports either passing files directly
370     * or passing area arguments directly through
371     * to the files api using file_storage::get_area_files
372     *
373     * @param mixed $ids one of:
374     *                   - single file id
375     *                   - single stored_file object
376     *                   - array of file ids or stored_file objects
377     *                   - null
378     * @return void
379     */
380    public function set_file_and_format_data($ids=null /* ..pass arguments to area files here. */) {
381        $args = func_get_args();
382        array_shift($args); // shift off $ids
383        if (empty($ids) && count($args) == 0) {
384            return;
385        }
386        $files = array();
387        $fs = get_file_storage();
388        if (!empty($ids)) {
389            if (is_numeric($ids) || $ids instanceof stored_file) {
390                $ids = array($ids);
391            }
392            foreach ($ids as $id) {
393                if ($id instanceof stored_file) {
394                    $files[] = $id;
395                } else {
396                    $files[] = $fs->get_file_by_id($id);
397                }
398            }
399        } else if (count($args) != 0) {
400            if (count($args) < 4) {
401                throw new portfolio_caller_exception('invalidfileareaargs', 'portfolio');
402            }
403            $files = array_values(call_user_func_array(array($fs, 'get_area_files'), $args));
404        }
405        switch (count($files)) {
406            case 0: return;
407            case 1: {
408                $this->singlefile = $files[0];
409                return;
410            }
411            default: {
412                $this->multifiles = $files;
413            }
414        }
415    }
416
417    /**
418     * The button-location always knows best
419     * what the formats are... so it should be trusted.
420     *
421     * @todo MDL-31298 - re-analyze set_formats_from_button comment
422     * @param array $formats array of PORTFOLIO_FORMAT_XX
423     * @return void
424     */
425    public function set_formats_from_button($formats) {
426        $base = $this->base_supported_formats();
427        if (count($base) != count($formats)
428                || count($base) != count(array_intersect($base, $formats))) {
429                $this->supportedformats = portfolio_most_specific_formats($formats, $base);
430                return;
431        }
432        // in the case where the button hasn't actually set anything,
433        // we need to run through again and resolve conflicts
434        // TODO revisit this comment - it looks to me like it's lying
435        $this->supportedformats = portfolio_most_specific_formats($formats, $formats);
436    }
437
438    /**
439     * Adds a new format to the list of supported formats.
440     * This functions also handles removing conflicting and less specific
441     * formats at the same time.
442     *
443     * @param string $format one of PORTFOLIO_FORMAT_XX
444     * @return void
445     */
446    protected function add_format($format) {
447        if (in_array($format, $this->supportedformats)) {
448            return;
449        }
450        $this->supportedformats = portfolio_most_specific_formats(array($format), $this->supportedformats);
451    }
452
453    /**
454     * Gets mimetype
455     *
456     * @return string
457     */
458    public function get_mimetype() {
459        if ($this->singlefile instanceof stored_file) {
460            return $this->singlefile->get_mimetype();
461        } else if (!empty($this->intendedmimetype)) {
462            return $this->intendedmimetype;
463        }
464    }
465
466    /**
467     * Array of arguments the caller expects to be passed through to it.
468     * This must be keyed on the argument name, and the array value is a boolean,
469     * whether it is required, or just optional
470     * eg array(
471     *     id            => true,
472     *     somethingelse => false
473     * )
474     */
475    public static function expected_callbackargs() {
476        throw new coding_exception('expected_callbackargs() method needs to be overridden in each subclass of portfolio_caller_base');
477    }
478
479
480    /**
481     * Return the context for this export. used for $PAGE->set_context
482     *
483     * @param moodle_page $PAGE global page object
484     */
485    public abstract function set_context($PAGE);
486}
487
488/**
489 * Base class for module callers.
490 *
491 * This just implements a few of the abstract functions
492 * from portfolio_caller_base so that caller authors
493 * don't need to.
494 * {@link http://docs.moodle.org/dev/Adding_a_Portfolio_Button_to_a_page}
495 * @see also portfolio_caller_base
496 *
497 * @package core_portfolio
498 * @category portfolio
499 * @copyright 2008 Penny Leach <penny@catalyst.net.nz>
500 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
501 */
502abstract class portfolio_module_caller_base extends portfolio_caller_base {
503
504    /** @var object coursemodule object. set this in the constructor like $this->cm = get_coursemodule_from_instance('forum', $this->forum->id); */
505    protected $cm;
506
507    /** @var int cmid */
508    protected $id;
509
510    /** @var stdclass course object */
511    protected $course;
512
513    /**
514     * Navigation passed to print_header.
515     * Override this to do something more specific than the module view page
516     * like adding more links to the breadcrumb.
517     *
518     * @return array
519     */
520    public function get_navigation() {
521        // No extra navigation by default, link to the course module already included.
522        $extranav = array();
523        return array($extranav, $this->cm);
524    }
525
526    /**
527     * The url to return to after export or on cancel.
528     * Defaults value is set to the module 'view' page.
529     * Override this if it's deeper inside the module.
530     *
531     * @return string
532     */
533    public function get_return_url() {
534        global $CFG;
535        return $CFG->wwwroot . '/mod/' . $this->cm->modname . '/view.php?id=' . $this->cm->id;
536    }
537
538    /**
539     * Override the parent get function
540     * to make sure when we're asked for a course,
541     * We retrieve the object from the database as needed.
542     *
543     * @param string $key the name of get function
544     * @return stdClass
545     */
546    public function get($key) {
547        if ($key != 'course') {
548            return parent::get($key);
549        }
550        global $DB;
551        if (empty($this->course)) {
552            $this->course = $DB->get_record('course', array('id' => $this->cm->course));
553        }
554        return $this->course;
555    }
556
557    /**
558     * Return a string to put at the header summarising this export.
559     * by default, this function just display the name and module instance name.
560     * Override this to do something more specific
561     *
562     * @return string
563     */
564    public function heading_summary() {
565        return get_string('exportingcontentfrom', 'portfolio', $this->display_name() . ': ' . $this->cm->name);
566    }
567
568    /**
569     * Overridden to return the course module context
570     *
571     * @param moodle_page $PAGE global PAGE
572     */
573    public function set_context($PAGE) {
574        $PAGE->set_cm($this->cm);
575    }
576}
577