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

754 lines
26 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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) 2008-2010 (original work) 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);
*
*/
/**
* Utility class that focuses on files.
*
* @author Lionel Lecaque, <lionel@taotesting.com>
* @package tao
*/
use oat\oatbox\filesystem\File;
class tao_helpers_File extends helpers_File
{
public const MIME_SVG = 'image/svg+xml';
/**
* Check if the path in parameter can be securly used into the application.
* (check the cross directory injection, the null byte injection, etc.)
* Use it when the path may be build from a user variable
*
* @author Lionel Lecaque, <lionel@taotesting.com>
* @param string $path The path to check.
* @param boolean $traversalSafe (optional, default is false) Check if the path is traversal safe.
* @return boolean States if the path is secure or not.
*/
public static function securityCheck($path, $traversalSafe = false)
{
$returnValue = true;
//security check: detect directory traversal (deny the ../)
if ($traversalSafe) {
if (preg_match("/\.\.\//", $path)) {
$returnValue = false;
common_Logger::w('directory traversal detected in ' . $path);
}
}
//security check: detect the null byte poison by finding the null char injection
if ($returnValue) {
for ($i = 0; $i < strlen($path); $i++) {
if (ord($path[$i]) === 0) {
$returnValue = false;
common_Logger::w('null char injection detected in ' . $path);
break;
}
}
}
return (bool) $returnValue;
}
/**
* Use this method to cleanly concat components of a path. It will remove extra slashes/backslashes.
*
* @author Lionel Lecaque, <lionel@taotesting.com>
* @param array $paths The path components to concatenate.
* @return string The concatenated path.
*/
public static function concat($paths)
{
$returnValue = (string) '';
foreach ($paths as $path) {
if (!preg_match("/\/$/", $returnValue) && !preg_match("/^\//", $path) && !empty($returnValue)) {
$returnValue .= '/';
}
$returnValue .= $path;
}
$returnValue = str_replace('//', '/', $returnValue);
return (string) $returnValue;
}
/**
* Remove a file. If the recursive parameter is set to true, the target file
* can be a directory that contains data.
*
* @author Lionel Lecaque, <lionel@taotesting.com>
* @param string $path The path to the file you want to remove.
* @param boolean $recursive (optional, default is false) Remove file content recursively (only if the path points to a directory).
* @return boolean Return true if the file is correctly removed, false otherwise.
*/
public static function remove($path, $recursive = false)
{
$returnValue = (bool) false;
if ($recursive) {
$returnValue = helpers_File::remove($path);
} elseif (is_file($path)) {
$returnValue = @unlink($path);
}
// else fail silently
return (bool) $returnValue;
}
/**
* Move file from source to destination.
*
* @author Lionel Lecaque, <lionel@taotesting.com>
* @param string $source A path to the source file.
* @param string $destination A path to the destination file.
* @return boolean Returns true if the file was successfully moved, false otherwise.
*/
public static function move($source, $destination)
{
$returnValue = (bool) false;
if (is_dir($source)) {
if (!file_exists($destination)) {
mkdir($destination, 0777, true);
}
$error = false;
foreach (scandir($source) as $file) {
if ($file != '.' && $file != '..') {
if (is_dir($source . '/' . $file)) {
if (!self::move($source . '/' . $file, $destination . '/' . $file, true)) {
$error = true;
}
} else {
if (!self::copy($source . '/' . $file, $destination . '/' . $file, true)) {
$error = true;
}
}
}
}
if (!$error) {
$returnValue = true;
}
self::remove($source, true);
} else {
if (file_exists($source) && file_exists($destination)) {
$returnValue = rename($source, $destination);
} else {
if (self::copy($source, $destination, true)) {
$returnValue = self::remove($source);
}
}
}
return (bool) $returnValue;
}
/**
* Retrieve mime-types that are recognized by the TAO platform.
*
* @author Lionel Lecaque, <lionel@taotesting.com>
* @return array An associative array of mime-types where keys are the extension related to the mime-type. Values of the array are mime-types.
*/
public static function getMimeTypeList()
{
$returnValue = [
'txt' => 'text/plain',
'htm' => 'text/html',
'html' => 'text/html',
'xhtml' => 'application/xhtml+xml',
'php' => 'text/html',
'css' => 'text/css',
'js' => 'application/javascript',
'json' => 'application/json',
'xml' => 'text/xml',
'rdf' => 'text/xml',
'swf' => 'application/x-shockwave-flash',
'flv' => 'video/x-flv',
'csv' => 'text/csv',
'rtx' => 'text/richtext',
'rtf' => 'text/rtf',
// images
'png' => 'image/png',
'jpe' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'jpg' => 'image/jpeg',
'gif' => 'image/gif',
'bmp' => 'image/bmp',
'ico' => 'image/vnd.microsoft.icon',
'tiff' => 'image/tiff',
'tif' => 'image/tiff',
'svg' => self::MIME_SVG,
'svgz' => self::MIME_SVG,
// archives
'zip' => 'application/zip',
'rar' => 'application/x-rar-compressed',
'exe' => 'application/x-msdownload',
'msi' => 'application/x-msdownload',
'cab' => 'application/vnd.ms-cab-compressed',
// audio/video
'mp3' => 'audio/mpeg',
'oga' => 'audio/ogg',
'ogg' => 'audio/ogg',
'aac' => 'audio/aac',
'qt' => 'video/quicktime',
'mov' => 'video/quicktime',
'mp4' => 'video/mp4',//(H.264 + AAC) for ie8, etc.
'webm' => 'video/webm',//(VP8 + Vorbis) for ie9, ff, chrome, android, opera
'ogv' => 'video/ogg',//ff, chrome, opera
// adobe
'pdf' => 'application/pdf',
'psd' => 'image/vnd.adobe.photoshop',
'ai' => 'application/postscript',
'eps' => 'application/postscript',
'ps' => 'application/postscript',
// ms office
'doc' => 'application/msword',
'rtf' => 'application/rtf',
'xls' => 'application/vnd.ms-excel',
'ppt' => 'application/vnd.ms-powerpoint',
// open office
'odt' => 'application/vnd.oasis.opendocument.text',
'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
// fonts
'woff' => 'application/x-font-woff',
'eot' => 'application/vnd.ms-fontobject',
'ttf' => 'application/x-font-ttf'
];
return (array) $returnValue;
}
/**
* Retrieve file extensions usually associated to a given mime-type.
*
* @author Lionel Lecaque, <lionel@taotesting.com>
* @param string $mimeType A mime-type which is recognized by the platform.
* @return string The extension usually associated to the mime-type. If it could not be retrieved, an empty string is returned.
*/
public static function getExtention($mimeType)
{
$returnValue = (string) '';
foreach (self::getMimeTypeList() as $key => $value) {
if ($value == trim($mimeType)) {
$returnValue = $key;
break;
}
}
return (string) $returnValue;
}
/**
* Retrieve file extensions of a file
*
* @param string $path the path of the file we want to get the extension
* @return string The extension of the parameter file
*/
public static function getFileExtention($path)
{
$ext = pathinfo($path, PATHINFO_EXTENSION);
if ($ext === '') {
$splitedPath = explode('.', $path);
$ext = end($splitedPath);
}
return $ext;
}
/**
* Get the mime-type of the file in parameter.
* different methods are used regarding the configuration of the server.
*
* @author Lionel Lecaque, <lionel@taotesting.com>
* @param string $path
* @param boolean $ext If set to true, the extension of the file will be used to retrieve the mime-type. If now extension can be found, 'text/plain' is returned by the method.
* @return string The associated mime-type.
*/
public static function getMimeType($path, $ext = false)
{
$mime_types = self::getMimeTypeList();
if (false == $ext) {
$ext = pathinfo($path, PATHINFO_EXTENSION);
if (array_key_exists($ext, $mime_types)) {
$mimetype = $mime_types[$ext];
} else {
$mimetype = '';
}
if (!in_array($ext, ['css', 'ogg', 'mp3', 'svg', 'svgz'])) {
if (file_exists($path)) {
if (function_exists('finfo_open')) {
$finfo = finfo_open(FILEINFO_MIME);
$mimetype = finfo_file($finfo, $path);
finfo_close($finfo);
} elseif (function_exists('mime_content_type')) {
$mimetype = mime_content_type($path);
}
if (!empty($mimetype)) {
if (preg_match("/; charset/", $mimetype)) {
$mimetypeInfos = explode(';', $mimetype);
$mimetype = $mimetypeInfos[0];
}
}
}
}
} else {
// find out the mime-type from the extension of the file.
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
if (array_key_exists($ext, $mime_types)) {
$mimetype = $mime_types[$ext];
}
}
// If no mime-type found ...
if (empty($mimetype)) {
$mimetype = 'application/octet-stream';
}
return (string) $mimetype;
}
/**
* creates a directory in the system's temp dir.
*
* @author Lionel Lecaque, <lionel@taotesting.com>
* @return string The path to the created folder.
*/
public static function createTempDir()
{
do {
$folder = sys_get_temp_dir() . DIRECTORY_SEPARATOR . "tmp" . mt_rand() . DIRECTORY_SEPARATOR;
} while (file_exists($folder));
mkdir($folder);
return $folder;
}
/**
* deletes a directory and its content.
*
* @author Lionel Lecaque, <lionel@taotesting.com>
* @param string directory absolute path of the directory
* @return boolean true if the directory and its content were deleted, false otherwise.
*/
public static function delTree($directory)
{
$files = array_diff(scandir($directory), ['.','..']);
foreach ($files as $file) {
$abspath = $directory . DIRECTORY_SEPARATOR . $file;
if (is_dir($abspath)) {
self::delTree($abspath);
} else {
unlink($abspath);
}
}
return rmdir($directory);
}
public static function isIdentical($path1, $path2)
{
return self::md5_dir($path1) == self::md5_dir($path2);
}
public static function md5_dir($path)
{
if (is_file($path)) {
$md5 = md5_file($path);
} elseif (is_dir($path)) {
$filemd5s = [];
// using scandir to get files in a fixed order
$files = scandir($path);
sort($files);
foreach ($files as $basename) {
if ($basename != '.' && $basename != '..') {
//$fileInfo->getFilename()
$filemd5s[] = $basename . self::md5_dir(self::concat([$path, $basename]));
}
}
$md5 = md5(implode('', $filemd5s));
} else {
throw new common_Exception(__FUNCTION__ . ' called on non file or directory "' . $path . '"');
}
return $md5;
}
/**
* Create a zip of a directory or file
*
* @param string $src path to the files to zip
* @param bool $withEmptyDir
* @return string path to the zip file
* @throws common_Exception if unable to create the zip
*/
public static function createZip($src, $withEmptyDir = false)
{
$zipArchive = new \ZipArchive();
$path = self::createTempDir() . 'file.zip';
if ($zipArchive->open($path, \ZipArchive::CREATE) !== true) {
throw new common_Exception('Unable to create zipfile ' . $path);
}
self::addFilesToZip($zipArchive, $src, DIRECTORY_SEPARATOR, $withEmptyDir);
$zipArchive->close();
return $path;
}
/**
* Add files or folders (and their content) to the Zip Archive that will contain all the files to the current export session.
* For instance, if you want to copy the file 'taoItems/data/i123/item.xml' as 'myitem.xml' to your archive call addFile('path_to_item_location/item.xml', 'myitem.xml').
* As a result, you will get a file entry in the final ZIP archive at '/i123/myitem.xml'.
*
* @param ZipArchive $zipArchive the archive to add to
* @param string $src | StreamInterface The path to the source file or folder to copy into the ZIP Archive.
* @param $dest
* @param bool $withEmptyDir
* @return integer The amount of files that were transfered from TAO to the ZIP archive within the method call.
*/
public static function addFilesToZip(ZipArchive $zipArchive, $src, $dest, $withEmptyDir = false)
{
$returnValue = null;
$done = 0;
if ($src instanceof \Psr\Http\Message\StreamInterface) {
if ($zipArchive->addFromString(ltrim($dest, "/\\"), $src->getContents())) {
$done++;
}
} elseif (is_resource($src)) {
fseek($src, 0);
$content = stream_get_contents($src);
if ($zipArchive->addFromString(ltrim($dest, "/\\"), $content)) {
$done++;
}
} elseif (is_dir($src)) {
// Go deeper in folder hierarchy !
$src = rtrim($src, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$dest = rtrim($dest, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
if ($withEmptyDir) {
$zipArchive->addEmptyDir($dest);
}
// Recursively copy.
$content = scandir($src);
foreach ($content as $file) {
// avoid . , .. , .svn etc ...
if (!preg_match("/^\./", $file)) {
$done += self::addFilesToZip($zipArchive, $src . $file, $dest . $file, $withEmptyDir);
}
}
} else {
// Simply copy the file. Beware of leading slashes
if ($zipArchive->addFile($src, ltrim($dest, DIRECTORY_SEPARATOR))) {
$done++;
}
}
$returnValue = $done;
return $returnValue;
}
/**
* Unzip archive file
*
* @param string|File $archiveFile
* @return string path to temporary directory zipfile was extracted to
*
* @throws \common_Exception
*/
public static function extractArchive($archiveFile)
{
if ($archiveFile instanceof File) {
if (!$archiveFile->exists()) {
throw new \common_Exception('Unable to open archive ' . '/' . $archiveFile->getPrefix());
}
$tmpDir = static::createTempDir();
$tmpFilePath = $tmpDir . uniqid($archiveFile->getBasename(), true) . '.zip';
$tmpFile = fopen($tmpFilePath, 'w');
$originalPackage = $archiveFile->readStream();
stream_copy_to_stream($originalPackage, $tmpFile);
fclose($originalPackage);
fclose($tmpFile);
$archiveFile = $tmpFilePath;
}
$archiveObj = new \ZipArchive();
$archiveHandle = $archiveObj->open($archiveFile);
if (true !== $archiveHandle) {
throw new \common_Exception('Unable to open archive ' . $archiveFile);
}
if (static::checkWhetherArchiveIsBomb($archiveObj)) {
throw new \common_Exception(sprintf('Source "%s" seems to be a ZIP bomb', $archiveFile));
}
$archiveDir = static::createTempDir();
if (!$archiveObj->extractTo($archiveDir)) {
$archiveObj->close();
throw new \common_Exception('Unable to extract to ' . $archiveDir);
}
$archiveObj->close();
if (isset($tmpFilePath) && file_exists($tmpFilePath)) {
unlink($tmpFilePath);
}
if (isset($tmpDir) && file_exists($tmpDir)) {
rmdir($tmpDir);
}
return $archiveDir;
}
/**
* Rename in Zip
*
* Rename an item in a ZIP archive. Works for files and directories.
*
* In case of renaming directories, the return value of this method will be the amount of files
* affected by the directory renaming.
*
* @param ZipArchive $zipArchive An open ZipArchive object.
* @param string $oldname
* @param string $newname
* @return int The amount of renamed entries.
*/
public static function renameInZip(ZipArchive $zipArchive, $oldname, $newname)
{
$i = 0;
$renameCount = 0;
while (($entryName = $zipArchive->getNameIndex($i)) || ($statIndex = $zipArchive->statIndex($i, ZipArchive::FL_UNCHANGED))) {
if ($entryName) {
$newEntryName = str_replace($oldname, $newname, $entryName);
if ($zipArchive->renameIndex($i, $newEntryName)) {
$renameCount++;
}
}
$i++;
}
return $renameCount;
}
/**
* Helps prevent decompression attacks.
* Since this method checks archive file size, it needs filename property to be set,
* so ZipArchive object should be already opened.
*
* @param \ZipArchive $archive
* @param int $minCompressionRatioToBeBomb archive content size / archive size
* @return bool
* @throws common_Exception
*
* @link https://en.wikipedia.org/wiki/Zip_bomb
*/
public static function checkWhetherArchiveIsBomb(\ZipArchive $archive, $minCompressionRatioToBeBomb = 200)
{
if (!$archive->filename) {
throw new common_Exception('ZIP archive should be opened before checking for a ZIP bomb');
}
$contentSize = 0;
for ($fileIndex = 0; $fileIndex < $archive->numFiles; $fileIndex++) {
$stats = $archive->statIndex($fileIndex);
$contentSize += $stats['size'];
}
$archiveFileSize = filesize($archive->filename);
return $archiveFileSize * $minCompressionRatioToBeBomb < $contentSize;
}
/**
* Exclude from Zip
*
* Exclude entries matching $pattern from a ZIP Archive.
*
* @param ZipArchive $zipArchive An open ZipArchive object.
* @param string $pattern A PCRE pattern.
* @return int The amount of excluded entries.
*/
public static function excludeFromZip(ZipArchive $zipArchive, $pattern)
{
$i = 0;
$exclusionCount = 0;
while (($entryName = $zipArchive->getNameIndex($i)) || ($statIndex = $zipArchive->statIndex($i, ZipArchive::FL_UNCHANGED))) {
if ($entryName) {
// Not previously removed index.
if (preg_match($pattern, $entryName) === 1 && $zipArchive->deleteIndex($i)) {
$exclusionCount++;
}
}
$i++;
}
return $exclusionCount;
}
/**
* Get All Zip Names
*
* Retrieve all ZIP name entries in a ZIP archive. In others words, all the paths in the
* archive having an entry.
*
* @param ZipArchive $zipArchive An open ZipArchive object.
* @return array An array of strings.
*/
public static function getAllZipNames(ZipArchive $zipArchive)
{
$i = 0;
$entries = [];
while (($entryName = $zipArchive->getNameIndex($i)) || ($statIndex = $zipArchive->statIndex($i, ZipArchive::FL_UNCHANGED))) {
if ($entryName) {
$entries[] = $entryName;
}
$i++;
}
return $entries;
}
/**
* Gets the local path to a publicly available resource
* no verification if the file should be accessible
*
* @param string $url
* @throws common_Exception
* @return string
*/
public static function getPathFromUrl($url)
{
if (substr($url, 0, strlen(ROOT_URL)) != ROOT_URL) {
throw new common_Exception($url . ' does not lie within the tao instalation path');
}
$subUrl = substr($url, strlen(ROOT_URL));
$parts = [];
foreach (explode('/', $subUrl) as $directory) {
$parts[] = urldecode($directory);
}
$path = ROOT_PATH . implode(DIRECTORY_SEPARATOR, $parts);
if (self::securityCheck($path)) {
return $path;
} else {
throw new common_Exception($url . ' is not secure');
}
}
/**
* Get a safe filename for a proposed filename.
*
* If directory is specified it will return a filename which is
* safe to not overwritte an existing file. This function is not injective.
*
* @param string $fileName
* @param string $directory
*
* @return string
*/
public static function getSafeFileName($fileName, $directory = null)
{
$lastDot = strrpos($fileName, '.');
$file = $lastDot ? substr($fileName, 0, $lastDot) : $fileName;
$ending = $lastDot ? substr($fileName, $lastDot + 1) : '';
$safeName = self::removeSpecChars($file);
$safeEnding = empty($ending)
? ''
: '.' . self::removeSpecChars($ending);
if ($directory != null && file_exists($directory . $safeName . $safeEnding)) {
$count = 1;
while (file_exists($directory . $safeName . '_' . $count . $safeEnding)) {
$count++;
}
$safeName = $safeName . '_' . $count;
}
return $safeName . $safeEnding;
}
/**
* Remove special characters for safe filenames
*
* @author Dieter Raber
*
* @param string $string
* @param string $repl
* @param string $lower
*
* @return string
*/
private static function removeSpecChars($string, $repl = '-', $lower = true)
{
$spec_chars = [
'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'Ae', 'Å' => 'A','Æ' => 'A', 'Ç' => 'C',
'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E', 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I',
'Ï' => 'I', 'Ð' => 'E', 'Ñ' => 'N', 'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O',
'Ö' => 'Oe', 'Ø' => 'O', 'Ù' => 'U', 'Ú' => 'U','Û' => 'U', 'Ü' => 'Ue', 'Ý' => 'Y',
'Þ' => 'T', 'ß' => 'ss', 'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'ae',
'å' => 'a', 'æ' => 'ae', 'ç' => 'c', 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e',
'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', 'ð' => 'e', 'ñ' => 'n', 'ò' => 'o',
'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'oe', 'ø' => 'o', 'ù' => 'u', 'ú' => 'u',
'û' => 'u', 'ü' => 'ue', 'ý' => 'y', 'þ' => 't', 'ÿ' => 'y', ' ' => $repl, '?' => $repl,
'\'' => $repl, '.' => $repl, '/' => $repl, '&' => $repl, ')' => $repl, '(' => $repl,
'[' => $repl, ']' => $repl, '_' => $repl, ',' => $repl, ':' => $repl, '-' => $repl,
'!' => $repl, '"' => $repl, '`' => $repl, '°' => $repl, '%' => $repl, ' ' => $repl,
' ' => $repl, '{' => $repl, '}' => $repl, '#' => $repl, '' => $repl
];
$string = strtr($string, $spec_chars);
$string = trim(preg_replace("~[^a-z0-9]+~i", $repl, $string), $repl);
return $lower ? strtolower($string) : $string;
}
/**
* Check if the directory is empty
*
* @param string $directory
* @return boolean
*/
public static function isDirEmpty($directory)
{
$path = self::concat([$directory, '*']);
return (count(glob($path, GLOB_NOSORT)) === 0 );
}
}