AutorÃa | Ultima modificación | Ver Log |
<?php// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** This file contains the class definition for the exporter object.** @package core_portfolio* @copyright 2008 Penny Leach <penny@catalyst.net.nz>* Martin Dougiamas <http://dougiamas.com>* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/defined('MOODLE_INTERNAL') || die();/*** The class that handles the various stages of the actual export* and the communication between the caller and the portfolio plugin.** This is stored in the database between page requests in serialized base64 encoded form* also contains helper methods for the plugin and caller to use (at the end of the file)* @see get_base_filearea - where to write files to* @see write_new_file - write some content to a file in the export filearea* @see copy_existing_file - copy an existing file into the export filearea* @see get_tempfiles - return list of all files in the export filearea** @package core_portfolio* @category portfolio* @copyright 2008 Penny Leach <penny@catalyst.net.nz>* Martin Dougiamas <http://dougiamas.com>* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class portfolio_exporter {/** @var portfolio_caller_base the caller object used during the export */private $caller;/** @var portfolio_plugin_base the portfolio plugin instanced used during the export */private $instance;/** @var bool if there has been no config form displayed to the user */private $noexportconfig;/*** @var stdClass the user currently exporting content always $USER,* but more conveniently placed here*/private $user;/*** @var string the file to include that contains the class defintion of* the portfolio instance plugin used to re-waken the object after sleep*/public $instancefile;/*** @var string the component that contains the class definition of* the caller object used to re-waken the object after sleep*/public $callercomponent;/** @var int the current stage of the export */private $stage;/** @var bool whether something (usually the portfolio plugin) has forced queuing */private $forcequeue;/*** @var int id of this export matches record in portfolio_tempdata table* and used for itemid for file storage.*/private $id;/** @var array of stages that have had the portfolio plugin already steal control from them */private $alreadystolen;/*** @var stored_file[] files that the exporter has written to this temp area keep track of* this in case of duplicates within one export see MDL-16390*/private $newfilehashes;/*** @var string selected exportformat this is also set in* export_config in the portfolio and caller classes*/private $format;/** @var bool queued - this is set after the event is triggered */private $queued = false;/** @var int expiry time - set the first time the object is saved out */private $expirytime;/*** @var bool deleted - this is set during the cleanup routine so* that subsequent save() calls can detect it*/private $deleted = false;/*** Construct a new exporter for use** @param portfolio_plugin_base $instance portfolio instance (passed by reference)* @param portfolio_caller_base $caller portfolio caller (passed by reference)* @param string $callercomponent the name of the callercomponent*/public function __construct($instance, portfolio_caller_base $caller, $callercomponent) {$this->instance = $instance;$this->caller = $caller;if ($instance) {$this->instancefile = 'portfolio/' . $instance->get('plugin') . '/lib.php';$this->instance->set('exporter', $this);}$this->callercomponent = $callercomponent;$this->stage = PORTFOLIO_STAGE_CONFIG;$this->caller->set('exporter', $this);$this->alreadystolen = array();$this->newfilehashes = array();}/*** Generic getter for properties belonging to this instance* <b>outside</b> the subclasses like name, visible etc.** @param string $field property's name* @return portfolio_format|mixed*/public function get($field) {if ($field == 'format') {return portfolio_format_object($this->format);} else if ($field == 'formatclass') {return $this->format;}if (property_exists($this, $field)) {return $this->{$field};}$a = (object)array('property' => $field, 'class' => get_class($this));throw new portfolio_export_exception($this, 'invalidproperty', 'portfolio', null, $a);}/*** Generic setter for properties belonging to this instance* <b>outside</b> the subclass like name, visible, etc.** @param string $field property's name* @param mixed $value property's value* @return bool* @throws portfolio_export_exception*/public function set($field, &$value) {if (property_exists($this, $field)) {$this->{$field} =& $value;if ($field == 'instance') {$this->instancefile = 'portfolio/' . $this->instance->get('plugin') . '/lib.php';$this->instance->set('exporter', $this);}return true;}$a = (object)array('property' => $field, 'class' => get_class($this));throw new portfolio_export_exception($this, 'invalidproperty', 'portfolio', null, $a);}/*** Sets this export to force queued.* Sometimes plugins need to set this randomly* if an external system changes its mind* about what's supported*/public function set_forcequeue() {$this->forcequeue = true;}/*** Process the given stage calling whatever functions are necessary** @param int $stage (see PORTFOLIO_STAGE_* constants)* @param bool $alreadystolen used to avoid letting plugins steal control twice.* @return bool whether or not to process the next stage. this is important as the function is called recursively.*/public function process_stage($stage, $alreadystolen=false) {$this->set('stage', $stage);if ($alreadystolen) {$this->alreadystolen[$stage] = true;} else {if (!array_key_exists($stage, $this->alreadystolen)) {$this->alreadystolen[$stage] = false;}}if (!$this->alreadystolen[$stage] && $url = $this->instance->steal_control($stage)) {$this->save();redirect($url); // does not return} else {$this->save();}$waiting = $this->instance->get_export_config('wait');if ($stage > PORTFOLIO_STAGE_QUEUEORWAIT && empty($waiting)) {$stage = PORTFOLIO_STAGE_FINISHED;}$functionmap = array(PORTFOLIO_STAGE_CONFIG => 'config',PORTFOLIO_STAGE_CONFIRM => 'confirm',PORTFOLIO_STAGE_QUEUEORWAIT => 'queueorwait',PORTFOLIO_STAGE_PACKAGE => 'package',PORTFOLIO_STAGE_CLEANUP => 'cleanup',PORTFOLIO_STAGE_SEND => 'send',PORTFOLIO_STAGE_FINISHED => 'finished');$function = 'process_stage_' . $functionmap[$stage];try {if ($this->$function()) {// if we get through here it means control was returned// as opposed to wanting to stop processing// eg to wait for user input.$this->save();$stage++;return $this->process_stage($stage);} else {$this->save();return false;}} catch (portfolio_caller_exception $e) {portfolio_export_rethrow_exception($this, $e);} catch (portfolio_plugin_exception $e) {portfolio_export_rethrow_exception($this, $e);} catch (portfolio_export_exception $e) {throw $e;} catch (Exception $e) {debugging(get_string('thirdpartyexception', 'portfolio', get_class($e)));debugging($e);portfolio_export_rethrow_exception($this, $e);}}/*** Helper function to return the portfolio instance** @return portfolio_plugin_base subclass*/public function instance() {return $this->instance;}/*** Helper function to return the caller object** @return portfolio_caller_base subclass*/public function caller() {return $this->caller;}/*** Processes the 'config' stage of the export** @return bool whether or not to process the next stage. this is important as the control function is called recursively.*/public function process_stage_config() {global $OUTPUT, $CFG;$pluginobj = $callerobj = null;if ($this->instance->has_export_config()) {$pluginobj = $this->instance;}if ($this->caller->has_export_config()) {$callerobj = $this->caller;}$formats = portfolio_supported_formats_intersect($this->caller->supported_formats(), $this->instance->supported_formats());$expectedtime = $this->instance->expected_time($this->caller->expected_time());if (count($formats) == 0) {// something went wrong, we should not have gotten this far.throw new portfolio_export_exception($this, 'nocommonformats', 'portfolio', null, array('location' => get_class($this->caller), 'formats' => implode(',', $formats)));}// even if neither plugin or caller wants any config, we have to let the user choose their format, and decide to wait.if ($pluginobj || $callerobj || count($formats) > 1 || ($expectedtime != PORTFOLIO_TIME_LOW && $expectedtime != PORTFOLIO_TIME_FORCEQUEUE)) {$customdata = array('instance' => $this->instance,'id' => $this->id,'plugin' => $pluginobj,'caller' => $callerobj,'userid' => $this->user->id,'formats' => $formats,'expectedtime' => $expectedtime,);require_once($CFG->libdir . '/portfolio/forms.php');$mform = new portfolio_export_form('', $customdata);if ($mform->is_cancelled()){$this->cancel_request();} else if ($fromform = $mform->get_data()){if (!confirm_sesskey()) {throw new portfolio_export_exception($this, 'confirmsesskeybad');}$pluginbits = array();$callerbits = array();foreach ($fromform as $key => $value) {if (strpos($key, 'plugin_') === 0) {$pluginbits[substr($key, 7)] = $value;} else if (strpos($key, 'caller_') === 0) {$callerbits[substr($key, 7)] = $value;}}$callerbits['format'] = $pluginbits['format'] = $fromform->format;$pluginbits['wait'] = $fromform->wait;if ($expectedtime == PORTFOLIO_TIME_LOW) {$pluginbits['wait'] = 1;$pluginbits['hidewait'] = 1;} else if ($expectedtime == PORTFOLIO_TIME_FORCEQUEUE) {$pluginbits['wait'] = 0;$pluginbits['hidewait'] = 1;$this->forcequeue = true;}$callerbits['hideformat'] = $pluginbits['hideformat'] = (count($formats) == 1);$this->caller->set_export_config($callerbits);$this->instance->set_export_config($pluginbits);$this->set('format', $fromform->format);return true;} else {$this->print_header(get_string('configexport', 'portfolio'));echo $OUTPUT->box_start();$mform->display();echo $OUTPUT->box_end();echo $OUTPUT->footer();return false;}} else {$this->noexportconfig = true;$format = array_shift($formats);$config = array('hidewait' => 1,'wait' => (($expectedtime == PORTFOLIO_TIME_LOW) ? 1 : 0),'format' => $format,'hideformat' => 1);$this->set('format', $format);$this->instance->set_export_config($config);$this->caller->set_export_config(array('format' => $format, 'hideformat' => 1));if ($expectedtime == PORTFOLIO_TIME_FORCEQUEUE) {$this->forcequeue = true;}return true;// do not break - fall through to confirm}}/*** Processes the 'confirm' stage of the export** @return bool whether or not to process the next stage. this is important as the control function is called recursively.*/public function process_stage_confirm() {global $CFG, $DB, $OUTPUT;$previous = $DB->get_records('portfolio_log',array('userid' => $this->user->id,'portfolio' => $this->instance->get('id'),'caller_sha1' => $this->caller->get_sha1(),));if (isset($this->noexportconfig) && empty($previous)) {return true;}$strconfirm = get_string('confirmexport', 'portfolio');$baseurl = $CFG->wwwroot . '/portfolio/add.php?sesskey=' . sesskey() . '&id=' . $this->get('id');$yesurl = $baseurl . '&stage=' . PORTFOLIO_STAGE_QUEUEORWAIT;$nourl = $baseurl . '&cancel=1';$this->print_header(get_string('confirmexport', 'portfolio'));echo $OUTPUT->box_start();echo $OUTPUT->heading(get_string('confirmsummary', 'portfolio'), 3);$mainsummary = array();if (!$this->instance->get_export_config('hideformat')) {$mainsummary[get_string('selectedformat', 'portfolio')] = get_string('format_' . $this->instance->get_export_config('format'), 'portfolio');}if (!$this->instance->get_export_config('hidewait')) {$mainsummary[get_string('selectedwait', 'portfolio')] = get_string(($this->instance->get_export_config('wait') ? 'yes' : 'no'));}if ($previous) {$previousstr = '';foreach ($previous as $row) {$previousstr .= userdate($row->time);if ($row->caller_class != get_class($this->caller)) {if (!empty($row->caller_file)) {portfolio_include_callback_file($row->caller_file);} else if (!empty($row->caller_component)) {portfolio_include_callback_file($row->caller_component);} else { // Ok, that's weird - this should never happen. Is the apocalypse coming?continue;}$previousstr .= ' (' . call_user_func(array($row->caller_class, 'display_name')) . ')';}$previousstr .= '<br />';}$mainsummary[get_string('exportedpreviously', 'portfolio')] = $previousstr;}if (!$csummary = $this->caller->get_export_summary()) {$csummary = array();}if (!$isummary = $this->instance->get_export_summary()) {$isummary = array();}$mainsummary = array_merge($mainsummary, $csummary, $isummary);$table = new html_table();$table->attributes['class'] = 'generaltable exportsummary';$table->data = array();foreach ($mainsummary as $string => $value) {$table->data[] = array($string, $value);}echo html_writer::table($table);echo $OUTPUT->confirm($strconfirm, $yesurl, $nourl);echo $OUTPUT->box_end();echo $OUTPUT->footer();return false;}/*** Processes the 'queueornext' stage of the export** @return bool whether or not to process the next stage. this is important as the control function is called recursively.*/public function process_stage_queueorwait() {global $DB;$wait = $this->instance->get_export_config('wait');if (empty($wait)) {$DB->set_field('portfolio_tempdata', 'queued', 1, array('id' => $this->id));$this->queued = true;return $this->process_stage_finished(true);}return true;}/*** Processes the 'package' stage of the export** @return bool whether or not to process the next stage. this is important as the control function is called recursively.* @throws portfolio_export_exception*/public function process_stage_package() {// now we've agreed on a format,// the caller is given control to package it up however it wants// and then the portfolio plugin is given control to do whatever it wants.try {$this->caller->prepare_package();} catch (portfolio_exception $e) {throw new portfolio_export_exception($this, 'callercouldnotpackage', 'portfolio', null, $e->getMessage());}catch (file_exception $e) {throw new portfolio_export_exception($this, 'callercouldnotpackage', 'portfolio', null, $e->getMessage());}try {$this->instance->prepare_package();}catch (portfolio_exception $e) {throw new portfolio_export_exception($this, 'plugincouldnotpackage', 'portfolio', null, $e->getMessage());}catch (file_exception $e) {throw new portfolio_export_exception($this, 'plugincouldnotpackage', 'portfolio', null, $e->getMessage());}return true;}/*** Processes the 'cleanup' stage of the export** @param bool $pullok normally cleanup is deferred for pull plugins until after the file is requested from portfolio/file.php* if you want to clean up earlier, pass true here (defaults to false)* @return bool whether or not to process the next stage. this is important as the control function is called recursively.*/public function process_stage_cleanup($pullok=false) {global $CFG, $DB;if (!$pullok && $this->get('instance') && !$this->get('instance')->is_push()) {return true;}if ($this->get('instance')) {// might not be set - before export really starts$this->get('instance')->cleanup();}$DB->delete_records('portfolio_tempdata', array('id' => $this->id));$fs = get_file_storage();$fs->delete_area_files(SYSCONTEXTID, 'portfolio', 'exporter', $this->id);$this->deleted = true;return true;}/*** Processes the 'send' stage of the export** @return bool whether or not to process the next stage. this is important as the control function is called recursively.*/public function process_stage_send() {// send the filetry {$this->instance->send_package();}catch (portfolio_plugin_exception $e) {// not catching anything more general here. plugins with dependencies on other libraries that throw exceptions should catch and rethrow.// eg curl exceptionthrow new portfolio_export_exception($this, 'failedtosendpackage', 'portfolio', null, $e->getMessage());}// only log push types, pull happens in send_fileif ($this->get('instance')->is_push()) {$this->log_transfer();}return true;}/*** Log the transfer** this should only be called after the file has been sent* either via push, or sent from a pull request.*/public function log_transfer() {global $DB;$l = array('userid' => $this->user->id,'portfolio' => $this->instance->get('id'),'caller_file'=> '','caller_component' => $this->callercomponent,'caller_sha1' => $this->caller->get_sha1(),'caller_class' => get_class($this->caller),'continueurl' => $this->instance->get_static_continue_url(),'returnurl' => $this->caller->get_return_url(),'tempdataid' => $this->id,'time' => time(),);$DB->insert_record('portfolio_log', $l);}/*** In some cases (mahara) we need to update this after the log has been done* because of MDL-20872** @param string $url link to be recorded to portfolio log*/public function update_log_url($url) {global $DB;$DB->set_field('portfolio_log', 'continueurl', $url, array('tempdataid' => $this->id));}/*** Processes the 'finish' stage of the export** @param bool $queued let the process to be queued* @return bool whether or not to process the next stage. this is important as the control function is called recursively.*/public function process_stage_finished($queued=false) {global $OUTPUT;$returnurl = $this->caller->get_return_url();$continueurl = $this->instance->get_interactive_continue_url();$extras = $this->instance->get_extra_finish_options();$key = 'exportcomplete';if ($queued || $this->forcequeue) {$key = 'exportqueued';if ($this->forcequeue) {$key = 'exportqueuedforced';}}$this->print_header(get_string($key, 'portfolio'), false);self::print_finish_info($returnurl, $continueurl, $extras);echo $OUTPUT->footer();return false;}/*** Local print header function to be reused across the export** @param string $headingstr full language string* @param bool $summary (optional) to print summary, default is set to true* @return void*/public function print_header($headingstr, $summary=true) {global $OUTPUT, $PAGE;$titlestr = get_string('exporting', 'portfolio');$headerstr = get_string('exporting', 'portfolio');$PAGE->set_title($titlestr);$PAGE->set_heading($headerstr);echo $OUTPUT->header();echo $OUTPUT->heading($headingstr);if (!$summary) {return;}echo $OUTPUT->box_start();echo $OUTPUT->box_start();echo $this->caller->heading_summary();echo $OUTPUT->box_end();if ($this->instance) {echo $OUTPUT->box_start();echo $this->instance->heading_summary();echo $OUTPUT->box_end();}echo $OUTPUT->box_end();}/*** Cancels a potfolio request and cleans up the tempdata* and redirects the user back to where they started** @param bool $logreturn options to return to porfolio log or caller return page* @return void* @uses exit*/public function cancel_request($logreturn=false) {global $CFG;if (!isset($this)) {return;}$this->process_stage_cleanup(true);if ($logreturn) {redirect($CFG->wwwroot . '/user/portfoliologs.php');}redirect($this->caller->get_return_url());exit;}/*** Writes out the contents of this object and all its data to the portfolio_tempdata table and sets the 'id' field.** @return void*/public function save() {global $DB;if (empty($this->id)) {$r = (object)array('data' => base64_encode(serialize($this)),'expirytime' => time() + (60*60*24),'userid' => $this->user->id,'instance' => (empty($this->instance)) ? null : $this->instance->get('id'),);$this->id = $DB->insert_record('portfolio_tempdata', $r);$this->expirytime = $r->expirytime;$this->save(); // call again so that id gets added to the save data.} else {if (!$r = $DB->get_record('portfolio_tempdata', array('id' => $this->id))) {if (!$this->deleted) {//debugging("tried to save current object, but failed - see MDL-20872");}return;}$r->data = base64_encode(serialize($this));$r->instance = (empty($this->instance)) ? null : $this->instance->get('id');$DB->update_record('portfolio_tempdata', $r);}}/*** Rewakens the data from the database given the id.* Makes sure to load the required files with the class definitions** @param int $id id of data* @return portfolio_exporter*/public static function rewaken_object($id) {global $DB, $CFG;require_once($CFG->libdir . '/filelib.php');require_once($CFG->libdir . '/portfolio/exporter.php');require_once($CFG->libdir . '/portfolio/caller.php');require_once($CFG->libdir . '/portfolio/plugin.php');if (!$data = $DB->get_record('portfolio_tempdata', array('id' => $id))) {// maybe it's been finished already by a pull plugin// so look in the logsif ($log = $DB->get_record('portfolio_log', array('tempdataid' => $id))) {self::print_cleaned_export($log);}throw new portfolio_exception('invalidtempid', 'portfolio');}$exporter = unserialize(base64_decode($data->data));if ($exporter->instancefile) {require_once($CFG->dirroot . '/' . $exporter->instancefile);}if (!empty($exporter->callerfile)) {portfolio_include_callback_file($exporter->callerfile);} else if (!empty($exporter->callercomponent)) {portfolio_include_callback_file($exporter->callercomponent);} else {return; // Should never get here!}$exporter = unserialize(serialize($exporter));if (!$exporter->get('id')) {// workaround for weird case// where the id doesn't get saved between a new insert// and the subsequent call that sets this field in the serialised data$exporter->set('id', $id);$exporter->save();}return $exporter;}/*** Helper function to create the beginnings of a file_record object* to create a new file in the portfolio_temporary working directory.* Use write_new_file or copy_existing_file externally* @see write_new_file* @see copy_existing_file** @param string $name filename of new record* @return object*/private function new_file_record_base($name) {return (object)array_merge($this->get_base_filearea(), array('filepath' => '/','filename' => $name,));}/*** Verifies a rewoken object.* Checks to make sure it belongs to the same user and session as is currently in use.** @param bool $readonly if we're reawakening this for a user to just display in the log view, don't verify the sessionkey* @throws portfolio_exception*/public function verify_rewaken($readonly=false) {global $USER, $CFG;if ($this->get('user')->id != $USER->id) { // make sure it belongs to the right userthrow new portfolio_exception('notyours', 'portfolio');}if (!$readonly && $this->get('instance') && !$this->get('instance')->allows_multiple_exports()) {$already = portfolio_existing_exports($this->get('user')->id, $this->get('instance')->get('plugin'));$already = array_keys($already);if (array_shift($already) != $this->get('id')) {$a = (object)array('plugin' => $this->get('instance')->get('plugin'),'link' => $CFG->wwwroot . '/user/portfoliologs.php',);throw new portfolio_exception('nomultipleexports', 'portfolio', '', $a);}}if (!$this->caller->check_permissions()) { // recall the caller permission checkthrow new portfolio_caller_exception('nopermissions', 'portfolio', $this->caller->get_return_url());}}/*** Copies a file from somewhere else in moodle* to the portfolio temporary working directory* associated with this export** @param stored_file $oldfile existing stored file object* @return stored_file|bool new file object*/public function copy_existing_file($oldfile) {if (array_key_exists($oldfile->get_contenthash(), $this->newfilehashes)) {return $this->newfilehashes[$oldfile->get_contenthash()];}$fs = get_file_storage();$file_record = $this->new_file_record_base($oldfile->get_filename());if ($dir = $this->get('format')->get_file_directory()) {$file_record->filepath = '/'. $dir . '/';}try {$newfile = $fs->create_file_from_storedfile($file_record, $oldfile->get_id());$this->newfilehashes[$newfile->get_contenthash()] = $newfile;return $newfile;} catch (file_exception $e) {return false;}}/*** Writes out some content to a file* in the portfolio temporary working directory* associated with this export.** @param string $content content to write* @param string $name filename to use* @param bool $manifest whether this is the main file or an secondary file (eg attachment)* @return stored_file*/public function write_new_file($content, $name, $manifest=true) {$fs = get_file_storage();$file_record = $this->new_file_record_base($name);if (empty($manifest) && ($dir = $this->get('format')->get_file_directory())) {$file_record->filepath = '/' . $dir . '/';}return $fs->create_file_from_string($file_record, $content);}/*** Zips all files in the temporary directory** @param string $filename name of resulting zipfile (optional, defaults to portfolio-export.zip)* @param string $filepath subpath in the filearea (optional, defaults to final)* @return stored_file|bool resulting stored_file object, or false*/public function zip_tempfiles($filename='portfolio-export.zip', $filepath='/final/') {$zipper = new zip_packer();list ($contextid, $component, $filearea, $itemid) = array_values($this->get_base_filearea());if ($newfile = $zipper->archive_to_storage($this->get_tempfiles(), $contextid, $component, $filearea, $itemid, $filepath, $filename, $this->user->id)) {return $newfile;}return false;}/*** Returns an arary of files in the temporary working directory* for this export.* Always use this instead of the files api directly** @param string $skipfile name of the file to be skipped* @return array of stored_file objects keyed by name*/public function get_tempfiles($skipfile='portfolio-export.zip') {$fs = get_file_storage();$files = $fs->get_area_files(SYSCONTEXTID, 'portfolio', 'exporter', $this->id, 'sortorder, itemid, filepath, filename', false);if (empty($files)) {return array();}$returnfiles = array();foreach ($files as $f) {if ($f->get_filename() == $skipfile) {continue;}$returnfiles[$f->get_filepath() . $f->get_filename()] = $f;}return $returnfiles;}/*** Returns the context, filearea, and itemid.* Parts of a filearea (not filepath) to be used by* plugins if they want to do things like zip up the contents of* the temp area to here, or something that can't be done just using* write_new_file, copy_existing_file or get_tempfiles** @return array contextid, filearea, itemid are the keys.*/public function get_base_filearea() {return array('contextid' => SYSCONTEXTID,'component' => 'portfolio','filearea' => 'exporter','itemid' => $this->id,);}/*** Wrapper function to print a friendly error to users* This is generally caused by them hitting an expired transfer* through the usage of the backbutton** @uses exit*/public static function print_expired_export() {global $CFG, $OUTPUT, $PAGE;$title = get_string('exportexpired', 'portfolio');$PAGE->navbar->add(get_string('exportexpired', 'portfolio'));$PAGE->set_title($title);$PAGE->set_heading($title);echo $OUTPUT->header();echo $OUTPUT->notification(get_string('exportexpireddesc', 'portfolio'));echo $OUTPUT->continue_button($CFG->wwwroot);echo $OUTPUT->footer();exit;}/*** Wrapper function to print a friendly error to users** @param stdClass $log portfolio_log object* @param portfolio_plugin_base $instance portfolio instance* @uses exit*/public static function print_cleaned_export($log, $instance=null) {global $CFG, $OUTPUT, $PAGE;if (empty($instance) || !$instance instanceof portfolio_plugin_base) {$instance = portfolio_instance($log->portfolio);}$title = get_string('exportalreadyfinished', 'portfolio');$PAGE->navbar->add($title);$PAGE->set_title($title);$PAGE->set_heading($title);echo $OUTPUT->header();echo $OUTPUT->notification(get_string('exportalreadyfinished', 'portfolio'));self::print_finish_info($log->returnurl, $instance->resolve_static_continue_url($log->continueurl));echo $OUTPUT->continue_button($CFG->wwwroot);echo $OUTPUT->footer();exit;}/*** Wrapper function to print continue and/or return link** @param string $returnurl link to previos page* @param string $continueurl continue to next page* @param array $extras (optional) other links to be display.*/public static function print_finish_info($returnurl, $continueurl, $extras=null) {if ($returnurl) {echo '<a href="' . $returnurl . '">' . get_string('returntowhereyouwere', 'portfolio') . '</a><br />';}if ($continueurl) {echo '<a href="' . $continueurl . '">' . get_string('continuetoportfolio', 'portfolio') . '</a><br />';}if (is_array($extras)) {foreach ($extras as $link => $string) {echo '<a href="' . $link . '">' . $string . '</a><br />';}}}}