Merge branch 'wip-MDL-33997-master' of git://github.com/marinaglancy/moodle
[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         foreach ($files as $archivepath => $file) {
104             $archivepath = trim($archivepath, '/');
106             if (is_null($file)) {
107                 // empty directories have null as content
108                 $ziparch->add_directory($archivepath.'/');
110             } else if (is_string($file)) {
111                 $this->archive_pathname($ziparch, $archivepath, $file);
113             } else if (is_array($file)) {
114                 $content = reset($file);
115                 $ziparch->add_file_from_string($archivepath, $content);
117             } else {
118                 $this->archive_stored($ziparch, $archivepath, $file);
119             }
120         }
122         return $ziparch->close();
123     }
125     /**
126      * Perform archiving file from stored file
127      *
128      * @param zip_archive $ziparch zip archive instance
129      * @param string $archivepath file path to archive
130      * @param stored_file $file stored_file object
131      */
132     private function archive_stored($ziparch, $archivepath, $file) {
133         $file->archive_file($ziparch, $archivepath);
135         if (!$file->is_directory()) {
136             return;
137         }
139         $baselength = strlen($file->get_filepath());
140         $fs = get_file_storage();
141         $files = $fs->get_directory_files($file->get_contextid(), $file->get_component(), $file->get_filearea(), $file->get_itemid(),
142                                           $file->get_filepath(), true, true);
143         foreach ($files as $file) {
144             $path = $file->get_filepath();
145             $path = substr($path, $baselength);
146             $path = $archivepath.'/'.$path;
147             if (!$file->is_directory()) {
148                 $path = $path.$file->get_filename();
149             }
150             $file->archive_file($ziparch, $path);
151         }
152     }
154     /**
155      * Perform archiving file from file path
156      *
157      * @param zip_archive $ziparch zip archive instance
158      * @param string $archivepath file path to archive
159      * @param string $file path name of the file
160      */
161     private function archive_pathname($ziparch, $archivepath, $file) {
162         if (!file_exists($file)) {
163             return;
164         }
166         if (is_file($file)) {
167             if (!is_readable($file)) {
168                 return;
169             }
170             $ziparch->add_file_from_pathname($archivepath, $file);
171             return;
172         }
173         if (is_dir($file)) {
174             if ($archivepath !== '') {
175                 $ziparch->add_directory($archivepath);
176             }
177             $files = new DirectoryIterator($file);
178             foreach ($files as $file) {
179                 if ($file->isDot()) {
180                     continue;
181                 }
182                 $newpath = $archivepath.'/'.$file->getFilename();
183                 $this->archive_pathname($ziparch, $newpath, $file->getPathname());
184             }
185             unset($files); //release file handles
186             return;
187         }
188     }
190     /**
191      * Unzip file to given file path (real OS filesystem), existing files are overwrited
192      *
193      * @todo MDL-31048 localise messages
194      * @param string|stored_file $archivefile full pathname of zip file or stored_file instance
195      * @param string $pathname target directory
196      * @return bool|array list of processed files; false if error
197      */
198     public function extract_to_pathname($archivefile, $pathname) {
199         global $CFG;
201         if (!is_string($archivefile)) {
202             return $archivefile->extract_to_pathname($this, $pathname);
203         }
205         $processed = array();
207         $pathname = rtrim($pathname, '/');
208         if (!is_readable($archivefile)) {
209             return false;
210         }
211        $ziparch = new zip_archive();
212         if (!$ziparch->open($archivefile, file_archive::OPEN)) {
213             return false;
214         }
216         foreach ($ziparch as $info) {
217             $size = $info->size;
218             $name = $info->pathname;
220             if ($name === '' or array_key_exists($name, $processed)) {
221                 //probably filename collisions caused by filename cleaning/conversion
222                 continue;
223             }
225             if ($info->is_directory) {
226                 $newdir = "$pathname/$name";
227                 // directory
228                 if (is_file($newdir) and !unlink($newdir)) {
229                     $processed[$name] = 'Can not create directory, file already exists'; // TODO: localise
230                     continue;
231                 }
232                 if (is_dir($newdir)) {
233                     //dir already there
234                     $processed[$name] = true;
235                 } else {
236                     if (mkdir($newdir, $CFG->directorypermissions, true)) {
237                         $processed[$name] = true;
238                     } else {
239                         $processed[$name] = 'Can not create directory'; // TODO: localise
240                     }
241                 }
242                 continue;
243             }
245             $parts = explode('/', trim($name, '/'));
246             $filename = array_pop($parts);
247             $newdir = rtrim($pathname.'/'.implode('/', $parts), '/');
249             if (!is_dir($newdir)) {
250                 if (!mkdir($newdir, $CFG->directorypermissions, true)) {
251                     $processed[$name] = 'Can not create directory'; // TODO: localise
252                     continue;
253                 }
254             }
256             $newfile = "$newdir/$filename";
257             if (!$fp = fopen($newfile, 'wb')) {
258                 $processed[$name] = 'Can not write target file'; // TODO: localise
259                 continue;
260             }
261             if (!$fz = $ziparch->get_stream($info->index)) {
262                 $processed[$name] = 'Can not read file from zip archive'; // TODO: localise
263                 fclose($fp);
264                 continue;
265             }
267             while (!feof($fz)) {
268                 $content = fread($fz, 262143);
269                 fwrite($fp, $content);
270             }
271             fclose($fz);
272             fclose($fp);
273             if (filesize($newfile) !== $size) {
274                 $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
275                 // something went wrong :-(
276                 @unlink($newfile);
277                 continue;
278             }
279             $processed[$name] = true;
280         }
281         $ziparch->close();
282         return $processed;
283     }
285     /**
286      * Unzip file to given file path (real OS filesystem), existing files are overwrited
287      *
288      * @todo MDL-31048 localise messages
289      * @param string|stored_file $archivefile full pathname of zip file or stored_file instance
290      * @param int $contextid context ID
291      * @param string $component component
292      * @param string $filearea file area
293      * @param int $itemid item ID
294      * @param string $pathbase file path
295      * @param int $userid user ID
296      * @return array|bool list of processed files; false if error
297      */
298     public function extract_to_storage($archivefile, $contextid, $component, $filearea, $itemid, $pathbase, $userid = NULL) {
299         global $CFG;
301         if (!is_string($archivefile)) {
302             return $archivefile->extract_to_storage($this, $contextid, $component, $filearea, $itemid, $pathbase, $userid);
303         }
305         check_dir_exists($CFG->tempdir.'/zip');
307         $pathbase = trim($pathbase, '/');
308         $pathbase = ($pathbase === '') ? '/' : '/'.$pathbase.'/';
309         $fs = get_file_storage();
311         $processed = array();
313         $ziparch = new zip_archive();
314         if (!$ziparch->open($archivefile, file_archive::OPEN)) {
315             return false;
316         }
318         foreach ($ziparch as $info) {
319             $size = $info->size;
320             $name = $info->pathname;
322             if ($name === '' or array_key_exists($name, $processed)) {
323                 //probably filename collisions caused by filename cleaning/conversion
324                 continue;
325             }
327             if ($info->is_directory) {
328                 $newfilepath = $pathbase.$name.'/';
329                 $fs->create_directory($contextid, $component, $filearea, $itemid, $newfilepath, $userid);
330                 $processed[$name] = true;
331                 continue;
332             }
334             $parts = explode('/', trim($name, '/'));
335             $filename = array_pop($parts);
336             $filepath = $pathbase;
337             if ($parts) {
338                 $filepath .= implode('/', $parts).'/';
339             }
341             if ($size < 2097151) {
342                 // small file
343                 if (!$fz = $ziparch->get_stream($info->index)) {
344                     $processed[$name] = 'Can not read file from zip archive'; // TODO: localise
345                     continue;
346                 }
347                 $content = '';
348                 while (!feof($fz)) {
349                     $content .= fread($fz, 262143);
350                 }
351                 fclose($fz);
352                 if (strlen($content) !== $size) {
353                     $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
354                     // something went wrong :-(
355                     unset($content);
356                     continue;
357                 }
359                 if ($file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
360                     if (!$file->delete()) {
361                         $processed[$name] = 'Can not delete existing file'; // TODO: localise
362                         continue;
363                     }
364                 }
365                 $file_record = new stdClass();
366                 $file_record->contextid = $contextid;
367                 $file_record->component = $component;
368                 $file_record->filearea  = $filearea;
369                 $file_record->itemid    = $itemid;
370                 $file_record->filepath  = $filepath;
371                 $file_record->filename  = $filename;
372                 $file_record->userid    = $userid;
373                 if ($fs->create_file_from_string($file_record, $content)) {
374                     $processed[$name] = true;
375                 } else {
376                     $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
377                 }
378                 unset($content);
379                 continue;
381             } else {
382                 // large file, would not fit into memory :-(
383                 $tmpfile = tempnam($CFG->tempdir.'/zip', 'unzip');
384                 if (!$fp = fopen($tmpfile, 'wb')) {
385                     @unlink($tmpfile);
386                     $processed[$name] = 'Can not write temp file'; // TODO: localise
387                     continue;
388                 }
389                 if (!$fz = $ziparch->get_stream($info->index)) {
390                     @unlink($tmpfile);
391                     $processed[$name] = 'Can not read file from zip archive'; // TODO: localise
392                     continue;
393                 }
394                 while (!feof($fz)) {
395                     $content = fread($fz, 262143);
396                     fwrite($fp, $content);
397                 }
398                 fclose($fz);
399                 fclose($fp);
400                 if (filesize($tmpfile) !== $size) {
401                     $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
402                     // something went wrong :-(
403                     @unlink($tmpfile);
404                     continue;
405                 }
407                 if ($file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
408                     if (!$file->delete()) {
409                         @unlink($tmpfile);
410                         $processed[$name] = 'Can not delete existing file'; // TODO: localise
411                         continue;
412                     }
413                 }
414                 $file_record = new stdClass();
415                 $file_record->contextid = $contextid;
416                 $file_record->component = $component;
417                 $file_record->filearea  = $filearea;
418                 $file_record->itemid    = $itemid;
419                 $file_record->filepath  = $filepath;
420                 $file_record->filename  = $filename;
421                 $file_record->userid    = $userid;
422                 if ($fs->create_file_from_pathname($file_record, $tmpfile)) {
423                     $processed[$name] = true;
424                 } else {
425                     $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
426                 }
427                 @unlink($tmpfile);
428                 continue;
429             }
430         }
431         $ziparch->close();
432         return $processed;
433     }
435     /**
436      * Returns array of info about all files in archive
437      *
438      * @param file_archive $archivefile
439      * @return array of file infos
440      */
441     public function list_files($archivefile) {
442         if (!is_string($archivefile)) {
443             return $archivefile->list_files();
444         }
446         $ziparch = new zip_archive();
447         if (!$ziparch->open($archivefile, file_archive::OPEN)) {
448             return false;
449         }
450         $list = $ziparch->list_files();
451         $ziparch->close();
452         return $list;
453     }