Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * This file contains the class definition for the exporter object.
  19   *
  20   * @package core_portfolio
  21   * @copyright 2008 Penny Leach <penny@catalyst.net.nz>
  22   *            Martin Dougiamas  <http://dougiamas.com>
  23   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  /**
  29   * The class that handles the various stages of the actual export
  30   * and the communication between the caller and the portfolio plugin.
  31   *
  32   * This is stored in the database between page requests in serialized base64 encoded form
  33   * also contains helper methods for the plugin and caller to use (at the end of the file)
  34   * @see get_base_filearea - where to write files to
  35   * @see write_new_file - write some content to a file in the export filearea
  36   * @see copy_existing_file - copy an existing file into the export filearea
  37   * @see get_tempfiles - return list of all files in the export filearea
  38   *
  39   * @package core_portfolio
  40   * @category portfolio
  41   * @copyright 2008 Penny Leach <penny@catalyst.net.nz>
  42   *            Martin Dougiamas  <http://dougiamas.com>
  43   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  44   */
  45  class portfolio_exporter {
  46  
  47      /** @var portfolio_caller_base the caller object used during the export */
  48      private $caller;
  49  
  50      /** @var portfolio_plugin_base the portfolio plugin instanced used during the export */
  51      private $instance;
  52  
  53      /** @var bool if there has been no config form displayed to the user */
  54      private $noexportconfig;
  55  
  56      /**
  57       * @var stdClass the user currently exporting content always $USER,
  58       *               but more conveniently placed here
  59       */
  60      private $user;
  61  
  62      /**
  63       * @var string the file to include that contains the class defintion of
  64       *             the portfolio instance plugin used to re-waken the object after sleep
  65       */
  66      public $instancefile;
  67  
  68      /**
  69       * @var string the component that contains the class definition of
  70       *             the caller object used to re-waken the object after sleep
  71       */
  72      public $callercomponent;
  73  
  74      /** @var int the current stage of the export */
  75      private $stage;
  76  
  77      /** @var bool whether something (usually the portfolio plugin) has forced queuing */
  78      private $forcequeue;
  79  
  80      /**
  81       * @var int id of this export matches record in portfolio_tempdata table
  82       *          and used for itemid for file storage.
  83       */
  84      private $id;
  85  
  86      /** @var array of stages that have had the portfolio plugin already steal control from them */
  87      private $alreadystolen;
  88  
  89      /**
  90       * @var stored_file[] files that the exporter has written to this temp area keep track of
  91       *                  this in case of duplicates within one export see MDL-16390
  92       */
  93      private $newfilehashes;
  94  
  95      /**
  96       * @var string selected exportformat this is also set in
  97       *             export_config in the portfolio and caller classes
  98       */
  99      private $format;
 100  
 101      /** @var bool queued - this is set after the event is triggered */
 102      private $queued = false;
 103  
 104      /** @var int expiry time - set the first time the object is saved out */
 105      private $expirytime;
 106  
 107      /**
 108       * @var bool deleted - this is set during the cleanup routine so
 109       *           that subsequent save() calls can detect it
 110       */
 111      private $deleted = false;
 112  
 113      /**
 114       * Construct a new exporter for use
 115       *
 116       * @param portfolio_plugin_base $instance portfolio instance (passed by reference)
 117       * @param portfolio_caller_base $caller portfolio caller (passed by reference)
 118       * @param string $callercomponent the name of the callercomponent
 119       */
 120      public function __construct($instance, portfolio_caller_base $caller, $callercomponent) {
 121          $this->instance = $instance;
 122          $this->caller = $caller;
 123          if ($instance) {
 124              $this->instancefile = 'portfolio/' . $instance->get('plugin') . '/lib.php';
 125              $this->instance->set('exporter', $this);
 126          }
 127          $this->callercomponent = $callercomponent;
 128          $this->stage = PORTFOLIO_STAGE_CONFIG;
 129          $this->caller->set('exporter', $this);
 130          $this->alreadystolen = array();
 131          $this->newfilehashes = array();
 132      }
 133  
 134      /**
 135       * Generic getter for properties belonging to this instance
 136       * <b>outside</b> the subclasses like name, visible etc.
 137       *
 138       * @param string $field property's name
 139       * @return portfolio_format|mixed
 140       */
 141      public function get($field) {
 142          if ($field == 'format') {
 143              return portfolio_format_object($this->format);
 144          } else if ($field == 'formatclass') {
 145              return $this->format;
 146          }
 147          if (property_exists($this, $field)) {
 148              return $this->{$field};
 149          }
 150          $a = (object)array('property' => $field, 'class' => get_class($this));
 151          throw new portfolio_export_exception($this, 'invalidproperty', 'portfolio', null, $a);
 152      }
 153  
 154      /**
 155       * Generic setter for properties belonging to this instance
 156       * <b>outside</b> the subclass like name, visible, etc.
 157       *
 158       * @param string $field property's name
 159       * @param mixed $value property's value
 160       * @return bool
 161       * @throws portfolio_export_exception
 162       */
 163      public function set($field, &$value) {
 164          if (property_exists($this, $field)) {
 165              $this->{$field} =& $value;
 166              if ($field == 'instance') {
 167                  $this->instancefile = 'portfolio/' . $this->instance->get('plugin') . '/lib.php';
 168                  $this->instance->set('exporter', $this);
 169              }
 170              return true;
 171          }
 172          $a = (object)array('property' => $field, 'class' => get_class($this));
 173          throw new portfolio_export_exception($this, 'invalidproperty', 'portfolio', null, $a);
 174  
 175      }
 176  
 177      /**
 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
 182       */
 183      public function set_forcequeue() {
 184          $this->forcequeue = true;
 185      }
 186  
 187      /**
 188       * Process the given stage calling whatever functions are necessary
 189       *
 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.
 193       */
 194      public function process_stage($stage, $alreadystolen=false) {
 195          $this->set('stage', $stage);
 196          if ($alreadystolen) {
 197              $this->alreadystolen[$stage] = true;
 198          } else {
 199              if (!array_key_exists($stage, $this->alreadystolen)) {
 200                  $this->alreadystolen[$stage] = false;
 201              }
 202          }
 203          if (!$this->alreadystolen[$stage] && $url = $this->instance->steal_control($stage)) {
 204              $this->save();
 205              redirect($url); // does not return
 206          } else {
 207              $this->save();
 208          }
 209  
 210          $waiting = $this->instance->get_export_config('wait');
 211          if ($stage > PORTFOLIO_STAGE_QUEUEORWAIT && empty($waiting)) {
 212              $stage = PORTFOLIO_STAGE_FINISHED;
 213          }
 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'
 222          );
 223  
 224          $function = 'process_stage_' . $functionmap[$stage];
 225          try {
 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.
 230                  $this->save();
 231                  $stage++;
 232                  return $this->process_stage($stage);
 233              } else {
 234                  $this->save();
 235                  return false;
 236              }
 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) {
 242              throw $e;
 243          } catch (Exception $e) {
 244              debugging(get_string('thirdpartyexception', 'portfolio', get_class($e)));
 245              debugging($e);
 246              portfolio_export_rethrow_exception($this, $e);
 247          }
 248      }
 249  
 250      /**
 251       * Helper function to return the portfolio instance
 252       *
 253       * @return portfolio_plugin_base subclass
 254       */
 255      public function instance() {
 256          return $this->instance;
 257      }
 258  
 259      /**
 260       * Helper function to return the caller object
 261       *
 262       * @return portfolio_caller_base subclass
 263       */
 264      public function caller() {
 265          return $this->caller;
 266      }
 267  
 268      /**
 269       * Processes the 'config' stage of the export
 270       *
 271       * @return bool whether or not to process the next stage. this is important as the control function is called recursively.
 272       */
 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;
 278          }
 279          if ($this->caller->has_export_config()) {
 280              $callerobj = $this->caller;
 281          }
 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)));
 287          }
 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)) {
 290              $customdata = array(
 291                  'instance' => $this->instance,
 292                  'id'       => $this->id,
 293                  'plugin' => $pluginobj,
 294                  'caller' => $callerobj,
 295                  'userid' => $this->user->id,
 296                  'formats' => $formats,
 297                  'expectedtime' => $expectedtime,
 298              );
 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');
 306                  }
 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;
 314                      }
 315                  }
 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;
 325                  }
 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);
 330                  return true;
 331              } else {
 332                  $this->print_header(get_string('configexport', 'portfolio'));
 333                  echo $OUTPUT->box_start();
 334                  $mform->display();
 335                  echo $OUTPUT->box_end();
 336                  echo $OUTPUT->footer();
 337                  return false;
 338              }
 339          } else {
 340              $this->noexportconfig = true;
 341              $format = array_shift($formats);
 342              $config = array(
 343                  'hidewait' => 1,
 344                  'wait' => (($expectedtime == PORTFOLIO_TIME_LOW) ? 1 : 0),
 345                  'format' => $format,
 346                  'hideformat' => 1
 347              );
 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;
 353              }
 354              return true;
 355              // do not break - fall through to confirm
 356          }
 357      }
 358  
 359      /**
 360       * Processes the 'confirm' stage of the export
 361       *
 362       * @return bool whether or not to process the next stage. this is important as the control function is called recursively.
 363       */
 364      public function process_stage_confirm() {
 365          global $CFG, $DB, $OUTPUT;
 366  
 367          $previous = $DB->get_records(
 368              'portfolio_log',
 369              array(
 370                  'userid'      => $this->user->id,
 371                  'portfolio'   => $this->instance->get('id'),
 372                  'caller_sha1' => $this->caller->get_sha1(),
 373              )
 374          );
 375          if (isset($this->noexportconfig) && empty($previous)) {
 376              return true;
 377          }
 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');
 388          }
 389          if (!$this->instance->get_export_config('hidewait')) {
 390              $mainsummary[get_string('selectedwait', 'portfolio')] = get_string(($this->instance->get_export_config('wait') ? 'yes' : 'no'));
 391          }
 392          if ($previous) {
 393              $previousstr = '';
 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?
 402                          continue;
 403                      }
 404                      $previousstr .= ' (' . call_user_func(array($row->caller_class, 'display_name')) . ')';
 405                  }
 406                  $previousstr .= '<br />';
 407              }
 408              $mainsummary[get_string('exportedpreviously', 'portfolio')] = $previousstr;
 409          }
 410          if (!$csummary = $this->caller->get_export_summary()) {
 411              $csummary = array();
 412          }
 413          if (!$isummary = $this->instance->get_export_summary()) {
 414              $isummary = array();
 415          }
 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);
 422          }
 423          echo html_writer::table($table);
 424          echo $OUTPUT->confirm($strconfirm, $yesurl, $nourl);
 425          echo $OUTPUT->box_end();
 426          echo $OUTPUT->footer();
 427          return false;
 428      }
 429  
 430      /**
 431       * Processes the 'queueornext' stage of the export
 432       *
 433       * @return bool whether or not to process the next stage. this is important as the control function is called recursively.
 434       */
 435      public function process_stage_queueorwait() {
 436          global $DB;
 437  
 438          $wait = $this->instance->get_export_config('wait');
 439          if (empty($wait)) {
 440              $DB->set_field('portfolio_tempdata', 'queued', 1, array('id' => $this->id));
 441              $this->queued = true;
 442              return $this->process_stage_finished(true);
 443          }
 444          return true;
 445      }
 446  
 447      /**
 448       * Processes the 'package' stage of the export
 449       *
 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
 452       */
 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.
 457          try {
 458              $this->caller->prepare_package();
 459          } catch (portfolio_exception $e) {
 460              throw new portfolio_export_exception($this, 'callercouldnotpackage', 'portfolio', null, $e->getMessage());
 461          }
 462          catch (file_exception $e) {
 463              throw new portfolio_export_exception($this, 'callercouldnotpackage', 'portfolio', null, $e->getMessage());
 464          }
 465          try {
 466              $this->instance->prepare_package();
 467          }
 468          catch (portfolio_exception $e) {
 469              throw new portfolio_export_exception($this, 'plugincouldnotpackage', 'portfolio', null, $e->getMessage());
 470          }
 471          catch (file_exception $e) {
 472              throw new portfolio_export_exception($this, 'plugincouldnotpackage', 'portfolio', null, $e->getMessage());
 473          }
 474          return true;
 475      }
 476  
 477      /**
 478       * Processes the 'cleanup' stage of the export
 479       *
 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.
 483       */
 484      public function process_stage_cleanup($pullok=false) {
 485          global $CFG, $DB;
 486  
 487          if (!$pullok && $this->get('instance') && !$this->get('instance')->is_push()) {
 488              return true;
 489          }
 490          if ($this->get('instance')) {
 491              // might not be set - before export really starts
 492              $this->get('instance')->cleanup();
 493          }
 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;
 498          return true;
 499      }
 500  
 501      /**
 502       * Processes the 'send' stage of the export
 503       *
 504       * @return bool whether or not to process the next stage. this is important as the control function is called recursively.
 505       */
 506      public function process_stage_send() {
 507          // send the file
 508          try {
 509              $this->instance->send_package();
 510          }
 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.
 513              // eg curl exception
 514              throw new portfolio_export_exception($this, 'failedtosendpackage', 'portfolio', null, $e->getMessage());
 515          }
 516          // only log push types, pull happens in send_file
 517          if ($this->get('instance')->is_push()) {
 518              $this->log_transfer();
 519          }
 520          return true;
 521      }
 522  
 523      /**
 524       * Log the transfer
 525       *
 526       * this should only be called after the file has been sent
 527       * either via push, or sent from a pull request.
 528       */
 529      public function log_transfer() {
 530          global $DB;
 531          $l = array(
 532              'userid' => $this->user->id,
 533              'portfolio' => $this->instance->get('id'),
 534              'caller_file'=> '',
 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,
 541              'time' => time(),
 542          );
 543          $DB->insert_record('portfolio_log', $l);
 544      }
 545  
 546      /**
 547       * In some cases (mahara) we need to update this after the log has been done
 548       * because of MDL-20872
 549       *
 550       * @param string $url link to be recorded to portfolio log
 551       */
 552      public function update_log_url($url) {
 553          global $DB;
 554          $DB->set_field('portfolio_log', 'continueurl', $url, array('tempdataid' => $this->id));
 555      }
 556  
 557      /**
 558       * Processes the 'finish' stage of the export
 559       *
 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.
 562       */
 563      public function process_stage_finished($queued=false) {
 564          global $OUTPUT;
 565          $returnurl = $this->caller->get_return_url();
 566          $continueurl = $this->instance->get_interactive_continue_url();
 567          $extras = $this->instance->get_extra_finish_options();
 568  
 569          $key = 'exportcomplete';
 570          if ($queued || $this->forcequeue) {
 571              $key = 'exportqueued';
 572              if ($this->forcequeue) {
 573                  $key = 'exportqueuedforced';
 574              }
 575          }
 576          $this->print_header(get_string($key, 'portfolio'), false);
 577          self::print_finish_info($returnurl, $continueurl, $extras);
 578          echo $OUTPUT->footer();
 579          return false;
 580      }
 581  
 582  
 583      /**
 584       * Local print header function to be reused across the export
 585       *
 586       * @param string $headingstr full language string
 587       * @param bool $summary (optional) to print summary, default is set to true
 588       * @return void
 589       */
 590      public function print_header($headingstr, $summary=true) {
 591          global $OUTPUT, $PAGE;
 592          $titlestr = get_string('exporting', 'portfolio');
 593          $headerstr = get_string('exporting', 'portfolio');
 594  
 595          $PAGE->set_title($titlestr);
 596          $PAGE->set_heading($headerstr);
 597          echo $OUTPUT->header();
 598          echo $OUTPUT->heading($headingstr);
 599  
 600          if (!$summary) {
 601              return;
 602          }
 603  
 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();
 612          }
 613          echo $OUTPUT->box_end();
 614      }
 615  
 616      /**
 617       * Cancels a potfolio request and cleans up the tempdata
 618       * and redirects the user back to where they started
 619       *
 620       * @param bool $logreturn options to return to porfolio log or caller return page
 621       * @return void
 622       * @uses exit
 623       */
 624      public function cancel_request($logreturn=false) {
 625          global $CFG;
 626          if (!isset($this)) {
 627              return;
 628          }
 629          $this->process_stage_cleanup(true);
 630          if ($logreturn) {
 631              redirect($CFG->wwwroot . '/user/portfoliologs.php');
 632          }
 633          redirect($this->caller->get_return_url());
 634          exit;
 635      }
 636  
 637      /**
 638       * Writes out the contents of this object and all its data to the portfolio_tempdata table and sets the 'id' field.
 639       *
 640       * @return void
 641       */
 642      public function save() {
 643          global $DB;
 644          if (empty($this->id)) {
 645              $r = (object)array(
 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'),
 650              );
 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.
 654          } else {
 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");
 658                  }
 659                  return;
 660              }
 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);
 664          }
 665      }
 666  
 667      /**
 668       * Rewakens the data from the database given the id.
 669       * Makes sure to load the required files with the class definitions
 670       *
 671       * @param int $id id of data
 672       * @return portfolio_exporter
 673       */
 674      public static function rewaken_object($id) {
 675          global $DB, $CFG;
 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);
 685              }
 686              throw new portfolio_exception('invalidtempid', 'portfolio');
 687          }
 688          $exporter = unserialize(base64_decode($data->data));
 689          if ($exporter->instancefile) {
 690              require_once($CFG->dirroot . '/' . $exporter->instancefile);
 691          }
 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);
 696          } else {
 697              return; // Should never get here!
 698          }
 699  
 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);
 706              $exporter->save();
 707          }
 708          return $exporter;
 709      }
 710  
 711      /**
 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
 717       *
 718       * @param string $name filename of new record
 719       * @return object
 720       */
 721      private function new_file_record_base($name) {
 722          return (object)array_merge($this->get_base_filearea(), array(
 723              'filepath' => '/',
 724              'filename' => $name,
 725          ));
 726      }
 727  
 728      /**
 729       * Verifies a rewoken object.
 730       * Checks to make sure it belongs to the same user and session as is currently in use.
 731       *
 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
 734       */
 735      public function verify_rewaken($readonly=false) {
 736          global $USER, $CFG;
 737          if ($this->get('user')->id != $USER->id) { // make sure it belongs to the right user
 738              throw new portfolio_exception('notyours', 'portfolio');
 739          }
 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);
 743  
 744              if (array_shift($already) != $this->get('id')) {
 745  
 746                  $a = (object)array(
 747                      'plugin'  => $this->get('instance')->get('plugin'),
 748                      'link'    => $CFG->wwwroot . '/user/portfoliologs.php',
 749                  );
 750                  throw new portfolio_exception('nomultipleexports', 'portfolio', '', $a);
 751              }
 752          }
 753          if (!$this->caller->check_permissions()) { // recall the caller permission check
 754              throw new portfolio_caller_exception('nopermissions', 'portfolio', $this->caller->get_return_url());
 755          }
 756      }
 757      /**
 758       * Copies a file from somewhere else in moodle
 759       * to the portfolio temporary working directory
 760       * associated with this export
 761       *
 762       * @param stored_file $oldfile existing stored file object
 763       * @return stored_file|bool new file object
 764       */
 765      public function copy_existing_file($oldfile) {
 766          if (array_key_exists($oldfile->get_contenthash(), $this->newfilehashes)) {
 767              return $this->newfilehashes[$oldfile->get_contenthash()];
 768          }
 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 . '/';
 773          }
 774          try {
 775              $newfile = $fs->create_file_from_storedfile($file_record, $oldfile->get_id());
 776              $this->newfilehashes[$newfile->get_contenthash()] = $newfile;
 777              return $newfile;
 778          } catch (file_exception $e) {
 779              return false;
 780          }
 781      }
 782  
 783      /**
 784       * Writes out some content to a file
 785       * in the portfolio temporary working directory
 786       * associated with this export.
 787       *
 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
 792       */
 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 . '/';
 798          }
 799          return $fs->create_file_from_string($file_record, $content);
 800      }
 801  
 802      /**
 803       * Zips all files in the temporary directory
 804       *
 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
 808       */
 809      public function zip_tempfiles($filename='portfolio-export.zip', $filepath='/final/') {
 810          $zipper = new zip_packer();
 811  
 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)) {
 814              return $newfile;
 815          }
 816          return false;
 817  
 818      }
 819  
 820      /**
 821       * Returns an arary of files in the temporary working directory
 822       * for this export.
 823       * Always use this instead of the files api directly
 824       *
 825       * @param string $skipfile name of the file to be skipped
 826       * @return array of stored_file objects keyed by name
 827       */
 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);
 831          if (empty($files)) {
 832              return array();
 833          }
 834          $returnfiles = array();
 835          foreach ($files as $f) {
 836              if ($f->get_filename() == $skipfile) {
 837                  continue;
 838              }
 839              $returnfiles[$f->get_filepath() . $f->get_filename()] = $f;
 840          }
 841          return $returnfiles;
 842      }
 843  
 844      /**
 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
 850       *
 851       * @return array contextid, filearea, itemid are the keys.
 852       */
 853      public function get_base_filearea() {
 854          return array(
 855              'contextid' => SYSCONTEXTID,
 856              'component' => 'portfolio',
 857              'filearea'  => 'exporter',
 858              'itemid'    => $this->id,
 859          );
 860      }
 861  
 862      /**
 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
 866       *
 867       * @uses exit
 868       */
 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();
 879          exit;
 880      }
 881  
 882      /**
 883       * Wrapper function to print a friendly error to users
 884       *
 885       * @param stdClass $log portfolio_log object
 886       * @param portfolio_plugin_base $instance portfolio instance
 887       * @uses exit
 888       */
 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);
 893          }
 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();
 903          exit;
 904      }
 905  
 906      /**
 907       * Wrapper function to print continue and/or return link
 908       *
 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.
 912       */
 913      public static function print_finish_info($returnurl, $continueurl, $extras=null) {
 914          if ($returnurl) {
 915              echo '<a href="' . $returnurl . '">' . get_string('returntowhereyouwere', 'portfolio') . '</a><br />';
 916          }
 917          if ($continueurl) {
 918              echo '<a href="' . $continueurl . '">' . get_string('continuetoportfolio', 'portfolio') . '</a><br />';
 919          }
 920          if (is_array($extras)) {
 921              foreach ($extras as $link => $string) {
 922                  echo '<a href="' . $link . '">' . $string . '</a><br />';
 923              }
 924          }
 925      }
 926  }