Commit c5bb2486 authored by Robert Lyon's avatar Robert Lyon Committed by Gerrit Code Review
Browse files

Merge "Download any folder as a zip file (from file browser) (Bug #1013022)"

parents 24e8700e bd3d6184
......@@ -78,6 +78,18 @@ class PluginBlocktypeFolder extends PluginBlocktype {
)
),
);
$elements['downloadfolderzip'] = array(
'type' => 'fieldset',
'legend' => get_string('zipdownloadheading', 'artefact.file'),
'elements' => array(
'folderdownloadzip' => array(
'type' => 'checkbox',
'title' => get_string('downloadfolderzip', 'artefact.file'),
'description' => get_string('downloadfolderzipdescription', 'artefact.file'),
'defaultvalue' => get_config_plugin('blocktype', 'folder', 'folderdownloadzip'),
),
),
);
return array(
'elements' => $elements,
);
......@@ -85,6 +97,7 @@ class PluginBlocktypeFolder extends PluginBlocktype {
public static function save_config_options($values) {
set_config_plugin('blocktype', 'folder', 'sortorder', $values['sortorder']);
set_config_plugin('blocktype', 'folder', 'folderdownloadzip', $values['folderdownloadzip']);
}
public static function postinst($prevversion) {
......@@ -110,12 +123,12 @@ class PluginBlocktypeFolder extends PluginBlocktype {
'options' => array('asc' => get_string('ascending'), 'desc' => get_string('descending')),
),
);
if (get_config_plugin('artefact', 'file', 'folderdownloadzip')) {
if (get_config_plugin('blocktype', 'folder', 'folderdownloadzip')) {
$elements['folderdownloadzip'] = array(
'type' => 'checkbox',
'labelhtml' => get_string('downloadfolderzipblock', 'artefact.file'),
'description' => get_string('downloadfolderzipdescriptionblock', 'artefact.file'),
'defaultvalue' => isset($configdata['folderdownloadzip']) ? $configdata['folderdownloadzip'] : get_config_plugin('artefact', 'file', 'folderdownloadzip'),
'defaultvalue' => isset($configdata['folderdownloadzip']) ? $configdata['folderdownloadzip'] : get_config_plugin('blocktype', 'folder', 'folderdownloadzip'),
);
}
return $elements;
......
......@@ -15,22 +15,25 @@ require(dirname(dirname(dirname(__FILE__))) . '/init.php');
safe_require('artefact', 'file');
require_once('file.php');
if (!get_config_plugin('artefact', 'file', 'folderdownloadzip')) {
throw new AccessDeniedException(get_string('accessdenied', 'error'));
}
$folderid = param_integer('folder', 0);
$viewid = param_integer('view', 0);
$groupid = param_integer('group', 0);
$institution = param_alpha('institution', 0);
/*
* Function to check if the specified artefact should be downloadable
* (ie. whether it or one of its parents is in the view and whether the
* current user can view the view)
*/
function can_download_artefact($artefact) {
global $USER;
global $viewid;
if (artefact_in_view($artefact->get('id'), $viewid)) {
if ($USER->can_view_artefact($artefact)) {
return true;
}
else if (artefact_in_view($artefact->get('id'), $viewid)) {
return can_view_view($viewid);
}
......@@ -45,6 +48,19 @@ function can_download_artefact($artefact) {
return false;
}
/*
* Function to clean up a string so that it can be used as a filename
*/
function zip_filename_from($name) {
$name = preg_replace('#\s+#', '_', strtolower($name));
// \pL is used to match any letter in any alphabet (http://php.net/manual/en/regexp.reference.unicode.php)
$name = preg_replace('#[^\pL0-9_\-]+#', '', $name);
if ($name != '') {
$name = '-' . $name;
}
return get_string('zipfilenameprefix', 'artefact.file') . $name . '.zip';
}
/*
* Function to clean up zip file created by this script
* from the temp directory in the dataroot.
......@@ -53,7 +69,7 @@ function zip_clean_temp_dir() {
global $USER;
$temp_path = get_config('dataroot').'temp/';
$regex = '#(^directory-([0-9]+)-([A-Za-z0-9\-_]+))-([0-9]+)\.zip#';
$regex = '#' . '([0-9]+)-([0-9]+)-' . get_string('zipfilenameprefix', 'artefact.file') . '-([\pL0-9_\-]+)\.zip#';
$zipfiles = glob($temp_path.'*.zip');
$zips = array();
......@@ -61,8 +77,8 @@ function zip_clean_temp_dir() {
foreach ($zipfiles as $zipfile) {
$zip = str_replace($temp_path, '', $zipfile);
if (preg_match($regex, $zip, $matches)) {
if ((int) $matches[2] == $USER->get('id')) {
$filename = $matches[1];
if ((int) $matches[1] == $USER->get('id')) {
$filename = $matches[3];
if (!isset($zips[$filename])) {
$zips[$filename] = array();
......@@ -71,7 +87,7 @@ function zip_clean_temp_dir() {
$zips[$filename][] = $zipfile;
$zips[$filename]['count']++;
$zips[$filename]['timecreated'] = (int) $matches[4];
$zips[$filename]['timecreated'] = (int) $matches[2];
}
}
}
......@@ -104,11 +120,10 @@ function zip_clean_temp_dir() {
}
/*
* Function to recursively add directories and files
* to the zip file.
* Function to start the process of adding directories to the zip file
* @returns an array of all files in the directory and subdirectories
*/
function zip_process_directory(&$zip, $folderid, $path) {
global $USER;
$folderinfo = get_record('artefact', 'id', $folderid);
if (empty($folderinfo)) {
......@@ -118,30 +133,50 @@ function zip_process_directory(&$zip, $folderid, $path) {
$folder = artefact_instance_from_id($folderinfo->id);
$foldercontent = $folder->folder_contents();
return zip_process_contents($zip, $foldercontent, $path);
}
/*
* Function to recursively add directories to the zip file
* @returns an array of all files in this and subdirectories
*/
function zip_process_contents(&$zip, $foldercontent, $path) {
$files = array();
$zip->addEmptyDir(rtrim($path, '/'));
if (!empty($foldercontent)) {
$folders = array();
if ($foldercontent) {
foreach ($foldercontent as $content) {
$item = artefact_instance_from_id($content->id);
if (!can_download_artefact($item)) {
continue;
}
if ($content->artefacttype === 'folder') {
if (!isset($folders[$content->id]) && $content->title != get_string('parentfolder', 'artefact.file')) {
$folders[$content->id] = $content;
}
$files = array_merge($files, zip_process_directory($zip, $content->id, $path . $content->title . '/'));
}
else {
if (can_download_artefact($item)) {
$zip->addFile($item->get_path(), $path.$item->download_title());
}
$files[] = array($item->get_path(), $path . $item->download_title());
}
}
}
foreach ($folders as $folder) {
$dir = artefact_instance_from_id($folder->id);
if (can_download_artefact($dir)) {
zip_process_directory($zip, $folder->id, $path.$folder->title.'/');
}
return $files;
}
/*
* Function to write the given files to the zip file. This assumes all required directories have been created.
* Writes files in chunks since ZipArchive doesn't unlock files until it's closed - large numbers of files could
* exceed the maximum number of files allowed to be open at once (eg. ulimit on Linux)
*/
function zip_write_contents(&$zip, $filepath, $allfiles) {
$chunks = array_chunk($allfiles, 500);
foreach ($chunks as $chunk) {
foreach ($chunk as $file) {
$zip->addFile($file[0], $file[1]);
}
$zip->close();
$zip->open($filepath);
}
}
......@@ -160,43 +195,135 @@ else {
// Clean up the temp directory before creating anymore zip files.
zip_clean_temp_dir();
$folderinfo = get_record('artefact', 'id', $folderid);
// Home folder
if ($folderid === 0) {
if (function_exists('zip_open')) {
global $USER;
$userid = $USER->get('id');
$select = '
SELECT a.id, a.artefacttype, a.title';
$from = '
FROM {artefact} a';
$in = "('".join("','", PluginArtefactFile::get_artefact_types())."')";
$where = "
WHERE artefacttype IN $in";
$phvals = array();
if ($institution) {
if ($institution == 'mahara' && !$USER->get('admin')) {
// If non-admins are browsing site files, only let them see the public folder & its contents
$publicfolder = ArtefactTypeFolder::admin_public_folder_id();
$from .= '
LEFT OUTER JOIN {artefact_parent_cache} pub ON (a.id = pub.artefact AND pub.parent = ?)';
$where .= '
AND (pub.parent = ? OR a.id = ?)';
$phvals = array($publicfolder, $publicfolder, $publicfolder);
}
$where .= '
AND a.institution = ? AND a.owner IS NULL';
$phvals[] = $institution;
}
else if ($groupid) {
$select .= ',
r.can_edit, r.can_view, r.can_republish, a.author';
$from .= '
LEFT OUTER JOIN (
SELECT ar.artefact, ar.can_edit, ar.can_view, ar.can_republish
FROM {artefact_access_role} ar
INNER JOIN {group_member} gm ON ar.role = gm.role
WHERE gm.group = ? AND gm.member = ?
) r ON r.artefact = a.id';
$phvals[] = $groupid;
$phvals[] = $USER->get('id');
$where .= '
AND a.group = ? AND a.owner IS NULL AND (r.can_view = 1 OR a.author = ?)';
$phvals[] = $groupid;
$phvals[] = $USER->get('id');
}
else {
$where .= '
AND a.institution IS NULL AND a.owner = ?';
$phvals[] = $userid;
}
$where .= '
AND a.parent IS NULL';
$can_edit_parent = true;
$can_view_parent = true;
if (empty($folderinfo)) {
throw new NotFoundException();
}
$contents = get_records_sql_assoc($select . $from . $where, $phvals);
if (function_exists('zip_open')) {
$folder = artefact_instance_from_id($folderinfo->id);
if (!empty($contents)) {
$zip = new ZipArchive();
if (can_download_artefact($folder)) {
$zip = new ZipArchive();
if ($groupid) {
$group = get_record_sql("SELECT g.name FROM {group} g WHERE g.id = ? AND g.deleted = 0", array($groupid));
$downloadname = zip_filename_from($group->name);
}
else if ($institution) {
$downloadname = zip_filename_from($institution);
}
else {
$downloadname = zip_filename_from(get_string('home', 'artefact.file'));
}
$filename = $USER->get('id') . '-' . time() . '-' . $downloadname;
$filepath = get_config('dataroot') . 'temp/' . $filename;
$name = preg_replace('#\s+#', '_', strtolower($folderinfo->title));
$name = preg_replace('#[^\pL0-9_\-]+#', '', $name);
if ($name != '') {
$name = '-' . $name;
}
$name = get_string('zipfilenameprefix', 'artefact.file') . $name;
if ($zip->open($filepath, ZIPARCHIVE::CREATE) !== true) {
throw new NotFoundException();
}
$filename = 'directory-'.$USER->get('id').'-'.$name.'-'.time().'.zip';
$filepath = get_config('dataroot').'temp/'.$filename;
$files = zip_process_contents($zip, $contents, '/');
zip_write_contents($zip, $filepath, $files);
$zip->close();
if ($zip->open($filepath, ZIPARCHIVE::CREATE) !== true) {
serve_file($filepath, $downloadname, 'application/zip', $options);
}
else {
throw new NotFoundException();
}
}
else {
throw new SystemException(get_string('phpzipneeded', 'artefact.file'));
}
}
else {
$folderinfo = get_record('artefact', 'id', $folderid);
if (empty($folderinfo)) {
throw new NotFoundException();
}
zip_process_directory($zip, $folderid, $folderinfo->title.'/');
if (function_exists('zip_open')) {
$folder = artefact_instance_from_id($folderinfo->id);
$zip->close();
if (can_download_artefact($folder)) {
$zip = new ZipArchive();
$foldername = $folderinfo->title;
$filename = 'directory-'.$USER->get('id').'-'.$foldername.'-'.time().'.zip';
$filepath = get_config('dataroot').'temp/'.$filename;
$downloadname = $name . '.zip';
serve_file($filepath, $downloadname, 'application/zip', $options);
if ($zip->open($filepath, ZIPARCHIVE::CREATE) !== true) {
throw new NotFoundException();
}
$files = zip_process_directory($zip, $folderid, $folderinfo->title.'/');
zip_write_contents($zip, $filepath, $files);
$zip->close();
$downloadname = zip_filename_from($foldername);
serve_file($filepath, $downloadname, 'application/zip', $options);
}
else {
throw new AccessDeniedException(get_string('accessdenied', 'error'));
}
}
else {
throw new AccessDeniedException(get_string('accessdenied', 'error'));
throw new SystemException(get_string('phpzipneeded', 'artefact.file'));
}
}
else {
throw new SystemException(get_string('phpzipneeded', 'artefact.file'));
}
......@@ -296,12 +296,13 @@ $string['filepermission.edit'] = 'Edit';
$string['filepermission.republish'] = 'Publish';
// Download Folder as zip
$string['zipdownloadheading'] = 'Folder downloads';
$string['downloadfolderzip'] = 'Download folders as a zip file';
$string['downloadfolderzipblock'] = 'Show download link';
$string['downloadfolderzipdescription'] = 'If checked, users can download a folder, displayed via the Media -> Folder block, as a zip file.';
$string['downloadfolderzipdescriptionblock'] = 'If checked, users can download the folder as a zip file.';
$string['downloadfolderziplink'] = 'Download';
$string['downloadfolderziplink'] = 'Download folder content as a zip file';
$string['folderdownloadnofolderfound'] = 'Unable to find the folder with ID %d';
$string['zipfilenameprefix'] = 'folder';
$string['keepzipfor'] = 'How long to keep zip files for';
$string['keepzipfordescription'] = 'How many seconds should the zips files be kept for.';
$string['keepzipfor'] = 'Length of time to keep zip files';
$string['keepzipfordescription'] = 'Zip files created during the downloading of folders should be kept for this amount of time (in seconds).';
......@@ -1081,7 +1081,7 @@ class ArtefactTypeFile extends ArtefactTypeFileBase {
public function download_title() {
$extn = $this->get('oldextension');
$name = $this->get('title');
if (substr($name, -1-strlen($extn)) == '.' . $extn) {
if (empty($extn) || substr($name, -1-strlen($extn)) == '.' . $extn) {
return $name;
}
return $name . (substr($name, -1) == '.' ? '' : '.') . $extn;
......@@ -1517,14 +1517,8 @@ class ArtefactTypeFile extends ArtefactTypeFileBase {
$elements['folderdownloadzip'] = array(
'type' => 'fieldset',
'legend' => get_string('downloadfolderzip', 'artefact.file'),
'legend' => get_string('zipdownloadheading', 'artefact.file'),
'elements' => array(
'folderdownloadzip' => array(
'type' => 'checkbox',
'title' => get_string('downloadfolderzip', 'artefact.file'),
'description' => get_string('downloadfolderzipdescription', 'artefact.file'),
'defaultvalue' => get_config_plugin('artefact', 'file', 'folderdownloadzip'),
),
'folderdownloadkeepzipfor' => array(
'type' => 'text',
'title' => get_string('keepzipfor', 'artefact.file'),
......@@ -1574,7 +1568,6 @@ class ArtefactTypeFile extends ArtefactTypeFileBase {
set_config_plugin('artefact', 'file', 'resizeonuploaduseroption', $values['resizeonuploaduseroption']);
set_config_plugin('artefact', 'file', 'resizeonuploadmaxwidth', $values['resizeonuploadmaxwidth']);
set_config_plugin('artefact', 'file', 'resizeonuploadmaxheight', $values['resizeonuploadmaxheight']);
set_config_plugin('artefact', 'file', 'folderdownloadzip', $values['folderdownloadzip']);
set_config_plugin('artefact', 'file', 'folderdownloadkeepzipfor', $values['folderdownloadkeepzipfor']);
$data = new StdClass;
$data->name = 'uploadcopyright';
......@@ -1709,7 +1702,7 @@ class ArtefactTypeFolder extends ArtefactTypeFileBase {
$smarty->assign('viewid', isset($options['viewid']) ? $options['viewid'] : 0);
$smarty->assign('simpledisplay', isset($options['simpledisplay']) ? $options['simpledisplay'] : false);
$smarty->assign('folderid', $this->get('id'));
$smarty->assign('downloadfolderzip', (isset($options['folderdownloadzip']) ? $options['folderdownloadzip'] : get_config_plugin('artefact', 'file', 'folderdownloadzip')));
$smarty->assign('downloadfolderzip', get_config_plugin('blocktype', 'folder', 'folderdownloadzip') ? !empty($options['folderdownloadzip']) : false);
if ($childrecords = $this->folder_contents()) {
$this->add_to_render_path($options);
......
......@@ -102,7 +102,7 @@
</div>
<div id="{$prefix}_filelist_container">
{include file="artefact:file:form/filelist.tpl" prefix=$prefix filelist=$filelist editable=$config.edit selectable=$config.select highlight=$highlight edit=$edit querybase=$querybase groupinfo=$groupinfo owner=$tabs.owner ownerid=$tabs.ownerid selectfolders=$config.selectfolders showtags=$config.showtags editmeta=$config.editmeta}
{include file="artefact:file:form/filelist.tpl" prefix=$prefix filelist=$filelist folderdownload=$folderdownload folderparams=$folderparams editable=$config.edit selectable=$config.select highlight=$highlight edit=$edit querybase=$querybase groupinfo=$groupinfo owner=$tabs.owner ownerid=$tabs.ownerid selectfolders=$config.selectfolders showtags=$config.showtags editmeta=$config.editmeta}
</div>
{* Edit form used when js is available *}
......
......@@ -100,4 +100,7 @@
{/foreach}
</tbody>
</table>
<p>
<a href="{$WWWROOT}artefact/file/downloadfolder.php?{$folderparams|safe}">{str tag=downloadfolderziplink section=artefact.file}</a>
</p>
{/if}
......@@ -171,6 +171,15 @@ function pieform_element_filebrowser(Pieform $form, $element) {
$smarty->assign('initjs', $initjs);
$smarty->assign('querybase', $element['page'] . (strpos($element['page'], '?') === false ? '?' : '&'));
$params = 'folder=' . $folder;
if ($group) {
$params .= '&group=' . $group;
}
if ($institution) {
$params .= '&institution=' . $institution;
}
$smarty->assign('folderparams', $params);
return $smarty->fetch('artefact:file:form/filebrowser.tpl');
}
......@@ -280,6 +289,16 @@ function pieform_element_filebrowser_build_filelist($form, $element, $folder, $h
$smarty->assign('querybase', $querybase);
$smarty->assign('prefix', $prefix);
$params = 'folder=' . ($folder === null ? 0 : $folder);
if ($group !== null) {
$params .= '&group=' . $group;
}
if ($institution !== null) {
$params .= '&institution=' . $institution;
}
$smarty->assign('folderparams', $params);
return array(
'data' => $filedata,
'html' => $smarty->fetch('artefact:file:form/filelist.tpl'),
......
......@@ -96,15 +96,29 @@ $options = array(
);
if ($artefact->get('artefacttype') == 'folder') {
// get folder block sort order - returns the first instance of folder on view
// why you'd want more than one folder block on the same view is m̶a̶d̶n̶e̶s̶s̶ user preference.
if ($block = get_records_sql_array('SELECT block FROM {view_artefact} WHERE view = ? AND artefact = ?', array($viewid, $baseobject->get('id')),0,1)) {
// get folder block sort order - returns the first instance of folder on view unless $blockid is set.
// Why you'd want more than one folder block on the same view is m̶a̶d̶n̶e̶s̶s̶ user preference.
// TO DO: get the clicking on a subfolder to carry the block id as well - that way we can get exact configdata
if ($block = get_records_sql_array('SELECT block FROM {view_artefact} WHERE view = ? AND artefact = ?', array($viewid, $baseobject->get('id')))) {
require_once(get_config('docroot') . 'blocktype/lib.php');
$bi = new BlockInstance($block[0]->block);
$key = 0;
// If we have a $blockid then we will use that one's configdata
if ($blockid) {
foreach ($block as $k => $b) {
if ($b->block == $blockid) {
$key = $k;
break;
}
}
}
$bi = new BlockInstance($block[$key]->block);
$configdata = $bi->get('configdata');
if (!empty($configdata['sortorder'])) {
$options['sortorder'] = $configdata['sortorder'];
}
if (!empty($configdata['folderdownloadzip'])) {
$options['folderdownloadzip'] = true;
}
}
}
$rendered = $artefact->render_self($options);
......
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