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
 * Implementation of zip packer.
19
 *
20
 * @package   core_files
21
 * @copyright 2008 Petr Skoda (http://skodak.org)
22
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
defined('MOODLE_INTERNAL') || die();
26
 
27
require_once("$CFG->libdir/filestorage/file_packer.php");
28
require_once("$CFG->libdir/filestorage/zip_archive.php");
29
 
30
/**
31
 * Utility class - handles all zipping and unzipping operations.
32
 *
33
 * @package   core_files
34
 * @category  files
35
 * @copyright 2008 Petr Skoda (http://skodak.org)
36
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37
 */
38
class zip_packer extends file_packer {
39
 
40
    /**
41
     * Zip files and store the result in file storage.
42
     *
43
     * @param array $files array with full zip paths (including directory information)
44
     *              as keys (archivepath=>ospathname or archivepath/subdir=>stored_file or archivepath=>array('content_as_string'))
45
     * @param int $contextid context ID
46
     * @param string $component component
47
     * @param string $filearea file area
48
     * @param int $itemid item ID
49
     * @param string $filepath file path
50
     * @param string $filename file name
51
     * @param int $userid user ID
52
     * @param bool $ignoreinvalidfiles true means ignore missing or invalid files, false means abort on any error
53
     * @param file_progress $progress Progress indicator callback or null if not required
54
     * @return stored_file|bool false if error stored_file instance if ok
55
     */
56
    public function archive_to_storage(array $files, $contextid,
57
            $component, $filearea, $itemid, $filepath, $filename,
58
            $userid = NULL, $ignoreinvalidfiles=true, file_progress $progress = null) {
59
        global $CFG;
60
 
61
        $fs = get_file_storage();
62
 
63
        check_dir_exists($CFG->tempdir.'/zip');
64
        $tmpfile = tempnam($CFG->tempdir.'/zip', 'zipstor');
65
 
66
        if ($result = $this->archive_to_pathname($files, $tmpfile, $ignoreinvalidfiles, $progress)) {
67
            if ($file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
68
                if (!$file->delete()) {
69
                    @unlink($tmpfile);
70
                    return false;
71
                }
72
            }
73
            $file_record = new stdClass();
74
            $file_record->contextid = $contextid;
75
            $file_record->component = $component;
76
            $file_record->filearea  = $filearea;
77
            $file_record->itemid    = $itemid;
78
            $file_record->filepath  = $filepath;
79
            $file_record->filename  = $filename;
80
            $file_record->userid    = $userid;
81
            $file_record->mimetype  = 'application/zip';
82
 
83
            $result = $fs->create_file_from_pathname($file_record, $tmpfile);
84
        }
85
        @unlink($tmpfile);
86
        return $result;
87
    }
88
 
89
    /**
90
     * Zip files and store the result in os file.
91
     *
92
     * @param array $files array with zip paths as keys (archivepath=>ospathname or archivepath=>stored_file or archivepath=>array('content_as_string'))
93
     * @param string $archivefile path to target zip file
94
     * @param bool $ignoreinvalidfiles true means ignore missing or invalid files, false means abort on any error
95
     * @param file_progress $progress Progress indicator callback or null if not required
96
     * @return bool true if file created, false if not
97
     */
98
    public function archive_to_pathname(array $files, $archivefile,
99
            $ignoreinvalidfiles=true, file_progress $progress = null) {
100
        $ziparch = new zip_archive();
101
        if (!$ziparch->open($archivefile, file_archive::OVERWRITE)) {
102
            return false;
103
        }
104
 
105
        $abort = false;
106
        foreach ($files as $archivepath => $file) {
107
            $archivepath = trim($archivepath, '/');
108
 
109
            // Record progress each time around this loop.
110
            if ($progress) {
111
                $progress->progress();
112
            }
113
 
114
            if (is_null($file)) {
115
                // Directories have null as content.
116
                if (!$ziparch->add_directory($archivepath.'/')) {
117
                    debugging("Can not zip '$archivepath' directory", DEBUG_DEVELOPER);
118
                    if (!$ignoreinvalidfiles) {
119
                        $abort = true;
120
                        break;
121
                    }
122
                }
123
 
124
            } else if (is_string($file)) {
125
                if (!$this->archive_pathname($ziparch, $archivepath, $file, $progress)) {
126
                    debugging("Can not zip '$archivepath' file", DEBUG_DEVELOPER);
127
                    if (!$ignoreinvalidfiles) {
128
                        $abort = true;
129
                        break;
130
                    }
131
                }
132
 
133
            } else if (is_array($file)) {
134
                $content = reset($file);
135
                if (!$ziparch->add_file_from_string($archivepath, $content)) {
136
                    debugging("Can not zip '$archivepath' file", DEBUG_DEVELOPER);
137
                    if (!$ignoreinvalidfiles) {
138
                        $abort = true;
139
                        break;
140
                    }
141
                }
142
 
143
            } else {
144
                if (!$this->archive_stored($ziparch, $archivepath, $file, $progress)) {
145
                    debugging("Can not zip '$archivepath' file", DEBUG_DEVELOPER);
146
                    if (!$ignoreinvalidfiles) {
147
                        $abort = true;
148
                        break;
149
                    }
150
                }
151
            }
152
        }
153
 
154
        if (!$ziparch->close()) {
155
            @unlink($archivefile);
156
            return false;
157
        }
158
 
159
        if ($abort) {
160
            @unlink($archivefile);
161
            return false;
162
        }
163
 
164
        return true;
165
    }
166
 
167
    /**
168
     * Perform archiving file from stored file.
169
     *
170
     * @param zip_archive $ziparch zip archive instance
171
     * @param string $archivepath file path to archive
172
     * @param stored_file $file stored_file object
173
     * @param file_progress $progress Progress indicator callback or null if not required
174
     * @return bool success
175
     */
176
    private function archive_stored($ziparch, $archivepath, $file, file_progress $progress = null) {
177
        $result = $file->archive_file($ziparch, $archivepath);
178
        if (!$result) {
179
            return false;
180
        }
181
 
182
        if (!$file->is_directory()) {
183
            return true;
184
        }
185
 
186
        $baselength = strlen($file->get_filepath());
187
        $fs = get_file_storage();
188
        $files = $fs->get_directory_files($file->get_contextid(), $file->get_component(), $file->get_filearea(), $file->get_itemid(),
189
                                          $file->get_filepath(), true, true);
190
        foreach ($files as $file) {
191
            // Record progress for each file.
192
            if ($progress) {
193
                $progress->progress();
194
            }
195
 
196
            $path = $file->get_filepath();
197
            $path = substr($path, $baselength);
198
            $path = $archivepath.'/'.$path;
199
            if (!$file->is_directory()) {
200
                $path = $path.$file->get_filename();
201
            }
202
            // Ignore result here, partial zipping is ok for now.
203
            $file->archive_file($ziparch, $path);
204
        }
205
 
206
        return true;
207
    }
208
 
209
    /**
210
     * Perform archiving file from file path.
211
     *
212
     * @param zip_archive $ziparch zip archive instance
213
     * @param string $archivepath file path to archive
214
     * @param string $file path name of the file
215
     * @param file_progress $progress Progress indicator callback or null if not required
216
     * @return bool success
217
     */
218
    private function archive_pathname($ziparch, $archivepath, $file,
219
            file_progress $progress = null) {
220
        // Record progress each time this function is called.
221
        if ($progress) {
222
            $progress->progress();
223
        }
224
 
225
        if (!file_exists($file)) {
226
            return false;
227
        }
228
 
229
        if (is_file($file)) {
230
            if (!is_readable($file)) {
231
                return false;
232
            }
233
            return $ziparch->add_file_from_pathname($archivepath, $file);
234
        }
235
        if (is_dir($file)) {
236
            if ($archivepath !== '') {
237
                $ziparch->add_directory($archivepath);
238
            }
239
            $files = new DirectoryIterator($file);
240
            foreach ($files as $file) {
241
                if ($file->isDot()) {
242
                    continue;
243
                }
244
                $newpath = $archivepath.'/'.$file->getFilename();
245
                $this->archive_pathname($ziparch, $newpath, $file->getPathname(), $progress);
246
            }
247
            unset($files); // Release file handles.
248
            return true;
249
        }
250
    }
251
 
252
    /**
253
     * Unzip file to given file path (real OS filesystem), existing files are overwritten.
254
     *
255
     * @todo MDL-31048 localise messages
256
     * @param string|stored_file $archivefile full pathname of zip file or stored_file instance
257
     * @param string $pathname target directory
258
     * @param array $onlyfiles only extract files present in the array. The path to files MUST NOT
259
     *              start with a /. Example: array('myfile.txt', 'directory/anotherfile.txt')
260
     * @param file_progress $progress Progress indicator callback or null if not required
261
     * @param bool $returnbool Whether to return a basic true/false indicating error state, or full per-file error
262
     * details.
263
     * @return bool|array list of processed files; false if error
264
     */
265
    public function extract_to_pathname($archivefile, $pathname,
266
            array $onlyfiles = null, file_progress $progress = null, $returnbool = false) {
267
        global $CFG;
268
 
269
        if (!is_string($archivefile)) {
270
            return $archivefile->extract_to_pathname($this, $pathname, $progress);
271
        }
272
 
273
        $processed = array();
274
        $success = true;
275
 
276
        $pathname = rtrim($pathname, '/');
277
        if (!is_readable($archivefile)) {
278
            return false;
279
        }
280
        $ziparch = new zip_archive();
281
        if (!$ziparch->open($archivefile, file_archive::OPEN)) {
282
            return false;
283
        }
284
 
285
        // Get the number of files (approx).
286
        if ($progress) {
287
            $approxmax = $ziparch->estimated_count();
288
            $done = 0;
289
        }
290
 
291
        foreach ($ziparch as $info) {
292
            // Notify progress.
293
            if ($progress) {
294
                $progress->progress($done, $approxmax);
295
                $done++;
296
            }
297
 
298
            $size = $info->size;
299
            $name = $info->pathname;
300
            $origname = $name;
301
 
302
            // File names cannot end with dots on Windows and trailing dots are replaced with underscore.
303
            if ($CFG->ostype === 'WINDOWS') {
304
                $name = preg_replace('~([^/]+)\.(/|$)~', '\1_\2', $name);
305
            }
306
 
307
            if ($name === '' or array_key_exists($name, $processed)) {
308
                // Probably filename collisions caused by filename cleaning/conversion.
309
                continue;
310
            } else if (is_array($onlyfiles) && !in_array($origname, $onlyfiles)) {
311
                // Skipping files which are not in the list.
312
                continue;
313
            }
314
 
315
            if ($info->is_directory) {
316
                $newdir = "$pathname/$name";
317
                // directory
318
                if (is_file($newdir) and !unlink($newdir)) {
319
                    $processed[$name] = 'Can not create directory, file already exists'; // TODO: localise
320
                    $success = false;
321
                    continue;
322
                }
323
                if (is_dir($newdir)) {
324
                    //dir already there
325
                    $processed[$name] = true;
326
                } else {
327
                    if (mkdir($newdir, $CFG->directorypermissions, true)) {
328
                        $processed[$name] = true;
329
                    } else {
330
                        $processed[$name] = 'Can not create directory'; // TODO: localise
331
                        $success = false;
332
                    }
333
                }
334
                continue;
335
            }
336
 
337
            $parts = explode('/', trim($name, '/'));
338
            $filename = array_pop($parts);
339
            $newdir = rtrim($pathname.'/'.implode('/', $parts), '/');
340
 
341
            if (!is_dir($newdir)) {
342
                if (!mkdir($newdir, $CFG->directorypermissions, true)) {
343
                    $processed[$name] = 'Can not create directory'; // TODO: localise
344
                    $success = false;
345
                    continue;
346
                }
347
            }
348
 
349
            $newfile = "$newdir/$filename";
350
 
351
            if (strpos($newfile, './') > 1 || $name !== $origname) {
352
                // The path to the entry contains a directory ending with dot. We cannot use extract_to() due to
353
                // upstream PHP bugs #69477, #74619 and #77214. Extract the file from its stream which is slower but
354
                // should work even in this case.
355
                if (!$fp = fopen($newfile, 'wb')) {
356
                    $processed[$name] = 'Can not write target file'; // TODO: localise.
357
                    $success = false;
358
                    continue;
359
                }
360
 
361
                if (!$fz = $ziparch->get_stream($info->index)) {
362
                    $processed[$name] = 'Can not read file from zip archive'; // TODO: localise.
363
                    $success = false;
364
                    fclose($fp);
365
                    continue;
366
                }
367
 
368
                while (!feof($fz)) {
369
                    $content = fread($fz, 262143);
370
                    fwrite($fp, $content);
371
                }
372
 
373
                fclose($fz);
374
                fclose($fp);
375
 
376
            } else {
377
                if (!$fz = $ziparch->extract_to($pathname, $info->index)) {
378
                    $processed[$name] = 'Can not read file from zip archive'; // TODO: localise.
379
                    $success = false;
380
                    continue;
381
                }
382
            }
383
 
384
            // Check that the file was correctly created in the destination.
385
            if (!file_exists($newfile)) {
386
                $processed[$name] = 'Unknown error during zip extraction (file not created).'; // TODO: localise.
387
                $success = false;
388
                continue;
389
            }
390
 
391
            // Check that the size of extracted file matches the expectation.
392
            if (filesize($newfile) !== $size) {
393
                $processed[$name] = 'Unknown error during zip extraction (file size mismatch).'; // TODO: localise.
394
                $success = false;
395
                @unlink($newfile);
396
                continue;
397
            }
398
 
399
            $processed[$name] = true;
400
        }
401
 
402
        $ziparch->close();
403
 
404
        if ($returnbool) {
405
            return $success;
406
        } else {
407
            return $processed;
408
        }
409
    }
410
 
411
    /**
412
     * Unzip file to given file path (real OS filesystem), existing files are overwritten.
413
     *
414
     * @todo MDL-31048 localise messages
415
     * @param string|stored_file $archivefile full pathname of zip file or stored_file instance
416
     * @param int $contextid context ID
417
     * @param string $component component
418
     * @param string $filearea file area
419
     * @param int $itemid item ID
420
     * @param string $pathbase file path
421
     * @param int $userid user ID
422
     * @param file_progress $progress Progress indicator callback or null if not required
423
     * @return array|bool list of processed files; false if error
424
     */
425
    public function extract_to_storage($archivefile, $contextid,
426
            $component, $filearea, $itemid, $pathbase, $userid = NULL,
427
            file_progress $progress = null) {
428
        global $CFG;
429
 
430
        if (!is_string($archivefile)) {
431
            return $archivefile->extract_to_storage($this, $contextid, $component,
432
                    $filearea, $itemid, $pathbase, $userid, $progress);
433
        }
434
 
435
        check_dir_exists($CFG->tempdir.'/zip');
436
 
437
        $pathbase = trim($pathbase, '/');
438
        $pathbase = ($pathbase === '') ? '/' : '/'.$pathbase.'/';
439
        $fs = get_file_storage();
440
 
441
        $processed = array();
442
 
443
        $ziparch = new zip_archive();
444
        if (!$ziparch->open($archivefile, file_archive::OPEN)) {
445
            return false;
446
        }
447
 
448
        // Get the number of files (approx).
449
        if ($progress) {
450
            $approxmax = $ziparch->estimated_count();
451
            $done = 0;
452
        }
453
 
454
        foreach ($ziparch as $info) {
455
            // Notify progress.
456
            if ($progress) {
457
                $progress->progress($done, $approxmax);
458
                $done++;
459
            }
460
 
461
            $size = $info->size;
462
            $name = $info->pathname;
463
 
464
            if ($name === '' or array_key_exists($name, $processed)) {
465
                //probably filename collisions caused by filename cleaning/conversion
466
                continue;
467
            }
468
 
469
            if ($info->is_directory) {
470
                $newfilepath = $pathbase.$name.'/';
471
                $fs->create_directory($contextid, $component, $filearea, $itemid, $newfilepath, $userid);
472
                $processed[$name] = true;
473
                continue;
474
            }
475
 
476
            $parts = explode('/', trim($name, '/'));
477
            $filename = array_pop($parts);
478
            $filepath = $pathbase;
479
            if ($parts) {
480
                $filepath .= implode('/', $parts).'/';
481
            }
482
 
483
            if ($size < 2097151) {
484
                // Small file.
485
                if (!$fz = $ziparch->get_stream($info->index)) {
486
                    $processed[$name] = 'Can not read file from zip archive'; // TODO: localise
487
                    continue;
488
                }
489
                $content = '';
490
                $realfilesize = 0;
491
                while (!feof($fz)) {
492
                    $content .= fread($fz, 262143);
493
                    $realfilesize = strlen($content); // Current file size.
494
 
495
                    // More was read than was expected, which indicates a malformed/malicious archive.
496
                    // Break and let the error handling below take care of the file clean up.
497
                    if ($realfilesize > $size) {
498
                        break;
499
                    }
500
                }
501
                fclose($fz);
502
                if (strlen($content) !== $size) {
503
                    $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
504
                    // something went wrong :-(
505
                    unset($content);
506
                    continue;
507
                }
508
 
509
                if ($file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
510
                    if (!$file->delete()) {
511
                        $processed[$name] = 'Can not delete existing file'; // TODO: localise
512
                        continue;
513
                    }
514
                }
515
                $file_record = new stdClass();
516
                $file_record->contextid = $contextid;
517
                $file_record->component = $component;
518
                $file_record->filearea  = $filearea;
519
                $file_record->itemid    = $itemid;
520
                $file_record->filepath  = $filepath;
521
                $file_record->filename  = $filename;
522
                $file_record->userid    = $userid;
523
                if ($fs->create_file_from_string($file_record, $content)) {
524
                    $processed[$name] = true;
525
                } else {
526
                    $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
527
                }
528
                unset($content);
529
                continue;
530
 
531
            } else {
532
                // large file, would not fit into memory :-(
533
                $tmpfile = tempnam($CFG->tempdir.'/zip', 'unzip');
534
                if (!$fp = fopen($tmpfile, 'wb')) {
535
                    @unlink($tmpfile);
536
                    $processed[$name] = 'Can not write temp file'; // TODO: localise
537
                    continue;
538
                }
539
                if (!$fz = $ziparch->get_stream($info->index)) {
540
                    @unlink($tmpfile);
541
                    $processed[$name] = 'Can not read file from zip archive'; // TODO: localise
542
                    continue;
543
                }
544
                $realfilesize = 0;
545
                while (!feof($fz)) {
546
                    $content = fread($fz, 262143);
547
                    $numofbytes = fwrite($fp, $content);
548
                    $realfilesize += $numofbytes; // Current file size.
549
 
550
                    // More was read than was expected, which indicates a malformed/malicious archive.
551
                    // Break and let the error handling below take care of the file clean up.
552
                    if ($realfilesize > $size) {
553
                        break;
554
                    }
555
                }
556
                fclose($fz);
557
                fclose($fp);
558
                if (filesize($tmpfile) !== $size) {
559
                    $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
560
                    // something went wrong :-(
561
                    @unlink($tmpfile);
562
                    continue;
563
                }
564
 
565
                if ($file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
566
                    if (!$file->delete()) {
567
                        @unlink($tmpfile);
568
                        $processed[$name] = 'Can not delete existing file'; // TODO: localise
569
                        continue;
570
                    }
571
                }
572
                $file_record = new stdClass();
573
                $file_record->contextid = $contextid;
574
                $file_record->component = $component;
575
                $file_record->filearea  = $filearea;
576
                $file_record->itemid    = $itemid;
577
                $file_record->filepath  = $filepath;
578
                $file_record->filename  = $filename;
579
                $file_record->userid    = $userid;
580
                if ($fs->create_file_from_pathname($file_record, $tmpfile)) {
581
                    $processed[$name] = true;
582
                } else {
583
                    $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
584
                }
585
                @unlink($tmpfile);
586
                continue;
587
            }
588
        }
589
        $ziparch->close();
590
        return $processed;
591
    }
592
 
593
    /**
594
     * Returns array of info about all files in archive.
595
     *
596
     * @param string|file_archive $archivefile
597
     * @return array of file infos
598
     */
599
    public function list_files($archivefile) {
600
        if (!is_string($archivefile)) {
601
            return $archivefile->list_files();
602
        }
603
 
604
        $ziparch = new zip_archive();
605
        if (!$ziparch->open($archivefile, file_archive::OPEN)) {
606
            return false;
607
        }
608
        $list = $ziparch->list_files();
609
        $ziparch->close();
610
        return $list;
611
    }
612
 
613
}