tao-test/app/generis/helpers/class.File.php

457 lines
15 KiB
PHP

<?php
/**
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; under version 2
* of the License (non-upgradable).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* Copyright (c) 2002-2008 (original work) Public Research Centre Henri Tudor & University of Luxembourg (under the project TAO & TAO2);
* 2008-2010 (update and modification) Deutsche Institut für Internationale Pädagogische Forschung (under the project TAO-TRANSFER);
* 2009-2012 (update and modification) Public Research Centre Henri Tudor (under the project TAO-SUSTAIN & TAO-DEV);
* 2013 (update and modification) Open Assessment Technologies SA (under the project TAO-PRODUCT);
*
*/
/**
* Generis Object Oriented API - helpers/class.File.php
*
* Utilities on files
*
* @access public
* @author Lionel Lecaque, <lionel@taotesting.com>
* @package generis
*/
class helpers_File
{
const SCAN_FILE = 1;
const SCAN_DIRECTORY = 2;
// --- ATTRIBUTES ---
/**
* matches [A-Za-z] | - | _
* @var array
*/
private static $ALLOWED_CHARACTERS = ['A' => '','B' => '','C' => '','D' => '','E' => '','F' => '','G' => '','H' => '','I' => '','J' => '','K' => '','L' => '','M' => '','N' => '','O' => '','P' => '','Q' => '','R' => '','S' => '','T' => '','U' => '','V' => '','W' => '','X' => '','Y' => '','Z' => '','a' => '','b' => '','c' => '','d' => '','e' => '','f' => '','g' => '','h' => '','i' => '','j' => '','k' => '','l' => '','m' => '','n' => '','o' => '','p' => '','q' => '','r' => '','s' => '','t' => '','u' => '','v' => '','w' => '','x' => '','y' => '','z' => '',0 => '',1 => '',2 => '',3 => '',4 => '',5 => '',6 => '',7 => '',8 => '',9 => '','_' => '','-' => ''];
/**
* Directory Mode
*
* @access public
* @deprecated use helpers_File::SCAN_DIRECTORY
* @var int
*/
public static $DIR = 2;
/**
* File Mode
*
* @access public
* @deprecated use helpers_File::SCAN_FILE
* @var int
*/
public static $FILE = 1;
// --- OPERATIONS ---
/**
* returns the relative path from the file/directory 'from'
* to the file/directory 'to'
*
* @access public
* @author Lionel Lecaque, <lionel@taotesting.com>
* @param string from
* @param string to
* @return string
*/
public static function getRelPath($from, $to)
{
$returnValue = (string) '';
$from = is_dir($from) ? $from : dirname($from);
$arrFrom = explode(DIRECTORY_SEPARATOR, rtrim($from, DIRECTORY_SEPARATOR));
$arrTo = explode(DIRECTORY_SEPARATOR, rtrim($to, DIRECTORY_SEPARATOR));
while (count($arrFrom) && count($arrTo) && ($arrFrom[0] == $arrTo[0])) {
array_shift($arrFrom);
array_shift($arrTo);
}
foreach ($arrFrom as $dir) {
$returnValue .= '..' . DIRECTORY_SEPARATOR;
}
$returnValue .= implode(DIRECTORY_SEPARATOR, $arrTo);
return (string) $returnValue;
}
/**
* Helps prevent 'path traversal' attacks
*
* @param string $filename path to file relative to $directory
* @param string $directory absolute path to directory
* @return bool
*/
public static function isFileInsideDirectory($filename, $directory)
{
return self::isAbsoluteFileInsideDirectory($directory . DIRECTORY_SEPARATOR . $filename, $directory);
}
/**
* Helps prevent 'path traversal' attacks
*
* @param string $filename absolute path to file
* @param string $directory absolute path to directory
* @return bool
*/
public static function isAbsoluteFileInsideDirectory($filename, $directory)
{
$canonicalDirectory = realpath($directory);
if (false === $canonicalDirectory) {
return false;
}
$canonicalFilename = realpath($filename);
if (false === $canonicalFilename) {
return false;
}
return 0 === strpos($canonicalFilename, $canonicalDirectory);
}
/**
* deletes a file or a direcstory recursively
*
* @access public
* @author Lionel Lecaque, <lionel@taotesting.com>
* @param string path
* @return boolean
*/
public static function remove($path)
{
$returnValue = (bool) false;
if (is_file($path)) {
$returnValue = unlink($path);
} elseif (is_dir($path)) {
/*
* //It seems to raise problemes on windows, depending on the php version the resource handler is not freed with DirectoryIterator preventing from further deletions // $iterator = new DirectoryIterator($path); foreach ($iterator as $fileinfo) { if (!$fileinfo->isDot()) { } }
*/
$handle = opendir($path);
if ($handle !== false) {
while (false !== ($entry = readdir($handle))) {
if ($entry != "." && $entry != "..") {
self::remove($path . DIRECTORY_SEPARATOR . $entry);
}
}
closedir($handle);
} else {
throw new common_exception_Error('"' . $path . '" cannot be opened for removal');
}
$returnValue = rmdir($path);
} else {
throw new common_exception_Error('"' . $path . '" cannot be removed since it\'s neither a file nor directory');
}
return (bool) $returnValue;
}
/**
* Empty a directory without deleting it
*
* @param string $path
* @param boolean $ignoreSystemFiles
* @return boolean
*/
public static function emptyDirectory($path, $ignoreSystemFiles = false)
{
$success = true;
$handle = opendir($path);
while (false !== ($entry = readdir($handle))) {
if ($entry != "." && $entry != "..") {
if ($entry[0] === '.' && $ignoreSystemFiles === true) {
continue;
}
$success = self::remove($path . DIRECTORY_SEPARATOR . $entry) ? $success : false;
}
}
closedir($handle);
return $success;
}
/**
* Copy a file from source to destination, may be done recursively and may ignore system files
*
* @access public
* @author Lionel Lecaque, <lionel@taotesting.com>
* @param string source
* @param string destination
* @param boolean recursive
* @param boolean ignoreSystemFiles
* @return boolean
*/
public static function copy($source, $destination, $recursive = true, $ignoreSystemFiles = true)
{
if (!is_readable($source)) {
return false;
}
$returnValue = (bool) false;
// Check for System File
$basename = basename($source);
if ($basename[0] === '.' && $ignoreSystemFiles === true) {
return false;
}
// Check for symlinks
if (is_link($source)) {
return symlink(readlink($source), $destination);
}
// Simple copy for a file
if (is_file($source)) {
// get path info of destination.
$destInfo = pathinfo($destination);
if (isset($destInfo['dirname']) && ! is_dir($destInfo['dirname'])) {
if (! mkdir($destInfo['dirname'], 0777, true)) {
return false;
}
}
return copy($source, $destination);
}
// Make destination directory
if ($recursive == true) {
if (! is_dir($destination)) {
// 0777 is default. See mkdir PHP Official documentation.
mkdir($destination, 0777, true);
}
// Loop through the folder
$dir = dir($source);
while (false !== $entry = $dir->read()) {
// Skip pointers
if ($entry === '.' || $entry === '..') {
continue;
}
// Deep copy directories
self::copy("${source}/${entry}", "${destination}/${entry}", $recursive, $ignoreSystemFiles);
}
// Clean up
$dir->close();
return true;
} else {
return false;
}
return (bool) $returnValue;
}
/**
* Scan directory located at $path depending on given $options array.
*
* Options are the following:
*
* * 'recursive' -> boolean
* * 'only' -> boolean ($FILE or $DIR)
* * 'absolute' -> boolean (returns absolute path or file name)
*
* @access public
* @author Lionel Lecaque, <lionel@taotesting.com>
* @param string path
* @param array options
* @return array An array of paths.
*/
public static function scandir($path, $options = [])
{
$returnValue = [];
$recursive = isset($options['recursive']) ? $options['recursive'] : false;
$only = isset($options['only']) ? $options['only'] : null;
$absolute = isset($options['absolute']) ? $options['absolute'] : false;
if (is_dir($path)) {
$iterator = new DirectoryIterator($path);
foreach ($iterator as $fileinfo) {
$fileName = $fileinfo->getFilename();
if ($absolute === true) {
$fileName = $fileinfo->getPathname();
}
if (! $fileinfo->isDot()) {
if (! is_null($only)) {
if ($only == self::SCAN_DIRECTORY && $fileinfo->isDir()) {
array_push($returnValue, $fileName);
} else {
if ($only == self::SCAN_FILE && $fileinfo->isFile()) {
array_push($returnValue, $fileName);
}
}
} else {
array_push($returnValue, $fileName);
}
if ($fileinfo->isDir() && $recursive) {
$returnValue = array_merge($returnValue, self::scandir(realpath($fileinfo->getPathname()), $options));
}
}
}
} else {
throw new common_Exception("An error occured : The function (" . __METHOD__ . ") of the class (" . __CLASS__ . ") is expecting a directory path as first parameter : " . $path);
}
return (array) $returnValue;
}
/**
* Convert url to path
* @param string $path
* @return string
*/
public static function urlToPath($path)
{
$path = parse_url($path);
return $path === null ? null : str_replace('/', DIRECTORY_SEPARATOR, $path['path']);
}
/**
* An alternative way to resolve the real path of a path. The resulting
* path might point to something non-existant on the file system contrary to
* PHP's realpath function.
*
* @param string $path The original path.
* @return string The resolved path.
*/
public static function truePath($path)
{
// From Magento Mass Import utils (MIT)
// http://sourceforge.net/p/magmi/git/ci/master/tree/magmi-0.8/inc/magmi_utils.php
// whether $path is unix or not
$unipath = strlen($path) == 0 || $path[0] != '/';
// attempts to detect if path is relative in which case, add cwd
if (strpos($path, ':') === false && $unipath) {
$path = getcwd() . DIRECTORY_SEPARATOR . $path;
}
// resolve path parts (single dot, double dot and double delimiters)
$path = str_replace([ '/', '\\' ], DIRECTORY_SEPARATOR, $path);
$parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen');
$absolutes = [];
foreach ($parts as $part) {
if ('.' == $part) {
continue;
}
if ('..' == $part) {
array_pop($absolutes);
} else {
$absolutes[ ] = $part;
}
}
$path = implode(DIRECTORY_SEPARATOR, $absolutes);
// put initial separator that could have been lost
$path = !$unipath ? '/' . $path : $path;
return $path;
}
/**
* Sanitize a string using an injective function
* to prevent collisions
*
* @param string $key
* @return string
*/
public static function sanitizeInjectively($string)
{
$sanitized = '';
foreach (str_split($string) as $char) {
$sanitized .= isset(self::$ALLOWED_CHARACTERS[$char]) ? $char : '=' . base64_encode($char);
}
return $sanitized;
}
/**
* Whether or not a given directory located at $path
* contains one or more files with extension $types.
*
* If $path is not readable or not a directory, false is returned.
*
* @param string $path
* @param string|array $types Types to look for with no dot. e.g. 'php', 'js', ...
* @param boolean $recursive Whether or not scan the directory recursively.
* @return boolean
*/
public static function containsFileType($path, $types = [], $recursive = true)
{
if (!is_array($types)) {
$types = [$types];
}
if (!is_dir($path)) {
return false;
}
foreach (self::scandir($path, ['absolute' => true, 'recursive' => $recursive]) as $item) {
if (is_file($item)) {
$pathParts = pathinfo($item);
// if .inc.php, returns 'php' as extension, so no worries with composed types
// if you are looking for some php code.
if (isset($pathParts['extension']) && in_array($pathParts['extension'], $types)) {
return true;
}
}
}
return false;
}
/**
* Create a unique file name on basis of the original one.
*
* @access private
* @author Jerome Bogaerts, <jerome@taotesting.com>
* @param string $originalName
* @return string
*/
public static function createFileName($originalName)
{
$returnValue = uniqid(hash('crc32', $originalName));
$ext = @pathinfo($originalName, PATHINFO_EXTENSION);
if (!empty($ext)) {
$returnValue .= '.' . $ext;
}
return $returnValue;
}
/**
* @param string $type
* @return boolean
*/
public static function isZipMimeType($type)
{
return in_array($type, [
'application/zip', 'application/x-zip', 'application/x-zip-compressed', 'application/octet-stream'
]);
}
}