2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 * This file contains the class definition for the exporter object.
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
26 defined('MOODLE_INTERNAL') ||
die();
29 * The class that handles the various stages of the actual export
30 * and the communication between the caller and the portfolio plugin.
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
39 * @package core_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
45 class portfolio_exporter
{
47 /** @var portfolio_caller_base the caller object used during the export */
50 /** @var portfolio_plugin_base the portfolio plugin instanced used during the export */
53 /** @var bool if there has been no config form displayed to the user */
54 private $noexportconfig;
57 * @var stdClass the user currently exporting content always $USER,
58 * but more conveniently placed here
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
69 * @var string the component that contains the class definition of
70 * the caller object used to re-waken the object after sleep
72 public $callercomponent;
74 /** @var int the current stage of the export */
77 /** @var bool whether something (usually the portfolio plugin) has forced queuing */
81 * @var int id of this export matches record in portfolio_tempdata table
82 * and used for itemid for file storage.
86 /** @var array of stages that have had the portfolio plugin already steal control from them */
87 private $alreadystolen;
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
93 private $newfilehashes;
96 * @var string selected exportformat this is also set in
97 * export_config in the portfolio and caller classes
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 */
108 * @var bool deleted - this is set during the cleanup routine so
109 * that subsequent save() calls can detect it
111 private $deleted = false;
114 * Construct a new exporter for use
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
120 public function __construct($instance, portfolio_caller_base
$caller, $callercomponent) {
121 $this->instance
= $instance;
122 $this->caller
= $caller;
124 $this->instancefile
= 'portfolio/' . $instance->get('plugin') . '/lib.php';
125 $this->instance
->set('exporter', $this);
127 $this->callercomponent
= $callercomponent;
128 $this->stage
= PORTFOLIO_STAGE_CONFIG
;
129 $this->caller
->set('exporter', $this);
130 $this->alreadystolen
= array();
131 $this->newfilehashes
= array();
135 * Generic getter for properties belonging to this instance
136 * <b>outside</b> the subclasses like name, visible etc.
138 * @param string $field property's name
139 * @return portfolio_format|mixed
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
;
147 if (property_exists($this, $field)) {
148 return $this->{$field};
150 $a = (object)array('property' => $field, 'class' => get_class($this));
151 throw new portfolio_export_exception($this, 'invalidproperty', 'portfolio', null, $a);
155 * Generic setter for properties belonging to this instance
156 * <b>outside</b> the subclass like name, visible, etc.
158 * @param string $field property's name
159 * @param mixed $value property's value
161 * @throws portfolio_export_exception
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);
172 $a = (object)array('property' => $field, 'class' => get_class($this));
173 throw new portfolio_export_exception($this, 'invalidproperty', 'portfolio', null, $a);
178 * Sets this export to force queued.
179 * Sometimes plugins need to set this randomly
180 * if an external system changes its mind
181 * about what's supported
183 public function set_forcequeue() {
184 $this->forcequeue
= true;
188 * Process the given stage calling whatever functions are necessary
190 * @param int $stage (see PORTFOLIO_STAGE_* constants)
191 * @param bool $alreadystolen used to avoid letting plugins steal control twice.
192 * @return bool whether or not to process the next stage. this is important as the function is called recursively.
194 public function process_stage($stage, $alreadystolen=false) {
195 $this->set('stage', $stage);
196 if ($alreadystolen) {
197 $this->alreadystolen
[$stage] = true;
199 if (!array_key_exists($stage, $this->alreadystolen
)) {
200 $this->alreadystolen
[$stage] = false;
203 if (!$this->alreadystolen
[$stage] && $url = $this->instance
->steal_control($stage)) {
205 redirect($url); // does not return
210 $waiting = $this->instance
->get_export_config('wait');
211 if ($stage > PORTFOLIO_STAGE_QUEUEORWAIT
&& empty($waiting)) {
212 $stage = PORTFOLIO_STAGE_FINISHED
;
214 $functionmap = array(
215 PORTFOLIO_STAGE_CONFIG
=> 'config',
216 PORTFOLIO_STAGE_CONFIRM
=> 'confirm',
217 PORTFOLIO_STAGE_QUEUEORWAIT
=> 'queueorwait',
218 PORTFOLIO_STAGE_PACKAGE
=> 'package',
219 PORTFOLIO_STAGE_CLEANUP
=> 'cleanup',
220 PORTFOLIO_STAGE_SEND
=> 'send',
221 PORTFOLIO_STAGE_FINISHED
=> 'finished'
224 $function = 'process_stage_' . $functionmap[$stage];
226 if ($this->$function()) {
227 // if we get through here it means control was returned
228 // as opposed to wanting to stop processing
229 // eg to wait for user input.
232 return $this->process_stage($stage);
237 } catch (portfolio_caller_exception
$e) {
238 portfolio_export_rethrow_exception($this, $e);
239 } catch (portfolio_plugin_exception
$e) {
240 portfolio_export_rethrow_exception($this, $e);
241 } catch (portfolio_export_exception
$e) {
243 } catch (Exception
$e) {
244 debugging(get_string('thirdpartyexception', 'portfolio', get_class($e)));
246 portfolio_export_rethrow_exception($this, $e);
251 * Helper function to return the portfolio instance
253 * @return portfolio_plugin_base subclass
255 public function instance() {
256 return $this->instance
;
260 * Helper function to return the caller object
262 * @return portfolio_caller_base subclass
264 public function caller() {
265 return $this->caller
;
269 * Processes the 'config' stage of the export
271 * @return bool whether or not to process the next stage. this is important as the control function is called recursively.
273 public function process_stage_config() {
274 global $OUTPUT, $CFG;
275 $pluginobj = $callerobj = null;
276 if ($this->instance
->has_export_config()) {
277 $pluginobj = $this->instance
;
279 if ($this->caller
->has_export_config()) {
280 $callerobj = $this->caller
;
282 $formats = portfolio_supported_formats_intersect($this->caller
->supported_formats(), $this->instance
->supported_formats());
283 $expectedtime = $this->instance
->expected_time($this->caller
->expected_time());
284 if (count($formats) == 0) {
285 // something went wrong, we should not have gotten this far.
286 throw new portfolio_export_exception($this, 'nocommonformats', 'portfolio', null, array('location' => get_class($this->caller
), 'formats' => implode(',', $formats)));
288 // even if neither plugin or caller wants any config, we have to let the user choose their format, and decide to wait.
289 if ($pluginobj ||
$callerobj ||
count($formats) > 1 ||
($expectedtime != PORTFOLIO_TIME_LOW
&& $expectedtime != PORTFOLIO_TIME_FORCEQUEUE
)) {
291 'instance' => $this->instance
,
293 'plugin' => $pluginobj,
294 'caller' => $callerobj,
295 'userid' => $this->user
->id
,
296 'formats' => $formats,
297 'expectedtime' => $expectedtime,
299 require_once($CFG->libdir
. '/portfolio/forms.php');
300 $mform = new portfolio_export_form('', $customdata);
301 if ($mform->is_cancelled()){
302 $this->cancel_request();
303 } else if ($fromform = $mform->get_data()){
304 if (!confirm_sesskey()) {
305 throw new portfolio_export_exception($this, 'confirmsesskeybad');
307 $pluginbits = array();
308 $callerbits = array();
309 foreach ($fromform as $key => $value) {
310 if (strpos($key, 'plugin_') === 0) {
311 $pluginbits[substr($key, 7)] = $value;
312 } else if (strpos($key, 'caller_') === 0) {
313 $callerbits[substr($key, 7)] = $value;
316 $callerbits['format'] = $pluginbits['format'] = $fromform->format
;
317 $pluginbits['wait'] = $fromform->wait
;
318 if ($expectedtime == PORTFOLIO_TIME_LOW
) {
319 $pluginbits['wait'] = 1;
320 $pluginbits['hidewait'] = 1;
321 } else if ($expectedtime == PORTFOLIO_TIME_FORCEQUEUE
) {
322 $pluginbits['wait'] = 0;
323 $pluginbits['hidewait'] = 1;
324 $this->forcequeue
= true;
326 $callerbits['hideformat'] = $pluginbits['hideformat'] = (count($formats) == 1);
327 $this->caller
->set_export_config($callerbits);
328 $this->instance
->set_export_config($pluginbits);
329 $this->set('format', $fromform->format
);
332 $this->print_header(get_string('configexport', 'portfolio'));
333 echo $OUTPUT->box_start();
335 echo $OUTPUT->box_end();
336 echo $OUTPUT->footer();
340 $this->noexportconfig
= true;
341 $format = array_shift($formats);
344 'wait' => (($expectedtime == PORTFOLIO_TIME_LOW
) ?
1 : 0),
348 $this->set('format', $format);
349 $this->instance
->set_export_config($config);
350 $this->caller
->set_export_config(array('format' => $format, 'hideformat' => 1));
351 if ($expectedtime == PORTFOLIO_TIME_FORCEQUEUE
) {
352 $this->forcequeue
= true;
355 // do not break - fall through to confirm
360 * Processes the 'confirm' stage of the export
362 * @return bool whether or not to process the next stage. this is important as the control function is called recursively.
364 public function process_stage_confirm() {
365 global $CFG, $DB, $OUTPUT;
367 $previous = $DB->get_records(
370 'userid' => $this->user
->id
,
371 'portfolio' => $this->instance
->get('id'),
372 'caller_sha1' => $this->caller
->get_sha1(),
375 if (isset($this->noexportconfig
) && empty($previous)) {
378 $strconfirm = get_string('confirmexport', 'portfolio');
379 $baseurl = $CFG->wwwroot
. '/portfolio/add.php?sesskey=' . sesskey() . '&id=' . $this->get('id');
380 $yesurl = $baseurl . '&stage=' . PORTFOLIO_STAGE_QUEUEORWAIT
;
381 $nourl = $baseurl . '&cancel=1';
382 $this->print_header(get_string('confirmexport', 'portfolio'));
383 echo $OUTPUT->box_start();
384 echo $OUTPUT->heading(get_string('confirmsummary', 'portfolio'), 3);
385 $mainsummary = array();
386 if (!$this->instance
->get_export_config('hideformat')) {
387 $mainsummary[get_string('selectedformat', 'portfolio')] = get_string('format_' . $this->instance
->get_export_config('format'), 'portfolio');
389 if (!$this->instance
->get_export_config('hidewait')) {
390 $mainsummary[get_string('selectedwait', 'portfolio')] = get_string(($this->instance
->get_export_config('wait') ?
'yes' : 'no'));
394 foreach ($previous as $row) {
395 $previousstr .= userdate($row->time
);
396 if ($row->caller_class
!= get_class($this->caller
)) {
397 if (!empty($row->caller_file
)) {
398 portfolio_include_callback_file($row->caller_file
);
399 } else if (!empty($row->caller_component
)) {
400 portfolio_include_callback_file($row->caller_component
);
401 } else { // Ok, that's weird - this should never happen. Is the apocalypse coming?
404 $previousstr .= ' (' . call_user_func(array($row->caller_class
, 'display_name')) . ')';
406 $previousstr .= '<br />';
408 $mainsummary[get_string('exportedpreviously', 'portfolio')] = $previousstr;
410 if (!$csummary = $this->caller
->get_export_summary()) {
413 if (!$isummary = $this->instance
->get_export_summary()) {
416 $mainsummary = array_merge($mainsummary, $csummary, $isummary);
417 $table = new html_table();
418 $table->attributes
['class'] = 'generaltable exportsummary';
419 $table->data
= array();
420 foreach ($mainsummary as $string => $value) {
421 $table->data
[] = array($string, $value);
423 echo html_writer
::table($table);
424 echo $OUTPUT->confirm($strconfirm, $yesurl, $nourl);
425 echo $OUTPUT->box_end();
426 echo $OUTPUT->footer();
431 * Processes the 'queueornext' stage of the export
433 * @return bool whether or not to process the next stage. this is important as the control function is called recursively.
435 public function process_stage_queueorwait() {
438 $wait = $this->instance
->get_export_config('wait');
440 $DB->set_field('portfolio_tempdata', 'queued', 1, array('id' => $this->id
));
441 $this->queued
= true;
442 return $this->process_stage_finished(true);
448 * Processes the 'package' stage of the export
450 * @return bool whether or not to process the next stage. this is important as the control function is called recursively.
451 * @throws portfolio_export_exception
453 public function process_stage_package() {
454 // now we've agreed on a format,
455 // the caller is given control to package it up however it wants
456 // and then the portfolio plugin is given control to do whatever it wants.
458 $this->caller
->prepare_package();
459 } catch (portfolio_exception
$e) {
460 throw new portfolio_export_exception($this, 'callercouldnotpackage', 'portfolio', null, $e->getMessage());
462 catch (file_exception
$e) {
463 throw new portfolio_export_exception($this, 'callercouldnotpackage', 'portfolio', null, $e->getMessage());
466 $this->instance
->prepare_package();
468 catch (portfolio_exception
$e) {
469 throw new portfolio_export_exception($this, 'plugincouldnotpackage', 'portfolio', null, $e->getMessage());
471 catch (file_exception
$e) {
472 throw new portfolio_export_exception($this, 'plugincouldnotpackage', 'portfolio', null, $e->getMessage());
478 * Processes the 'cleanup' stage of the export
480 * @param bool $pullok normally cleanup is deferred for pull plugins until after the file is requested from portfolio/file.php
481 * if you want to clean up earlier, pass true here (defaults to false)
482 * @return bool whether or not to process the next stage. this is important as the control function is called recursively.
484 public function process_stage_cleanup($pullok=false) {
487 if (!$pullok && $this->get('instance') && !$this->get('instance')->is_push()) {
490 if ($this->get('instance')) {
491 // might not be set - before export really starts
492 $this->get('instance')->cleanup();
494 $DB->delete_records('portfolio_tempdata', array('id' => $this->id
));
495 $fs = get_file_storage();
496 $fs->delete_area_files(SYSCONTEXTID
, 'portfolio', 'exporter', $this->id
);
497 $this->deleted
= true;
502 * Processes the 'send' stage of the export
504 * @return bool whether or not to process the next stage. this is important as the control function is called recursively.
506 public function process_stage_send() {
509 $this->instance
->send_package();
511 catch (portfolio_plugin_exception
$e) {
512 // not catching anything more general here. plugins with dependencies on other libraries that throw exceptions should catch and rethrow.
514 throw new portfolio_export_exception($this, 'failedtosendpackage', 'portfolio', null, $e->getMessage());
516 // only log push types, pull happens in send_file
517 if ($this->get('instance')->is_push()) {
518 $this->log_transfer();
526 * this should only be called after the file has been sent
527 * either via push, or sent from a pull request.
529 public function log_transfer() {
532 'userid' => $this->user
->id
,
533 'portfolio' => $this->instance
->get('id'),
535 'caller_component' => $this->callercomponent
,
536 'caller_sha1' => $this->caller
->get_sha1(),
537 'caller_class' => get_class($this->caller
),
538 'continueurl' => $this->instance
->get_static_continue_url(),
539 'returnurl' => $this->caller
->get_return_url(),
540 'tempdataid' => $this->id
,
543 $DB->insert_record('portfolio_log', $l);
547 * In some cases (mahara) we need to update this after the log has been done
548 * because of MDL-20872
550 * @param string $url link to be recorded to portfolio log
552 public function update_log_url($url) {
554 $DB->set_field('portfolio_log', 'continueurl', $url, array('tempdataid' => $this->id
));
558 * Processes the 'finish' stage of the export
560 * @param bool $queued let the process to be queued
561 * @return bool whether or not to process the next stage. this is important as the control function is called recursively.
563 public function process_stage_finished($queued=false) {
565 $returnurl = $this->caller
->get_return_url();
566 $continueurl = $this->instance
->get_interactive_continue_url();
567 $extras = $this->instance
->get_extra_finish_options();
569 $key = 'exportcomplete';
570 if ($queued ||
$this->forcequeue
) {
571 $key = 'exportqueued';
572 if ($this->forcequeue
) {
573 $key = 'exportqueuedforced';
576 $this->print_header(get_string($key, 'portfolio'), false);
577 self
::print_finish_info($returnurl, $continueurl, $extras);
578 echo $OUTPUT->footer();
584 * Local print header function to be reused across the export
586 * @param string $headingstr full language string
587 * @param bool $summary (optional) to print summary, default is set to true
590 public function print_header($headingstr, $summary=true) {
591 global $OUTPUT, $PAGE;
592 $titlestr = get_string('exporting', 'portfolio');
593 $headerstr = get_string('exporting', 'portfolio');
595 $PAGE->set_title($titlestr);
596 $PAGE->set_heading($headerstr);
597 echo $OUTPUT->header();
598 echo $OUTPUT->heading($headingstr);
604 echo $OUTPUT->box_start();
605 echo $OUTPUT->box_start();
606 echo $this->caller
->heading_summary();
607 echo $OUTPUT->box_end();
608 if ($this->instance
) {
609 echo $OUTPUT->box_start();
610 echo $this->instance
->heading_summary();
611 echo $OUTPUT->box_end();
613 echo $OUTPUT->box_end();
617 * Cancels a potfolio request and cleans up the tempdata
618 * and redirects the user back to where they started
620 * @param bool $logreturn options to return to porfolio log or caller return page
624 public function cancel_request($logreturn=false) {
629 $this->process_stage_cleanup(true);
631 redirect($CFG->wwwroot
. '/user/portfoliologs.php');
633 redirect($this->caller
->get_return_url());
638 * Writes out the contents of this object and all its data to the portfolio_tempdata table and sets the 'id' field.
642 public function save() {
644 if (empty($this->id
)) {
646 'data' => base64_encode(serialize($this)),
647 'expirytime' => time() +
(60*60*24),
648 'userid' => $this->user
->id
,
649 'instance' => (empty($this->instance
)) ?
null : $this->instance
->get('id'),
651 $this->id
= $DB->insert_record('portfolio_tempdata', $r);
652 $this->expirytime
= $r->expirytime
;
653 $this->save(); // call again so that id gets added to the save data.
655 if (!$r = $DB->get_record('portfolio_tempdata', array('id' => $this->id
))) {
656 if (!$this->deleted
) {
657 //debugging("tried to save current object, but failed - see MDL-20872");
661 $r->data
= base64_encode(serialize($this));
662 $r->instance
= (empty($this->instance
)) ?
null : $this->instance
->get('id');
663 $DB->update_record('portfolio_tempdata', $r);
668 * Rewakens the data from the database given the id.
669 * Makes sure to load the required files with the class definitions
671 * @param int $id id of data
672 * @return portfolio_exporter
674 public static function rewaken_object($id) {
676 require_once($CFG->libdir
. '/filelib.php');
677 require_once($CFG->libdir
. '/portfolio/exporter.php');
678 require_once($CFG->libdir
. '/portfolio/caller.php');
679 require_once($CFG->libdir
. '/portfolio/plugin.php');
680 if (!$data = $DB->get_record('portfolio_tempdata', array('id' => $id))) {
681 // maybe it's been finished already by a pull plugin
682 // so look in the logs
683 if ($log = $DB->get_record('portfolio_log', array('tempdataid' => $id))) {
684 self
::print_cleaned_export($log);
686 throw new portfolio_exception('invalidtempid', 'portfolio');
688 $exporter = unserialize(base64_decode($data->data
));
689 if ($exporter->instancefile
) {
690 require_once($CFG->dirroot
. '/' . $exporter->instancefile
);
692 if (!empty($exporter->callerfile
)) {
693 portfolio_include_callback_file($exporter->callerfile
);
694 } else if (!empty($exporter->callercomponent
)) {
695 portfolio_include_callback_file($exporter->callercomponent
);
697 return; // Should never get here!
700 $exporter = unserialize(serialize($exporter));
701 if (!$exporter->get('id')) {
702 // workaround for weird case
703 // where the id doesn't get saved between a new insert
704 // and the subsequent call that sets this field in the serialised data
705 $exporter->set('id', $id);
712 * Helper function to create the beginnings of a file_record object
713 * to create a new file in the portfolio_temporary working directory.
714 * Use write_new_file or copy_existing_file externally
715 * @see write_new_file
716 * @see copy_existing_file
718 * @param string $name filename of new record
721 private function new_file_record_base($name) {
722 return (object)array_merge($this->get_base_filearea(), array(
729 * Verifies a rewoken object.
730 * Checks to make sure it belongs to the same user and session as is currently in use.
732 * @param bool $readonly if we're reawakening this for a user to just display in the log view, don't verify the sessionkey
733 * @throws portfolio_exception
735 public function verify_rewaken($readonly=false) {
737 if ($this->get('user')->id
!= $USER->id
) { // make sure it belongs to the right user
738 throw new portfolio_exception('notyours', 'portfolio');
740 if (!$readonly && $this->get('instance') && !$this->get('instance')->allows_multiple_exports()) {
741 $already = portfolio_existing_exports($this->get('user')->id
, $this->get('instance')->get('plugin'));
742 $already = array_keys($already);
744 if (array_shift($already) != $this->get('id')) {
747 'plugin' => $this->get('instance')->get('plugin'),
748 'link' => $CFG->wwwroot
. '/user/portfoliologs.php',
750 throw new portfolio_exception('nomultipleexports', 'portfolio', '', $a);
753 if (!$this->caller
->check_permissions()) { // recall the caller permission check
754 throw new portfolio_caller_exception('nopermissions', 'portfolio', $this->caller
->get_return_url());
758 * Copies a file from somewhere else in moodle
759 * to the portfolio temporary working directory
760 * associated with this export
762 * @param stored_file $oldfile existing stored file object
763 * @return stored_file|bool new file object
765 public function copy_existing_file($oldfile) {
766 if (array_key_exists($oldfile->get_contenthash(), $this->newfilehashes
)) {
767 return $this->newfilehashes
[$oldfile->get_contenthash()];
769 $fs = get_file_storage();
770 $file_record = $this->new_file_record_base($oldfile->get_filename());
771 if ($dir = $this->get('format')->get_file_directory()) {
772 $file_record->filepath
= '/'. $dir . '/';
775 $newfile = $fs->create_file_from_storedfile($file_record, $oldfile->get_id());
776 $this->newfilehashes
[$newfile->get_contenthash()] = $newfile;
778 } catch (file_exception
$e) {
784 * Writes out some content to a file
785 * in the portfolio temporary working directory
786 * associated with this export.
788 * @param string $content content to write
789 * @param string $name filename to use
790 * @param bool $manifest whether this is the main file or an secondary file (eg attachment)
791 * @return stored_file
793 public function write_new_file($content, $name, $manifest=true) {
794 $fs = get_file_storage();
795 $file_record = $this->new_file_record_base($name);
796 if (empty($manifest) && ($dir = $this->get('format')->get_file_directory())) {
797 $file_record->filepath
= '/' . $dir . '/';
799 return $fs->create_file_from_string($file_record, $content);
803 * Zips all files in the temporary directory
805 * @param string $filename name of resulting zipfile (optional, defaults to portfolio-export.zip)
806 * @param string $filepath subpath in the filearea (optional, defaults to final)
807 * @return stored_file|bool resulting stored_file object, or false
809 public function zip_tempfiles($filename='portfolio-export.zip', $filepath='/final/') {
810 $zipper = new zip_packer();
812 list ($contextid, $component, $filearea, $itemid) = array_values($this->get_base_filearea());
813 if ($newfile = $zipper->archive_to_storage($this->get_tempfiles(), $contextid, $component, $filearea, $itemid, $filepath, $filename, $this->user
->id
)) {
821 * Returns an arary of files in the temporary working directory
823 * Always use this instead of the files api directly
825 * @param string $skipfile name of the file to be skipped
826 * @return array of stored_file objects keyed by name
828 public function get_tempfiles($skipfile='portfolio-export.zip') {
829 $fs = get_file_storage();
830 $files = $fs->get_area_files(SYSCONTEXTID
, 'portfolio', 'exporter', $this->id
, 'sortorder, itemid, filepath, filename', false);
834 $returnfiles = array();
835 foreach ($files as $f) {
836 if ($f->get_filename() == $skipfile) {
839 $returnfiles[$f->get_filepath() . $f->get_filename()] = $f;
845 * Returns the context, filearea, and itemid.
846 * Parts of a filearea (not filepath) to be used by
847 * plugins if they want to do things like zip up the contents of
848 * the temp area to here, or something that can't be done just using
849 * write_new_file, copy_existing_file or get_tempfiles
851 * @return array contextid, filearea, itemid are the keys.
853 public function get_base_filearea() {
855 'contextid' => SYSCONTEXTID
,
856 'component' => 'portfolio',
857 'filearea' => 'exporter',
858 'itemid' => $this->id
,
863 * Wrapper function to print a friendly error to users
864 * This is generally caused by them hitting an expired transfer
865 * through the usage of the backbutton
869 public static function print_expired_export() {
870 global $CFG, $OUTPUT, $PAGE;
871 $title = get_string('exportexpired', 'portfolio');
872 $PAGE->navbar
->add(get_string('exportexpired', 'portfolio'));
873 $PAGE->set_title($title);
874 $PAGE->set_heading($title);
875 echo $OUTPUT->header();
876 echo $OUTPUT->notification(get_string('exportexpireddesc', 'portfolio'));
877 echo $OUTPUT->continue_button($CFG->wwwroot
);
878 echo $OUTPUT->footer();
883 * Wrapper function to print a friendly error to users
885 * @param stdClass $log portfolio_log object
886 * @param portfolio_plugin_base $instance portfolio instance
889 public static function print_cleaned_export($log, $instance=null) {
890 global $CFG, $OUTPUT, $PAGE;
891 if (empty($instance) ||
!$instance instanceof portfolio_plugin_base
) {
892 $instance = portfolio_instance($log->portfolio
);
894 $title = get_string('exportalreadyfinished', 'portfolio');
895 $PAGE->navbar
->add($title);
896 $PAGE->set_title($title);
897 $PAGE->set_heading($title);
898 echo $OUTPUT->header();
899 echo $OUTPUT->notification(get_string('exportalreadyfinished', 'portfolio'));
900 self
::print_finish_info($log->returnurl
, $instance->resolve_static_continue_url($log->continueurl
));
901 echo $OUTPUT->continue_button($CFG->wwwroot
);
902 echo $OUTPUT->footer();
907 * Wrapper function to print continue and/or return link
909 * @param string $returnurl link to previos page
910 * @param string $continueurl continue to next page
911 * @param array $extras (optional) other links to be display.
913 public static function print_finish_info($returnurl, $continueurl, $extras=null) {
915 echo '<a href="' . $returnurl . '">' . get_string('returntowhereyouwere', 'portfolio') . '</a><br />';
918 echo '<a href="' . $continueurl . '">' . get_string('continuetoportfolio', 'portfolio') . '</a><br />';
920 if (is_array($extras)) {
921 foreach ($extras as $link => $string) {
922 echo '<a href="' . $link . '">' . $string . '</a><br />';