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 class definition for the exporter object.
19 *
20 * @package core_portfolio
21 * @copyright 2008 Penny Leach <penny@catalyst.net.nz>
22 *            Martin Dougiamas  <http://dougiamas.com>
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25
26defined('MOODLE_INTERNAL') || die();
27
28/**
29 * The class that handles the various stages of the actual export
30 * and the communication between the caller and the portfolio plugin.
31 *
32 * This is stored in the database between page requests in serialized base64 encoded form
33 * also contains helper methods for the plugin and caller to use (at the end of the file)
34 * @see get_base_filearea - where to write files to
35 * @see write_new_file - write some content to a file in the export filearea
36 * @see copy_existing_file - copy an existing file into the export filearea
37 * @see get_tempfiles - return list of all files in the export filearea
38 *
39 * @package core_portfolio
40 * @category portfolio
41 * @copyright 2008 Penny Leach <penny@catalyst.net.nz>
42 *            Martin Dougiamas  <http://dougiamas.com>
43 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
44 */
45class portfolio_exporter {
46
47    /** @var portfolio_caller_base the caller object used during the export */
48    private $caller;
49
50    /** @var portfolio_plugin_base the portfolio plugin instanced used during the export */
51    private $instance;
52
53    /** @var bool if there has been no config form displayed to the user */
54    private $noexportconfig;
55
56    /**
57     * @var stdClass the user currently exporting content always $USER,
58     *               but more conveniently placed here
59     */
60    private $user;
61
62    /**
63     * @var string the file to include that contains the class defintion of
64     *             the portfolio instance plugin used to re-waken the object after sleep
65     */
66    public $instancefile;
67
68    /**
69     * @var string the component that contains the class definition of
70     *             the caller object used to re-waken the object after sleep
71     */
72    public $callercomponent;
73
74    /** @var int the current stage of the export */
75    private $stage;
76
77    /** @var bool whether something (usually the portfolio plugin) has forced queuing */
78    private $forcequeue;
79
80    /**
81     * @var int id of this export matches record in portfolio_tempdata table
82     *          and used for itemid for file storage.
83     */
84    private $id;
85
86    /** @var array of stages that have had the portfolio plugin already steal control from them */
87    private $alreadystolen;
88
89    /**
90     * @var stored_file files that the exporter has written to this temp area keep track of
91     *                  this in case of duplicates within one export see MDL-16390
92     */
93    private $newfilehashes;
94
95    /**
96     * @var string selected exportformat this is also set in
97     *             export_config in the portfolio and caller classes
98     */
99    private $format;
100
101    /** @var bool queued - this is set after the event is triggered */
102    private $queued = false;
103
104    /** @var int expiry time - set the first time the object is saved out */
105    private $expirytime;
106
107    /**
108     * @var bool deleted - this is set during the cleanup routine so
109     *           that subsequent save() calls can detect it
110     */
111    private $deleted = false;
112
113    /**
114     * Construct a new exporter for use
115     *
116     * @param portfolio_plugin_base $instance portfolio instance (passed by reference)
117     * @param portfolio_caller_base $caller portfolio caller (passed by reference)
118     * @param string $callercomponent the name of the callercomponent
119     */
120    public function __construct($instance, portfolio_caller_base $caller, $callercomponent) {
121        $this->instance = $instance;
122        $this->caller = $caller;
123        if ($instance) {
124            $this->instancefile = 'portfolio/' . $instance->get('plugin') . '/lib.php';
125            $this->instance->set('exporter', $this);
126        }
127        $this->callercomponent = $callercomponent;
128        $this->stage = PORTFOLIO_STAGE_CONFIG;
129        $this->caller->set('exporter', $this);
130        $this->alreadystolen = array();
131        $this->newfilehashes = array();
132    }
133
134    /**
135     * Generic getter for properties belonging to this instance
136     * <b>outside</b> the subclasses like name, visible etc.
137     *
138     * @param string $field property's name
139     * @return portfolio_format|mixed
140     */
141    public function get($field) {
142        if ($field == 'format') {
143            return portfolio_format_object($this->format);
144        } else if ($field == 'formatclass') {
145            return $this->format;
146        }
147        if (property_exists($this, $field)) {
148            return $this->{$field};
149        }
150        $a = (object)array('property' => $field, 'class' => get_class($this));
151        throw new portfolio_export_exception($this, 'invalidproperty', 'portfolio', null, $a);
152    }
153
154    /**
155     * Generic setter for properties belonging to this instance
156     * <b>outside</b> the subclass like name, visible, etc.
157     *
158     * @param string $field property's name
159     * @param mixed $value property's value
160     * @return bool
161     * @throws portfolio_export_exception
162     */
163    public function set($field, &$value) {
164        if (property_exists($this, $field)) {
165            $this->{$field} =& $value;
166            if ($field == 'instance') {
167                $this->instancefile = 'portfolio/' . $this->instance->get('plugin') . '/lib.php';
168                $this->instance->set('exporter', $this);
169            }
170            $this->dirty = true;
171            return true;
172        }
173        $a = (object)array('property' => $field, 'class' => get_class($this));
174        throw new portfolio_export_exception($this, 'invalidproperty', 'portfolio', null, $a);
175
176    }
177
178    /**
179     * Sets this export to force queued.
180     * Sometimes plugins need to set this randomly
181     * if an external system changes its mind
182     * about what's supported
183     */
184    public function set_forcequeue() {
185        $this->forcequeue = true;
186    }
187
188    /**
189     * Process the given stage calling whatever functions are necessary
190     *
191     * @param int $stage (see PORTFOLIO_STAGE_* constants)
192     * @param bool $alreadystolen used to avoid letting plugins steal control twice.
193     * @return bool whether or not to process the next stage. this is important as the function is called recursively.
194     */
195    public function process_stage($stage, $alreadystolen=false) {
196        $this->set('stage', $stage);
197        if ($alreadystolen) {
198            $this->alreadystolen[$stage] = true;
199        } else {
200            if (!array_key_exists($stage, $this->alreadystolen)) {
201                $this->alreadystolen[$stage] = false;
202            }
203        }
204        if (!$this->alreadystolen[$stage] && $url = $this->instance->steal_control($stage)) {
205            $this->save();
206            redirect($url); // does not return
207        } else {
208            $this->save();
209        }
210
211        $waiting = $this->instance->get_export_config('wait');
212        if ($stage > PORTFOLIO_STAGE_QUEUEORWAIT && empty($waiting)) {
213            $stage = PORTFOLIO_STAGE_FINISHED;
214        }
215        $functionmap = array(
216            PORTFOLIO_STAGE_CONFIG        => 'config',
217            PORTFOLIO_STAGE_CONFIRM       => 'confirm',
218            PORTFOLIO_STAGE_QUEUEORWAIT   => 'queueorwait',
219            PORTFOLIO_STAGE_PACKAGE       => 'package',
220            PORTFOLIO_STAGE_CLEANUP       => 'cleanup',
221            PORTFOLIO_STAGE_SEND          => 'send',
222            PORTFOLIO_STAGE_FINISHED      => 'finished'
223        );
224
225        $function = 'process_stage_' . $functionmap[$stage];
226        try {
227            if ($this->$function()) {
228                // if we get through here it means control was returned
229                // as opposed to wanting to stop processing
230                // eg to wait for user input.
231                $this->save();
232                $stage++;
233                return $this->process_stage($stage);
234            } else {
235                $this->save();
236                return false;
237            }
238        } catch (portfolio_caller_exception $e) {
239            portfolio_export_rethrow_exception($this, $e);
240        } catch (portfolio_plugin_exception $e) {
241            portfolio_export_rethrow_exception($this, $e);
242        } catch (portfolio_export_exception $e) {
243            throw $e;
244        } catch (Exception $e) {
245            debugging(get_string('thirdpartyexception', 'portfolio', get_class($e)));
246            debugging($e);
247            portfolio_export_rethrow_exception($this, $e);
248        }
249    }
250
251    /**
252     * Helper function to return the portfolio instance
253     *
254     * @return portfolio_plugin_base subclass
255     */
256    public function instance() {
257        return $this->instance;
258    }
259
260    /**
261     * Helper function to return the caller object
262     *
263     * @return portfolio_caller_base subclass
264     */
265    public function caller() {
266        return $this->caller;
267    }
268
269    /**
270     * Processes the 'config' stage of the export
271     *
272     * @return bool whether or not to process the next stage. this is important as the control function is called recursively.
273     */
274    public function process_stage_config() {
275        global $OUTPUT, $CFG;
276        $pluginobj = $callerobj = null;
277        if ($this->instance->has_export_config()) {
278            $pluginobj = $this->instance;
279        }
280        if ($this->caller->has_export_config()) {
281            $callerobj = $this->caller;
282        }
283        $formats = portfolio_supported_formats_intersect($this->caller->supported_formats(), $this->instance->supported_formats());
284        $expectedtime = $this->instance->expected_time($this->caller->expected_time());
285        if (count($formats) == 0) {
286            // something went wrong, we should not have gotten this far.
287            throw new portfolio_export_exception($this, 'nocommonformats', 'portfolio', null, array('location' => get_class($this->caller), 'formats' => implode(',', $formats)));
288        }
289        // even if neither plugin or caller wants any config, we have to let the user choose their format, and decide to wait.
290        if ($pluginobj || $callerobj || count($formats) > 1 || ($expectedtime != PORTFOLIO_TIME_LOW && $expectedtime != PORTFOLIO_TIME_FORCEQUEUE)) {
291            $customdata = array(
292                'instance' => $this->instance,
293                'id'       => $this->id,
294                'plugin' => $pluginobj,
295                'caller' => $callerobj,
296                'userid' => $this->user->id,
297                'formats' => $formats,
298                'expectedtime' => $expectedtime,
299            );
300            require_once($CFG->libdir . '/portfolio/forms.php');
301            $mform = new portfolio_export_form('', $customdata);
302            if ($mform->is_cancelled()){
303                $this->cancel_request();
304            } else if ($fromform = $mform->get_data()){
305                if (!confirm_sesskey()) {
306                    throw new portfolio_export_exception($this, 'confirmsesskeybad');
307                }
308                $pluginbits = array();
309                $callerbits = array();
310                foreach ($fromform as $key => $value) {
311                    if (strpos($key, 'plugin_') === 0) {
312                        $pluginbits[substr($key, 7)]  = $value;
313                    } else if (strpos($key, 'caller_') === 0) {
314                        $callerbits[substr($key, 7)] = $value;
315                    }
316                }
317                $callerbits['format'] = $pluginbits['format'] = $fromform->format;
318                $pluginbits['wait'] = $fromform->wait;
319                if ($expectedtime == PORTFOLIO_TIME_LOW) {
320                    $pluginbits['wait'] = 1;
321                    $pluginbits['hidewait'] = 1;
322                } else if ($expectedtime == PORTFOLIO_TIME_FORCEQUEUE) {
323                    $pluginbits['wait'] = 0;
324                    $pluginbits['hidewait'] = 1;
325                    $this->forcequeue = true;
326                }
327                $callerbits['hideformat'] = $pluginbits['hideformat'] = (count($formats) == 1);
328                $this->caller->set_export_config($callerbits);
329                $this->instance->set_export_config($pluginbits);
330                $this->set('format', $fromform->format);
331                return true;
332            } else {
333                $this->print_header(get_string('configexport', 'portfolio'));
334                echo $OUTPUT->box_start();
335                $mform->display();
336                echo $OUTPUT->box_end();
337                echo $OUTPUT->footer();
338                return false;
339            }
340        } else {
341            $this->noexportconfig = true;
342            $format = array_shift($formats);
343            $config = array(
344                'hidewait' => 1,
345                'wait' => (($expectedtime == PORTFOLIO_TIME_LOW) ? 1 : 0),
346                'format' => $format,
347                'hideformat' => 1
348            );
349            $this->set('format', $format);
350            $this->instance->set_export_config($config);
351            $this->caller->set_export_config(array('format' => $format, 'hideformat' => 1));
352            if ($expectedtime == PORTFOLIO_TIME_FORCEQUEUE) {
353                $this->forcequeue = true;
354            }
355            return true;
356            // do not break - fall through to confirm
357        }
358    }
359
360    /**
361     * Processes the 'confirm' stage of the export
362     *
363     * @return bool whether or not to process the next stage. this is important as the control function is called recursively.
364     */
365    public function process_stage_confirm() {
366        global $CFG, $DB, $OUTPUT;
367
368        $previous = $DB->get_records(
369            'portfolio_log',
370            array(
371                'userid'      => $this->user->id,
372                'portfolio'   => $this->instance->get('id'),
373                'caller_sha1' => $this->caller->get_sha1(),
374            )
375        );
376        if (isset($this->noexportconfig) && empty($previous)) {
377            return true;
378        }
379        $strconfirm = get_string('confirmexport', 'portfolio');
380        $baseurl = $CFG->wwwroot . '/portfolio/add.php?sesskey=' . sesskey() . '&id=' . $this->get('id');
381        $yesurl = $baseurl . '&stage=' . PORTFOLIO_STAGE_QUEUEORWAIT;
382        $nourl  = $baseurl . '&cancel=1';
383        $this->print_header(get_string('confirmexport', 'portfolio'));
384        echo $OUTPUT->box_start();
385        echo $OUTPUT->heading(get_string('confirmsummary', 'portfolio'), 3);
386        $mainsummary = array();
387        if (!$this->instance->get_export_config('hideformat')) {
388            $mainsummary[get_string('selectedformat', 'portfolio')] = get_string('format_' . $this->instance->get_export_config('format'), 'portfolio');
389        }
390        if (!$this->instance->get_export_config('hidewait')) {
391            $mainsummary[get_string('selectedwait', 'portfolio')] = get_string(($this->instance->get_export_config('wait') ? 'yes' : 'no'));
392        }
393        if ($previous) {
394            $previousstr = '';
395            foreach ($previous as $row) {
396                $previousstr .= userdate($row->time);
397                if ($row->caller_class != get_class($this->caller)) {
398                    if (!empty($row->caller_file)) {
399                        portfolio_include_callback_file($row->caller_file);
400                    } else if (!empty($row->caller_component)) {
401                        portfolio_include_callback_file($row->caller_component);
402                    } else { // Ok, that's weird - this should never happen. Is the apocalypse coming?
403                        continue;
404                    }
405                    $previousstr .= ' (' . call_user_func(array($row->caller_class, 'display_name')) . ')';
406                }
407                $previousstr .= '<br />';
408            }
409            $mainsummary[get_string('exportedpreviously', 'portfolio')] = $previousstr;
410        }
411        if (!$csummary = $this->caller->get_export_summary()) {
412            $csummary = array();
413        }
414        if (!$isummary = $this->instance->get_export_summary()) {
415            $isummary = array();
416        }
417        $mainsummary = array_merge($mainsummary, $csummary, $isummary);
418        $table = new html_table();
419        $table->attributes['class'] = 'generaltable exportsummary';
420        $table->data = array();
421        foreach ($mainsummary as $string => $value) {
422            $table->data[] = array($string, $value);
423        }
424        echo html_writer::table($table);
425        echo $OUTPUT->confirm($strconfirm, $yesurl, $nourl);
426        echo $OUTPUT->box_end();
427        echo $OUTPUT->footer();
428        return false;
429    }
430
431    /**
432     * Processes the 'queueornext' stage of the export
433     *
434     * @return bool whether or not to process the next stage. this is important as the control function is called recursively.
435     */
436    public function process_stage_queueorwait() {
437        global $DB;
438
439        $wait = $this->instance->get_export_config('wait');
440        if (empty($wait)) {
441            $DB->set_field('portfolio_tempdata', 'queued', 1, array('id' => $this->id));
442            $this->queued = true;
443            return $this->process_stage_finished(true);
444        }
445        return true;
446    }
447
448    /**
449     * Processes the 'package' stage of the export
450     *
451     * @return bool whether or not to process the next stage. this is important as the control function is called recursively.
452     * @throws portfolio_export_exception
453     */
454    public function process_stage_package() {
455        // now we've agreed on a format,
456        // the caller is given control to package it up however it wants
457        // and then the portfolio plugin is given control to do whatever it wants.
458        try {
459            $this->caller->prepare_package();
460        } catch (portfolio_exception $e) {
461            throw new portfolio_export_exception($this, 'callercouldnotpackage', 'portfolio', null, $e->getMessage());
462        }
463        catch (file_exception $e) {
464            throw new portfolio_export_exception($this, 'callercouldnotpackage', 'portfolio', null, $e->getMessage());
465        }
466        try {
467            $this->instance->prepare_package();
468        }
469        catch (portfolio_exception $e) {
470            throw new portfolio_export_exception($this, 'plugincouldnotpackage', 'portfolio', null, $e->getMessage());
471        }
472        catch (file_exception $e) {
473            throw new portfolio_export_exception($this, 'plugincouldnotpackage', 'portfolio', null, $e->getMessage());
474        }
475        return true;
476    }
477
478    /**
479     * Processes the 'cleanup' stage of the export
480     *
481     * @param bool $pullok normally cleanup is deferred for pull plugins until after the file is requested from portfolio/file.php
482     *                        if you want to clean up earlier, pass true here (defaults to false)
483     * @return bool whether or not to process the next stage. this is important as the control function is called recursively.
484     */
485    public function process_stage_cleanup($pullok=false) {
486        global $CFG, $DB;
487
488        if (!$pullok && $this->get('instance') && !$this->get('instance')->is_push()) {
489            return true;
490        }
491        if ($this->get('instance')) {
492            // might not be set - before export really starts
493            $this->get('instance')->cleanup();
494        }
495        $DB->delete_records('portfolio_tempdata', array('id' => $this->id));
496        $fs = get_file_storage();
497        $fs->delete_area_files(SYSCONTEXTID, 'portfolio', 'exporter', $this->id);
498        $this->deleted = true;
499        return true;
500    }
501
502    /**
503     * Processes the 'send' stage of the export
504     *
505     * @return bool whether or not to process the next stage. this is important as the control function is called recursively.
506     */
507    public function process_stage_send() {
508        // send the file
509        try {
510            $this->instance->send_package();
511        }
512        catch (portfolio_plugin_exception $e) {
513            // not catching anything more general here. plugins with dependencies on other libraries that throw exceptions should catch and rethrow.
514            // eg curl exception
515            throw new portfolio_export_exception($this, 'failedtosendpackage', 'portfolio', null, $e->getMessage());
516        }
517        // only log push types, pull happens in send_file
518        if ($this->get('instance')->is_push()) {
519            $this->log_transfer();
520        }
521        return true;
522    }
523
524    /**
525     * Log the transfer
526     *
527     * this should only be called after the file has been sent
528     * either via push, or sent from a pull request.
529     */
530    public function log_transfer() {
531        global $DB;
532        $l = array(
533            'userid' => $this->user->id,
534            'portfolio' => $this->instance->get('id'),
535            'caller_file'=> '',
536            'caller_component' => $this->callercomponent,
537            'caller_sha1' => $this->caller->get_sha1(),
538            'caller_class' => get_class($this->caller),
539            'continueurl' => $this->instance->get_static_continue_url(),
540            'returnurl' => $this->caller->get_return_url(),
541            'tempdataid' => $this->id,
542            'time' => time(),
543        );
544        $DB->insert_record('portfolio_log', $l);
545    }
546
547    /**
548     * In some cases (mahara) we need to update this after the log has been done
549     * because of MDL-20872
550     *
551     * @param string $url link to be recorded to portfolio log
552     */
553    public function update_log_url($url) {
554        global $DB;
555        $DB->set_field('portfolio_log', 'continueurl', $url, array('tempdataid' => $this->id));
556    }
557
558    /**
559     * Processes the 'finish' stage of the export
560     *
561     * @param bool $queued let the process to be queued
562     * @return bool whether or not to process the next stage. this is important as the control function is called recursively.
563     */
564    public function process_stage_finished($queued=false) {
565        global $OUTPUT;
566        $returnurl = $this->caller->get_return_url();
567        $continueurl = $this->instance->get_interactive_continue_url();
568        $extras = $this->instance->get_extra_finish_options();
569
570        $key = 'exportcomplete';
571        if ($queued || $this->forcequeue) {
572            $key = 'exportqueued';
573            if ($this->forcequeue) {
574                $key = 'exportqueuedforced';
575            }
576        }
577        $this->print_header(get_string($key, 'portfolio'), false);
578        self::print_finish_info($returnurl, $continueurl, $extras);
579        echo $OUTPUT->footer();
580        return false;
581    }
582
583
584    /**
585     * Local print header function to be reused across the export
586     *
587     * @param string $headingstr full language string
588     * @param bool $summary (optional) to print summary, default is set to true
589     * @return void
590     */
591    public function print_header($headingstr, $summary=true) {
592        global $OUTPUT, $PAGE;
593        $titlestr = get_string('exporting', 'portfolio');
594        $headerstr = get_string('exporting', 'portfolio');
595
596        $PAGE->set_title($titlestr);
597        $PAGE->set_heading($headerstr);
598        echo $OUTPUT->header();
599        echo $OUTPUT->heading($headingstr);
600
601        if (!$summary) {
602            return;
603        }
604
605        echo $OUTPUT->box_start();
606        echo $OUTPUT->box_start();
607        echo $this->caller->heading_summary();
608        echo $OUTPUT->box_end();
609        if ($this->instance) {
610            echo $OUTPUT->box_start();
611            echo $this->instance->heading_summary();
612            echo $OUTPUT->box_end();
613        }
614        echo $OUTPUT->box_end();
615    }
616
617    /**
618     * Cancels a potfolio request and cleans up the tempdata
619     * and redirects the user back to where they started
620     *
621     * @param bool $logreturn options to return to porfolio log or caller return page
622     * @return void
623     * @uses exit
624     */
625    public function cancel_request($logreturn=false) {
626        global $CFG;
627        if (!isset($this)) {
628            return;
629        }
630        $this->process_stage_cleanup(true);
631        if ($logreturn) {
632            redirect($CFG->wwwroot . '/user/portfoliologs.php');
633        }
634        redirect($this->caller->get_return_url());
635        exit;
636    }
637
638    /**
639     * Writes out the contents of this object and all its data to the portfolio_tempdata table and sets the 'id' field.
640     *
641     * @return void
642     */
643    public function save() {
644        global $DB;
645        if (empty($this->id)) {
646            $r = (object)array(
647                'data' => base64_encode(serialize($this)),
648                'expirytime' => time() + (60*60*24),
649                'userid' => $this->user->id,
650                'instance' => (empty($this->instance)) ? null : $this->instance->get('id'),
651            );
652            $this->id = $DB->insert_record('portfolio_tempdata', $r);
653            $this->expirytime = $r->expirytime;
654            $this->save(); // call again so that id gets added to the save data.
655        } else {
656            if (!$r = $DB->get_record('portfolio_tempdata', array('id' => $this->id))) {
657                if (!$this->deleted) {
658                    //debugging("tried to save current object, but failed - see MDL-20872");
659                }
660                return;
661            }
662            $r->data = base64_encode(serialize($this));
663            $r->instance = (empty($this->instance)) ? null : $this->instance->get('id');
664            $DB->update_record('portfolio_tempdata', $r);
665        }
666    }
667
668    /**
669     * Rewakens the data from the database given the id.
670     * Makes sure to load the required files with the class definitions
671     *
672     * @param int $id id of data
673     * @return portfolio_exporter
674     */
675    public static function rewaken_object($id) {
676        global $DB, $CFG;
677        require_once($CFG->libdir . '/filelib.php');
678        require_once($CFG->libdir . '/portfolio/exporter.php');
679        require_once($CFG->libdir . '/portfolio/caller.php');
680        require_once($CFG->libdir . '/portfolio/plugin.php');
681        if (!$data = $DB->get_record('portfolio_tempdata', array('id' => $id))) {
682            // maybe it's been finished already by a pull plugin
683            // so look in the logs
684            if ($log = $DB->get_record('portfolio_log', array('tempdataid' => $id))) {
685                self::print_cleaned_export($log);
686            }
687            throw new portfolio_exception('invalidtempid', 'portfolio');
688        }
689        $exporter = unserialize(base64_decode($data->data));
690        if ($exporter->instancefile) {
691            require_once($CFG->dirroot . '/' . $exporter->instancefile);
692        }
693        if (!empty($exporter->callerfile)) {
694            portfolio_include_callback_file($exporter->callerfile);
695        } else if (!empty($exporter->callercomponent)) {
696            portfolio_include_callback_file($exporter->callercomponent);
697        } else {
698            return; // Should never get here!
699        }
700
701        $exporter = unserialize(serialize($exporter));
702        if (!$exporter->get('id')) {
703            // workaround for weird case
704            // where the id doesn't get saved between a new insert
705            // and the subsequent call that sets this field in the serialised data
706            $exporter->set('id', $id);
707            $exporter->save();
708        }
709        return $exporter;
710    }
711
712    /**
713     * Helper function to create the beginnings of a file_record object
714     * to create a new file in the portfolio_temporary working directory.
715     * Use write_new_file or copy_existing_file externally
716     * @see write_new_file
717     * @see copy_existing_file
718     *
719     * @param string $name filename of new record
720     * @return object
721     */
722    private function new_file_record_base($name) {
723        return (object)array_merge($this->get_base_filearea(), array(
724            'filepath' => '/',
725            'filename' => $name,
726        ));
727    }
728
729    /**
730     * Verifies a rewoken object.
731     * Checks to make sure it belongs to the same user and session as is currently in use.
732     *
733     * @param bool $readonly if we're reawakening this for a user to just display in the log view, don't verify the sessionkey
734     * @throws portfolio_exception
735     */
736    public function verify_rewaken($readonly=false) {
737        global $USER, $CFG;
738        if ($this->get('user')->id != $USER->id) { // make sure it belongs to the right user
739            throw new portfolio_exception('notyours', 'portfolio');
740        }
741        if (!$readonly && $this->get('instance') && !$this->get('instance')->allows_multiple_exports()) {
742            $already = portfolio_existing_exports($this->get('user')->id, $this->get('instance')->get('plugin'));
743            $already = array_keys($already);
744
745            if (array_shift($already) != $this->get('id')) {
746
747                $a = (object)array(
748                    'plugin'  => $this->get('instance')->get('plugin'),
749                    'link'    => $CFG->wwwroot . '/user/portfoliologs.php',
750                );
751                throw new portfolio_exception('nomultipleexports', 'portfolio', '', $a);
752            }
753        }
754        if (!$this->caller->check_permissions()) { // recall the caller permission check
755            throw new portfolio_caller_exception('nopermissions', 'portfolio', $this->caller->get_return_url());
756        }
757    }
758    /**
759     * Copies a file from somewhere else in moodle
760     * to the portfolio temporary working directory
761     * associated with this export
762     *
763     * @param stored_file $oldfile existing stored file object
764     * @return stored_file|bool new file object
765     */
766    public function copy_existing_file($oldfile) {
767        if (array_key_exists($oldfile->get_contenthash(), $this->newfilehashes)) {
768            return $this->newfilehashes[$oldfile->get_contenthash()];
769        }
770        $fs = get_file_storage();
771        $file_record = $this->new_file_record_base($oldfile->get_filename());
772        if ($dir = $this->get('format')->get_file_directory()) {
773            $file_record->filepath = '/'. $dir . '/';
774        }
775        try {
776            $newfile = $fs->create_file_from_storedfile($file_record, $oldfile->get_id());
777            $this->newfilehashes[$newfile->get_contenthash()] = $newfile;
778            return $newfile;
779        } catch (file_exception $e) {
780            return false;
781        }
782    }
783
784    /**
785     * Writes out some content to a file
786     * in the portfolio temporary working directory
787     * associated with this export.
788     *
789     * @param string $content content to write
790     * @param string $name filename to use
791     * @param bool $manifest whether this is the main file or an secondary file (eg attachment)
792     * @return stored_file
793     */
794    public function write_new_file($content, $name, $manifest=true) {
795        $fs = get_file_storage();
796        $file_record = $this->new_file_record_base($name);
797        if (empty($manifest) && ($dir = $this->get('format')->get_file_directory())) {
798            $file_record->filepath = '/' . $dir . '/';
799        }
800        return $fs->create_file_from_string($file_record, $content);
801    }
802
803    /**
804     * Zips all files in the temporary directory
805     *
806     * @param string $filename name of resulting zipfile (optional, defaults to portfolio-export.zip)
807     * @param string $filepath subpath in the filearea (optional, defaults to final)
808     * @return stored_file|bool resulting stored_file object, or false
809     */
810    public function zip_tempfiles($filename='portfolio-export.zip', $filepath='/final/') {
811        $zipper = new zip_packer();
812
813        list ($contextid, $component, $filearea, $itemid) = array_values($this->get_base_filearea());
814        if ($newfile = $zipper->archive_to_storage($this->get_tempfiles(), $contextid, $component, $filearea, $itemid, $filepath, $filename, $this->user->id)) {
815            return $newfile;
816        }
817        return false;
818
819    }
820
821    /**
822     * Returns an arary of files in the temporary working directory
823     * for this export.
824     * Always use this instead of the files api directly
825     *
826     * @param string $skipfile name of the file to be skipped
827     * @return array of stored_file objects keyed by name
828     */
829    public function get_tempfiles($skipfile='portfolio-export.zip') {
830        $fs = get_file_storage();
831        $files = $fs->get_area_files(SYSCONTEXTID, 'portfolio', 'exporter', $this->id, 'sortorder, itemid, filepath, filename', false);
832        if (empty($files)) {
833            return array();
834        }
835        $returnfiles = array();
836        foreach ($files as $f) {
837            if ($f->get_filename() == $skipfile) {
838                continue;
839            }
840            $returnfiles[$f->get_filepath() . $f->get_filename()] = $f;
841        }
842        return $returnfiles;
843    }
844
845    /**
846     * Returns the context, filearea, and itemid.
847     * Parts of a filearea (not filepath) to be used by
848     * plugins if they want to do things like zip up the contents of
849     * the temp area to here, or something that can't be done just using
850     * write_new_file, copy_existing_file or get_tempfiles
851     *
852     * @return array contextid, filearea, itemid are the keys.
853     */
854    public function get_base_filearea() {
855        return array(
856            'contextid' => SYSCONTEXTID,
857            'component' => 'portfolio',
858            'filearea'  => 'exporter',
859            'itemid'    => $this->id,
860        );
861    }
862
863    /**
864     * Wrapper function to print a friendly error to users
865     * This is generally caused by them hitting an expired transfer
866     * through the usage of the backbutton
867     *
868     * @uses exit
869     */
870    public static function print_expired_export() {
871        global $CFG, $OUTPUT, $PAGE;
872        $title = get_string('exportexpired', 'portfolio');
873        $PAGE->navbar->add(get_string('exportexpired', 'portfolio'));
874        $PAGE->set_title($title);
875        $PAGE->set_heading($title);
876        echo $OUTPUT->header();
877        echo $OUTPUT->notification(get_string('exportexpireddesc', 'portfolio'));
878        echo $OUTPUT->continue_button($CFG->wwwroot);
879        echo $OUTPUT->footer();
880        exit;
881    }
882
883    /**
884     * Wrapper function to print a friendly error to users
885     *
886     * @param stdClass $log portfolio_log object
887     * @param portfolio_plugin_base $instance portfolio instance
888     * @uses exit
889     */
890    public static function print_cleaned_export($log, $instance=null) {
891        global $CFG, $OUTPUT, $PAGE;
892        if (empty($instance) || !$instance instanceof portfolio_plugin_base) {
893            $instance = portfolio_instance($log->portfolio);
894        }
895        $title = get_string('exportalreadyfinished', 'portfolio');
896        $PAGE->navbar->add($title);
897        $PAGE->set_title($title);
898        $PAGE->set_heading($title);
899        echo $OUTPUT->header();
900        echo $OUTPUT->notification(get_string('exportalreadyfinished', 'portfolio'));
901        self::print_finish_info($log->returnurl, $instance->resolve_static_continue_url($log->continueurl));
902        echo $OUTPUT->continue_button($CFG->wwwroot);
903        echo $OUTPUT->footer();
904        exit;
905    }
906
907    /**
908     * Wrapper function to print continue and/or return link
909     *
910     * @param string $returnurl link to previos page
911     * @param string $continueurl continue to next page
912     * @param array $extras (optional) other links to be display.
913     */
914    public static function print_finish_info($returnurl, $continueurl, $extras=null) {
915        if ($returnurl) {
916            echo '<a href="' . $returnurl . '">' . get_string('returntowhereyouwere', 'portfolio') . '</a><br />';
917        }
918        if ($continueurl) {
919            echo '<a href="' . $continueurl . '">' . get_string('continuetoportfolio', 'portfolio') . '</a><br />';
920        }
921        if (is_array($extras)) {
922            foreach ($extras as $link => $string) {
923                echo '<a href="' . $link . '">' . $string . '</a><br />';
924            }
925        }
926    }
927}
928