c30b4791225164a3d4b4899896405b24fe26ad4b
[moodle.git] / lib / filestorage / zip_packer.php
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/>.
18 /**
19  * Implementation of zip packer.
20  *
21  * @package   core_files
22  * @copyright 2008 Petr Skoda (http://skodak.org)
23  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
28 require_once("$CFG->libdir/filestorage/file_packer.php");
29 require_once("$CFG->libdir/filestorage/zip_archive.php");
31 /**
32  * Utility class - handles all zipping and unzipping operations.
33  *
34  * @package   core_files
35  * @category  files
36  * @copyright 2008 Petr Skoda (http://skodak.org)
37  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38  */
39 class zip_packer extends file_packer {
41     /**
42      * Zip files and store the result in file storage
43      *
44      * @param array $files array with full zip paths (including directory information)
45      *              as keys (archivepath=>ospathname or archivepath/subdir=>stored_file or archivepath=>array('content_as_string'))
46      * @param int $contextid context ID
47      * @param string $component component
48      * @param string $filearea file area
49      * @param int $itemid item ID
50      * @param string $filepath file path
51      * @param string $filename file name
52      * @param int $userid user ID
53      * @return stored_file|bool false if error stored file instance if ok
54      */
55     public function archive_to_storage($files, $contextid, $component, $filearea, $itemid, $filepath, $filename, $userid = NULL) {
56         global $CFG;
58         $fs = get_file_storage();
60         check_dir_exists($CFG->tempdir.'/zip');
61         $tmpfile = tempnam($CFG->tempdir.'/zip', 'zipstor');
63         if ($result = $this->archive_to_pathname($files, $tmpfile)) {
64             if ($file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
65                 if (!$file->delete()) {
66                     @unlink($tmpfile);
67                     return false;
68                 }
69             }
70             $file_record = new stdClass();
71             $file_record->contextid = $contextid;
72             $file_record->component = $component;
73             $file_record->filearea  = $filearea;
74             $file_record->itemid    = $itemid;
75             $file_record->filepath  = $filepath;
76             $file_record->filename  = $filename;
77             $file_record->userid    = $userid;
78             $file_record->mimetype  = 'application/zip';
80             $result = $fs->create_file_from_pathname($file_record, $tmpfile);
81         }
82         @unlink($tmpfile);
83         return $result;
84     }
86     /**
87      * Zip files and store the result in os file
88      *
89      * @param array $files array with zip paths as keys (archivepath=>ospathname or archivepath=>stored_file or archivepath=>array('content_as_string'))
90      * @param string $archivefile path to target zip file
91      * @return bool success
92      */
93     public function archive_to_pathname($files, $archivefile) {
94         if (!is_array($files)) {
95             return false;
96         }
98         $ziparch = new zip_archive();
99         if (!$ziparch->open($archivefile, file_archive::OVERWRITE)) {
100             return false;
101         }
103         $result = false; // One processed file or dir means success here.
105         foreach ($files as $archivepath => $file) {
106             $archivepath = trim($archivepath, '/');
108             if (is_null($file)) {
109                 // empty directories have null as content
110                 if ($ziparch->add_directory($archivepath.'/')) {
111                     $result = true;
112                 } else {
113                     debugging("Can not zip '$archivepath' directory", DEBUG_DEVELOPER);
114                 }
116             } else if (is_string($file)) {
117                 if ($this->archive_pathname($ziparch, $archivepath, $file)) {
118                     $result = true;
119                 } else {
120                     debugging("Can not zip '$archivepath' file", DEBUG_DEVELOPER);
121                 }
123             } else if (is_array($file)) {
124                 $content = reset($file);
125                 if ($ziparch->add_file_from_string($archivepath, $content)) {
126                     $result = true;
127                 } else {
128                     debugging("Can not zip '$archivepath' file", DEBUG_DEVELOPER);
129                 }
131             } else {
132                 if ($this->archive_stored($ziparch, $archivepath, $file)) {
133                     $result = true;
134                 } else {
135                     debugging("Can not zip '$archivepath' file", DEBUG_DEVELOPER);
136                 }
137             }
138         }
140         // We can consider that there was an error if the file generated does not contain anything.
141         if ($ziparch->count() == 0) {
142             $result = false;
143             debugging("Nothing was added to the zip file", DEBUG_DEVELOPER);
144         }
146         return ($ziparch->close() && $result);
147     }
149     /**
150      * Perform archiving file from stored file
151      *
152      * @param zip_archive $ziparch zip archive instance
153      * @param string $archivepath file path to archive
154      * @param stored_file $file stored_file object
155      * @return bool success
156      */
157     private function archive_stored($ziparch, $archivepath, $file) {
158         $result = $file->archive_file($ziparch, $archivepath);
159         if (!$result) {
160             return false;
161         }
163         if (!$file->is_directory()) {
164             return true;
165         }
167         $baselength = strlen($file->get_filepath());
168         $fs = get_file_storage();
169         $files = $fs->get_directory_files($file->get_contextid(), $file->get_component(), $file->get_filearea(), $file->get_itemid(),
170                                           $file->get_filepath(), true, true);
171         foreach ($files as $file) {
172             $path = $file->get_filepath();
173             $path = substr($path, $baselength);
174             $path = $archivepath.'/'.$path;
175             if (!$file->is_directory()) {
176                 $path = $path.$file->get_filename();
177             }
178             // Ignore result here, partial zipping is ok for now.
179             $file->archive_file($ziparch, $path);
180         }
182         return true;
183     }
185     /**
186      * Perform archiving file from file path
187      *
188      * @param zip_archive $ziparch zip archive instance
189      * @param string $archivepath file path to archive
190      * @param string $file path name of the file
191      * @return bool success
192      */
193     private function archive_pathname($ziparch, $archivepath, $file) {
194         if (!file_exists($file)) {
195             return false;
196         }
198         if (is_file($file)) {
199             if (!is_readable($file)) {
200                 return false;
201             }
202             return $ziparch->add_file_from_pathname($archivepath, $file);
203         }
204         if (is_dir($file)) {
205             if ($archivepath !== '') {
206                 $ziparch->add_directory($archivepath);
207             }
208             $files = new DirectoryIterator($file);
209             foreach ($files as $file) {
210                 if ($file->isDot()) {
211                     continue;
212                 }
213                 $newpath = $archivepath.'/'.$file->getFilename();
214                 $this->archive_pathname($ziparch, $newpath, $file->getPathname());
215             }
216             unset($files); //release file handles
217             return true;
218         }
219     }
221     /**
222      * Unzip file to given file path (real OS filesystem), existing files are overwrited
223      *
224      * @todo MDL-31048 localise messages
225      * @param string|stored_file $archivefile full pathname of zip file or stored_file instance
226      * @param string $pathname target directory
227      * @param array $onlyfiles only extract files present in the array. The path to files MUST NOT
228      *              start with a /. Example: array('myfile.txt', 'directory/anotherfile.txt')
229      * @return bool|array list of processed files; false if error
230      */
231     public function extract_to_pathname($archivefile, $pathname, array $onlyfiles = null) {
232         global $CFG;
234         if (!is_string($archivefile)) {
235             return $archivefile->extract_to_pathname($this, $pathname);
236         }
238         $processed = array();
240         $pathname = rtrim($pathname, '/');
241         if (!is_readable($archivefile)) {
242             return false;
243         }
244         $ziparch = new zip_archive();
245         if (!$ziparch->open($archivefile, file_archive::OPEN)) {
246             return false;
247         }
249         foreach ($ziparch as $info) {
250             $size = $info->size;
251             $name = $info->pathname;
253             if ($name === '' or array_key_exists($name, $processed)) {
254                 // Probably filename collisions caused by filename cleaning/conversion.
255                 continue;
256             } else if (is_array($onlyfiles) && !in_array($name, $onlyfiles)) {
257                 // Skipping files which are not in the list.
258                 continue;
259             }
261             if ($info->is_directory) {
262                 $newdir = "$pathname/$name";
263                 // directory
264                 if (is_file($newdir) and !unlink($newdir)) {
265                     $processed[$name] = 'Can not create directory, file already exists'; // TODO: localise
266                     continue;
267                 }
268                 if (is_dir($newdir)) {
269                     //dir already there
270                     $processed[$name] = true;
271                 } else {
272                     if (mkdir($newdir, $CFG->directorypermissions, true)) {
273                         $processed[$name] = true;
274                     } else {
275                         $processed[$name] = 'Can not create directory'; // TODO: localise
276                     }
277                 }
278                 continue;
279             }
281             $parts = explode('/', trim($name, '/'));
282             $filename = array_pop($parts);
283             $newdir = rtrim($pathname.'/'.implode('/', $parts), '/');
285             if (!is_dir($newdir)) {
286                 if (!mkdir($newdir, $CFG->directorypermissions, true)) {
287                     $processed[$name] = 'Can not create directory'; // TODO: localise
288                     continue;
289                 }
290             }
292             $newfile = "$newdir/$filename";
293             if (!$fp = fopen($newfile, 'wb')) {
294                 $processed[$name] = 'Can not write target file'; // TODO: localise
295                 continue;
296             }
297             if (!$fz = $ziparch->get_stream($info->index)) {
298                 $processed[$name] = 'Can not read file from zip archive'; // TODO: localise
299                 fclose($fp);
300                 continue;
301             }
303             while (!feof($fz)) {
304                 $content = fread($fz, 262143);
305                 fwrite($fp, $content);
306             }
307             fclose($fz);
308             fclose($fp);
309             if (filesize($newfile) !== $size) {
310                 $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
311                 // something went wrong :-(
312                 @unlink($newfile);
313                 continue;
314             }
315             $processed[$name] = true;
316         }
317         $ziparch->close();
318         return $processed;
319     }
321     /**
322      * Unzip file to given file path (real OS filesystem), existing files are overwrited
323      *
324      * @todo MDL-31048 localise messages
325      * @param string|stored_file $archivefile full pathname of zip file or stored_file instance
326      * @param int $contextid context ID
327      * @param string $component component
328      * @param string $filearea file area
329      * @param int $itemid item ID
330      * @param string $pathbase file path
331      * @param int $userid user ID
332      * @return array|bool list of processed files; false if error
333      */
334     public function extract_to_storage($archivefile, $contextid, $component, $filearea, $itemid, $pathbase, $userid = NULL) {
335         global $CFG;
337         if (!is_string($archivefile)) {
338             return $archivefile->extract_to_storage($this, $contextid, $component, $filearea, $itemid, $pathbase, $userid);
339         }
341         check_dir_exists($CFG->tempdir.'/zip');
343         $pathbase = trim($pathbase, '/');
344         $pathbase = ($pathbase === '') ? '/' : '/'.$pathbase.'/';
345         $fs = get_file_storage();
347         $processed = array();
349         $ziparch = new zip_archive();
350         if (!$ziparch->open($archivefile, file_archive::OPEN)) {
351             return false;
352         }
354         foreach ($ziparch as $info) {
355             $size = $info->size;
356             $name = $info->pathname;
358             if ($name === '' or array_key_exists($name, $processed)) {
359                 //probably filename collisions caused by filename cleaning/conversion
360                 continue;
361             }
363             if ($info->is_directory) {
364                 $newfilepath = $pathbase.$name.'/';
365                 $fs->create_directory($contextid, $component, $filearea, $itemid, $newfilepath, $userid);
366                 $processed[$name] = true;
367                 continue;
368             }
370             $parts = explode('/', trim($name, '/'));
371             $filename = array_pop($parts);
372             $filepath = $pathbase;
373             if ($parts) {
374                 $filepath .= implode('/', $parts).'/';
375             }
377             if ($size < 2097151) {
378                 // small file
379                 if (!$fz = $ziparch->get_stream($info->index)) {
380                     $processed[$name] = 'Can not read file from zip archive'; // TODO: localise
381                     continue;
382                 }
383                 $content = '';
384                 while (!feof($fz)) {
385                     $content .= fread($fz, 262143);
386                 }
387                 fclose($fz);
388                 if (strlen($content) !== $size) {
389                     $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
390                     // something went wrong :-(
391                     unset($content);
392                     continue;
393                 }
395                 if ($file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
396                     if (!$file->delete()) {
397                         $processed[$name] = 'Can not delete existing file'; // TODO: localise
398                         continue;
399                     }
400                 }
401                 $file_record = new stdClass();
402                 $file_record->contextid = $contextid;
403                 $file_record->component = $component;
404                 $file_record->filearea  = $filearea;
405                 $file_record->itemid    = $itemid;
406                 $file_record->filepath  = $filepath;
407                 $file_record->filename  = $filename;
408                 $file_record->userid    = $userid;
409                 if ($fs->create_file_from_string($file_record, $content)) {
410                     $processed[$name] = true;
411                 } else {
412                     $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
413                 }
414                 unset($content);
415                 continue;
417             } else {
418                 // large file, would not fit into memory :-(
419                 $tmpfile = tempnam($CFG->tempdir.'/zip', 'unzip');
420                 if (!$fp = fopen($tmpfile, 'wb')) {
421                     @unlink($tmpfile);
422                     $processed[$name] = 'Can not write temp file'; // TODO: localise
423                     continue;
424                 }
425                 if (!$fz = $ziparch->get_stream($info->index)) {
426                     @unlink($tmpfile);
427                     $processed[$name] = 'Can not read file from zip archive'; // TODO: localise
428                     continue;
429                 }
430                 while (!feof($fz)) {
431                     $content = fread($fz, 262143);
432                     fwrite($fp, $content);
433                 }
434                 fclose($fz);
435                 fclose($fp);
436                 if (filesize($tmpfile) !== $size) {
437                     $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
438                     // something went wrong :-(
439                     @unlink($tmpfile);
440                     continue;
441                 }
443                 if ($file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
444                     if (!$file->delete()) {
445                         @unlink($tmpfile);
446                         $processed[$name] = 'Can not delete existing file'; // TODO: localise
447                         continue;
448                     }
449                 }
450                 $file_record = new stdClass();
451                 $file_record->contextid = $contextid;
452                 $file_record->component = $component;
453                 $file_record->filearea  = $filearea;
454                 $file_record->itemid    = $itemid;
455                 $file_record->filepath  = $filepath;
456                 $file_record->filename  = $filename;
457                 $file_record->userid    = $userid;
458                 if ($fs->create_file_from_pathname($file_record, $tmpfile)) {
459                     $processed[$name] = true;
460                 } else {
461                     $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
462                 }
463                 @unlink($tmpfile);
464                 continue;
465             }
466         }
467         $ziparch->close();
468         return $processed;
469     }
471     /**
472      * Returns array of info about all files in archive
473      *
474      * @param string|file_archive $archivefile
475      * @return array of file infos
476      */
477     public function list_files($archivefile) {
478         if (!is_string($archivefile)) {
479             return $archivefile->list_files();
480         }
482         $ziparch = new zip_archive();
483         if (!$ziparch->open($archivefile, file_archive::OPEN)) {
484             return false;
485         }
486         $list = $ziparch->list_files();
487         $ziparch->close();
488         return $list;
489     }