<?php
/**
* @class files
* @brief Files manipulation utilities
*
* @package Clearbricks
* @subpackage Common
*
* @copyright Olivier Meunier & Association Dotclear
* @copyright GPL-2.0-only
*/
class files
{
public static $dir_mode = null; ///< Default directories mode
public static $mimeType = ///< MIME types
[
'odt' => 'application/vnd.oasis.opendocument.text',
'odp' => 'application/vnd.oasis.opendocument.presentation',
'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
'sxw' => 'application/vnd.sun.xml.writer',
'sxc' => 'application/vnd.sun.xml.calc',
'sxi' => 'application/vnd.sun.xml.impress',
'ppt' => 'application/mspowerpoint',
'doc' => 'application/msword',
'xls' => 'application/msexcel',
'pdf' => 'application/pdf',
'ps' => 'application/postscript',
'ai' => 'application/postscript',
'eps' => 'application/postscript',
'json' => 'application/json',
'xml' => 'application/xml',
'bin' => 'application/octet-stream',
'exe' => 'application/octet-stream',
'bz2' => 'application/x-bzip',
'deb' => 'application/x-debian-package',
'gz' => 'application/x-gzip',
'jar' => 'application/x-java-archive',
'rar' => 'application/rar',
'rpm' => 'application/x-redhat-package-manager',
'tar' => 'application/x-tar',
'tgz' => 'application/x-gtar',
'zip' => 'application/zip',
'aiff' => 'audio/x-aiff',
'ua' => 'audio/basic',
'mp3' => 'audio/mpeg3',
'mid' => 'audio/x-midi',
'midi' => 'audio/x-midi',
'ogg' => 'application/ogg',
'ra' => 'audio/x-pn-realaudio',
'ram' => 'audio/x-pn-realaudio',
'wav' => 'audio/x-wav',
'wma' => 'audio/x-ms-wma',
'swf' => 'application/x-shockwave-flash',
'swfl' => 'application/x-shockwave-flash',
'js' => 'application/javascript',
'bmp' => 'image/bmp',
'gif' => 'image/gif',
'ico' => 'image/vnd.microsoft.icon',
'jpeg' => 'image/jpeg',
'jpg' => 'image/jpeg',
'jpe' => 'image/jpeg',
'png' => 'image/png',
'svg' => 'image/svg+xml',
'tiff' => 'image/tiff',
'tif' => 'image/tiff',
'webp' => 'image/webp',
'xbm' => 'image/x-xbitmap',
'css' => 'text/css',
'csv' => 'text/csv',
'html' => 'text/html',
'htm' => 'text/html',
'txt' => 'text/plain',
'rtf' => 'text/richtext',
'rtx' => 'text/richtext',
'mpg' => 'video/mpeg',
'mpeg' => 'video/mpeg',
'mpe' => 'video/mpeg',
'ogv' => 'video/ogg',
'viv' => 'video/vnd.vivo',
'vivo' => 'video/vnd.vivo',
'qt' => 'video/quicktime',
'mov' => 'video/quicktime',
'mp4' => 'video/mp4',
'm4v' => 'video/x-m4v',
'flv' => 'video/x-flv',
'avi' => 'video/x-msvideo',
'wmv' => 'video/x-ms-wmv',
];
/**
* Directory scanning
*
* Returns a directory child files and directories.
*
* @param string $d Path to scan
* @param boolean $order Order results
* @return array
*/
public static function scandir(string $d, bool $order = true): array
{
$res = [];
$dh = @opendir($d);
if ($dh === false) {
throw new Exception(__('Unable to open directory.'));
}
while (($f = readdir($dh)) !== false) {
$res[] = $f;
}
closedir($dh);
if ($order) {
sort($res);
}
return $res;
}
/**
* File extension
*
* Returns a file extension.
*
* @param string $f File name
* @return string
*/
public static function getExtension(string $f): string
{
if (function_exists('pathinfo')) {
return strtolower(pathinfo($f, PATHINFO_EXTENSION));
}
$f = explode('.', basename($f));
if (count($f) <= 1) {
return '';
}
return strtolower($f[count($f) - 1]);
}
/**
* MIME type
*
* Returns a file MIME type, based on static var {@link $mimeType}
*
* @param string $f File name
* @return string
*/
public static function getMimeType(string $f): string
{
$ext = self::getExtension($f);
$types = self::mimeTypes();
if (isset($types[$ext])) {
return $types[$ext];
}
return 'application/octet-stream';
}
/**
* MIME types
*
* Returns all defined MIME types.
*
* @return array
*/
public static function mimeTypes(): array
{
return self::$mimeType;
}
/**
* New MIME types
*
* Append new MIME types to defined MIME types.
*
* @param array $tab New MIME types.
*/
public static function registerMimeTypes(array $tab): void
{
self::$mimeType = array_merge(self::$mimeType, $tab);
}
/**
* Is a file or directory deletable.
*
* Returns true if $f is a file or directory and is deletable.
*
* @param string $f File or directory
* @return boolean
*/
public static function isDeletable(string $f): bool
{
if (is_file($f)) {
return is_writable(dirname($f));
} elseif (is_dir($f)) {
return (is_writable(dirname($f)) && count(files::scandir($f)) <= 2);
}
return false;
}
/**
* Recursive removal
*
* Remove recursively a directory.
*
* @param string $dir Directory patch
* @return boolean
*/
public static function deltree(string $dir): bool
{
$current_dir = opendir($dir);
while ($entryname = readdir($current_dir)) {
if (is_dir($dir . '/' . $entryname) and ($entryname != '.' and $entryname != '..')) {
if (!files::deltree($dir . '/' . $entryname)) {
return false;
}
} elseif ($entryname != '.' and $entryname != '..') {
if (!@unlink($dir . '/' . $entryname)) {
return false;
}
}
}
closedir($current_dir);
return @rmdir($dir);
}
/**
* Touch file
*
* Set file modification time to now.
*
* @param string $f File to change
*/
public static function touch(string $f): void
{
if (is_writable($f)) {
if (function_exists('touch')) {
@touch($f);
} else {
# Very bad hack
@file_put_contents($f, file_get_contents($f));
}
}
}
/**
* Directory creation.
*
* Creates directory $f. If $r is true, attempts to create needed parents
* directories.
*
* @param string $f Directory to create
* @param boolean $r Create parent directories
*/
public static function makeDir(string $f, bool $r = false): void
{
if (empty($f)) {
return;
}
if (DIRECTORY_SEPARATOR == '\\') {
$f = str_replace('/', '\\', $f);
}
if (is_dir($f)) {
return;
}
if ($r) {
$dir = path::real($f, false);
$dirs = [];
while (!is_dir($dir)) {
array_unshift($dirs, basename($dir));
$dir = dirname($dir);
}
foreach ($dirs as $d) {
$dir .= DIRECTORY_SEPARATOR . $d;
if ($d != '' && !is_dir($dir)) {
self::makeDir($dir);
}
}
} else {
if (@mkdir($f) === false) {
throw new Exception(__('Unable to create directory.'));
}
self::inheritChmod($f);
}
}
/**
* Mode inheritage
*
* Sets file or directory mode according to its parent.
*
* @param string $file File to change
*/
public static function inheritChmod(string $file): bool
{
if (!function_exists('fileperms') || !function_exists('chmod')) {
return false;
}
if (self::$dir_mode != null) {
return @chmod($file, self::$dir_mode);
}
return @chmod($file, fileperms(dirname($file)));
}
/**
* Changes file content.
*
* Writes $f_content into $f file.
*
* @param string $f File to edit
* @param string $f_content Content to write
*/
public static function putContent(string $f, string $f_content): bool
{
if (file_exists($f) && !is_writable($f)) {
throw new Exception(__('File is not writable.'));
}
$fp = @fopen($f, 'w');
if ($fp === false) {
throw new Exception(__('Unable to open file.'));
}
fwrite($fp, $f_content, strlen($f_content));
fclose($fp);
return true;
}
/**
* Human readable file size.
*
* @param integer $size Bytes
* @return string
*/
public static function size(int $size): string
{
$kb = 1024;
$mb = 1024 * $kb;
$gb = 1024 * $mb;
$tb = 1024 * $gb;
if ($size < $kb) {
return $size . ' B';
} elseif ($size < $mb) {
return round($size / $kb, 2) . ' KB';
} elseif ($size < $gb) {
return round($size / $mb, 2) . ' MB';
} elseif ($size < $tb) {
return round($size / $gb, 2) . ' GB';
}
return round($size / $tb, 2) . ' TB';
}
/**
* Converts a human readable file size to bytes.
*
* @param string $v Size
* @return float
*/
public static function str2bytes(string $v): float
{
$v = trim($v);
$last = strtolower(substr($v, -1, 1));
$v = (float) substr($v, 0, -1);
switch ($last) {
case 'g':
$v *= 1024;
case 'm':
$v *= 1024;
case 'k':
$v *= 1024;
}
return $v;
}
/**
* Upload status
*
* Returns true if upload status is ok, throws an exception instead.
*
* @param array $file File array as found in $_FILES
* @return boolean
*/
public static function uploadStatus(array $file): bool
{
if (!isset($file['error'])) {
throw new Exception(__('Not an uploaded file.'));
}
switch ($file['error']) {
case UPLOAD_ERR_OK:
return true;
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
throw new Exception(__('The uploaded file exceeds the maximum file size allowed.'));
case UPLOAD_ERR_PARTIAL:
throw new Exception(__('The uploaded file was only partially uploaded.'));
case UPLOAD_ERR_NO_FILE:
throw new Exception(__('No file was uploaded.'));
case UPLOAD_ERR_NO_TMP_DIR:
throw new Exception(__('Missing a temporary folder.'));
case UPLOAD_ERR_CANT_WRITE:
throw new Exception(__('Failed to write file to disk.'));
case UPLOAD_ERR_EXTENSION:
throw new Exception(__('A PHP extension stopped the file upload.'));
default:
return true;
}
}
# Packages generation methods
#
/**
* Recursive directory scanning
*
* Returns an array of a given directory's content. The array contains
* two arrays: dirs and files. Directory's content is fetched recursively.
*
* @param string $dirName Directory name
* @param array $contents Contents array. Leave it empty
* @return array
*/
public static function getDirList(string $dirName, array &$contents = null): array
{
if (!$contents) {
$contents = ['dirs' => [], 'files' => []];
}
$exclude_list = ['.', '..', '.svn'];
$dirName = preg_replace('|/$|', '', $dirName);
if (!is_dir($dirName)) {
throw new Exception(sprintf(__('%s is not a directory.'), $dirName));
}
$contents['dirs'][] = $dirName;
$d = @dir($dirName);
if ($d === false) {
throw new Exception(__('Unable to open directory.'));
}
while ($entry = $d->read()) {
if (!in_array($entry, $exclude_list)) {
if (is_dir($dirName . '/' . $entry)) {
files::getDirList($dirName . '/' . $entry, $contents);
} else {
$contents['files'][] = $dirName . '/' . $entry;
}
}
}
$d->close();
return $contents;
}
/**
* Filename cleanup
*
* Removes unwanted characters in a filename.
*
* @param string $n Filename
* @return string
*/
public static function tidyFileName(string $n): string
{
$n = text::deaccent($n);
$n = preg_replace('/^[.]/u', '', $n);
return preg_replace('/[^A-Za-z0-9._-]/u', '_', $n);
}
}
/**
* @class path
* @brief Path manipulation utilities
*
* @package Clearbricks
* @subpackage Common
*/
class path
{
/**
* Returns the real path of a file.
*
* If parameter $strict is true, file should exist. Returns false if
* file does not exist.
*
* @param string $p Filename
* @param boolean $strict File should exists
* @return string|false
*/
public static function real(string $p, bool $strict = true)
{
$os = (DIRECTORY_SEPARATOR == '\\') ? 'win' : 'nix';
# Absolute path?
if ($os == 'win') {
$_abs = preg_match('/^\w+:/', $p);
} else {
$_abs = substr($p, 0, 1) == '/';
}
# Standard path form
if ($os == 'win') {
$p = str_replace('\\', '/', $p);
}
# Adding root if !$_abs
if (!$_abs) {
$p = dirname($_SERVER['SCRIPT_FILENAME']) . '/' . $p;
}
# Clean up
$p = preg_replace('|/+|', '/', $p);
if (strlen($p) > 1) {
$p = preg_replace('|/$|', '', $p);
}
$_start = '';
if ($os == 'win') {
[$_start, $p] = explode(':', $p);
$_start .= ':/';
} else {
$_start = '/';
}
$p = substr($p, 1);
# Go through
$P = explode('/', $p);
$res = [];
for ($i = 0; $i < count($P); $i++) {
if ($P[$i] == '.') {
continue;
}
if ($P[$i] == '..') {
if (count($res) > 0) {
array_pop($res);
}
} else {
array_push($res, $P[$i]);
}
}
$p = $_start . implode('/', $res);
if ($strict && !@file_exists($p)) {
return false;
}
return $p;
}
/**
* Returns a clean file path
*
* @param string $p File path
* @return string
*/
public static function clean(?string $p): string
{
$p = preg_replace(['|^\.\.|', '|/\.\.|', '|\.\.$|'], '', (string) $p); // Remove double point (upper directory)
$p = preg_replace('|/{2,}|', '/', (string) $p); // Replace double slashes by one
$p = preg_replace('|/$|', '', (string) $p); // Remove trailing slash
return $p;
}
/**
* Path information
*
* Returns an array of information:
* - dirname
* - basename
* - extension
* - base (basename without extension)
*
* @param string $f File path
*/
public static function info(string $f): array
{
$p = pathinfo($f);
$res = [];
$res['dirname'] = (string) $p['dirname'];
$res['basename'] = (string) $p['basename'];
$res['extension'] = $p['extension'] ?? '';
$res['base'] = preg_replace('/\.' . preg_quote($res['extension'], '/') . '$/', '', $res['basename']);
return $res;
}
/**
* Full path with root
*
* Returns a path with root concatenation unless path begins with a slash
*
* @param string $p File path
* @param string $root Root path
* @return string
*/
public static function fullFromRoot(string $p, string $root): string
{
if (substr($p, 0, 1) == '/') {
return $p;
}
return $root . '/' . $p;
}
}