. /** * 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 * @return bool|array list of processed files; false if error */ public function extract_to_pathname($archivefile, $pathname, array $onlyfiles = null, file_progress $progress = null) { global $CFG; if (!is_string($archivefile)) { return $archivefile->extract_to_pathname($this, $pathname, $progress); } $processed = array(); $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; if ($name === '' or array_key_exists($name, $processed)) { // Probably filename collisions caused by filename cleaning/conversion. continue; } else if (is_array($onlyfiles) && !in_array($name, $onlyfiles)) { // Skipping files which are not in the list. continue; } if ($info->is_directory) { $newdir = "$pathname/$name"; // directory if (is_file($newdir) and !unlink($newdir)) { $processed[$name] = 'Can not create directory, file already exists'; // TODO: localise 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 } } 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 continue; } } $newfile = "$newdir/$filename"; if (!$fp = fopen($newfile, 'wb')) { $processed[$name] = 'Can not write target file'; // TODO: localise continue; } if (!$fz = $ziparch->get_stream($info->index)) { $processed[$name] = 'Can not read file from zip archive'; // TODO: localise fclose($fp); continue; } while (!feof($fz)) { $content = fread($fz, 262143); fwrite($fp, $content); } fclose($fz); fclose($fp); if (filesize($newfile) !== $size) { $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise // something went wrong :-( @unlink($newfile); continue; } $processed[$name] = true; } $ziparch->close(); 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/conversion continue; } 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: localise continue; } $content = ''; while (!feof($fz)) { $content .= fread($fz, 262143); } 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: localise continue; } } $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: localise continue; } if (!$fz = $ziparch->get_stream($info->index)) { @unlink($tmpfile); $processed[$name] = 'Can not read file from zip archive'; // TODO: localise continue; } while (!feof($fz)) { $content = fread($fz, 262143); fwrite($fp, $content); } 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: localise continue; } } $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; } }