Moodle

When using a UNC path for the moodledata directory, ZIP functions list, unzip and zip do not work

Details

  • Type: Bug Bug
  • Status: Closed Closed
  • Priority: Major Major
  • Resolution: Won't Fix
  • Affects Version/s: 1.8.2
  • Fix Version/s: None
  • Component/s: Libraries
  • Labels:
    None
  • Environment:
    Windows 2003R2 SP2, PHP5.2.3 SQL 5.0.45
  • Database:
    MySQL
  • Affected Branches:
    MOODLE_18_STABLE

Description

When using a UNC path for the moodledata directory, the ZIP functions do not work, this appears to be due to the function cleardoubleslashes

//Replace 1 or more slashes or backslashes to 1 slash
function cleardoubleslashes ($path) {
return preg_replace('/(\/|\\\){1,}/','/',$path);

which remove the needed // before the path to files in the zip functions.

With the current functions the path moodle creates to a zip file, or to a file to be zipped ends up like the example below

/moodleserver/moodleshare/1/resourcefolder/resourse.ext

where it needs to be

//moodleserver/moodleshare/1/resourcefolder/resourse.ext

otherwise it reports it cannot access the file

I have made the following changes in the files and functions below to get this working for us, however this will likely break an install on a *nix platform, or on a windows platform with a local moodledata directory, so for this bug to be fixed properly, some sort of error checking on the moodledata folder, or a config option for UNC based moodledata directories may be needed

I have not provided line numbers as I have added in my own comments to the php files to track where I made the changes, which has obviously move the line numbers about.

function unzip_file and function unzip_cleanfilename in moodlelib.php (to fix unzip options)

function unzip_file ($zipfile, $destination = '', $showstatus = true) {
//Unzip one zip file to a destination dir
//Both parameters must be FULL paths
//If destination isn't specified, it will be the
//SAME directory where the zip file resides.

global $CFG;

//Extract everything from zipfile
// EDIT *** CHRIS SHEARING *** Added "/" . to allow for UNC Path
$path_parts = pathinfo("/" . cleardoubleslashes($zipfile));
$zippath = $path_parts["dirname"]; //The path of the zip file
$zipfilename = $path_parts["basename"]; //The name of the zip file
$extension = $path_parts["extension"]; //The extension of the file

//If no file, error
if (empty($zipfilename)) { return false; }

//If no extension, error
if (empty($extension)) { return false; } }

//Clear $zipfile
// EDIT *** CHRIS SHEARING *** Added "/" . to allow for UNC Path
$zipfile = "/" . cleardoubleslashes($zipfile);

//Check zipfile exists
if (!file_exists($zipfile)) { return false; }

//If no destination, passed let's go with the same directory
if (empty($destination)) { $destination = $zippath; }

//Clear $destination
// EDIT *** CHRIS SHEARING *** Added "/" . to allow for UNC Path
$destpath = rtrim("/" . cleardoubleslashes($destination), "/");

//Check destination path exists
if (!is_dir($destpath)) { return false; } }

//Check destination path is writable. TODO!!

//Everything is ready:
// -$zippath is the path where the zip file resides (dir)
// -$zipfilename is the name of the zip file (without path)
// -$destpath is the destination path where the zip file will uncompressed (dir)
print_object($zippath); //Debug
print_object($zipfilename); //Debug
print_object($destpath); //Debug

$list = null;

if (empty($CFG->unzip)) { // Use built-in php-based unzip function

include_once("$CFG->libdir/pclzip/pclzip.lib.php");
// EDIT *** CHRIS SHEARING *** Added "/" . to allow for UNC Path
$archive = new PclZip("/" . cleardoubleslashes("$zippath/$zipfilename"));
if (!$list = $archive->extract(PCLZIP_OPT_PATH, $destpath,
PCLZIP_CB_PRE_EXTRACT, 'unzip_cleanfilename',
PCLZIP_OPT_EXTRACT_DIR_RESTRICTION, $destpath)) { notice($archive->errorInfo(true)); return false; }

} else { // Use external unzip program

$separator = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' ? ' &' : ' ;';
$redirection = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' ? '' : ' 2>&1';

$command = 'cd '.escapeshellarg($zippath).$separator.
escapeshellarg($CFG->unzip).' -o '.
escapeshellarg(cleardoubleslashes("$zippath/$zipfilename")).' -d '.
escapeshellarg($destpath).$redirection;
//All converted to backslashes in WIN
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { $command = str_replace('/','\\',$command); }
Exec($command,$list);
}

//Display some info about the unzip execution
if ($showstatus) { unzip_show_status($list,$destpath); }

return true;
}

function unzip_cleanfilename ($p_event, &$p_header) {
//This function is used as callback in unzip_file() function
//to clean illegal characters for given platform and to prevent directory traversal.
//Produces the same result as info-zip unzip.
print_object($p_header); //Debug
print_object($p_event); //Debug
$p_header['filename'] = ereg_replace('[[:cntrl:]]', '', $p_header['filename']); //strip control chars first!
$p_header['filename'] = ereg_replace('\.\.+', '', $p_header['filename']); //directory traversal protection
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { $p_header['filename'] = ereg_replace('[:*"?<>|]', '_', $p_header['filename']); //replace illegal chars $p_header['filename'] = ereg_replace('^([a-zA-Z])_', '\1:', $p_header['filename']); //repair drive letter } else { //Add filtering for other systems here // BSD: none (tested) // Linux: ?? // MacosX: ?? }
// EDIT *** CHRIS SHEARING *** Added "/" . to allow for UNC Path
$p_header['filename'] = "/" . cleardoubleslashes($p_header['filename']); //normalize the slashes/backslashes
print_object($p_header); //Debug
print_object($p_event); //Debug
return 1;
}

case "listzip" in files/index.php (to fix the listzip option)

case "listzip":
html_header($course, $wdir);
if (($file != '') and confirm_sesskey()) {
$strname = get_string("name");
$strsize = get_string("size");
$strmodified = get_string("modified");
$strok = get_string("ok");
$strlistfiles = get_string("listfiles", "", $file);

echo "<p align=\"center\">$strlistfiles:</p>";
$file = basename($file);

include_once("$CFG->libdir/pclzip/pclzip.lib.php");
// EDIT *** CHRIS SHEARING *** Added "/" . to allow for UNC Path
$archive = new PclZip("/" . cleardoubleslashes("$basedir$wdir/$file"));
if (!$list = $archive->listContent(cleardoubleslashes("$basedir$wdir"))) { notify($archive->errorInfo(true)); } else {
echo "<table cellpadding=\"4\" cellspacing=\"2\" border=\"0\" width=\"640\" class=\"files\">";
echo "<tr class=\"file\"><th align=\"left\" class=\"header name\" scope=\"col\">$strname</th><th align=\"right\" class=\"header size\" scope=\"col\">$strsize</th><th align=\"right\" class=\"header date\" scope=\"col\">$strmodified</th></tr>";
foreach ($list as $item) {
echo "<tr>";
print_cell("left", s($item['filename']), 'name');
if (! $item['folder']) { print_cell("right", display_size($item['size']), 'size'); } else { echo "<td> </td>"; }
$filedate = userdate($item['mtime'], get_string("strftimedatetime"));
print_cell("right", $filedate, 'date');
echo "</tr>";
}
echo "</table>";
}
echo "<br /><center><form action=\"index.php\" method=\"get\">";
echo "<div>";
echo ' <input type="hidden" name="choose" value="'.$choose.'" />';
echo " <input type=\"hidden\" name=\"id\" value=\"$id\" />";
echo " <input type=\"hidden\" name=\"wdir\" value=\"$wdir\" />";
echo " <input type=\"hidden\" name=\"action\" value=\"cancel\" />";
echo " <input type=\"submit\" value=\"$strok\" />";
echo "</div>";
echo "</form>";
echo "</center>";
} else { displaydir($wdir); }
html_footer();
break;

function zip_files in moodlelib.php and function PclZipUtilPathReduction($p_dir) in pclzip.lib.php (I think this is actually an external library, and not a moodle one though, not sure if we can edit this!) (to fix zip creation)

function zip_files ($originalfiles, $destination) {
//Zip an array of files/dirs to a destination zip file
//Both parameters must be FULL paths to the files/dirs

global $CFG;

//Extract everything from destination
$path_parts = pathinfo(cleardoubleslashes($destination));
$destpath = $path_parts["dirname"]; //The path of the zip file
$destfilename = $path_parts["basename"]; //The name of the zip file
$extension = $path_parts["extension"]; //The extension of the file

//If no file, error
if (empty($destfilename)) { return false; }

//If no extension, add it
if (empty($extension)) { $extension = 'zip'; $destfilename = $destfilename.'.'.$extension; }

//Check destination path exists
if (!is_dir($destpath)) { return false; } }

//Check destination path is writable. TODO!!

//Clean destination filename
$destfilename = clean_filename($destfilename);

//Now check and prepare every file
$files = array();
$origpath = NULL;

foreach ($originalfiles as $file) { //Iterate over each file
//Check for every file
// EDIT *** CHRIS SHEARING *** Added "/" . to allow for UNC Path
$tempfile = "/" . cleardoubleslashes($file); // no doubleslashes!
//Calculate the base path for all files if it isn't set
if ($origpath === NULL) { $origpath = rtrim(cleardoubleslashes(dirname($tempfile)), "/"); }
//See if the file is readable

if (!is_readable($tempfile)) { //Is readable continue; }
//See if the file/dir is in the same directory than the rest
if (rtrim(cleardoubleslashes(dirname($tempfile)), "/") != $origpath) { continue; }
//Add the file to the array
$files[] = $tempfile;
}

//Everything is ready:
// -$origpath is the path where ALL the files to be compressed reside (dir).
// -$destpath is the destination path where the zip file will go (dir).
// -$files is an array of files/dirs to compress (fullpath)
// -$destfilename is the name of the zip file (without path)

// print_object($files); //Debug

if (empty($CFG->zip)) { // Use built-in php-based zip function

include_once("$CFG->libdir/pclzip/pclzip.lib.php");
//rewrite filenames because the old method with PCLZIP_OPT_REMOVE_PATH does not work under win32
$zipfiles = array();
$start = strlen($origpath)+1;
foreach($files as $file) { $tf = array(); $tf[PCLZIP_ATT_FILE_NAME] = $file; $tf[PCLZIP_ATT_FILE_NEW_FULL_NAME] = substr($file, $start); $zipfiles[] = $tf; }
//create the archive
// EDIT *** CHRIS SHEARING *** Added "/" . to allow for UNC Path
$archive = new PclZip("/" . cleardoubleslashes("$destpath/$destfilename"));
if (($list = $archive->create($zipfiles) == 0)) { notice($archive->errorInfo(true)); return false; }
} else { // Use external zip program

$filestozip = "";
foreach ($files as $filetozip) { $filestozip .= escapeshellarg(basename($filetozip)); $filestozip .= " "; }
//Construct the command
$separator = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' ? ' &' : ' ;';
$command = 'cd '.escapeshellarg($origpath).$separator.
escapeshellarg($CFG->zip).' -r '.
escapeshellarg(cleardoubleslashes("$destpath/$destfilename")).' '.$filestozip;
//All converted to backslashes in WIN
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { $command = str_replace('/','\',$command); } }
Exec($command);
}
return true;
}

Function PclZipUtilPathReduction($p_dir) in pclzip.lib.php

function PclZipUtilPathReduction($p_dir)
{
//-(MAGIC-PclTrace)-//PclTraceFctStart(_FILE, __LINE_, "PclZipUtilPathReduction", "dir='$p_dir'");
$v_result = "";

// ----- Look for not empty path
if ($p_dir != "") {
// ----- Explode path by directory names
$v_list = explode("/", $p_dir);

// ----- Study directories from last to first
$v_skip = 0;
for ($i=sizeof($v_list)1; $i>=0; $i-) {
// ----- Look for current path
if ($v_list[$i] == ".") { // ----- Ignore this directory // Should be the first $i=0, but no check is done }
else if ($v_list[$i] == "..") { $v_skip++; }
else if ($v_list[$i] == "") {
// ----- First '/' i.e. root slash
if ($i == 0) {
// EDIT *** CHRIS SHEARING *** Added "/" . to allow for UNC Path
$v_result = "//".$v_result;
if ($v_skip > 0) { // ----- It is an invalid path, so the path is not modified // TBC $v_result = $p_dir; //--(MAGIC-PclTrace)--//PclTraceFctMessage(__FILE__, __LINE__, 3, "Invalid path is unchanged"); $v_skip = 0; }
}
// ----- Last '/' i.e. indicates a directory
else if ($i == (sizeof($v_list)-1)) { $v_result = $v_list[$i]; }
// ----- Double '/' inside the path
else { // ----- Ignore only the double '//' in path, // but not the first and last '/' }
}
else {
// ----- Look for item to skip
if ($v_skip > 0) { $v_skip--; }
else { $v_result = $v_list[$i].($i!=(sizeof($v_list)-1)?"/".$v_result:""); }
}
}

// ----- Look for skip
if ($v_skip > 0) {
while ($v_skip > 0) { $v_result = '../'.$v_result; $v_skip--; }
}
}

// ----- Return
//-(MAGIC-PclTrace)-//PclTraceFctEnd(_FILE, __LINE_, $v_result);
return $v_result;
}

Activity

Hide
Umar Akram added a comment -

Hi,

When i'm trying to unzip (restore) a course i get the following error:

Course restore: backup-3070_012-20080227-0937.zip

  • Creating temporary structures
  • Deleting old data
  • Copying zip file
  • Unzipping backup
    PCLZIP_ERR_DIRECTORY_RESTRICTION (-21) : Filename is outside PCLZIP_OPT_EXTRACT_DIR_RESTRICTION

When i've added your code above into moodlelib.php to fix this problem i got the following error;
Parse error: parse error in vol1:/WEB/halo19at/lib/moodlelib.php on line 7221

I'm using Moodle v1.9Beta4 version. Upgraded from Mooodle 1.65

Awaiting fix for this issue.
Many Thanks
Umar

Show
Umar Akram added a comment - Hi, When i'm trying to unzip (restore) a course i get the following error: Course restore: backup-3070_012-20080227-0937.zip
  • Creating temporary structures
  • Deleting old data
  • Copying zip file
  • Unzipping backup PCLZIP_ERR_DIRECTORY_RESTRICTION (-21) : Filename is outside PCLZIP_OPT_EXTRACT_DIR_RESTRICTION
When i've added your code above into moodlelib.php to fix this problem i got the following error; Parse error: parse error in vol1:/WEB/halo19at/lib/moodlelib.php on line 7221 I'm using Moodle v1.9Beta4 version. Upgraded from Mooodle 1.65 Awaiting fix for this issue. Many Thanks Umar
Hide
Kiran Dhamane added a comment -

Changed the function definition of cleardoubleslashes in moodlelib.php to:
/********Code***********/
function cleardoubleslashes ($path) {
if(strncmp("//",$path,2)==0)
{
return "/".preg_replace('/(\/|\\\){1,}/','/',$path);
}
return preg_replace('/(\/|\\\){1,}/','/',$path);
}
/********Code**********/
Still testing for regression issues, so use the above at your own risk.
Couldn't understand the significance of the change in pclzip.lib.php in the above fix

Thanks,
Kiran

Show
Kiran Dhamane added a comment - Changed the function definition of cleardoubleslashes in moodlelib.php to: /********Code***********/ function cleardoubleslashes ($path) { if(strncmp("//",$path,2)==0) { return "/".preg_replace('/(\/|\\\){1,}/','/',$path); } return preg_replace('/(\/|\\\){1,}/','/',$path); } /********Code**********/ Still testing for regression issues, so use the above at your own risk. Couldn't understand the significance of the change in pclzip.lib.php in the above fix Thanks, Kiran
Hide
Lim Kai Yang added a comment -

Modify cleardoubleslashes in moodelib.php to check if the string contain double slashes/ backslashes (for network server access)
No modification if double slashes are found.

function cleardoubleslashes ($path) {
if (substr($path, 0, 2) == "\\\\" or substr($path, 0, 2) == "//")
return $path;
else
return preg_replace('/(\/|\\\){1,}/','/',$path);
}

Further more, I found out that, PCLZIP does not do any modification on "\"
so, configuring $CFG->dataroot = "\\\\servername\pathname" is better than "//servername/pathname"
I have tried to backup to zip and also unzip the backup and both execute successfully.

Show
Lim Kai Yang added a comment - Modify cleardoubleslashes in moodelib.php to check if the string contain double slashes/ backslashes (for network server access) No modification if double slashes are found. function cleardoubleslashes ($path) { if (substr($path, 0, 2) == "\\\\" or substr($path, 0, 2) == "//") return $path; else return preg_replace('/(\/|\\\){1,}/','/',$path); } Further more, I found out that, PCLZIP does not do any modification on "\" so, configuring $CFG->dataroot = "\\\\servername\pathname" is better than "//servername/pathname" I have tried to backup to zip and also unzip the backup and both execute successfully.
Hide
Alastair Hole added a comment -

I have just come up against this issue, fixed using similar technique as above, i.e. modifying cleardoubleslashes with the same logic. i.e. if the string starts with // or
then return these first two characters intact.
Sucessfully using config:
$CFG->dataroot = '//path/to/unc/moodledata';

Please can we have a fix for this included in a future release?
Many Thanks

Show
Alastair Hole added a comment - I have just come up against this issue, fixed using similar technique as above, i.e. modifying cleardoubleslashes with the same logic. i.e. if the string starts with // or
then return these first two characters intact. Sucessfully using config: $CFG->dataroot = '//path/to/unc/moodledata'; Please can we have a fix for this included in a future release? Many Thanks
Hide
Petr Škoda (skodak) added a comment - - edited

the problem with UNC is that it can not be set as current directory, unfortunately PHP functions sometimes require that. I am afraid 1.9.x will never support this, maybe some later 2.0.x will fully support this, sorry

Show
Petr Škoda (skodak) added a comment - - edited the problem with UNC is that it can not be set as current directory, unfortunately PHP functions sometimes require that. I am afraid 1.9.x will never support this, maybe some later 2.0.x will fully support this, sorry
Hide
Michael de Raadt added a comment -

Thanks for reporting this issue.

We have detected that this issue has been inactive for over a year has been recorded as affecting versions that are no longer supported.

If you believe that this issue is still relevant to current versions (2.1 and beyond), please comment on the issue. Issues left inactive for a further month will be closed.

Michael d;

lqjjLKA0p6

Show
Michael de Raadt added a comment - Thanks for reporting this issue. We have detected that this issue has been inactive for over a year has been recorded as affecting versions that are no longer supported. If you believe that this issue is still relevant to current versions (2.1 and beyond), please comment on the issue. Issues left inactive for a further month will be closed. Michael d; lqjjLKA0p6
Hide
Michael de Raadt added a comment -

I'm closing this issue as it has become inactive and does not appear to affect a current supported version. If you are encountering this problem or one similar, please launch a new issue.

Show
Michael de Raadt added a comment - I'm closing this issue as it has become inactive and does not appear to affect a current supported version. If you are encountering this problem or one similar, please launch a new issue.

Dates

  • Created:
    Updated:
    Resolved: