Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
}