|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 <?php 00029 defined('MOODLE_INTERNAL') || die(); 00030 00041 class portfolio_exporter { 00042 00046 private $caller; 00047 00050 private $instance; 00051 00055 private $noexportconfig; 00056 00061 private $user; 00062 00067 public $instancefile; 00068 00074 public $callerfile; 00075 00079 private $stage; 00080 00085 private $forcequeue; 00086 00092 private $id; 00093 00097 private $alreadystolen; 00098 00104 private $newfilehashes; 00105 00110 private $format; 00111 00115 private $queued = false; 00116 00120 private $expirytime; 00121 00126 private $deleted = false; 00127 00135 public function __construct(&$instance, &$caller, $callerfile) { 00136 $this->instance =& $instance; 00137 $this->caller =& $caller; 00138 if ($instance) { 00139 $this->instancefile = 'portfolio/' . $instance->get('plugin') . '/lib.php'; 00140 $this->instance->set('exporter', $this); 00141 } 00142 $this->callerfile = $callerfile; 00143 $this->stage = PORTFOLIO_STAGE_CONFIG; 00144 $this->caller->set('exporter', $this); 00145 $this->alreadystolen = array(); 00146 $this->newfilehashes = array(); 00147 } 00148 00149 /* 00150 * generic getter for properties belonging to this instance 00151 * <b>outside</b> the subclasses 00152 * like name, visible etc. 00153 */ 00154 public function get($field) { 00155 if ($field == 'format') { 00156 return portfolio_format_object($this->format); 00157 } else if ($field == 'formatclass') { 00158 return $this->format; 00159 } 00160 if (property_exists($this, $field)) { 00161 return $this->{$field}; 00162 } 00163 $a = (object)array('property' => $field, 'class' => get_class($this)); 00164 throw new portfolio_export_exception($this, 'invalidproperty', 'portfolio', null, $a); 00165 } 00166 00172 public function set($field, &$value) { 00173 if (property_exists($this, $field)) { 00174 $this->{$field} =& $value; 00175 if ($field == 'instance') { 00176 $this->instancefile = 'portfolio/' . $this->instance->get('plugin') . '/lib.php'; 00177 $this->instance->set('exporter', $this); 00178 } 00179 $this->dirty = true; 00180 return true; 00181 } 00182 $a = (object)array('property' => $field, 'class' => get_class($this)); 00183 throw new portfolio_export_exception($this, 'invalidproperty', 'portfolio', null, $a); 00184 00185 } 00186 00193 public function set_forcequeue() { 00194 $this->forcequeue = true; 00195 } 00196 00205 public function process_stage($stage, $alreadystolen=false) { 00206 $this->set('stage', $stage); 00207 if ($alreadystolen) { 00208 $this->alreadystolen[$stage] = true; 00209 } else { 00210 if (!array_key_exists($stage, $this->alreadystolen)) { 00211 $this->alreadystolen[$stage] = false; 00212 } 00213 } 00214 if (!$this->alreadystolen[$stage] && $url = $this->instance->steal_control($stage)) { 00215 $this->save(); 00216 redirect($url); // does not return 00217 } else { 00218 $this->save(); 00219 } 00220 00221 $waiting = $this->instance->get_export_config('wait'); 00222 if ($stage > PORTFOLIO_STAGE_QUEUEORWAIT && empty($waiting)) { 00223 $stage = PORTFOLIO_STAGE_FINISHED; 00224 } 00225 $functionmap = array( 00226 PORTFOLIO_STAGE_CONFIG => 'config', 00227 PORTFOLIO_STAGE_CONFIRM => 'confirm', 00228 PORTFOLIO_STAGE_QUEUEORWAIT => 'queueorwait', 00229 PORTFOLIO_STAGE_PACKAGE => 'package', 00230 PORTFOLIO_STAGE_CLEANUP => 'cleanup', 00231 PORTFOLIO_STAGE_SEND => 'send', 00232 PORTFOLIO_STAGE_FINISHED => 'finished' 00233 ); 00234 00235 $function = 'process_stage_' . $functionmap[$stage]; 00236 try { 00237 if ($this->$function()) { 00238 // if we get through here it means control was returned 00239 // as opposed to wanting to stop processing 00240 // eg to wait for user input. 00241 $this->save(); 00242 $stage++; 00243 return $this->process_stage($stage); 00244 } else { 00245 $this->save(); 00246 return false; 00247 } 00248 } catch (portfolio_caller_exception $e) { 00249 portfolio_export_rethrow_exception($this, $e); 00250 } catch (portfolio_plugin_exception $e) { 00251 portfolio_export_rethrow_exception($this, $e); 00252 } catch (portfolio_export_exception $e) { 00253 throw $e; 00254 } catch (Exception $e) { 00255 debugging(get_string('thirdpartyexception', 'portfolio', get_class($e))); 00256 debugging($e); 00257 portfolio_export_rethrow_exception($this, $e); 00258 } 00259 } 00260 00266 public function instance() { 00267 return $this->instance; 00268 } 00269 00275 public function caller() { 00276 return $this->caller; 00277 } 00278 00284 public function process_stage_config() { 00285 global $OUTPUT, $CFG; 00286 $pluginobj = $callerobj = null; 00287 if ($this->instance->has_export_config()) { 00288 $pluginobj = $this->instance; 00289 } 00290 if ($this->caller->has_export_config()) { 00291 $callerobj = $this->caller; 00292 } 00293 $formats = portfolio_supported_formats_intersect($this->caller->supported_formats(), $this->instance->supported_formats()); 00294 $expectedtime = $this->instance->expected_time($this->caller->expected_time()); 00295 if (count($formats) == 0) { 00296 // something went wrong, we should not have gotten this far. 00297 throw new portfolio_export_exception($this, 'nocommonformats', 'portfolio', null, array('location' => get_class($this->caller), 'formats' => implode(',', $formats))); 00298 } 00299 // even if neither plugin or caller wants any config, we have to let the user choose their format, and decide to wait. 00300 if ($pluginobj || $callerobj || count($formats) > 1 || ($expectedtime != PORTFOLIO_TIME_LOW && $expectedtime != PORTFOLIO_TIME_FORCEQUEUE)) { 00301 $customdata = array( 00302 'instance' => $this->instance, 00303 'id' => $this->id, 00304 'plugin' => $pluginobj, 00305 'caller' => $callerobj, 00306 'userid' => $this->user->id, 00307 'formats' => $formats, 00308 'expectedtime' => $expectedtime, 00309 ); 00310 require_once($CFG->libdir . '/portfolio/forms.php'); 00311 $mform = new portfolio_export_form('', $customdata); 00312 if ($mform->is_cancelled()){ 00313 $this->cancel_request(); 00314 } else if ($fromform = $mform->get_data()){ 00315 if (!confirm_sesskey()) { 00316 throw new portfolio_export_exception($this, 'confirmsesskeybad'); 00317 } 00318 $pluginbits = array(); 00319 $callerbits = array(); 00320 foreach ($fromform as $key => $value) { 00321 if (strpos($key, 'plugin_') === 0) { 00322 $pluginbits[substr($key, 7)] = $value; 00323 } else if (strpos($key, 'caller_') === 0) { 00324 $callerbits[substr($key, 7)] = $value; 00325 } 00326 } 00327 $callerbits['format'] = $pluginbits['format'] = $fromform->format; 00328 $pluginbits['wait'] = $fromform->wait; 00329 if ($expectedtime == PORTFOLIO_TIME_LOW) { 00330 $pluginbits['wait'] = 1; 00331 $pluginbits['hidewait'] = 1; 00332 } else if ($expectedtime == PORTFOLIO_TIME_FORCEQUEUE) { 00333 $pluginbits['wait'] = 0; 00334 $pluginbits['hidewait'] = 1; 00335 $this->forcequeue = true; 00336 } 00337 $callerbits['hideformat'] = $pluginbits['hideformat'] = (count($formats) == 1); 00338 $this->caller->set_export_config($callerbits); 00339 $this->instance->set_export_config($pluginbits); 00340 $this->set('format', $fromform->format); 00341 return true; 00342 } else { 00343 $this->print_header(get_string('configexport', 'portfolio')); 00344 echo $OUTPUT->box_start(); 00345 $mform->display(); 00346 echo $OUTPUT->box_end(); 00347 echo $OUTPUT->footer(); 00348 return false;; 00349 } 00350 } else { 00351 $this->noexportconfig = true; 00352 $format = array_shift($formats); 00353 $config = array( 00354 'hidewait' => 1, 00355 'wait' => (($expectedtime == PORTFOLIO_TIME_LOW) ? 1 : 0), 00356 'format' => $format, 00357 'hideformat' => 1 00358 ); 00359 $this->set('format', $format); 00360 $this->instance->set_export_config($config); 00361 $this->caller->set_export_config(array('format' => $format, 'hideformat' => 1)); 00362 if ($expectedtime == PORTFOLIO_TIME_FORCEQUEUE) { 00363 $this->forcequeue = true; 00364 } 00365 return true; 00366 // do not break - fall through to confirm 00367 } 00368 } 00369 00375 public function process_stage_confirm() { 00376 global $CFG, $DB, $OUTPUT; 00377 00378 $previous = $DB->get_records( 00379 'portfolio_log', 00380 array( 00381 'userid' => $this->user->id, 00382 'portfolio' => $this->instance->get('id'), 00383 'caller_sha1' => $this->caller->get_sha1(), 00384 ) 00385 ); 00386 if (isset($this->noexportconfig) && empty($previous)) { 00387 return true; 00388 } 00389 $strconfirm = get_string('confirmexport', 'portfolio'); 00390 $baseurl = $CFG->wwwroot . '/portfolio/add.php?sesskey=' . sesskey() . '&id=' . $this->get('id'); 00391 $yesurl = $baseurl . '&stage=' . PORTFOLIO_STAGE_QUEUEORWAIT; 00392 $nourl = $baseurl . '&cancel=1'; 00393 $this->print_header(get_string('confirmexport', 'portfolio')); 00394 echo $OUTPUT->box_start(); 00395 echo $OUTPUT->heading(get_string('confirmsummary', 'portfolio'), 3); 00396 $mainsummary = array(); 00397 if (!$this->instance->get_export_config('hideformat')) { 00398 $mainsummary[get_string('selectedformat', 'portfolio')] = get_string('format_' . $this->instance->get_export_config('format'), 'portfolio'); 00399 } 00400 if (!$this->instance->get_export_config('hidewait')) { 00401 $mainsummary[get_string('selectedwait', 'portfolio')] = get_string(($this->instance->get_export_config('wait') ? 'yes' : 'no')); 00402 } 00403 if ($previous) { 00404 $previousstr = ''; 00405 foreach ($previous as $row) { 00406 $previousstr .= userdate($row->time); 00407 if ($row->caller_class != get_class($this->caller)) { 00408 require_once($CFG->dirroot . '/' . $row->caller_file); 00409 $previousstr .= ' (' . call_user_func(array($row->caller_class, 'display_name')) . ')'; 00410 } 00411 $previousstr .= '<br />'; 00412 } 00413 $mainsummary[get_string('exportedpreviously', 'portfolio')] = $previousstr; 00414 } 00415 if (!$csummary = $this->caller->get_export_summary()) { 00416 $csummary = array(); 00417 } 00418 if (!$isummary = $this->instance->get_export_summary()) { 00419 $isummary = array(); 00420 } 00421 $mainsummary = array_merge($mainsummary, $csummary, $isummary); 00422 $table = new html_table(); 00423 $table->attributes['class'] = 'generaltable exportsummary'; 00424 $table->data = array(); 00425 foreach ($mainsummary as $string => $value) { 00426 $table->data[] = array($string, $value); 00427 } 00428 echo html_writer::table($table); 00429 echo $OUTPUT->confirm($strconfirm, $yesurl, $nourl); 00430 echo $OUTPUT->box_end(); 00431 echo $OUTPUT->footer(); 00432 return false; 00433 } 00434 00440 public function process_stage_queueorwait() { 00441 $wait = $this->instance->get_export_config('wait'); 00442 if (empty($wait)) { 00443 events_trigger('portfolio_send', $this->id); 00444 $this->queued = true; 00445 return $this->process_stage_finished(true); 00446 } 00447 return true; 00448 } 00449 00455 public function process_stage_package() { 00456 // now we've agreed on a format, 00457 // the caller is given control to package it up however it wants 00458 // and then the portfolio plugin is given control to do whatever it wants. 00459 try { 00460 $this->caller->prepare_package(); 00461 } catch (portfolio_exception $e) { 00462 throw new portfolio_export_exception($this, 'callercouldnotpackage', 'portfolio', null, $e->getMessage()); 00463 } 00464 catch (file_exception $e) { 00465 throw new portfolio_export_exception($this, 'callercouldnotpackage', 'portfolio', null, $e->getMessage()); 00466 } 00467 try { 00468 $this->instance->prepare_package(); 00469 } 00470 catch (portfolio_exception $e) { 00471 throw new portfolio_export_exception($this, 'plugincouldnotpackage', 'portfolio', null, $e->getMessage()); 00472 } 00473 catch (file_exception $e) { 00474 throw new portfolio_export_exception($this, 'plugincouldnotpackage', 'portfolio', null, $e->getMessage()); 00475 } 00476 return true; 00477 } 00478 00486 public function process_stage_cleanup($pullok=false) { 00487 global $CFG, $DB; 00488 00489 if (!$pullok && $this->get('instance') && !$this->get('instance')->is_push()) { 00490 return true; 00491 } 00492 if ($this->get('instance')) { 00493 // might not be set - before export really starts 00494 $this->get('instance')->cleanup(); 00495 } 00496 $DB->delete_records('portfolio_tempdata', array('id' => $this->id)); 00497 $fs = get_file_storage(); 00498 $fs->delete_area_files(SYSCONTEXTID, 'portfolio', 'exporter', $this->id); 00499 $this->deleted = true; 00500 return true; 00501 } 00502 00508 public function process_stage_send() { 00509 // send the file 00510 try { 00511 $this->instance->send_package(); 00512 } 00513 catch (portfolio_plugin_exception $e) { 00514 // not catching anything more general here. plugins with dependencies on other libraries that throw exceptions should catch and rethrow. 00515 // eg curl exception 00516 throw new portfolio_export_exception($this, 'failedtosendpackage', 'portfolio', null, $e->getMessage()); 00517 } 00518 // only log push types, pull happens in send_file 00519 if ($this->get('instance')->is_push()) { 00520 $this->log_transfer(); 00521 } 00522 return true; 00523 } 00524 00530 public function log_transfer() { 00531 global $DB; 00532 $l = array( 00533 'userid' => $this->user->id, 00534 'portfolio' => $this->instance->get('id'), 00535 'caller_file' => $this->callerfile, 00536 'caller_sha1' => $this->caller->get_sha1(), 00537 'caller_class' => get_class($this->caller), 00538 'continueurl' => $this->instance->get_static_continue_url(), 00539 'returnurl' => $this->caller->get_return_url(), 00540 'tempdataid' => $this->id, 00541 'time' => time(), 00542 ); 00543 $DB->insert_record('portfolio_log', $l); 00544 } 00545 00550 public function update_log_url($url) { 00551 global $DB; 00552 $DB->set_field('portfolio_log', 'continueurl', $url, array('tempdataid' => $this->id)); 00553 } 00554 00560 public function process_stage_finished($queued=false) { 00561 global $OUTPUT; 00562 $returnurl = $this->caller->get_return_url(); 00563 $continueurl = $this->instance->get_interactive_continue_url(); 00564 $extras = $this->instance->get_extra_finish_options(); 00565 00566 $key = 'exportcomplete'; 00567 if ($queued || $this->forcequeue) { 00568 $key = 'exportqueued'; 00569 if ($this->forcequeue) { 00570 $key = 'exportqueuedforced'; 00571 } 00572 } 00573 $this->print_header(get_string($key, 'portfolio'), false); 00574 self::print_finish_info($returnurl, $continueurl, $extras); 00575 echo $OUTPUT->footer(); 00576 return false; 00577 } 00578 00579 00585 public function print_header($headingstr, $summary=true) { 00586 global $OUTPUT, $PAGE; 00587 $titlestr = get_string('exporting', 'portfolio'); 00588 $headerstr = get_string('exporting', 'portfolio'); 00589 00590 $PAGE->set_title($titlestr); 00591 $PAGE->set_heading($headerstr); 00592 echo $OUTPUT->header(); 00593 echo $OUTPUT->heading($headingstr); 00594 00595 if (!$summary) { 00596 return; 00597 } 00598 00599 echo $OUTPUT->box_start(); 00600 echo $OUTPUT->box_start(); 00601 echo $this->caller->heading_summary(); 00602 echo $OUTPUT->box_end(); 00603 if ($this->instance) { 00604 echo $OUTPUT->box_start(); 00605 echo $this->instance->heading_summary(); 00606 echo $OUTPUT->box_end(); 00607 } 00608 echo $OUTPUT->box_end(); 00609 } 00610 00615 public function cancel_request($logreturn=false) { 00616 global $CFG; 00617 if (!isset($this)) { 00618 return; 00619 } 00620 $this->process_stage_cleanup(true); 00621 if ($logreturn) { 00622 redirect($CFG->wwwroot . '/user/portfoliologs.php'); 00623 } 00624 redirect($this->caller->get_return_url()); 00625 exit; 00626 } 00627 00631 public function save() { 00632 global $DB; 00633 if (empty($this->id)) { 00634 $r = (object)array( 00635 'data' => base64_encode(serialize($this)), 00636 'expirytime' => time() + (60*60*24), 00637 'userid' => $this->user->id, 00638 'instance' => (empty($this->instance)) ? null : $this->instance->get('id'), 00639 ); 00640 $this->id = $DB->insert_record('portfolio_tempdata', $r); 00641 $this->expirytime = $r->expirytime; 00642 $this->save(); // call again so that id gets added to the save data. 00643 } else { 00644 if (!$r = $DB->get_record('portfolio_tempdata', array('id' => $this->id))) { 00645 if (!$this->deleted) { 00646 //debugging("tried to save current object, but failed - see MDL-20872"); 00647 } 00648 return; 00649 } 00650 $r->data = base64_encode(serialize($this)); 00651 $r->instance = (empty($this->instance)) ? null : $this->instance->get('id'); 00652 $DB->update_record('portfolio_tempdata', $r); 00653 } 00654 } 00655 00664 public static function rewaken_object($id) { 00665 global $DB, $CFG; 00666 require_once($CFG->libdir . '/filelib.php'); 00667 require_once($CFG->libdir . '/portfolio/exporter.php'); 00668 require_once($CFG->libdir . '/portfolio/caller.php'); 00669 require_once($CFG->libdir . '/portfolio/plugin.php'); 00670 if (!$data = $DB->get_record('portfolio_tempdata', array('id' => $id))) { 00671 // maybe it's been finished already by a pull plugin 00672 // so look in the logs 00673 if ($log = $DB->get_record('portfolio_log', array('tempdataid' => $id))) { 00674 self::print_cleaned_export($log); 00675 } 00676 throw new portfolio_exception('invalidtempid', 'portfolio'); 00677 } 00678 $exporter = unserialize(base64_decode($data->data)); 00679 if ($exporter->instancefile) { 00680 require_once($CFG->dirroot . '/' . $exporter->instancefile); 00681 } 00682 require_once($CFG->dirroot . '/' . $exporter->callerfile); 00683 $exporter = unserialize(serialize($exporter)); 00684 if (!$exporter->get('id')) { 00685 // workaround for weird case 00686 // where the id doesn't get saved between a new insert 00687 // and the subsequent call that sets this field in the serialised data 00688 $exporter->set('id', $id); 00689 $exporter->save(); 00690 } 00691 return $exporter; 00692 } 00693 00701 private function new_file_record_base($name) { 00702 return (object)array_merge($this->get_base_filearea(), array( 00703 'filepath' => '/', 00704 'filename' => $name, 00705 )); 00706 } 00707 00718 public function verify_rewaken($readonly=false) { 00719 global $USER, $CFG; 00720 if ($this->get('user')->id != $USER->id) { // make sure it belongs to the right user 00721 throw new portfolio_exception('notyours', 'portfolio'); 00722 } 00723 if (!$readonly && $this->get('instance') && !$this->get('instance')->allows_multiple_exports() 00724 && ($already = portfolio_existing_exports($this->get('user')->id, $this->get('instance')->get('plugin'))) 00725 && array_shift(array_keys($already)) != $this->get('id') 00726 ) { 00727 $a = (object)array( 00728 'plugin' => $this->get('instance')->get('plugin'), 00729 'link' => $CFG->wwwroot . '/user/portfoliologs.php', 00730 ); 00731 throw new portfolio_exception('nomultipleexports', 'portfolio', '', $a); 00732 } 00733 if (!$this->caller->check_permissions()) { // recall the caller permission check 00734 throw new portfolio_caller_exception('nopermissions', 'portfolio', $this->caller->get_return_url()); 00735 } 00736 } 00745 public function copy_existing_file($oldfile) { 00746 if (array_key_exists($oldfile->get_contenthash(), $this->newfilehashes)) { 00747 return $this->newfilehashes[$oldfile->get_contenthash()]; 00748 } 00749 $fs = get_file_storage(); 00750 $file_record = $this->new_file_record_base($oldfile->get_filename()); 00751 if ($dir = $this->get('format')->get_file_directory()) { 00752 $file_record->filepath = '/'. $dir . '/'; 00753 } 00754 try { 00755 $newfile = $fs->create_file_from_storedfile($file_record, $oldfile->get_id()); 00756 $this->newfilehashes[$newfile->get_contenthash()] = $newfile; 00757 return $newfile; 00758 } catch (file_exception $e) { 00759 return false; 00760 } 00761 } 00762 00773 public function write_new_file($content, $name, $manifest=true) { 00774 $fs = get_file_storage(); 00775 $file_record = $this->new_file_record_base($name); 00776 if (empty($manifest) && ($dir = $this->get('format')->get_file_directory())) { 00777 $file_record->filepath = '/' . $dir . '/'; 00778 } 00779 return $fs->create_file_from_string($file_record, $content); 00780 } 00781 00790 public function zip_tempfiles($filename='portfolio-export.zip', $filepath='/final/') { 00791 $zipper = new zip_packer(); 00792 00793 list ($contextid, $component, $filearea, $itemid) = array_values($this->get_base_filearea()); 00794 if ($newfile = $zipper->archive_to_storage($this->get_tempfiles(), $contextid, $component, $filearea, $itemid, $filepath, $filename, $this->user->id)) { 00795 return $newfile; 00796 } 00797 return false; 00798 00799 } 00800 00808 public function get_tempfiles($skipfile='portfolio-export.zip') { 00809 $fs = get_file_storage(); 00810 $files = $fs->get_area_files(SYSCONTEXTID, 'portfolio', 'exporter', $this->id, '', false); 00811 if (empty($files)) { 00812 return array(); 00813 } 00814 $returnfiles = array(); 00815 foreach ($files as $f) { 00816 if ($f->get_filename() == $skipfile) { 00817 continue; 00818 } 00819 $returnfiles[$f->get_filepath() . '/' . $f->get_filename()] = $f; 00820 } 00821 return $returnfiles; 00822 } 00823 00833 public function get_base_filearea() { 00834 return array( 00835 'contextid' => SYSCONTEXTID, 00836 'component' => 'portfolio', 00837 'filearea' => 'exporter', 00838 'itemid' => $this->id, 00839 ); 00840 } 00841 00847 public static function print_expired_export() { 00848 global $CFG, $OUTPUT, $PAGE; 00849 $title = get_string('exportexpired', 'portfolio'); 00850 $PAGE->navbar->add(get_string('exportexpired', 'portfolio')); 00851 $PAGE->set_title($title); 00852 $PAGE->set_heading($title); 00853 echo $OUTPUT->header(); 00854 echo $OUTPUT->notification(get_string('exportexpireddesc', 'portfolio')); 00855 echo $OUTPUT->continue_button($CFG->wwwroot); 00856 echo $OUTPUT->footer(); 00857 exit; 00858 } 00859 00860 public static function print_cleaned_export($log, $instance=null) { 00861 global $CFG, $OUTPUT, $PAGE; 00862 if (empty($instance) || !$instance instanceof portfolio_plugin_base) { 00863 $instance = portfolio_instance($log->portfolio); 00864 } 00865 $title = get_string('exportalreadyfinished', 'portfolio'); 00866 $PAGE->navbar->add($title); 00867 $PAGE->set_title($title); 00868 $PAGE->set_heading($title); 00869 echo $OUTPUT->header(); 00870 echo $OUTPUT->notification(get_string('exportalreadyfinished', 'portfolio')); 00871 self::print_finish_info($log->returnurl, $instance->resolve_static_continue_url($log->continueurl)); 00872 echo $OUTPUT->continue_button($CFG->wwwroot); 00873 echo $OUTPUT->footer(); 00874 exit; 00875 } 00876 00877 public static function print_finish_info($returnurl, $continueurl, $extras=null) { 00878 if ($returnurl) { 00879 echo '<a href="' . $returnurl . '">' . get_string('returntowhereyouwere', 'portfolio') . '</a><br />'; 00880 } 00881 if ($continueurl) { 00882 echo '<a href="' . $continueurl . '">' . get_string('continuetoportfolio', 'portfolio') . '</a><br />'; 00883 } 00884 if (is_array($extras)) { 00885 foreach ($extras as $link => $string) { 00886 echo '<a href="' . $link . '">' . $string . '</a><br />'; 00887 } 00888 } 00889 } 00890 }