Commit 6e85afa3 authored by Robert Lyon's avatar Robert Lyon Committed by Gerrit Code Review

Merge "Bug 1602452: Using Phar to extract archive artefacts"

parents 190fb1b4 7563daad
...@@ -58,12 +58,9 @@ if (!empty($folderid)) { ...@@ -58,12 +58,9 @@ if (!empty($folderid)) {
throw new AccessDeniedException(get_string('cannotextractfileinfoldersubmitted', 'artefact.file')); throw new AccessDeniedException(get_string('cannotextractfileinfoldersubmitted', 'artefact.file'));
} }
} }
try {
$zipinfo = $file->read_archive(); // Read the archive information, throw an ArchiveException if error
} $zipinfo = $file->read_archive();
catch (SystemException $e) {
$message = get_string('invalidarchive', 'artefact.file');
}
if ($zipinfo) { if ($zipinfo) {
$quotaallowed = false; $quotaallowed = false;
...@@ -147,7 +144,12 @@ function unzip_artefact_submit(Pieform $form, $values) { ...@@ -147,7 +144,12 @@ function unzip_artefact_submit(Pieform $form, $values) {
$from = files_page($file); $from = files_page($file);
if (count($zipinfo->names) > 10) { if (count($zipinfo->names) > 10) {
$SESSION->set('unzip', array('file' => $file->get('id'), 'from' => $from, 'artefacts' => count($zipinfo->names), 'zipinfo' => $zipinfo)); $SESSION->set('unzip', array('file' => $file->get('id'),
'from' => $from,
'artefacts' => count($zipinfo->names),
'zipinfo' => $zipinfo
)
);
$smarty = smarty(); $smarty = smarty();
$smarty->display('artefact:file:extract-progress.tpl'); $smarty->display('artefact:file:extract-progress.tpl');
exit; exit;
......
...@@ -310,7 +310,13 @@ $string['filesextractedfromarchive'] = 'Files extracted from archive'; ...@@ -310,7 +310,13 @@ $string['filesextractedfromarchive'] = 'Files extracted from archive';
$string['filesextractedfromziparchive'] = 'Files extracted from Zip archive'; $string['filesextractedfromziparchive'] = 'Files extracted from Zip archive';
$string['fileswillbeextractedintofolder'] = 'Files will be extracted into %s'; $string['fileswillbeextractedintofolder'] = 'Files will be extracted into %s';
$string['insufficientquotaforunzip'] = 'Your remaining file quota is too small to unzip this file. You can either delete files to free up space or contact your administrator to have your quota increased.'; $string['insufficientquotaforunzip'] = 'Your remaining file quota is too small to unzip this file. You can either delete files to free up space or contact your administrator to have your quota increased.';
$string['invalidarchive'] = 'Error reading archive file.'; $string['invalidarchive1'] = 'Invalid archive file.';
$string['invalidarchivehandle'] = 'Invalid archive file handle.';
$string['cannotopenarchive'] = 'Can not open the archive file %s.';
$string['cannotreadarchivecontent'] = 'Can not read the archive content.';
$string['cannotextractarchive'] = 'Unable to extract archive into %s.';
$string['cannotcopytemparchive'] = 'Unable to copy the archive file from %s to %s.';
$string['cannotdeletetemparchive'] = 'Unable to delete the temporary archive file %s.';
$string['pleasewaitwhileyourfilesarebeingunzipped'] = 'Please wait while your files are being unzipped.'; $string['pleasewaitwhileyourfilesarebeingunzipped'] = 'Please wait while your files are being unzipped.';
$string['spacerequired'] = 'Space required'; $string['spacerequired'] = 'Space required';
$string['unzipprogress'] = '%s files/folders created.'; $string['unzipprogress'] = '%s files/folders created.';
......
...@@ -2428,9 +2428,10 @@ class ArtefactTypeProfileIcon extends ArtefactTypeImage { ...@@ -2428,9 +2428,10 @@ class ArtefactTypeProfileIcon extends ArtefactTypeImage {
class ArtefactTypeArchive extends ArtefactTypeFile { class ArtefactTypeArchive extends ArtefactTypeFile {
protected $archivetype; protected $archivetype;
protected $handle; protected $handle = null; // Handle for the temporary archive file
protected $info; protected $info;
protected $data = array(); protected $data = array();
protected $temparchivepathlength = 0; // The length of the phar path to the temporary archive
public function __construct($id = 0, $data = null) { public function __construct($id = 0, $data = null) {
parent::__construct($id, $data); parent::__construct($id, $data);
...@@ -2451,50 +2452,46 @@ class ArtefactTypeArchive extends ArtefactTypeFile { ...@@ -2451,50 +2452,46 @@ class ArtefactTypeArchive extends ArtefactTypeFile {
$type = $descriptions[$validtypes[$data->filetype]->description]; $type = $descriptions[$validtypes[$data->filetype]->description];
// Add tmp extension
$path = self::copy_to_temp($path);
if ($path === false) {
return false;
}
try {
$archive_obj = new PharData($path);
}
catch (UnexpectedValueException $e) {
$path = self::delete_from_temp($path);
return false;
}
$is_valid = false;
if (is_null($type)) { if (is_null($type)) {
if (self::is_zip($path)) { if ($archive_obj->isFileFormat(Phar::ZIP)) {
$data->filetype = 'application/zip'; $data->filetype = 'application/zip';
$data->archivetype = 'zip'; $data->archivetype = 'zip';
return true; $is_valid = true;
} }
if ($data->filetype = self::is_tar($path)) { if ($archive_obj->isFileFormat(Phar::TAR)) {
switch ($archive_obj->isCompressed()) {
case Phar::GZ: $data->filetype = 'application/x-gzip';
case Phar::BZ2: $data->filetype = 'application/x-bzip2';
default: $data->filetype = 'application/x-tar';
}
$data->archivetype = 'tar'; $data->archivetype = 'tar';
return true; $is_valid = true;
} }
} }
else if ($type == 'zip' && self::is_zip($path) || $type == 'tar' && self::is_tar($path)) { else if ($type == 'zip' && ($archive_obj->isFileFormat(Phar::ZIP))
|| $type == 'tar' && ($archive_obj->isFileFormat(Phar::TAR))) {
$data->archivetype = $type; $data->archivetype = $type;
return true; $is_valid = true;
} }
return false;
}
public static function is_zip($path) { // Remove the temporary after validate the archive file
if (function_exists('zip_read')) { self::delete_from_temp($path);
$zip = zip_open($path);
if (is_resource($zip)) {
zip_close($zip);
return true;
}
}
return false;
}
public static function is_tar($path) { return $is_valid;
require_once('Archive/Tar.php');
if (!$tar = new Archive_Tar($path)) {
return false;
}
$list = $tar->listContent();
if (empty($list)) {
return false;
}
switch ($tar->_compress_type) {
case 'gz': return 'application/x-gzip';
case 'bz2': return 'application/x-bzip2';
case 'none': return 'application/x-tar';
}
return false;
} }
public static function archive_file_descriptions() { public static function archive_file_descriptions() {
...@@ -2519,19 +2516,45 @@ class ArtefactTypeArchive extends ArtefactTypeFile { ...@@ -2519,19 +2516,45 @@ class ArtefactTypeArchive extends ArtefactTypeFile {
return false; return false;
} }
public function open_archive() { /**
if ($this->archivetype == 'zip') { * Copy the archive file to the system temporary directory
$this->handle = zip_open($this->get_path()); * and add .tmp to a file name and return the new filename
if (!is_resource($this->handle)) { * This is a workaround for read and extract a zip/tar file using PharData
$this->handle = null; *
throw new NotFoundException(); * THIS FUNCTION MUST BE PAIRED WITH delete_from_temp()
} *
* @param String $path: path to the archive file
* @return String new path
* @throws SystemException if can not copy
*/
public static function copy_to_temp($path) {
$tempdir = get_config('dataroot') . 'artefact/file/temp';
check_dir_exists($tempdir);
$name = tempnam($tempdir, '');
unlink($name);
$name .= '.tmp';
if (file_exists($path)
&& copy($path, $name)) {
return $name;
} }
else if ($this->archivetype == 'tar') { else {
require_once('Archive/Tar.php'); throw new SystemException(get_string('cannotcopytemparchive', 'artefact/file', $path, $name));
if (!$this->handle = new Archive_Tar($this->get_path())) { }
throw new NotFoundException(); }
}
/**
* Delete the temporary archive
* @param String $path: path to the temporary file
* @return void
* @throws SystemException if can not delete
*/
public static function delete_from_temp($path) {
if (file_exists($path)
&& unlink($path)) {
return;
}
else {
throw new SystemException(get_string('cannotdeletetemparchive', 'artefact/file', $path));
} }
} }
...@@ -2539,67 +2562,91 @@ class ArtefactTypeArchive extends ArtefactTypeFile { ...@@ -2539,67 +2562,91 @@ class ArtefactTypeArchive extends ArtefactTypeFile {
$this->info = $zipinfo; $this->info = $zipinfo;
} }
private function read_entry($name, $isfolder, $size) { /**
$path = explode('/', $name); * Recursively read the content of an archive
if ($isfolder) { * and update $this->info
array_pop($path); *
* @param string $dir
* = null : read the content of $this->handle
* @throws ArchiveException
*/
private function read_archive_folder($dir=null) {
if ($this->temparchivepathlength === 0) {
throw new ArchiveException(get_string('invalidtemparchivepathlength', 'artefact.file'));
} }
try {
if (!isset($dir) && !empty($this->handle)) {
$a = $this->handle;
}
else if (isset($dir)) {
$a = new PharData($dir, RecursiveDirectoryIterator::KEY_AS_PATHNAME);
}
else {
throw new Exception(get_string('invalidarchivehandle', 'artefact.file'));
}
foreach ($a as $i) {
$name = substr($i->getPathName(), $this->temparchivepathlength + 1);
if ($i->isDir()) {
$this->info->foldernames[$name] = 1;
$this->info->names[] = $name;
$this->info->folders++;
$folder = ''; $this->read_archive_folder($i->getPathName());
for ($i = 0; $i < count($path) - 1; $i++) { }
$folder .= $path[$i] . '/'; else {
if (!isset($this->foldernames[$folder])) { $this->info->names[] = $name;
$this->foldernames[$folder] = 1; $this->info->files++;
$this->info->names[] = $folder; $this->info->totalsize += $i->getCompressedSize();
$this->info->folders++; }
} }
} }
catch (Exception $e) {
if (!$isfolder) { throw new ArchiveException(get_string('cannotreadarchivecontent', 'artefact.file') . $e->getMessage());
$this->info->names[] = $name;
$this->info->files++;
$this->info->totalsize += $size;
} }
} }
public function read_archive() { /**
if (!$this->handle) { * Read the archive content from a temporary archive file
$this->open_archive(); * and update $this->info
} *
if ($this->info) { * @param Bool $keeptemphandle = true: the temporary file and handle
return $this->info; * will NOT be deleted for later use
} * @return Object archive info
$this->info = (object) array( * @throws ArchiveException
'files' => 0, */
'folders' => 0, public function read_archive($keeptemphandle=false) {
'totalsize' => 0, // In mahara all physical file is stored without extension.
'names' => array(), // For some reasons, PharData can not properly read a file without extension
); // We create a temporary file with an extension 'tmp' from the original archive
// We will delete the file after read/extract actions
$this->foldernames = array(); $tmparchivepath = self::copy_to_temp($this->get_path());
try {
$this->handle = new PharData($tmparchivepath, RecursiveDirectoryIterator::KEY_AS_PATHNAME);
$this->temparchivepathlength = strlen('phar://' . $tmparchivepath);
}
catch (UnexpectedValueException $e) {
self::delete_from_temp($tmparchivepath);
throw new ArchiveException(get_string('cannotopenarchive', 'artefact.file', $tmparchivepath));
}
if (empty($this->info)) {
$this->info = (object) array(
'files' => 0,
'folders' => 0,
'totalsize' => 0,
'names' => array(),
'foldernames' => array()
);
if ($this->archivetype == 'zip') { $this->read_archive_folder();
while ($entry = zip_read($this->handle)) {
$name = zip_entry_name($entry);
$isfolder = substr($name, -1) == '/';
$size = $isfolder ? 0 : zip_entry_filesize($entry);
$this->read_entry($name, $isfolder, $size);
}
}
else if ($this->archivetype == 'tar') {
$list = $this->handle->listContent();
if (empty($list)) {
throw new SystemException("Unknown archive type");
}
foreach ($list as $entry) {
$isfolder = substr($entry['filename'], -1) == '/';
$size = $isfolder ? 0 : $entry['size'];
$this->read_entry($entry['filename'], $isfolder, $size);
}
} }
else {
throw new SystemException("Unknown archive type"); if (!$keeptemphandle) {
// Delete the temporary file
self::delete_from_temp($tmparchivepath);
$this->handle = null;
$this->temparchivepathlength = 0;
} }
$this->info->displaysize = ArtefactTypeFile::short_size($this->info->totalsize); $this->info->displaysize = ArtefactTypeFile::short_size($this->info->totalsize);
return $this->info; return $this->info;
} }
...@@ -2641,13 +2688,7 @@ class ArtefactTypeArchive extends ArtefactTypeFile { ...@@ -2641,13 +2688,7 @@ class ArtefactTypeArchive extends ArtefactTypeFile {
public function create_folder($folder) { public function create_folder($folder) {
$newfolder = new ArtefactTypeFolder(0, $this->data['template']); $newfolder = new ArtefactTypeFolder(0, $this->data['template']);
$newfolder->commit(); $newfolder->commit();
if ($this->archivetype == 'zip') { $this->data['folderids'][$folder] = $newfolder->get('id');
$folderindex = ($folder == '.' ? ($this->data['template']->title . '/') : ($folder . $this->data['template']->title . '/'));
}
else {
$folderindex = ($folder == '.' ? '' : ($folder . '/')) . $this->data['template']->title;
}
$this->data['folderids'][$folderindex] = $newfolder->get('id');
$this->data['folderscreated']++; $this->data['folderscreated']++;
} }
...@@ -2669,90 +2710,53 @@ class ArtefactTypeArchive extends ArtefactTypeFile { ...@@ -2669,90 +2710,53 @@ class ArtefactTypeArchive extends ArtefactTypeFile {
$tempdir = get_config('dataroot') . 'artefact/file/temp'; $tempdir = get_config('dataroot') . 'artefact/file/temp';
check_dir_exists($tempdir); check_dir_exists($tempdir);
if ($this->archivetype == 'tar') { $this->read_archive($keeptemphandle=true);
$this->read_archive();
// Untar everything into a temp directory first
$tempsubdir = tempnam($tempdir, '');
unlink($tempsubdir);
mkdir($tempsubdir, get_config('directorypermissions'));
if (!$this->handle->extract($tempsubdir)) {
throw new SystemException("Unable to extract archive into $tempsubdir");
}
$i = 0;
foreach ($this->info->names as $name) {
$folder = dirname($name);
$this->data['template']->parent = $this->data['folderids'][$folder];
$this->data['template']->title = basename($name);
// set the file extension for later use (eg by flowplayer)
$this->data['template']->extension = pathinfo($this->data['template']->title, PATHINFO_EXTENSION);
$this->data['template']->oldextension = $this->data['template']->extension;
if (substr($name, -1) == '/') { // This is a workaround to extract correctly files in an archive using PharData
$this->create_folder($folder); foreach ($this->info->names as $name) {
} if (empty($this->info->foldernames[$name])) {
else { file_get_contents($this->handle[$name], null, null, 0, 0);
ArtefactTypeFile::save_file($tempsubdir . '/' . $name, $this->data['template'], $quotauser, true);
$this->data['filescreated']++;
}
if ($progresscallback && ++$i % 5 == 0) {
call_user_func($progresscallback, $i);
}
} }
}
} else if ($this->archivetype == 'zip') { // Untar everything into a temp directory first
$tempsubdir = tempnam($tempdir, '');
$this->open_archive(); unlink($tempsubdir);
mkdir($tempsubdir, get_config('directorypermissions'));
$tempfile = tempnam($tempdir, ''); if (!$this->handle->extractTo($tempsubdir)) {
$i = 0; throw new ArchiveException(get_string('cannotextractarchive', 'artefact.file', $tempsubdir));
}
while ($entry = zip_read($this->handle)) {
$name = zip_entry_name($entry);
$folder = dirname($name);
// Create parent folders if necessary
if (!isset($this->data['folderids'][$folder])) {
$parent = '.';
$child = '';
$path = explode('/', $folder);
for ($i = 0; $i < count($path); $i++) {
$child .= $path[$i] . '/';
if (!isset($this->data['folderids'][$child])) {
$this->data['template']->parent = $this->data['folderids'][$parent];
$this->data['template']->title = $path[$i];
$this->create_folder($parent); if (!$keeptemphandle) {
} // Delete the temporary file
$parent = $child; self::delete_from_temp($tmparchivepath);
} $this->handle = null;
} $this->temparchivepathlength = 0;
}
$this->data['template']->parent = $this->data['folderids'][($folder == '.' ? '.' : ($folder . '/'))]; // Store extracted folders and files into mahara
$this->data['template']->title = basename($name); $i = 0;
foreach ($this->info->names as $name) {
$this->data['template']->parent = $this->data['folderids'][dirname($name)];
$this->data['template']->title = basename($name);
// set the file extension for later use (eg by flowplayer) // set the file extension for later use (eg by flowplayer)
$this->data['template']->extension = pathinfo($this->data['template']->title, PATHINFO_EXTENSION); $this->data['template']->extension = pathinfo($this->data['template']->title, PATHINFO_EXTENSION);
$this->data['template']->oldextension = $this->data['template']->extension; $this->data['template']->oldextension = $this->data['template']->extension;
if (substr($name, -1) != '/') { if (!empty($this->info->foldernames[$name])) {
$h = fopen($tempfile, 'w'); $this->create_folder($name);
$size = zip_entry_filesize($entry); }
$contents = zip_entry_read($entry, $size); else {
fwrite($h, $contents); ArtefactTypeFile::save_file($tempsubdir . '/' . $name, $this->data['template'], $quotauser, true);
fclose($h); $this->data['filescreated']++;
}
ArtefactTypeFile::save_file($tempfile, $this->data['template'], $quotauser, true); if ($progresscallback && ++$i % 5 == 0) {
$this->data['filescreated']++; call_user_func($progresscallback, $i);
}
if ($progresscallback && ++$i % 5 == 0) {
call_user_func($progresscallback, $i);
}
} }
} }
return $this->data; return $this->data;
} }
......
...@@ -837,6 +837,9 @@ $string['phpuploaderror_6'] = 'Missing a temporary folder.'; ...@@ -837,6 +837,9 @@ $string['phpuploaderror_6'] = 'Missing a temporary folder.';
$string['phpuploaderror_7'] = 'Failed to write file to disk. Check that your filesystem has enough space to write to the Mahara dataroot and/or the PHP \'upload_tmp_dir\' directories.'; $string['phpuploaderror_7'] = 'Failed to write file to disk. Check that your filesystem has enough space to write to the Mahara dataroot and/or the PHP \'upload_tmp_dir\' directories.';
$string['phpuploaderror_8'] = 'File upload stopped by extension.'; $string['phpuploaderror_8'] = 'File upload stopped by extension.';
$string['adminphpuploaderror'] = 'A file upload error was probably caused by your server configuration.'; $string['adminphpuploaderror'] = 'A file upload error was probably caused by your server configuration.';
$string['noinputnamesupplied'] = 'No input name is provided.';
$string['cannotrenametempfile'] = 'Can not rename the temporary file.';
$string['failedmovingfiletodataroot'] = 'Can not move uploaded file to dataroot.';
$string['youraccounthasbeensuspendedtext2'] = 'Your account at %s has been suspended by %s.'; // @todo: more info? $string['youraccounthasbeensuspendedtext2'] = 'Your account at %s has been suspended by %s.'; // @todo: more info?
$string['youraccounthasbeensuspendedtextcron'] = 'Your account at %s has been suspended.'; $string['youraccounthasbeensuspendedtextcron'] = 'Your account at %s has been suspended.';
......
...@@ -1062,3 +1062,17 @@ class ExportException extends SystemException { ...@@ -1062,3 +1062,17 @@ class ExportException extends SystemException {
return $this->getMessage(); return $this->getMessage();
} }
} }
/**
* An exception related to read/write/extract archive artefact
*/
class ArchiveException extends SystemException {
public function strings() {
return array_merge(parent::strings(),
array('message' => get_string('invalidarchive1', 'artefact.file'),
'title' => get_string('invalidarchive1', 'artefact.file')));
}
public function render_exception() {
return $this->getMessage();
}
}
...@@ -71,11 +71,11 @@ class upload_manager { ...@@ -71,11 +71,11 @@ class upload_manager {
return false; return false;
} }
else { else {
return get_string('noinputnamesupplied'); return get_string('noinputnamesupplied', 'mahara');
} }
} }
$file = $_FILES[$name];
$file = $_FILES[$name];
$maxsize = get_config('maxuploadsize'); $maxsize = get_config('maxuploadsize');
if (isset($this->inputindex)) { if (isset($this->inputindex)) {
$size = $file['size'][$this->inputindex]; $size = $file['size'][$this->inputindex];
...@@ -190,7 +190,7 @@ class upload_manager { ...@@ -190,7 +190,7 @@ class upload_manager {
chmod($destination . '/' . $newname, get_config('filepermissions')); chmod($destination . '/' . $newname, get_config('filepermissions'));
return false; return false;
} }
return get_string('failedmovingfiletodataroot'); return get_string('failedmovingfiletodataroot', 'mahara');
} }
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment