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)) {
throw new AccessDeniedException(get_string('cannotextractfileinfoldersubmitted', 'artefact.file'));
}
}
try {
$zipinfo = $file->read_archive();
}
catch (SystemException $e) {
$message = get_string('invalidarchive', 'artefact.file');
}
// Read the archive information, throw an ArchiveException if error
$zipinfo = $file->read_archive();
if ($zipinfo) {
$quotaallowed = false;
......@@ -147,7 +144,12 @@ function unzip_artefact_submit(Pieform $form, $values) {
$from = files_page($file);
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->display('artefact:file:extract-progress.tpl');
exit;
......
......@@ -310,7 +310,13 @@ $string['filesextractedfromarchive'] = 'Files extracted from archive';
$string['filesextractedfromziparchive'] = 'Files extracted from Zip archive';
$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['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['spacerequired'] = 'Space required';
$string['unzipprogress'] = '%s files/folders created.';
......
......@@ -2428,9 +2428,10 @@ class ArtefactTypeProfileIcon extends ArtefactTypeImage {
class ArtefactTypeArchive extends ArtefactTypeFile {
protected $archivetype;
protected $handle;
protected $handle = null; // Handle for the temporary archive file
protected $info;
protected $data = array();
protected $temparchivepathlength = 0; // The length of the phar path to the temporary archive
public function __construct($id = 0, $data = null) {
parent::__construct($id, $data);
......@@ -2451,50 +2452,46 @@ class ArtefactTypeArchive extends ArtefactTypeFile {
$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 (self::is_zip($path)) {
if ($archive_obj->isFileFormat(Phar::ZIP)) {
$data->filetype = 'application/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';
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;
return true;
$is_valid = true;
}
return false;
}
public static function is_zip($path) {
if (function_exists('zip_read')) {
$zip = zip_open($path);
if (is_resource($zip)) {
zip_close($zip);
return true;
}
}
return false;
}
// Remove the temporary after validate the archive file
self::delete_from_temp($path);
public static function is_tar($path) {
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;
return $is_valid;
}
public static function archive_file_descriptions() {
......@@ -2519,19 +2516,45 @@ class ArtefactTypeArchive extends ArtefactTypeFile {
return false;
}
public function open_archive() {
if ($this->archivetype == 'zip') {
$this->handle = zip_open($this->get_path());
if (!is_resource($this->handle)) {
$this->handle = null;
throw new NotFoundException();
}
/**
* Copy the archive file to the system temporary directory
* and add .tmp to a file name and return the new filename
* This is a workaround for read and extract a zip/tar file using PharData
*
* 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') {
require_once('Archive/Tar.php');
if (!$this->handle = new Archive_Tar($this->get_path())) {
throw new NotFoundException();
}
else {
throw new SystemException(get_string('cannotcopytemparchive', 'artefact/file', $path, $name));
}
}
/**
* 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 {
$this->info = $zipinfo;
}
private function read_entry($name, $isfolder, $size) {
$path = explode('/', $name);
if ($isfolder) {
array_pop($path);
/**
* Recursively read the content of an archive
* and update $this->info
*
* @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 = '';
for ($i = 0; $i < count($path) - 1; $i++) {
$folder .= $path[$i] . '/';
if (!isset($this->foldernames[$folder])) {
$this->foldernames[$folder] = 1;
$this->info->names[] = $folder;
$this->info->folders++;
$this->read_archive_folder($i->getPathName());
}
else {
$this->info->names[] = $name;
$this->info->files++;
$this->info->totalsize += $i->getCompressedSize();
}
}
}
if (!$isfolder) {
$this->info->names[] = $name;
$this->info->files++;
$this->info->totalsize += $size;
catch (Exception $e) {
throw new ArchiveException(get_string('cannotreadarchivecontent', 'artefact.file') . $e->getMessage());
}
}
public function read_archive() {
if (!$this->handle) {
$this->open_archive();
}
if ($this->info) {
return $this->info;
}
$this->info = (object) array(
'files' => 0,
'folders' => 0,
'totalsize' => 0,
'names' => array(),
);
$this->foldernames = array();
/**
* Read the archive content from a temporary archive file
* and update $this->info
*
* @param Bool $keeptemphandle = true: the temporary file and handle
* will NOT be deleted for later use
* @return Object archive info
* @throws ArchiveException
*/
public function read_archive($keeptemphandle=false) {
// In mahara all physical file is stored without extension.
// 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
$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') {
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);
}
$this->read_archive_folder();
}
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);
return $this->info;
}
......@@ -2641,13 +2688,7 @@ class ArtefactTypeArchive extends ArtefactTypeFile {
public function create_folder($folder) {
$newfolder = new ArtefactTypeFolder(0, $this->data['template']);
$newfolder->commit();
if ($this->archivetype == 'zip') {
$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['folderids'][$folder] = $newfolder->get('id');
$this->data['folderscreated']++;
}
......@@ -2669,90 +2710,53 @@ class ArtefactTypeArchive extends ArtefactTypeFile {
$tempdir = get_config('dataroot') . 'artefact/file/temp';
check_dir_exists($tempdir);
if ($this->archivetype == 'tar') {
$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;
$this->read_archive($keeptemphandle=true);
if (substr($name, -1) == '/') {
$this->create_folder($folder);
}
else {
ArtefactTypeFile::save_file($tempsubdir . '/' . $name, $this->data['template'], $quotauser, true);
$this->data['filescreated']++;
}
if ($progresscallback && ++$i % 5 == 0) {
call_user_func($progresscallback, $i);
}
// This is a workaround to extract correctly files in an archive using PharData
foreach ($this->info->names as $name) {
if (empty($this->info->foldernames[$name])) {
file_get_contents($this->handle[$name], null, null, 0, 0);
}
}
} else if ($this->archivetype == 'zip') {
$this->open_archive();
$tempfile = tempnam($tempdir, '');
$i = 0;
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];
// Untar everything into a temp directory first
$tempsubdir = tempnam($tempdir, '');
unlink($tempsubdir);
mkdir($tempsubdir, get_config('directorypermissions'));
if (!$this->handle->extractTo($tempsubdir)) {
throw new ArchiveException(get_string('cannotextractarchive', 'artefact.file', $tempsubdir));
}
$this->create_folder($parent);
}
$parent = $child;
}
}
if (!$keeptemphandle) {
// Delete the temporary file
self::delete_from_temp($tmparchivepath);
$this->handle = null;
$this->temparchivepathlength = 0;
}
$this->data['template']->parent = $this->data['folderids'][($folder == '.' ? '.' : ($folder . '/'))];
$this->data['template']->title = basename($name);
// Store extracted folders and files into mahara
$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)
$this->data['template']->extension = pathinfo($this->data['template']->title, PATHINFO_EXTENSION);
$this->data['template']->oldextension = $this->data['template']->extension;
// 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) != '/') {
$h = fopen($tempfile, 'w');
$size = zip_entry_filesize($entry);
$contents = zip_entry_read($entry, $size);
fwrite($h, $contents);
fclose($h);
if (!empty($this->info->foldernames[$name])) {
$this->create_folder($name);
}
else {
ArtefactTypeFile::save_file($tempsubdir . '/' . $name, $this->data['template'], $quotauser, true);
$this->data['filescreated']++;
}
ArtefactTypeFile::save_file($tempfile, $this->data['template'], $quotauser, true);
$this->data['filescreated']++;
}
if ($progresscallback && ++$i % 5 == 0) {
call_user_func($progresscallback, $i);
}
if ($progresscallback && ++$i % 5 == 0) {
call_user_func($progresscallback, $i);
}
}
return $this->data;
}
......
......@@ -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_8'] = 'File upload stopped by extension.';
$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['youraccounthasbeensuspendedtextcron'] = 'Your account at %s has been suspended.';
......
......@@ -1062,3 +1062,17 @@ class ExportException extends SystemException {
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 {
return false;
}
else {
return get_string('noinputnamesupplied');
return get_string('noinputnamesupplied', 'mahara');
}
}
$file = $_FILES[$name];
$file = $_FILES[$name];
$maxsize = get_config('maxuploadsize');
if (isset($this->inputindex)) {
$size = $file['size'][$this->inputindex];
......@@ -190,7 +190,7 @@ class upload_manager {
chmod($destination . '/' . $newname, get_config('filepermissions'));
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