Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body