<?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',
        'rtf'  => 'application/rtf',

        '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($d, $order = true)
    {
        $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($f)
    {
        if (function_exists('pathinfo')) {
            return strtolower(pathinfo($f, PATHINFO_EXTENSION));
        } else {
            $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($f)
    {
        $ext   = self::getExtension($f);
        $types = self::mimeTypes();

        if (isset($types[$ext])) {
            return $types[$ext];
        } else {
            return 'application/octet-stream';
        }
    }

    /**
     * MIME types
     *
     * Returns all defined MIME types.
     *
     * @return array
     */
    public static function mimeTypes()
    {
        return self::$mimeType;
    }

    /**
     * New MIME types
     *
     * Append new MIME types to defined MIME types.
     *
     * @param array        $tab        New MIME types.
     */
    public static function registerMimeTypes($tab)
    {
        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($f)
    {
        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($dir)
    {
        $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($f)
    {
        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($f, $r = false)
    {
        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($file)
    {
        if (!function_exists('fileperms') || !function_exists('chmod')) {
            return false;
        }

        if (self::$dir_mode != null) {
            return @chmod($file, self::$dir_mode);
        } else {
            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($f, $f_content)
    {
        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($size)
    {
        $kb = 1024;
        $mb = 1024 * $kb;
        $gb = 1024 * $mb;
        $tb = 1024 * $gb;

        if ($size < $kb) {
            return $size . " B";
        } else if ($size < $mb) {
            return round($size / $kb, 2) . " KB";
        } else if ($size < $gb) {
            return round($size / $mb, 2) . " MB";
        } else if ($size < $tb) {
            return round($size / $gb, 2) . " GB";
        } else {
            return round($size / $tb, 2) . " TB";
        }
    }

    /**
     * Converts a human readable file size to bytes.
     *
     * @param string    $v            Size
     * @return integer
     */
    public static function str2bytes($v)
    {
        $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($file)
    {
        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.'));
                return false;
            case UPLOAD_ERR_PARTIAL:
                throw new Exception(__('The uploaded file was only partially uploaded.'));
                return false;
            case UPLOAD_ERR_NO_FILE:
                throw new Exception(__('No file was uploaded.'));
                return false;
            case UPLOAD_ERR_NO_TMP_DIR:
                throw new Exception(__('Missing a temporary folder.'));
                return false;
            case UPLOAD_ERR_CANT_WRITE:
                throw new Exception(__('Failed to write file to disk.'));
                return false;
            case UPLOAD_ERR_EXTENSION:
                throw new Exception(__('A PHP extension stopped the file upload.'));
                return false;
            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($dirName, &$contents = null)
    {
        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($n)
    {
        $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
     */
    public static function real($p, $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') {
            list($_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($p)
    {
        $p = str_replace('..', '', $p);
        $p = preg_replace('|/{2,}|', '/', $p);
        $p = preg_replace('|/$|', '', $p);

        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($f)
    {
        $p   = pathinfo($f);
        $res = [];

        $res['dirname']   = $p['dirname'];
        $res['basename']  = $p['basename'];
        $res['extension'] = isset($p['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($p, $root)
    {
        if (substr($p, 0, 1) == '/') {
            return $p;
        }

        return $root . '/' . $p;
    }
}
