Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?php// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** Implementation of zip packer.** @package core_files* @copyright 2008 Petr Skoda (http://skodak.org)* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/defined('MOODLE_INTERNAL') || die();require_once("$CFG->libdir/filestorage/file_packer.php");require_once("$CFG->libdir/filestorage/zip_archive.php");/*** Utility class - handles all zipping and unzipping operations.** @package core_files* @category files* @copyright 2008 Petr Skoda (http://skodak.org)* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class zip_packer extends file_packer {/*** Zip files and store the result in file storage.** @param array $files array with full zip paths (including directory information)* as keys (archivepath=>ospathname or archivepath/subdir=>stored_file or archivepath=>array('content_as_string'))* @param int $contextid context ID* @param string $component component* @param string $filearea file area* @param int $itemid item ID* @param string $filepath file path* @param string $filename file name* @param int $userid user ID* @param bool $ignoreinvalidfiles true means ignore missing or invalid files, false means abort on any error* @param file_progress $progress Progress indicator callback or null if not required* @return stored_file|bool false if error stored_file instance if ok*/public function archive_to_storage(array $files, $contextid,$component, $filearea, $itemid, $filepath, $filename,$userid = NULL, $ignoreinvalidfiles=true, file_progress $progress = null) {global $CFG;$fs = get_file_storage();check_dir_exists($CFG->tempdir.'/zip');$tmpfile = tempnam($CFG->tempdir.'/zip', 'zipstor');if ($result = $this->archive_to_pathname($files, $tmpfile, $ignoreinvalidfiles, $progress)) {if ($file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) {if (!$file->delete()) {@unlink($tmpfile);return false;}}$file_record = new stdClass();$file_record->contextid = $contextid;$file_record->component = $component;$file_record->filearea = $filearea;$file_record->itemid = $itemid;$file_record->filepath = $filepath;$file_record->filename = $filename;$file_record->userid = $userid;$file_record->mimetype = 'application/zip';$result = $fs->create_file_from_pathname($file_record, $tmpfile);}@unlink($tmpfile);return $result;}/*** Zip files and store the result in os file.** @param array $files array with zip paths as keys (archivepath=>ospathname or archivepath=>stored_file or archivepath=>array('content_as_string'))* @param string $archivefile path to target zip file* @param bool $ignoreinvalidfiles true means ignore missing or invalid files, false means abort on any error* @param file_progress $progress Progress indicator callback or null if not required* @return bool true if file created, false if not*/public function archive_to_pathname(array $files, $archivefile,$ignoreinvalidfiles=true, file_progress $progress = null) {$ziparch = new zip_archive();if (!$ziparch->open($archivefile, file_archive::OVERWRITE)) {return false;}$abort = false;foreach ($files as $archivepath => $file) {$archivepath = trim($archivepath, '/');// Record progress each time around this loop.if ($progress) {$progress->progress();}if (is_null($file)) {// Directories have null as content.if (!$ziparch->add_directory($archivepath.'/')) {debugging("Can not zip '$archivepath' directory", DEBUG_DEVELOPER);if (!$ignoreinvalidfiles) {$abort = true;break;}}} else if (is_string($file)) {if (!$this->archive_pathname($ziparch, $archivepath, $file, $progress)) {debugging("Can not zip '$archivepath' file", DEBUG_DEVELOPER);if (!$ignoreinvalidfiles) {$abort = true;break;}}} else if (is_array($file)) {$content = reset($file);if (!$ziparch->add_file_from_string($archivepath, $content)) {debugging("Can not zip '$archivepath' file", DEBUG_DEVELOPER);if (!$ignoreinvalidfiles) {$abort = true;break;}}} else {if (!$this->archive_stored($ziparch, $archivepath, $file, $progress)) {debugging("Can not zip '$archivepath' file", DEBUG_DEVELOPER);if (!$ignoreinvalidfiles) {$abort = true;break;}}}}if (!$ziparch->close()) {@unlink($archivefile);return false;}if ($abort) {@unlink($archivefile);return false;}return true;}/*** Perform archiving file from stored file.** @param zip_archive $ziparch zip archive instance* @param string $archivepath file path to archive* @param stored_file $file stored_file object* @param file_progress $progress Progress indicator callback or null if not required* @return bool success*/private function archive_stored($ziparch, $archivepath, $file, file_progress $progress = null) {$result = $file->archive_file($ziparch, $archivepath);if (!$result) {return false;}if (!$file->is_directory()) {return true;}$baselength = strlen($file->get_filepath());$fs = get_file_storage();$files = $fs->get_directory_files($file->get_contextid(), $file->get_component(), $file->get_filearea(), $file->get_itemid(),$file->get_filepath(), true, true);foreach ($files as $file) {// Record progress for each file.if ($progress) {$progress->progress();}$path = $file->get_filepath();$path = substr($path, $baselength);$path = $archivepath.'/'.$path;if (!$file->is_directory()) {$path = $path.$file->get_filename();}// Ignore result here, partial zipping is ok for now.$file->archive_file($ziparch, $path);}return true;}/*** Perform archiving file from file path.** @param zip_archive $ziparch zip archive instance* @param string $archivepath file path to archive* @param string $file path name of the file* @param file_progress $progress Progress indicator callback or null if not required* @return bool success*/private function archive_pathname($ziparch, $archivepath, $file,file_progress $progress = null) {// Record progress each time this function is called.if ($progress) {$progress->progress();}if (!file_exists($file)) {return false;}if (is_file($file)) {if (!is_readable($file)) {return false;}return $ziparch->add_file_from_pathname($archivepath, $file);}if (is_dir($file)) {if ($archivepath !== '') {$ziparch->add_directory($archivepath);}$files = new DirectoryIterator($file);foreach ($files as $file) {if ($file->isDot()) {continue;}$newpath = $archivepath.'/'.$file->getFilename();$this->archive_pathname($ziparch, $newpath, $file->getPathname(), $progress);}unset($files); // Release file handles.return true;}}/*** Unzip file to given file path (real OS filesystem), existing files are overwritten.** @todo MDL-31048 localise messages* @param string|stored_file $archivefile full pathname of zip file or stored_file instance* @param string $pathname target directory* @param array $onlyfiles only extract files present in the array. The path to files MUST NOT* start with a /. Example: array('myfile.txt', 'directory/anotherfile.txt')* @param file_progress $progress Progress indicator callback or null if not required* @param bool $returnbool Whether to return a basic true/false indicating error state, or full per-file error* details.* @return bool|array list of processed files; false if error*/public function extract_to_pathname($archivefile, $pathname,array $onlyfiles = null, file_progress $progress = null, $returnbool = false) {global $CFG;if (!is_string($archivefile)) {return $archivefile->extract_to_pathname($this, $pathname, $progress);}$processed = array();$success = true;$pathname = rtrim($pathname, '/');if (!is_readable($archivefile)) {return false;}$ziparch = new zip_archive();if (!$ziparch->open($archivefile, file_archive::OPEN)) {return false;}// Get the number of files (approx).if ($progress) {$approxmax = $ziparch->estimated_count();$done = 0;}foreach ($ziparch as $info) {// Notify progress.if ($progress) {$progress->progress($done, $approxmax);$done++;}$size = $info->size;$name = $info->pathname;$origname = $name;// File names cannot end with dots on Windows and trailing dots are replaced with underscore.if ($CFG->ostype === 'WINDOWS') {$name = preg_replace('~([^/]+)\.(/|$)~', '\1_\2', $name);}if ($name === '' or array_key_exists($name, $processed)) {// Probably filename collisions caused by filename cleaning/conversion.continue;} else if (is_array($onlyfiles) && !in_array($origname, $onlyfiles)) {// Skipping files which are not in the list.continue;}if ($info->is_directory) {$newdir = "$pathname/$name";// directoryif (is_file($newdir) and !unlink($newdir)) {$processed[$name] = 'Can not create directory, file already exists'; // TODO: localise$success = false;continue;}if (is_dir($newdir)) {//dir already there$processed[$name] = true;} else {if (mkdir($newdir, $CFG->directorypermissions, true)) {$processed[$name] = true;} else {$processed[$name] = 'Can not create directory'; // TODO: localise$success = false;}}continue;}$parts = explode('/', trim($name, '/'));$filename = array_pop($parts);$newdir = rtrim($pathname.'/'.implode('/', $parts), '/');if (!is_dir($newdir)) {if (!mkdir($newdir, $CFG->directorypermissions, true)) {$processed[$name] = 'Can not create directory'; // TODO: localise$success = false;continue;}}$newfile = "$newdir/$filename";if (strpos($newfile, './') > 1 || $name !== $origname) {// The path to the entry contains a directory ending with dot. We cannot use extract_to() due to// upstream PHP bugs #69477, #74619 and #77214. Extract the file from its stream which is slower but// should work even in this case.if (!$fp = fopen($newfile, 'wb')) {$processed[$name] = 'Can not write target file'; // TODO: localise.$success = false;continue;}if (!$fz = $ziparch->get_stream($info->index)) {$processed[$name] = 'Can not read file from zip archive'; // TODO: localise.$success = false;fclose($fp);continue;}while (!feof($fz)) {$content = fread($fz, 262143);fwrite($fp, $content);}fclose($fz);fclose($fp);} else {if (!$fz = $ziparch->extract_to($pathname, $info->index)) {$processed[$name] = 'Can not read file from zip archive'; // TODO: localise.$success = false;continue;}}// Check that the file was correctly created in the destination.if (!file_exists($newfile)) {$processed[$name] = 'Unknown error during zip extraction (file not created).'; // TODO: localise.$success = false;continue;}// Check that the size of extracted file matches the expectation.if (filesize($newfile) !== $size) {$processed[$name] = 'Unknown error during zip extraction (file size mismatch).'; // TODO: localise.$success = false;@unlink($newfile);continue;}$processed[$name] = true;}$ziparch->close();if ($returnbool) {return $success;} else {return $processed;}}/*** Unzip file to given file path (real OS filesystem), existing files are overwritten.** @todo MDL-31048 localise messages* @param string|stored_file $archivefile full pathname of zip file or stored_file instance* @param int $contextid context ID* @param string $component component* @param string $filearea file area* @param int $itemid item ID* @param string $pathbase file path* @param int $userid user ID* @param file_progress $progress Progress indicator callback or null if not required* @return array|bool list of processed files; false if error*/public function extract_to_storage($archivefile, $contextid,$component, $filearea, $itemid, $pathbase, $userid = NULL,file_progress $progress = null) {global $CFG;if (!is_string($archivefile)) {return $archivefile->extract_to_storage($this, $contextid, $component,$filearea, $itemid, $pathbase, $userid, $progress);}check_dir_exists($CFG->tempdir.'/zip');$pathbase = trim($pathbase, '/');$pathbase = ($pathbase === '') ? '/' : '/'.$pathbase.'/';$fs = get_file_storage();$processed = array();$ziparch = new zip_archive();if (!$ziparch->open($archivefile, file_archive::OPEN)) {return false;}// Get the number of files (approx).if ($progress) {$approxmax = $ziparch->estimated_count();$done = 0;}foreach ($ziparch as $info) {// Notify progress.if ($progress) {$progress->progress($done, $approxmax);$done++;}$size = $info->size;$name = $info->pathname;if ($name === '' or array_key_exists($name, $processed)) {//probably filename collisions caused by filename cleaning/conversioncontinue;}if ($info->is_directory) {$newfilepath = $pathbase.$name.'/';$fs->create_directory($contextid, $component, $filearea, $itemid, $newfilepath, $userid);$processed[$name] = true;continue;}$parts = explode('/', trim($name, '/'));$filename = array_pop($parts);$filepath = $pathbase;if ($parts) {$filepath .= implode('/', $parts).'/';}if ($size < 2097151) {// Small file.if (!$fz = $ziparch->get_stream($info->index)) {$processed[$name] = 'Can not read file from zip archive'; // TODO: localisecontinue;}$content = '';$realfilesize = 0;while (!feof($fz)) {$content .= fread($fz, 262143);$realfilesize = strlen($content); // Current file size.// More was read than was expected, which indicates a malformed/malicious archive.// Break and let the error handling below take care of the file clean up.if ($realfilesize > $size) {break;}}fclose($fz);if (strlen($content) !== $size) {$processed[$name] = 'Unknown error during zip extraction'; // TODO: localise// something went wrong :-(unset($content);continue;}if ($file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) {if (!$file->delete()) {$processed[$name] = 'Can not delete existing file'; // TODO: localisecontinue;}}$file_record = new stdClass();$file_record->contextid = $contextid;$file_record->component = $component;$file_record->filearea = $filearea;$file_record->itemid = $itemid;$file_record->filepath = $filepath;$file_record->filename = $filename;$file_record->userid = $userid;if ($fs->create_file_from_string($file_record, $content)) {$processed[$name] = true;} else {$processed[$name] = 'Unknown error during zip extraction'; // TODO: localise}unset($content);continue;} else {// large file, would not fit into memory :-($tmpfile = tempnam($CFG->tempdir.'/zip', 'unzip');if (!$fp = fopen($tmpfile, 'wb')) {@unlink($tmpfile);$processed[$name] = 'Can not write temp file'; // TODO: localisecontinue;}if (!$fz = $ziparch->get_stream($info->index)) {@unlink($tmpfile);$processed[$name] = 'Can not read file from zip archive'; // TODO: localisecontinue;}$realfilesize = 0;while (!feof($fz)) {$content = fread($fz, 262143);$numofbytes = fwrite($fp, $content);$realfilesize += $numofbytes; // Current file size.// More was read than was expected, which indicates a malformed/malicious archive.// Break and let the error handling below take care of the file clean up.if ($realfilesize > $size) {break;}}fclose($fz);fclose($fp);if (filesize($tmpfile) !== $size) {$processed[$name] = 'Unknown error during zip extraction'; // TODO: localise// something went wrong :-(@unlink($tmpfile);continue;}if ($file = $fs->get_file($contextid, $component, $filearea, $itemid, $filepath, $filename)) {if (!$file->delete()) {@unlink($tmpfile);$processed[$name] = 'Can not delete existing file'; // TODO: localisecontinue;}}$file_record = new stdClass();$file_record->contextid = $contextid;$file_record->component = $component;$file_record->filearea = $filearea;$file_record->itemid = $itemid;$file_record->filepath = $filepath;$file_record->filename = $filename;$file_record->userid = $userid;if ($fs->create_file_from_pathname($file_record, $tmpfile)) {$processed[$name] = true;} else {$processed[$name] = 'Unknown error during zip extraction'; // TODO: localise}@unlink($tmpfile);continue;}}$ziparch->close();return $processed;}/*** Returns array of info about all files in archive.** @param string|file_archive $archivefile* @return array of file infos*/public function list_files($archivefile) {if (!is_string($archivefile)) {return $archivefile->list_files();}$ziparch = new zip_archive();if (!$ziparch->open($archivefile, file_archive::OPEN)) {return false;}$list = $ziparch->list_files();$ziparch->close();return $list;}}