core/model/modx/modfilehandler.class.php
<?php
/*
* This file is part of MODX Revolution.
*
* Copyright (c) MODX, LLC. All Rights Reserved.
*
* For complete copyright and license information, see the COPYRIGHT and LICENSE
* files found in the top-level directory of this distribution.
*/
/**
* Assists with directory/file manipulation
*
* @package modx
*/
class modFileHandler {
/**
* An array of configuration properties for the class
* @var array $config
*/
public $config = array();
/**
* The current context in which this File Manager instance should operate
* @var modContext|null $context
*/
public $context = null;
/**
* The constructor for the modFileHandler class
*
* @param modX &$modx A reference to the modX object.
* @param array $config An array of options.
*/
function __construct(modX &$modx, array $config = array()) {
$this->modx =& $modx;
$this->config = array_merge($this->config, $this->modx->_userConfig, $config);
if (!isset($this->config['context'])) {
$this->config['context'] = $this->modx->context->get('key');
}
$this->context = $this->modx->getContext($this->config['context']);
}
/**
* Dynamically creates a modDirectory or modFile object.
*
* The object is created based on the type of resource provided.
*
* @param string $path The absolute path to the filesystem resource.
* @param array $options Optional. An array of options for the object.
* @param string $overrideClass Optional. If provided, will force creation
* of the object as the specified class.
* @return modFile|modDirectory The appropriate modFile/modDirectory object
*/
public function make($path, array $options = array(), $overrideClass = '') {
$path = $this->sanitizePath($path);
if (!empty($overrideClass)) {
$class = $overrideClass;
} else {
if (is_dir($path)) {
$path = $this->postfixSlash($path);
$class = 'modDirectory';
} else {
$class = 'modFile';
}
}
return new $class($this, $path, $options);
}
/**
* Get the modX base path for the user.
*
* @return string The base path
*/
public function getBasePath() {
$basePath = $this->context->getOption('filemanager_path', '', $this->config);
/* expand placeholders */
$basePath = str_replace(array(
'{base_path}',
'{core_path}',
'{assets_path}',
), array(
$this->context->getOption('base_path', MODX_BASE_PATH, $this->config),
$this->context->getOption('core_path', MODX_CORE_PATH, $this->config),
$this->context->getOption('assets_path', MODX_ASSETS_PATH, $this->config),
), $basePath);
return !empty($basePath) ? $this->postfixSlash($basePath) : $basePath;
}
/**
* Get base URL of file manager
*
* @return string The base URL
*/
public function getBaseUrl() {
$baseUrl = $this->context->getOption('filemanager_url', $this->context->getOption('rb_base_url', MODX_BASE_URL, $this->config), $this->config);
/* expand placeholders */
$baseUrl = str_replace(array(
'{base_url}',
'{core_url}',
'{assets_url}',
), array(
$this->context->getOption('base_url', MODX_BASE_PATH, $this->config),
$this->context->getOption('core_url', MODX_CORE_PATH, $this->config),
$this->context->getOption('assets_url', MODX_ASSETS_PATH, $this->config),
), $baseUrl);
return !empty($baseUrl) ? $this->postfixSlash($baseUrl) : $baseUrl;
}
/**
* Sanitize the specified path
*
* @param string $path The path to clean
* @return string The sanitized path
*/
public function sanitizePath($path) {
return preg_replace(array("/\.*[\/|\\\]/i", "/[\/|\\\]+/i"), array('/', '/'), $path);
}
/**
* Ensures that the passed path has a / at the end
*
* @param string $path
* @return string The postfixed path
*/
public function postfixSlash($path) {
$len = strlen($path);
if (substr($path, $len - 1, $len) != '/') {
$path .= '/';
}
return $path;
}
/**
* Gets the directory path for a given file
*
* @param string $fileName The path for a file
* @return string The directory path of the given file
*/
public function getDirectoryFromFile($fileName) {
$dir = dirname($fileName);
return $this->postfixSlash($dir);
}
/**
* Tells if a file is a binary file or not.
*
* @param string $file
* @return boolean True if a binary file.
*/
public function isBinary($file) {
if (!file_exists($file) || !is_file($file)) {
return false;
}
if (filesize($file) > 0 && class_exists('\finfo')) {
$finfo = new \finfo(FILEINFO_MIME);
$mimeType = strtolower($finfo->file($file));
// Some mimetypes include a character set, e.g. application/json; charset=utf-8
// so we filter out the last part to make comparison easier
if (strpos($mimeType, ';') > 0) {
$mimeType = substr($mimeType, 0, strpos($mimeType, ';'));
}
return substr($mimeType, 0, 4) !== 'text'
&& !in_array($mimeType, array(
'application/json',
'application/ld+json',
'application/x-httpd-php', // also restricted by default based on extension
'application/x-sh',
'image/svg+xml',
'application/xhtml+xml',
'application/xml',
), true);
}
$fh = @fopen($file, 'r');
$blk = @fread($fh, 512);
@fclose($fh);
@clearstatcache();
return (substr_count($blk, "^ -~" /*. "^\r\n"*/) / 512 > 0.3) || (substr_count($blk, "\x00") > 0);
}
}
/**
* Abstract class for handling file system resources (files or folders).
*
* Not to be instantiated directly - you should implement your own derivative class.
*
* @package modx
*/
abstract class modFileSystemResource {
/**
* @var string The absolute path of the file system resource
*/
protected $path;
/**
* @var modFileHandler A reference to a modFileHandler instance
*/
public $fileHandler;
/**
* @var array An array of file system resource specific options
*/
public $options = array();
/**
* Constructor for modFileSystemResource
*
* @param modFileHandler $fh A reference to the modFileHandler object
* @param string $path The path to the fs resource
* @param array $options An array of specific options
*/
function __construct(modFileHandler &$fh, $path, array $options = array()) {
$this->fileHandler =& $fh;
$this->path = $path;
$this->options = array_merge(array(
), $options);
}
/**
* Get the path of the fs resource.
* @return string The path of the fs resource
*/
public function getPath() {
return $this->path;
}
/**
* Validate chmod mode.
*
* @param $mode
* @return bool
*/
public function isValidMode($mode) {
if (!preg_match('/^[0-7]{4}$/', $mode)) {
return false;
}
return true;
}
/**
* Chmods the resource to the specified mode.
*
* @param string $mode
* @return boolean True if successful
*/
public function chmod($mode) {
$mode = $this->parseMode($mode);
return @chmod($this->path, $mode);
}
/**
* Sets the group permission for the fs resource
* @param mixed $grp
* @return boolean True if successful
*/
public function chgrp($grp) {
if ($this->isLink() && function_exists('lchgrp')) {
return @lchgrp($this->path, $grp);
} else {
return @chgrp($this->path, $grp);
}
}
/**
* Sets the owner for the fs resource
*
* @param mixed $owner
* @return boolean True if successful
*/
public function chown($owner) {
if ($this->isLink() && function_exists('lchown')) {
return @lchown($this->path, $owner);
} else {
return @chown($this->path, $owner);
}
}
/**
* Check to see if the fs resource exists
*
* @return boolean True if exists
*/
public function exists() {
return file_exists($this->path);
}
/**
* Check to see if the fs resource is readable
*
* @return boolean True if readable
*/
public function isReadable() {
return is_readable($this->path);
}
/**
* Check to see if the fs resource is writable
*
* @return boolean True if writable
*/
public function isWritable() {
return is_writable($this->path);
}
/**
* Check to see if fs resource is symlink
*
* @return boolean True if symlink
*/
public function isLink() {
return is_link($this->path);
}
/**
* Gets the permission group for the fs resource
*
* @return string The group name of the fs resource
*/
public function getGroup() {
return filegroup($this->path);
}
/**
* Alias for chgrp
*
* @see chgrp
* @param string $grp
* @return boolean
*/
public function setGroup($grp) {
return $this->chgrp($grp);
}
/**
* Renames the file/folder
*
* @param string $newPath The new path for the fs resource
* @return boolean True if successful
*/
public function rename($newPath) {
$newPath = $this->fileHandler->sanitizePath($newPath);
if (!$this->isWritable()) return false;
if (file_exists($newPath)) return false;
return @rename($this->path, $newPath);
}
/**
* Alias for rename
*
* @param string $newPath The new path to move fs resource
* @return boolean True if successful
*/
public function move($newPath) {
return $this->rename($newPath);
}
/**
* Parses a string mode into octal format
*
* @param string $mode The octal to parse
* @return string The new mode in decimal format
*/
protected function parseMode($mode = '') {
return octdec($mode);
}
/**
* Gets the parent containing directory of this fs resource
*
* @param boolean $raw Whether or not to return a modDirectory or string path
* @return modDirectory|string Returns either a modDirectory object of the
* parent directory, or the absolute path of the parent, depending on
* whether or not $raw is set to true.
*/
public function getParentDirectory($raw = false) {
$ppath = dirname($this->path) . '/';
$ppath = str_replace('//', '/', $ppath);
if ($raw) return $ppath;
$directory = $this->fileHandler->make($ppath,array(),'modDirectory');
return $directory;
}
}
/**
* File implementation of modFileSystemResource
*
* @package modx
*/
class modFile extends modFileSystemResource {
/**
* @var string The content of the resource
*/
protected $content = '';
/**
* @see modFileSystemResource.parseMode
* @param string $mode
* @return string
*/
protected function parseMode($mode = '') {
if (empty($mode)) {
$mode = $this->fileHandler->context->getOption('new_file_permissions', '0644', $this->fileHandler->config);
}
return parent::parseMode($mode);
}
/**
* Actually create the file on the file system
*
* @param string $content The content of the file to write
* @param string $mode The perms to write with the file
* @return boolean True if successful
*/
public function create($content = '', $mode = 'w+') {
if ($this->exists()) return false;
$result = false;
$fp = @fopen($this->path, 'w+');
if ($fp) {
@fwrite($fp, $content);
@fclose($fp);
$result = file_exists($this->path);
if ($result) {
$mode = $this->parseMode();
if (empty($mode)) {
$mode = octdec($this->fileHandler->modx->getOption('new_file_permissions', null, '0644'));
}
@chmod($this->path, $mode);
}
}
return $result;
}
/**
* Temporarily set (but not save) the content of the file
* @param string $content The content
*/
public function setContent($content) {
$this->content = $content;
}
/**
* Get the contents of the file
*
* @return string The contents of the file
*/
public function getContents() {
$content = @file_get_contents($this->path);
if ($content === false) {
$content = $this->content;
}
return $content;
}
/**
* Alias for save()
*
* @see modDirectory::write
* @param string $content
* @param string $mode
* @return boolean
*/
public function write($content = null, $mode = 'w+') {
return $this->save($content, $mode);
}
/**
* Writes the content of the modFile object to the actual file.
*
* @param string $content Optional. If not using setContent, this will set
* the content to write.
* @param string $mode The mode in which to write
* @return boolean The result of the fwrite
*/
public function save($content = null, $mode = 'w+') {
if ($content !== null) $this->content = $content;
$result = false;
$fp = @fopen($this->path, $mode);
if ($fp) {
$result = @fwrite($fp, $this->content);
@fclose($fp);
}
return $result;
}
/**
* Unpack a zip archive to a specified location.
*
* @uses compression.xPDOZip OR compression.PclZip
*
* @param string $this->getPath() An absolute file system location to a valid zip archive.
* @param string $to A file system location to extract the contents of the archive to.
* @param array $options an array of optional options, primarily for the xPDOZip class
* @return array|string|boolean An array of unpacked files, a string in case of cli functions or false on failure.
*/
public function unpack($to = '', $options = array()) {
$results = false;
/** @var xPDOZip $archive */
$archive = $this->fileHandler->modx->getService('archive', 'compression.xPDOZip', XPDO_CORE_PATH, $this->path);
if ($archive) {
if (isset($options['check_filetype']) && $options['check_filetype'] == true) {
$options[xPDOZip::ALLOWED_EXTENSIONS] = $this->getAllowedExtensions();
}
$results = $archive->unpack($to, $options);
}
return $results;
}
/**
* Gets the size of the file
*
* @return int The size of the file, in bytes
*/
public function getSize() {
$size = @filesize($this->path);
if ($size === false) {
if ( function_exists('mb_strlen') ) {
$size = mb_strlen($this->content, '8bit');
} else {
$size = strlen($this->content);
}
}
return $size;
}
/**
* Gets the last accessed time of the file
*
* @param string $timeFormat The format, in strftime format, of the time
* @return string The formatted time
*/
public function getLastAccessed($timeFormat = '%b %d, %Y %I:%M:%S %p') {
return strftime($timeFormat, fileatime($this->path));
}
/**
* Gets the last modified time of the file
*
* @param string $timeFormat The format, in strftime format, of the time
* @return string The formatted time
*/
public function getLastModified($timeFormat = '%b %d, %Y %I:%M:%S %p') {
return strftime($timeFormat, filemtime($this->path));
}
/**
* Gets the file extension of the file
*
* @return string The file extension of the file
*/
public function getExtension() {
return pathinfo($this->path, PATHINFO_EXTENSION);
}
/**
* Gets the basename, or only the filename without the path, of the file
*
* @return string The basename of the file
*/
public function getBaseName() {
return ltrim(strrchr($this->path, '/'), '/');
}
/**
* Sends the file as a download
*
* @param array $options Optional configuration options like mimetype and filename
*
* @noreturn downloadable file
*/
public function download($options = array()) {
$options = array_merge(array(
'mimetype' => 'application/octet-stream',
'filename' => '"' . $this->getBasename() . '"',
), $options);
$output = $this->getContents();
header('Content-type: ' . $options['mimetype']);
header('Content-Disposition: attachment; filename=' . $options['filename']);
header('Content-Length: ' . $this->getSize());
echo $output;
die();
}
/**
* Deletes the file from the filesystem
*
* @return boolean True if successful
*/
public function remove() {
if (!$this->exists()) return false;
return @unlink($this->path);
}
/**
* Get allowed extensions
* @TODO use this for an upload check too
*
* @return mixed
*/
public function getAllowedExtensions() {
if (!$this->fileHandler->modx->getOption('allowedExtensions')) {
$allowedFiles = $this->fileHandler->modx->getOption('upload_files') ? explode(',', $this->fileHandler->modx->getOption('upload_files')) : array();
$allowedImages = $this->fileHandler->modx->getOption('upload_images') ? explode(',', $this->fileHandler->modx->getOption('upload_images')) : array();
$allowedMedia = $this->fileHandler->modx->getOption('upload_media') ? explode(',', $this->fileHandler->modx->getOption('upload_media')) : array();
$allowedFlash = $this->fileHandler->modx->getOption('upload_flash') ? explode(',', $this->fileHandler->modx->getOption('upload_flash')) : array();
$allowedExtensions = array_unique(array_merge($allowedFiles, $allowedImages, $allowedMedia, $allowedFlash));
$this->fileHandler->modx->setOption('allowedExtensions', $allowedExtensions);
}
return $this->fileHandler->modx->getOption('allowedExtensions');
}
}
/**
* Representation of a directory
*
* @package modx
*/
class modDirectory extends modFileSystemResource {
/**
* Actually creates the new directory on the file system.
*
* @param string $mode Optional. The permissions of the new directory.
* @return boolean True if successful
*/
public function create($mode = '') {
$mode = $this->parseMode($mode);
if (empty($mode)) {
$mode = octdec($this->fileHandler->modx->getOption('new_folder_permissions',null,'0775'));
}
if ($this->exists()) return false;
return $this->fileHandler->modx->cacheManager->writeTree($this->path,array(
'new_folder_permissions' => $mode,
));
}
/**
* @see modFileSystemResource::parseMode
*
* @param string $mode
* @return boolean
*/
protected function parseMode($mode = '') {
if (empty($mode)) {
$mode = $this->fileHandler->context->getOption('new_folder_permissions', '0755', $this->fileHandler->config);
}
return parent::parseMode($mode);
}
/**
* Removes the directory from the file system, recursively removing
* subdirectories and files.
*
* @param array $options Options for removal.
* @return boolean True if successful
*/
public function remove($options = array()) {
if ($this->path == '/') return false;
$options = array_merge(array(
'deleteTop' => true,
'skipDirs' => false,
'extensions' => array(),
), $options);
$this->fileHandler->modx->getCacheManager();
return $this->fileHandler->modx->cacheManager->deleteTree($this->path, $options);
}
/**
* Iterates over a modDirectory object and returns an array of all containing files and optionally directories,
* can run recursive, filter by file extension(s) or filenames and sort the resulting list with the specified sort options
* an anonymous callback function can be passed to modify the output on the fly, by default an array of paths is returned
*
* @param array $options Options for iterating the directory.
* @option boolean recursive If subdirectories should be scanned as well
* @option boolean sort If the resulting array should be sorted
* @option string sortdir What sort order should be applied: SORT_ASC|SORT_DESC
* @optoin string sortflag What sort flag should be applied: SORT_REGULAR, SORT_NATURAL, SORT_NUMERIC etc
* @option boolean skiphidden If hidden directories and files should be ignored, defaults to true
* @option boolean skipdirs If directories should be skipped in the resulting array, defaults to true
* @option string|array skip Comma separated list or array of filenames (including extension) that should be ignored
* @option string|array extensions Comma separated list or array of file extensions to filter files by
* @option boolean|function callback Anonymous function to modify each output item, $item will be passed as argument
*
* @return array
*/
public function getList($options = array()) {
$options = array_merge(array(
'recursive' => false,
'sort' => false,
'sortdir' => SORT_ASC,
'sortflag' => SORT_REGULAR,
'skiphidden' => true,
'skipdirs' => true,
'skip' => array(),
'extensions' => array(),
'callback' => false,
), $options);
$items = array();
$mb = $this->fileHandler->modx->getOption('use_multibyte', null, false);
$mbencoding = $this->fileHandler->modx->getOption('modx_charset', null, 'UTF-8');
$extensions = !is_array($options['extensions']) ? explode(',', $options['extensions']) : $options['extensions'];
$skip = !is_array($options['skip']) ? explode(',', $options['skip']) : $options['skip'];
$iterator = $options['recursive'] ? new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->path, FilesystemIterator::CURRENT_AS_SELF)) : new DirectoryIterator($this->path);
foreach ($iterator as $item) {
$skipfile = !empty($skip) ? in_array($item->getFilename(), $skip) : false;
$ishidden = false;
if ($options['skiphidden']) {
// check for hidden folder, also hide with visible ones inside
// but don't skip weird filenames like "...and-there-was-silence.avi"
if ($item->isDot() || preg_match('/(\/\.\w+|\\\.\w+)/', $item->getPath())) {
continue;
}
// check for hidden file (probably works only on UNIX filesystems)
$ishidden = preg_match('/^(\.\w+)/i', $item->getFilename());
} else if (!$options['skipdirs']) {
// always exclude . and .. directory navigators, only relevant when including folders
$ishidden = $item->isDot();
}
if (($item->isFile() || $item->isDir() && !$options['skipdirs']) && !$ishidden && !$skipfile) {
$additem = true;
if (!empty($options['extensions'])) {
// if min PHP version is 5.3.6 we can use $item->getExtension()
$extension = pathinfo($item->getPathname(), PATHINFO_EXTENSION);
$extension = $mb ? mb_strtolower($extension, $mbencoding) : strtolower($extension);
if (!in_array($extension, $extensions)) {
$additem = false;
}
}
if (!$additem) {
continue;
} else if (is_callable($options['callback'])) {
$callback = call_user_func($options['callback'], $item);
if (!empty($callback)) {
$items[] = $callback;
}
} else {
$items[] = $item->isDir() ? $item->getPathname() . DIRECTORY_SEPARATOR : $item->getPathname();
}
}
}
if (!empty($options['sort'])) {
array_multisort($items, $options['sortdir'], $options['sortflag'], $items);
}
return $items;
}
}