MDL-33791 Portfolio: Fixed security issue with passing file paths.
[moodle.git] / lib / portfolio / exporter.php
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/>.
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  */
26 defined('MOODLE_INTERNAL') || die();
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  */
45 class portfolio_exporter {
47     /** @var portfolio_caller_base the caller object used during the export */
48     private $caller;
50     /** @var portfolio_plugin_base the portfolio plugin instanced used during the export */
51     private $instance;
53     /** @var bool if there has been no config form displayed to the user */
54     private $noexportconfig;
56     /**
57      * @var stdClass the user currently exporting content always $USER,
58      *               but more conveniently placed here
59      */
60     private $user;
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;
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;
74     /** @var int the current stage of the export */
75     private $stage;
77     /** @var bool whether something (usually the portfolio plugin) has forced queuing */
78     private $forcequeue;
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;
86     /** @var array of stages that have had the portfolio plugin already steal control from them */
87     private $alreadystolen;
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;
95     /**
96      * @var string selected exportformat this is also set in
97      *             export_config in the portfolio and caller classes
98      */
99     private $format;
101     /** @var bool queued - this is set after the event is triggered */
102     private $queued = false;
104     /** @var int expiry time - set the first time the object is saved out */
105     private $expirytime;
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;
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, &$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     }
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     }
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);
176     }
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     }
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         }
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         );
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     }
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     }
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     }
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     }
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;
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     }
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         $wait = $this->instance->get_export_config('wait');
438         if (empty($wait)) {
439             events_trigger('portfolio_send', $this->id);
440             $this->queued = true;
441             return $this->process_stage_finished(true);
442         }
443         return true;
444     }
446     /**
447      * Processes the 'package' stage of the export
448      *
449      * @return bool whether or not to process the next stage. this is important as the control function is called recursively.
450      * @throws portfolio_export_exception
451      */
452     public function process_stage_package() {
453         // now we've agreed on a format,
454         // the caller is given control to package it up however it wants
455         // and then the portfolio plugin is given control to do whatever it wants.
456         try {
457             $this->caller->prepare_package();
458         } catch (portfolio_exception $e) {
459             throw new portfolio_export_exception($this, 'callercouldnotpackage', 'portfolio', null, $e->getMessage());
460         }
461         catch (file_exception $e) {
462             throw new portfolio_export_exception($this, 'callercouldnotpackage', 'portfolio', null, $e->getMessage());
463         }
464         try {
465             $this->instance->prepare_package();
466         }
467         catch (portfolio_exception $e) {
468             throw new portfolio_export_exception($this, 'plugincouldnotpackage', 'portfolio', null, $e->getMessage());
469         }
470         catch (file_exception $e) {
471             throw new portfolio_export_exception($this, 'plugincouldnotpackage', 'portfolio', null, $e->getMessage());
472         }
473         return true;
474     }
476     /**
477      * Processes the 'cleanup' stage of the export
478      *
479      * @param bool $pullok normally cleanup is deferred for pull plugins until after the file is requested from portfolio/file.php
480      *                        if you want to clean up earlier, pass true here (defaults to false)
481      * @return bool whether or not to process the next stage. this is important as the control function is called recursively.
482      */
483     public function process_stage_cleanup($pullok=false) {
484         global $CFG, $DB;
486         if (!$pullok && $this->get('instance') && !$this->get('instance')->is_push()) {
487             return true;
488         }
489         if ($this->get('instance')) {
490             // might not be set - before export really starts
491             $this->get('instance')->cleanup();
492         }
493         $DB->delete_records('portfolio_tempdata', array('id' => $this->id));
494         $fs = get_file_storage();
495         $fs->delete_area_files(SYSCONTEXTID, 'portfolio', 'exporter', $this->id);
496         $this->deleted = true;
497         return true;
498     }
500     /**
501      * Processes the 'send' stage of the export
502      *
503      * @return bool whether or not to process the next stage. this is important as the control function is called recursively.
504      */
505     public function process_stage_send() {
506         // send the file
507         try {
508             $this->instance->send_package();
509         }
510         catch (portfolio_plugin_exception $e) {
511             // not catching anything more general here. plugins with dependencies on other libraries that throw exceptions should catch and rethrow.
512             // eg curl exception
513             throw new portfolio_export_exception($this, 'failedtosendpackage', 'portfolio', null, $e->getMessage());
514         }
515         // only log push types, pull happens in send_file
516         if ($this->get('instance')->is_push()) {
517             $this->log_transfer();
518         }
519         return true;
520     }
522     /**
523      * Log the transfer
524      *
525      * this should only be called after the file has been sent
526      * either via push, or sent from a pull request.
527      */
528     public function log_transfer() {
529         global $DB;
530         $l = array(
531             'userid' => $this->user->id,
532             'portfolio' => $this->instance->get('id'),
533             'caller_file'=> '',
534             'caller_component' => $this->callercomponent,
535             'caller_sha1' => $this->caller->get_sha1(),
536             'caller_class' => get_class($this->caller),
537             'continueurl' => $this->instance->get_static_continue_url(),
538             'returnurl' => $this->caller->get_return_url(),
539             'tempdataid' => $this->id,
540             'time' => time(),
541         );
542         $DB->insert_record('portfolio_log', $l);
543     }
545     /**
546      * In some cases (mahara) we need to update this after the log has been done
547      * because of MDL-20872
548      *
549      * @param string $url link to be recorded to portfolio log
550      */
551     public function update_log_url($url) {
552         global $DB;
553         $DB->set_field('portfolio_log', 'continueurl', $url, array('tempdataid' => $this->id));
554     }
556     /**
557      * Processes the 'finish' stage of the export
558      *
559      * @param bool $queued let the process to be queued
560      * @return bool whether or not to process the next stage. this is important as the control function is called recursively.
561      */
562     public function process_stage_finished($queued=false) {
563         global $OUTPUT;
564         $returnurl = $this->caller->get_return_url();
565         $continueurl = $this->instance->get_interactive_continue_url();
566         $extras = $this->instance->get_extra_finish_options();
568         $key = 'exportcomplete';
569         if ($queued || $this->forcequeue) {
570             $key = 'exportqueued';
571             if ($this->forcequeue) {
572                 $key = 'exportqueuedforced';
573             }
574         }
575         $this->print_header(get_string($key, 'portfolio'), false);
576         self::print_finish_info($returnurl, $continueurl, $extras);
577         echo $OUTPUT->footer();
578         return false;
579     }
582     /**
583      * Local print header function to be reused across the export
584      *
585      * @param string $headingstr full language string
586      * @param bool $summary (optional) to print summary, default is set to true
587      * @return void
588      */
589     public function print_header($headingstr, $summary=true) {
590         global $OUTPUT, $PAGE;
591         $titlestr = get_string('exporting', 'portfolio');
592         $headerstr = get_string('exporting', 'portfolio');
594         $PAGE->set_title($titlestr);
595         $PAGE->set_heading($headerstr);
596         echo $OUTPUT->header();
597         echo $OUTPUT->heading($headingstr);
599         if (!$summary) {
600             return;
601         }
603         echo $OUTPUT->box_start();
604         echo $OUTPUT->box_start();
605         echo $this->caller->heading_summary();
606         echo $OUTPUT->box_end();
607         if ($this->instance) {
608             echo $OUTPUT->box_start();
609             echo $this->instance->heading_summary();
610             echo $OUTPUT->box_end();
611         }
612         echo $OUTPUT->box_end();
613     }
615     /**
616      * Cancels a potfolio request and cleans up the tempdata
617      * and redirects the user back to where they started
618      *
619      * @param bool $logreturn options to return to porfolio log or caller return page
620      * @return void
621      * @uses exit
622      */
623     public function cancel_request($logreturn=false) {
624         global $CFG;
625         if (!isset($this)) {
626             return;
627         }
628         $this->process_stage_cleanup(true);
629         if ($logreturn) {
630             redirect($CFG->wwwroot . '/user/portfoliologs.php');
631         }
632         redirect($this->caller->get_return_url());
633         exit;
634     }
636     /**
637      * Writes out the contents of this object and all its data to the portfolio_tempdata table and sets the 'id' field.
638      *
639      * @return void
640      */
641     public function save() {
642         global $DB;
643         if (empty($this->id)) {
644             $r = (object)array(
645                 'data' => base64_encode(serialize($this)),
646                 'expirytime' => time() + (60*60*24),
647                 'userid' => $this->user->id,
648                 'instance' => (empty($this->instance)) ? null : $this->instance->get('id'),
649             );
650             $this->id = $DB->insert_record('portfolio_tempdata', $r);
651             $this->expirytime = $r->expirytime;
652             $this->save(); // call again so that id gets added to the save data.
653         } else {
654             if (!$r = $DB->get_record('portfolio_tempdata', array('id' => $this->id))) {
655                 if (!$this->deleted) {
656                     //debugging("tried to save current object, but failed - see MDL-20872");
657                 }
658                 return;
659             }
660             $r->data = base64_encode(serialize($this));
661             $r->instance = (empty($this->instance)) ? null : $this->instance->get('id');
662             $DB->update_record('portfolio_tempdata', $r);
663         }
664     }
666     /**
667      * Rewakens the data from the database given the id.
668      * Makes sure to load the required files with the class definitions
669      *
670      * @param int $id id of data
671      * @return portfolio_exporter
672      */
673     public static function rewaken_object($id) {
674         global $DB, $CFG;
675         require_once($CFG->libdir . '/filelib.php');
676         require_once($CFG->libdir . '/portfolio/exporter.php');
677         require_once($CFG->libdir . '/portfolio/caller.php');
678         require_once($CFG->libdir . '/portfolio/plugin.php');
679         if (!$data = $DB->get_record('portfolio_tempdata', array('id' => $id))) {
680             // maybe it's been finished already by a pull plugin
681             // so look in the logs
682             if ($log = $DB->get_record('portfolio_log', array('tempdataid' => $id))) {
683                 self::print_cleaned_export($log);
684             }
685             throw new portfolio_exception('invalidtempid', 'portfolio');
686         }
687         $exporter = unserialize(base64_decode($data->data));
688         if ($exporter->instancefile) {
689             require_once($CFG->dirroot . '/' . $exporter->instancefile);
690         }
691         if (!empty($exporter->callerfile)) {
692             portfolio_include_callback_file($exporter->callerfile);
693         } else if (!empty($exporter->callercomponent)) {
694             portfolio_include_callback_file($exporter->callercomponent);
695         } else {
696             return; // Should never get here!
697         }
699         $exporter = unserialize(serialize($exporter));
700         if (!$exporter->get('id')) {
701             // workaround for weird case
702             // where the id doesn't get saved between a new insert
703             // and the subsequent call that sets this field in the serialised data
704             $exporter->set('id', $id);
705             $exporter->save();
706         }
707         return $exporter;
708     }
710     /**
711      * Helper function to create the beginnings of a file_record object
712      * to create a new file in the portfolio_temporary working directory.
713      * Use write_new_file or copy_existing_file externally
714      * @see write_new_file
715      * @see copy_existing_file
716      *
717      * @param string $name filename of new record
718      * @return object
719      */
720     private function new_file_record_base($name) {
721         return (object)array_merge($this->get_base_filearea(), array(
722             'filepath' => '/',
723             'filename' => $name,
724         ));
725     }
727     /**
728      * Verifies a rewoken object.
729      * Checks to make sure it belongs to the same user and session as is currently in use.
730      *
731      * @param bool $readonly if we're reawakening this for a user to just display in the log view, don't verify the sessionkey
732      * @throws portfolio_exception
733      */
734     public function verify_rewaken($readonly=false) {
735         global $USER, $CFG;
736         if ($this->get('user')->id != $USER->id) { // make sure it belongs to the right user
737             throw new portfolio_exception('notyours', 'portfolio');
738         }
739         if (!$readonly && $this->get('instance') && !$this->get('instance')->allows_multiple_exports()) {
740             $already = portfolio_existing_exports($this->get('user')->id, $this->get('instance')->get('plugin'));
741             $already = array_keys($already);
743             if (array_shift($already) != $this->get('id')) {
745                 $a = (object)array(
746                     'plugin'  => $this->get('instance')->get('plugin'),
747                     'link'    => $CFG->wwwroot . '/user/portfoliologs.php',
748                 );
749                 throw new portfolio_exception('nomultipleexports', 'portfolio', '', $a);
750             }
751         }
752         if (!$this->caller->check_permissions()) { // recall the caller permission check
753             throw new portfolio_caller_exception('nopermissions', 'portfolio', $this->caller->get_return_url());
754         }
755     }
756     /**
757      * Copies a file from somewhere else in moodle
758      * to the portfolio temporary working directory
759      * associated with this export
760      *
761      * @param stored_file $oldfile existing stored file object
762      * @return stored_file|bool new file object
763      */
764     public function copy_existing_file($oldfile) {
765         if (array_key_exists($oldfile->get_contenthash(), $this->newfilehashes)) {
766             return $this->newfilehashes[$oldfile->get_contenthash()];
767         }
768         $fs = get_file_storage();
769         $file_record = $this->new_file_record_base($oldfile->get_filename());
770         if ($dir = $this->get('format')->get_file_directory()) {
771             $file_record->filepath = '/'. $dir . '/';
772         }
773         try {
774             $newfile = $fs->create_file_from_storedfile($file_record, $oldfile->get_id());
775             $this->newfilehashes[$newfile->get_contenthash()] = $newfile;
776             return $newfile;
777         } catch (file_exception $e) {
778             return false;
779         }
780     }
782     /**
783      * Writes out some content to a file
784      * in the portfolio temporary working directory
785      * associated with this export.
786      *
787      * @param string $content content to write
788      * @param string $name filename to use
789      * @param bool $manifest whether this is the main file or an secondary file (eg attachment)
790      * @return stored_file
791      */
792     public function write_new_file($content, $name, $manifest=true) {
793         $fs = get_file_storage();
794         $file_record = $this->new_file_record_base($name);
795         if (empty($manifest) && ($dir = $this->get('format')->get_file_directory())) {
796             $file_record->filepath = '/' . $dir . '/';
797         }
798         return $fs->create_file_from_string($file_record, $content);
799     }
801     /**
802      * Zips all files in the temporary directory
803      *
804      * @param string $filename name of resulting zipfile (optional, defaults to portfolio-export.zip)
805      * @param string $filepath subpath in the filearea (optional, defaults to final)
806      * @return stored_file|bool resulting stored_file object, or false
807      */
808     public function zip_tempfiles($filename='portfolio-export.zip', $filepath='/final/') {
809         $zipper = new zip_packer();
811         list ($contextid, $component, $filearea, $itemid) = array_values($this->get_base_filearea());
812         if ($newfile = $zipper->archive_to_storage($this->get_tempfiles(), $contextid, $component, $filearea, $itemid, $filepath, $filename, $this->user->id)) {
813             return $newfile;
814         }
815         return false;
817     }
819     /**
820      * Returns an arary of files in the temporary working directory
821      * for this export.
822      * Always use this instead of the files api directly
823      *
824      * @param string $skipfile name of the file to be skipped
825      * @return array of stored_file objects keyed by name
826      */
827     public function get_tempfiles($skipfile='portfolio-export.zip') {
828         $fs = get_file_storage();
829         $files = $fs->get_area_files(SYSCONTEXTID, 'portfolio', 'exporter', $this->id, 'sortorder, itemid, filepath, filename', false);
830         if (empty($files)) {
831             return array();
832         }
833         $returnfiles = array();
834         foreach ($files as $f) {
835             if ($f->get_filename() == $skipfile) {
836                 continue;
837             }
838             $returnfiles[$f->get_filepath() . '/' . $f->get_filename()] = $f;
839         }
840         return $returnfiles;
841     }
843     /**
844      * Returns the context, filearea, and itemid.
845      * Parts of a filearea (not filepath) to be used by
846      * plugins if they want to do things like zip up the contents of
847      * the temp area to here, or something that can't be done just using
848      * write_new_file, copy_existing_file or get_tempfiles
849      *
850      * @return array contextid, filearea, itemid are the keys.
851      */
852     public function get_base_filearea() {
853         return array(
854             'contextid' => SYSCONTEXTID,
855             'component' => 'portfolio',
856             'filearea'  => 'exporter',
857             'itemid'    => $this->id,
858         );
859     }
861     /**
862      * Wrapper function to print a friendly error to users
863      * This is generally caused by them hitting an expired transfer
864      * through the usage of the backbutton
865      *
866      * @uses exit
867      */
868     public static function print_expired_export() {
869         global $CFG, $OUTPUT, $PAGE;
870         $title = get_string('exportexpired', 'portfolio');
871         $PAGE->navbar->add(get_string('exportexpired', 'portfolio'));
872         $PAGE->set_title($title);
873         $PAGE->set_heading($title);
874         echo $OUTPUT->header();
875         echo $OUTPUT->notification(get_string('exportexpireddesc', 'portfolio'));
876         echo $OUTPUT->continue_button($CFG->wwwroot);
877         echo $OUTPUT->footer();
878         exit;
879     }
881     /**
882      * Wrapper function to print a friendly error to users
883      *
884      * @param stdClass $log portfolio_log object
885      * @param portfolio_plugin_base $instance portfolio instance
886      * @uses exit
887      */
888     public static function print_cleaned_export($log, $instance=null) {
889         global $CFG, $OUTPUT, $PAGE;
890         if (empty($instance) || !$instance instanceof portfolio_plugin_base) {
891             $instance = portfolio_instance($log->portfolio);
892         }
893         $title = get_string('exportalreadyfinished', 'portfolio');
894         $PAGE->navbar->add($title);
895         $PAGE->set_title($title);
896         $PAGE->set_heading($title);
897         echo $OUTPUT->header();
898         echo $OUTPUT->notification(get_string('exportalreadyfinished', 'portfolio'));
899         self::print_finish_info($log->returnurl, $instance->resolve_static_continue_url($log->continueurl));
900         echo $OUTPUT->continue_button($CFG->wwwroot);
901         echo $OUTPUT->footer();
902         exit;
903     }
905     /**
906      * Wrapper function to print continue and/or return link
907      *
908      * @param string $returnurl link to previos page
909      * @param string $continueurl continue to next page
910      * @param array $extras (optional) other links to be display.
911      */
912     public static function print_finish_info($returnurl, $continueurl, $extras=null) {
913         if ($returnurl) {
914             echo '<a href="' . $returnurl . '">' . get_string('returntowhereyouwere', 'portfolio') . '</a><br />';
915         }
916         if ($continueurl) {
917             echo '<a href="' . $continueurl . '">' . get_string('continuetoportfolio', 'portfolio') . '</a><br />';
918         }
919         if (is_array($extras)) {
920             foreach ($extras as $link => $string) {
921                 echo '<a href="' . $link . '">' . $string . '</a><br />';
922             }
923         }
924     }