init
This commit is contained in:
118
kirby/src/Filesystem/Asset.php
Normal file
118
kirby/src/Filesystem/Asset.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Filesystem;
|
||||
|
||||
use Kirby\Cms\FileModifications;
|
||||
use Kirby\Cms\HasMethods;
|
||||
use Kirby\Exception\BadMethodCallException;
|
||||
|
||||
/**
|
||||
* Anything in your public path can be converted
|
||||
* to an Asset object to use the same handy file
|
||||
* methods as for any other Kirby files. Pass a
|
||||
* relative path to the class to create the asset.
|
||||
*
|
||||
* @package Kirby Filesystem
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Asset
|
||||
{
|
||||
use IsFile;
|
||||
use FileModifications;
|
||||
use HasMethods;
|
||||
|
||||
/**
|
||||
* Relative file path
|
||||
*/
|
||||
protected string|null $path;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new Asset object for the given path.
|
||||
*/
|
||||
public function __construct(string $path)
|
||||
{
|
||||
$this->root = $this->kirby()->root('index') . '/' . $path;
|
||||
$this->url = $this->kirby()->url('base') . '/' . $path;
|
||||
|
||||
$path = dirname($path);
|
||||
$this->path = $path === '.' ? '' : $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic caller for asset methods
|
||||
*
|
||||
* @throws \Kirby\Exception\BadMethodCallException
|
||||
*/
|
||||
public function __call(string $method, array $arguments = []): mixed
|
||||
{
|
||||
// public property access
|
||||
if (isset($this->$method) === true) {
|
||||
return $this->$method;
|
||||
}
|
||||
|
||||
// asset method proxy
|
||||
if (method_exists($this->asset(), $method)) {
|
||||
return $this->asset()->$method(...$arguments);
|
||||
}
|
||||
|
||||
// asset methods
|
||||
if ($this->hasMethod($method)) {
|
||||
return $this->callMethod($method, $arguments);
|
||||
}
|
||||
|
||||
throw new BadMethodCallException('The method: "' . $method . '" does not exist');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a unique id for the asset
|
||||
*/
|
||||
public function id(): string
|
||||
{
|
||||
return $this->root();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a unique media hash
|
||||
*/
|
||||
public function mediaHash(): string
|
||||
{
|
||||
return crc32($this->filename()) . '-' . $this->modified();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the relative path starting at the media folder
|
||||
*/
|
||||
public function mediaPath(): string
|
||||
{
|
||||
return 'assets/' . $this->path() . '/' . $this->mediaHash() . '/' . $this->filename();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the file in the public media folder
|
||||
*/
|
||||
public function mediaRoot(): string
|
||||
{
|
||||
return $this->kirby()->root('media') . '/' . $this->mediaPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute Url to the file in the public media folder
|
||||
*/
|
||||
public function mediaUrl(): string
|
||||
{
|
||||
return $this->kirby()->url('media') . '/' . $this->mediaPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path of the file from the web root,
|
||||
* excluding the filename
|
||||
*/
|
||||
public function path(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
}
|
||||
612
kirby/src/Filesystem/Dir.php
Normal file
612
kirby/src/Filesystem/Dir.php
Normal file
@@ -0,0 +1,612 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Filesystem;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Cms\Helpers;
|
||||
use Kirby\Cms\Page;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* The `Dir` class provides methods
|
||||
* for dealing with directories on the
|
||||
* file system level, like creating,
|
||||
* listing, moving, copying or
|
||||
* evaluating directories etc.
|
||||
*
|
||||
* For the Cms, it includes methods,
|
||||
* that handle scanning directories
|
||||
* and converting the results into
|
||||
* children, files and other page stuff.
|
||||
*
|
||||
* @package Kirby Filesystem
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class Dir
|
||||
{
|
||||
/**
|
||||
* Ignore when scanning directories
|
||||
*/
|
||||
public static array $ignore = [
|
||||
'.',
|
||||
'..',
|
||||
'.DS_Store',
|
||||
'.gitignore',
|
||||
'.git',
|
||||
'.svn',
|
||||
'.htaccess',
|
||||
'Thumb.db',
|
||||
'@eaDir'
|
||||
];
|
||||
|
||||
public static string $numSeparator = '_';
|
||||
|
||||
/**
|
||||
* Copy the directory to a new destination
|
||||
*
|
||||
* @param array|false $ignore List of full paths to skip during copying
|
||||
* or `false` to copy all files, including
|
||||
* those listed in `Dir::$ignore`
|
||||
*/
|
||||
public static function copy(
|
||||
string $dir,
|
||||
string $target,
|
||||
bool $recursive = true,
|
||||
array|false $ignore = []
|
||||
): bool {
|
||||
if (is_dir($dir) === false) {
|
||||
throw new Exception('The directory "' . $dir . '" does not exist');
|
||||
}
|
||||
|
||||
if (is_dir($target) === true) {
|
||||
throw new Exception('The target directory "' . $target . '" exists');
|
||||
}
|
||||
|
||||
if (static::make($target) !== true) {
|
||||
throw new Exception('The target directory "' . $target . '" could not be created');
|
||||
}
|
||||
|
||||
foreach (static::read($dir, $ignore === false ? [] : null) as $name) {
|
||||
$root = $dir . '/' . $name;
|
||||
|
||||
if (
|
||||
is_array($ignore) === true &&
|
||||
in_array($root, $ignore) === true
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_dir($root) === true) {
|
||||
if ($recursive === true) {
|
||||
static::copy($root, $target . '/' . $name, true, $ignore);
|
||||
}
|
||||
} else {
|
||||
F::copy($root, $target . '/' . $name);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all subdirectories
|
||||
*/
|
||||
public static function dirs(
|
||||
string $dir,
|
||||
array|null $ignore = null,
|
||||
bool $absolute = false
|
||||
): array {
|
||||
$scan = static::read($dir, $ignore, true);
|
||||
$result = array_values(array_filter($scan, 'is_dir'));
|
||||
|
||||
if ($absolute !== true) {
|
||||
$result = array_map('basename', $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the directory exists on disk
|
||||
*/
|
||||
public static function exists(string $dir): bool
|
||||
{
|
||||
return is_dir($dir) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all files
|
||||
*/
|
||||
public static function files(
|
||||
string $dir,
|
||||
array|null $ignore = null,
|
||||
bool $absolute = false
|
||||
): array {
|
||||
$scan = static::read($dir, $ignore, true);
|
||||
$result = array_values(array_filter($scan, 'is_file'));
|
||||
|
||||
if ($absolute !== true) {
|
||||
$result = array_map('basename', $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the directory and all subdirectories
|
||||
*
|
||||
* @todo Remove support for `$ignore = null` in a major release
|
||||
* @param array|false|null $ignore Array of absolut file paths;
|
||||
* `false` to disable `Dir::$ignore` list
|
||||
* (passing null is deprecated)
|
||||
*/
|
||||
public static function index(
|
||||
string $dir,
|
||||
bool $recursive = false,
|
||||
array|false|null $ignore = [],
|
||||
string $path = null
|
||||
): array {
|
||||
$result = [];
|
||||
$dir = realpath($dir);
|
||||
$items = static::read($dir, $ignore === false ? [] : null);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$root = $dir . '/' . $item;
|
||||
|
||||
if (
|
||||
is_array($ignore) === true &&
|
||||
in_array($root, $ignore) === true
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$entry = $path !== null ? $path . '/' . $item : $item;
|
||||
$result[] = $entry;
|
||||
|
||||
if ($recursive === true && is_dir($root) === true) {
|
||||
$result = [
|
||||
...$result,
|
||||
...static::index($root, true, $ignore, $entry)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the folder has any contents
|
||||
*/
|
||||
public static function isEmpty(string $dir): bool
|
||||
{
|
||||
return count(static::read($dir)) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the directory is readable
|
||||
*/
|
||||
public static function isReadable(string $dir): bool
|
||||
{
|
||||
return is_readable($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the directory is writable
|
||||
*/
|
||||
public static function isWritable(string $dir): bool
|
||||
{
|
||||
return is_writable($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the directory and analyzes files,
|
||||
* content, meta info and children. This is used
|
||||
* in `Kirby\Cms\Page`, `Kirby\Cms\Site` and
|
||||
* `Kirby\Cms\User` objects to fetch all
|
||||
* relevant information.
|
||||
*
|
||||
* Don't use outside the Cms context.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function inventory(
|
||||
string $dir,
|
||||
string $contentExtension = 'txt',
|
||||
array|null $contentIgnore = null,
|
||||
bool $multilang = false
|
||||
): array {
|
||||
$inventory = [
|
||||
'children' => [],
|
||||
'files' => [],
|
||||
'template' => 'default',
|
||||
];
|
||||
|
||||
$dir = realpath($dir);
|
||||
|
||||
if ($dir === false) {
|
||||
return $inventory;
|
||||
}
|
||||
|
||||
// a temporary store for all content files
|
||||
$content = [];
|
||||
|
||||
// read and sort all items naturally to avoid sorting issues later
|
||||
$items = static::read($dir, $contentIgnore);
|
||||
natsort($items);
|
||||
|
||||
// loop through all directory items and collect all relevant information
|
||||
foreach ($items as $item) {
|
||||
// ignore all items with a leading dot or underscore
|
||||
if (in_array(substr($item, 0, 1), ['.', '_']) === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$root = $dir . '/' . $item;
|
||||
|
||||
// collect all directories as children
|
||||
if (is_dir($root) === true) {
|
||||
$inventory['children'][] = static::inventoryChild(
|
||||
$item,
|
||||
$root,
|
||||
$contentExtension,
|
||||
$multilang
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$extension = pathinfo($item, PATHINFO_EXTENSION);
|
||||
|
||||
// don't track files with these extensions
|
||||
if (in_array($extension, ['htm', 'html', 'php']) === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// collect all content files separately,
|
||||
// not as inventory entries
|
||||
if ($extension === $contentExtension) {
|
||||
$filename = pathinfo($item, PATHINFO_FILENAME);
|
||||
|
||||
// remove the language codes from all content filenames
|
||||
if ($multilang === true) {
|
||||
$filename = pathinfo($filename, PATHINFO_FILENAME);
|
||||
}
|
||||
|
||||
$content[] = $filename;
|
||||
continue;
|
||||
}
|
||||
|
||||
// collect all other files
|
||||
$inventory['files'][$item] = [
|
||||
'filename' => $item,
|
||||
'extension' => $extension,
|
||||
'root' => $root,
|
||||
];
|
||||
}
|
||||
|
||||
$content = array_unique($content);
|
||||
|
||||
$inventory['template'] = static::inventoryTemplate(
|
||||
$content,
|
||||
$inventory['files']
|
||||
);
|
||||
|
||||
return $inventory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect information for a child for the inventory
|
||||
*/
|
||||
protected static function inventoryChild(
|
||||
string $item,
|
||||
string $root,
|
||||
string $contentExtension = 'txt',
|
||||
bool $multilang = false
|
||||
): array {
|
||||
// extract the slug and num of the directory
|
||||
if ($separator = strpos($item, static::$numSeparator)) {
|
||||
$num = (int)substr($item, 0, $separator);
|
||||
$slug = substr($item, $separator + 1);
|
||||
}
|
||||
|
||||
// determine the model
|
||||
if (empty(Page::$models) === false) {
|
||||
if ($multilang === true) {
|
||||
$code = App::instance()->defaultLanguage()->code();
|
||||
$contentExtension = $code . '.' . $contentExtension;
|
||||
}
|
||||
|
||||
// look if a content file can be found
|
||||
// for any of the available models
|
||||
foreach (Page::$models as $modelName => $modelClass) {
|
||||
if (is_file($root . '/' . $modelName . '.' . $contentExtension) === true) {
|
||||
$model = $modelName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'dirname' => $item,
|
||||
'model' => $model ?? null,
|
||||
'num' => $num ?? null,
|
||||
'root' => $root,
|
||||
'slug' => $slug ?? $item,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the main template for the inventory
|
||||
* from all collected content files, ignore file meta files
|
||||
*/
|
||||
protected static function inventoryTemplate(
|
||||
array $content,
|
||||
array $files,
|
||||
): string {
|
||||
foreach ($content as $name) {
|
||||
// is a meta file corresponding to an actual file, i.e. cover.jpg
|
||||
if (isset($files[$name]) === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// it's most likely the template
|
||||
// (will overwrite and use the last match for historic reasons)
|
||||
$template = $name;
|
||||
}
|
||||
|
||||
return $template ?? 'default';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a (symbolic) link to a directory
|
||||
*/
|
||||
public static function link(string $source, string $link): bool
|
||||
{
|
||||
static::make(dirname($link), true);
|
||||
|
||||
if (is_dir($link) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_dir($source) === false) {
|
||||
throw new Exception(sprintf('The directory "%s" does not exist and cannot be linked', $source));
|
||||
}
|
||||
|
||||
try {
|
||||
return symlink($source, $link) === true;
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new directory
|
||||
*
|
||||
* @param string $dir The path for the new directory
|
||||
* @param bool $recursive Create all parent directories, which don't exist
|
||||
* @return bool True: the dir has been created, false: creating failed
|
||||
* @throws \Exception If a file with the provided path already exists or the parent directory is not writable
|
||||
*/
|
||||
public static function make(string $dir, bool $recursive = true): bool
|
||||
{
|
||||
if (empty($dir) === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_dir($dir) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_file($dir) === true) {
|
||||
throw new Exception(sprintf('A file with the name "%s" already exists', $dir));
|
||||
}
|
||||
|
||||
$parent = dirname($dir);
|
||||
|
||||
if ($recursive === true && is_dir($parent) === false) {
|
||||
static::make($parent, true);
|
||||
}
|
||||
|
||||
if (is_writable($parent) === false) {
|
||||
throw new Exception(sprintf('The directory "%s" cannot be created', $dir));
|
||||
}
|
||||
|
||||
return Helpers::handleErrors(
|
||||
fn (): bool => mkdir($dir),
|
||||
// if the dir was already created (race condition),
|
||||
fn (int $errno, string $errstr): bool => Str::endsWith($errstr, 'File exists'),
|
||||
// consider it a success
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively check when the dir and all
|
||||
* subfolders have been modified for the last time.
|
||||
*
|
||||
* @param string $dir The path of the directory
|
||||
* @param 'date'|'intl'|'strftime'|null $handler Custom date handler or `null`
|
||||
* for the globally configured one
|
||||
*/
|
||||
public static function modified(
|
||||
string $dir,
|
||||
string $format = null,
|
||||
string|null $handler = null
|
||||
): int|string {
|
||||
$modified = filemtime($dir);
|
||||
$items = static::read($dir);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$newModified = match (is_file($dir . '/' . $item)) {
|
||||
true => filemtime($dir . '/' . $item),
|
||||
false => static::modified($dir . '/' . $item)
|
||||
};
|
||||
$modified = ($newModified > $modified) ? $newModified : $modified;
|
||||
}
|
||||
|
||||
return Str::date($modified, $format, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a directory to a new location
|
||||
*
|
||||
* @param string $old The current path of the directory
|
||||
* @param string $new The desired path where the dir should be moved to
|
||||
* @return bool true: the directory has been moved, false: moving failed
|
||||
*/
|
||||
public static function move(string $old, string $new): bool
|
||||
{
|
||||
if ($old === $new) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_dir($old) === false || is_dir($new) === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (static::make(dirname($new), true) !== true) {
|
||||
throw new Exception('The parent directory cannot be created');
|
||||
}
|
||||
|
||||
return rename($old, $new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a nicely formatted size of all the contents of the folder
|
||||
*
|
||||
* @param string $dir The path of the directory
|
||||
* @param string|false|null $locale Locale for number formatting,
|
||||
* `null` for the current locale,
|
||||
* `false` to disable number formatting
|
||||
*/
|
||||
public static function niceSize(
|
||||
string $dir,
|
||||
string|false|null $locale = null
|
||||
): string {
|
||||
return F::niceSize(static::size($dir), $locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads all files from a directory and returns them as an array.
|
||||
* It skips unwanted invisible stuff.
|
||||
*
|
||||
* @param string $dir The path of directory
|
||||
* @param array $ignore Optional array with filenames, which should be ignored
|
||||
* @param bool $absolute If true, the full path for each item will be returned
|
||||
* @return array An array of filenames
|
||||
*/
|
||||
public static function read(
|
||||
string $dir,
|
||||
array|null $ignore = null,
|
||||
bool $absolute = false
|
||||
): array {
|
||||
if (is_dir($dir) === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// create the ignore pattern
|
||||
$ignore ??= static::$ignore;
|
||||
$ignore = array_merge($ignore, ['.', '..']);
|
||||
|
||||
// scan for all files and dirs
|
||||
$result = array_values((array)array_diff(scandir($dir), $ignore));
|
||||
|
||||
// add absolute paths
|
||||
if ($absolute === true) {
|
||||
$result = array_map(fn ($item) => $dir . '/' . $item, $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a folder including all containing files and folders
|
||||
*/
|
||||
public static function remove(string $dir): bool
|
||||
{
|
||||
$dir = realpath($dir);
|
||||
|
||||
if (is_dir($dir) === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_link($dir) === true) {
|
||||
return F::unlink($dir);
|
||||
}
|
||||
|
||||
foreach (scandir($dir) as $childName) {
|
||||
if (in_array($childName, ['.', '..']) === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$child = $dir . '/' . $childName;
|
||||
|
||||
if (is_dir($child) === true && is_link($child) === false) {
|
||||
static::remove($child);
|
||||
} else {
|
||||
F::unlink($child);
|
||||
}
|
||||
}
|
||||
|
||||
return rmdir($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the size of the directory
|
||||
*
|
||||
* @param string $dir The path of the directory
|
||||
* @param bool $recursive Include all subfolders and their files
|
||||
*/
|
||||
public static function size(string $dir, bool $recursive = true): int|false
|
||||
{
|
||||
if (is_dir($dir) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get size for all direct files
|
||||
$size = F::size(static::files($dir, null, true));
|
||||
|
||||
// if recursive, add sizes of all subdirectories
|
||||
if ($recursive === true) {
|
||||
foreach (static::dirs($dir, null, true) as $subdir) {
|
||||
$size += static::size($subdir);
|
||||
}
|
||||
}
|
||||
|
||||
return $size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the directory or any subdirectory has been
|
||||
* modified after the given timestamp
|
||||
*/
|
||||
public static function wasModifiedAfter(string $dir, int $time): bool
|
||||
{
|
||||
if (filemtime($dir) > $time) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$content = static::read($dir);
|
||||
|
||||
foreach ($content as $item) {
|
||||
$subdir = $dir . '/' . $item;
|
||||
|
||||
if (filemtime($subdir) > $time) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
is_dir($subdir) === true &&
|
||||
static::wasModifiedAfter($subdir, $time) === true
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
943
kirby/src/Filesystem/F.php
Normal file
943
kirby/src/Filesystem/F.php
Normal file
@@ -0,0 +1,943 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Filesystem;
|
||||
|
||||
use Exception;
|
||||
use IntlDateFormatter;
|
||||
use Kirby\Cms\Helpers;
|
||||
use Kirby\Http\Response;
|
||||
use Kirby\Toolkit\I18n;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Throwable;
|
||||
use ZipArchive;
|
||||
|
||||
/**
|
||||
* The `F` class provides methods for
|
||||
* dealing with files on the file system
|
||||
* level, like creating, reading,
|
||||
* deleting, copying or validatings files.
|
||||
*
|
||||
* @package Kirby Filesystem
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class F
|
||||
{
|
||||
public static array $types = [
|
||||
'archive' => [
|
||||
'gz',
|
||||
'gzip',
|
||||
'tar',
|
||||
'tgz',
|
||||
'zip',
|
||||
],
|
||||
'audio' => [
|
||||
'aif',
|
||||
'aiff',
|
||||
'm4a',
|
||||
'midi',
|
||||
'mp3',
|
||||
'wav',
|
||||
],
|
||||
'code' => [
|
||||
'css',
|
||||
'js',
|
||||
'json',
|
||||
'java',
|
||||
'htm',
|
||||
'html',
|
||||
'php',
|
||||
'rb',
|
||||
'py',
|
||||
'scss',
|
||||
'xml',
|
||||
'yaml',
|
||||
'yml',
|
||||
],
|
||||
'document' => [
|
||||
'csv',
|
||||
'doc',
|
||||
'docx',
|
||||
'dotx',
|
||||
'indd',
|
||||
'md',
|
||||
'mdown',
|
||||
'pdf',
|
||||
'ppt',
|
||||
'pptx',
|
||||
'rtf',
|
||||
'txt',
|
||||
'xl',
|
||||
'xls',
|
||||
'xlsx',
|
||||
'xltx',
|
||||
],
|
||||
'image' => [
|
||||
'ai',
|
||||
'avif',
|
||||
'bmp',
|
||||
'gif',
|
||||
'eps',
|
||||
'ico',
|
||||
'j2k',
|
||||
'jp2',
|
||||
'jpeg',
|
||||
'jpg',
|
||||
'jpe',
|
||||
'png',
|
||||
'ps',
|
||||
'psd',
|
||||
'svg',
|
||||
'tif',
|
||||
'tiff',
|
||||
'webp'
|
||||
],
|
||||
'video' => [
|
||||
'avi',
|
||||
'flv',
|
||||
'm4v',
|
||||
'mov',
|
||||
'movie',
|
||||
'mpe',
|
||||
'mpg',
|
||||
'mp4',
|
||||
'ogg',
|
||||
'ogv',
|
||||
'swf',
|
||||
'webm',
|
||||
],
|
||||
];
|
||||
|
||||
public static array $units = [
|
||||
'B',
|
||||
'KB',
|
||||
'MB',
|
||||
'GB',
|
||||
'TB',
|
||||
'PB',
|
||||
'EB',
|
||||
'ZB',
|
||||
'YB'
|
||||
];
|
||||
|
||||
/**
|
||||
* Appends new content to an existing file
|
||||
*
|
||||
* @param string $file The path for the file
|
||||
* @param mixed $content Either a string or an array. Arrays will be converted to JSON.
|
||||
*/
|
||||
public static function append(string $file, $content): bool
|
||||
{
|
||||
return static::write($file, $content, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file content as base64 encoded string
|
||||
*
|
||||
* @param string $file The path for the file
|
||||
*/
|
||||
public static function base64(string $file): string
|
||||
{
|
||||
return base64_encode(static::read($file));
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a file to a new location.
|
||||
*/
|
||||
public static function copy(string $source, string $target, bool $force = false): bool
|
||||
{
|
||||
if (file_exists($source) === false || (file_exists($target) === true && $force === false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$directory = dirname($target);
|
||||
|
||||
// create the parent directory if it does not exist
|
||||
if (is_dir($directory) === false) {
|
||||
Dir::make($directory, true);
|
||||
}
|
||||
|
||||
return copy($source, $target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Just an alternative for dirname() to stay consistent
|
||||
*
|
||||
* <code>
|
||||
*
|
||||
* $dirname = F::dirname('/var/www/test.txt');
|
||||
* // dirname is /var/www
|
||||
*
|
||||
* </code>
|
||||
*
|
||||
* @param string $file The path
|
||||
*/
|
||||
public static function dirname(string $file): string
|
||||
{
|
||||
return dirname($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the file exists on disk
|
||||
*/
|
||||
public static function exists(string $file, string|null $in = null): bool
|
||||
{
|
||||
try {
|
||||
static::realpath($file, $in);
|
||||
return true;
|
||||
} catch (Exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the extension of a file
|
||||
*
|
||||
* @param string|null $file The filename or path
|
||||
* @param string|null $extension Set an optional extension to overwrite the current one
|
||||
*/
|
||||
public static function extension(
|
||||
string|null $file = null,
|
||||
string|null $extension = null
|
||||
): string {
|
||||
// overwrite the current extension
|
||||
if ($extension !== null) {
|
||||
return static::name($file) . '.' . $extension;
|
||||
}
|
||||
|
||||
// return the current extension
|
||||
return Str::lower(pathinfo($file, PATHINFO_EXTENSION));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a file extension to a mime type
|
||||
*/
|
||||
public static function extensionToMime(string $extension): string|null
|
||||
{
|
||||
return Mime::fromExtension($extension);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file type for a passed extension
|
||||
*/
|
||||
public static function extensionToType(string $extension): string|false
|
||||
{
|
||||
foreach (static::$types as $type => $extensions) {
|
||||
if (in_array($extension, $extensions) === true) {
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all extensions for a certain file type
|
||||
*/
|
||||
public static function extensions(string|null $type = null): array
|
||||
{
|
||||
if ($type === null) {
|
||||
return array_keys(Mime::types());
|
||||
}
|
||||
|
||||
return static::$types[$type] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the filename from a file path
|
||||
*
|
||||
* <code>
|
||||
*
|
||||
* $filename = F::filename('/var/www/test.txt');
|
||||
* // filename is test.txt
|
||||
*
|
||||
* </code>
|
||||
*
|
||||
* @param string $name The path
|
||||
*/
|
||||
public static function filename(string $name): string
|
||||
{
|
||||
return pathinfo($name, PATHINFO_BASENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate opcode cache for file.
|
||||
*
|
||||
* @param string $file The path of the file
|
||||
*/
|
||||
public static function invalidateOpcodeCache(string $file): bool
|
||||
{
|
||||
if (
|
||||
function_exists('opcache_invalidate') &&
|
||||
strlen(ini_get('opcache.restrict_api')) === 0
|
||||
) {
|
||||
return opcache_invalidate($file, true);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a file is of a certain type
|
||||
*
|
||||
* @param string $file Full path to the file
|
||||
* @param string $value An extension or mime type
|
||||
*/
|
||||
public static function is(string $file, string $value): bool
|
||||
{
|
||||
// check for the extension
|
||||
if (in_array($value, static::extensions()) === true) {
|
||||
return static::extension($file) === $value;
|
||||
}
|
||||
|
||||
// check for the mime type
|
||||
if (strpos($value, '/') !== false) {
|
||||
return static::mime($file) === $value;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the file is readable
|
||||
*/
|
||||
public static function isReadable(string $file): bool
|
||||
{
|
||||
return is_readable($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the file is writable
|
||||
*/
|
||||
public static function isWritable(string $file): bool
|
||||
{
|
||||
if (file_exists($file) === false) {
|
||||
return is_writable(dirname($file));
|
||||
}
|
||||
|
||||
return is_writable($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a (symbolic) link to a file
|
||||
*/
|
||||
public static function link(string $source, string $link, string $method = 'link'): bool
|
||||
{
|
||||
Dir::make(dirname($link), true);
|
||||
|
||||
if (is_file($link) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_file($source) === false) {
|
||||
throw new Exception(sprintf('The file "%s" does not exist and cannot be linked', $source));
|
||||
}
|
||||
|
||||
try {
|
||||
return $method($source, $link) === true;
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a file and returns the result or `false` if the
|
||||
* file to load does not exist
|
||||
*
|
||||
* @param array $data Optional array of variables to extract in the variable scope
|
||||
*/
|
||||
public static function load(
|
||||
string $file,
|
||||
mixed $fallback = null,
|
||||
array $data = [],
|
||||
bool $allowOutput = true
|
||||
) {
|
||||
if (is_file($file) === false) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
// we use the loadIsolated() method here to prevent the included
|
||||
// file from overwriting our $fallback in this variable scope; see
|
||||
// https://www.php.net/manual/en/function.include.php#example-124
|
||||
$callback = fn () => static::loadIsolated($file, $data);
|
||||
|
||||
// if the loaded file should not produce any output,
|
||||
// call the loaidIsolated method from the Response class
|
||||
// which checks for unintended ouput and throws an error if detected
|
||||
if ($allowOutput === false) {
|
||||
$result = Response::guardAgainstOutput($callback);
|
||||
} else {
|
||||
$result = $callback();
|
||||
}
|
||||
|
||||
if (
|
||||
$fallback !== null &&
|
||||
gettype($result) !== gettype($fallback)
|
||||
) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* A super simple class autoloader
|
||||
* @since 3.7.0
|
||||
*/
|
||||
public static function loadClasses(
|
||||
array $classmap,
|
||||
string|null $base = null
|
||||
): void {
|
||||
// convert all classnames to lowercase
|
||||
$classmap = array_change_key_case($classmap);
|
||||
|
||||
spl_autoload_register(
|
||||
fn ($class) => Response::guardAgainstOutput(function () use ($class, $classmap, $base) {
|
||||
$class = strtolower($class);
|
||||
|
||||
if (isset($classmap[$class]) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($base) {
|
||||
include $base . '/' . $classmap[$class];
|
||||
} else {
|
||||
include $classmap[$class];
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a file with as little as possible in the variable scope
|
||||
*
|
||||
* @param array $data Optional array of variables to extract in the variable scope
|
||||
*/
|
||||
protected static function loadIsolated(string $file, array $data = [])
|
||||
{
|
||||
// extract the $data variables in this scope to be accessed by the included file;
|
||||
// protect $file against being overwritten by a $data variable
|
||||
$___file___ = $file;
|
||||
extract($data);
|
||||
|
||||
return include $___file___;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a file using `include_once()` and
|
||||
* returns whether loading was successful
|
||||
*/
|
||||
public static function loadOnce(
|
||||
string $file,
|
||||
bool $allowOutput = true
|
||||
): bool {
|
||||
if (is_file($file) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$callback = fn () => include_once $file;
|
||||
|
||||
if ($allowOutput === false) {
|
||||
Response::guardAgainstOutput($callback);
|
||||
} else {
|
||||
$callback();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mime type of a file
|
||||
*/
|
||||
public static function mime(string $file): string|null
|
||||
{
|
||||
return Mime::type($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a mime type to a file extension
|
||||
*/
|
||||
public static function mimeToExtension(string|null $mime = null): string|false
|
||||
{
|
||||
return Mime::toExtension($mime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type for a given mime
|
||||
*/
|
||||
public static function mimeToType(string $mime): string|false
|
||||
{
|
||||
return static::extensionToType(Mime::toExtension($mime));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file's last modification time.
|
||||
*
|
||||
* @param 'date'|'intl'|'strftime'|null $handler Custom date handler or `null`
|
||||
* for the globally configured one
|
||||
*/
|
||||
public static function modified(
|
||||
string $file,
|
||||
string|IntlDateFormatter|null $format = null,
|
||||
string|null $handler = null
|
||||
): string|int|false {
|
||||
if (file_exists($file) !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$modified = filemtime($file);
|
||||
|
||||
return Str::date($modified, $format, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a file to a new location
|
||||
*
|
||||
* @param string $oldRoot The current path for the file
|
||||
* @param string $newRoot The path to the new location
|
||||
* @param bool $force Force move if the target file exists
|
||||
*/
|
||||
public static function move(string $oldRoot, string $newRoot, bool $force = false): bool
|
||||
{
|
||||
// check if the file exists
|
||||
if (file_exists($oldRoot) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file_exists($newRoot) === true) {
|
||||
if ($force === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// delete the existing file
|
||||
static::remove($newRoot);
|
||||
}
|
||||
|
||||
$directory = dirname($newRoot);
|
||||
|
||||
// create the parent directory if it does not exist
|
||||
if (is_dir($directory) === false) {
|
||||
Dir::make($directory, true);
|
||||
}
|
||||
|
||||
// atomically moving the file will only work if
|
||||
// source and target are on the same filesystem
|
||||
if (stat($oldRoot)['dev'] === stat($directory)['dev']) {
|
||||
// same filesystem, we can move the file
|
||||
return rename($oldRoot, $newRoot) === true;
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
// not the same filesystem; we need to copy
|
||||
// the file and unlink the source afterwards
|
||||
if (copy($oldRoot, $newRoot) === true) {
|
||||
return unlink($oldRoot) === true;
|
||||
}
|
||||
|
||||
// copying failed, ensure the new root isn't there
|
||||
// (e.g. if the file could be created but there's no
|
||||
// more remaining disk space to write its contents)
|
||||
static::remove($newRoot);
|
||||
return false;
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the name from a file path or filename without extension
|
||||
*
|
||||
* @param string $name The path or filename
|
||||
*/
|
||||
public static function name(string $name): string
|
||||
{
|
||||
return pathinfo($name, PATHINFO_FILENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an integer size into a human readable format
|
||||
*
|
||||
* @param int|string|array $size The file size, a file path or array of paths
|
||||
* @param string|false|null $locale Locale for number formatting,
|
||||
* `null` for the current locale,
|
||||
* `false` to disable number formatting
|
||||
*/
|
||||
public static function niceSize(
|
||||
int|string|array $size,
|
||||
string|false|null $locale = null
|
||||
): string {
|
||||
// file mode
|
||||
if (is_string($size) === true || is_array($size) === true) {
|
||||
$size = static::size($size);
|
||||
}
|
||||
|
||||
// make sure it's an int
|
||||
$size = (int)$size;
|
||||
|
||||
// avoid errors for invalid sizes
|
||||
if ($size <= 0) {
|
||||
return '0 KB';
|
||||
}
|
||||
|
||||
// the math magic
|
||||
$size = round($size / 1024 ** ($unit = floor(log($size, 1024))), 2);
|
||||
|
||||
// format the number if requested
|
||||
if ($locale !== false) {
|
||||
$size = I18n::formatNumber($size, $locale);
|
||||
}
|
||||
|
||||
return $size . ' ' . static::$units[$unit];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the content of a file or requests the
|
||||
* contents of a remote HTTP or HTTPS URL
|
||||
*
|
||||
* @param string $file The path for the file or an absolute URL
|
||||
*/
|
||||
public static function read(string $file): string|false
|
||||
{
|
||||
if (
|
||||
is_readable($file) !== true &&
|
||||
Str::startsWith($file, 'https://') !== true &&
|
||||
Str::startsWith($file, 'http://') !== true
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return file_get_contents($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the name of the file without
|
||||
* touching the extension
|
||||
*
|
||||
* @param bool $overwrite Force overwrite existing files
|
||||
*/
|
||||
public static function rename(string $file, string $newName, bool $overwrite = false): string|false
|
||||
{
|
||||
// create the new name
|
||||
$name = static::safeName(basename($newName));
|
||||
|
||||
// overwrite the root
|
||||
$newRoot = rtrim(dirname($file) . '/' . $name . '.' . F::extension($file), '.');
|
||||
|
||||
// nothing has changed
|
||||
if ($newRoot === $file) {
|
||||
return $newRoot;
|
||||
}
|
||||
|
||||
if (F::move($file, $newRoot, $overwrite) !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $newRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the file if the file can be found.
|
||||
*/
|
||||
public static function realpath(string $file, string|null $in = null): string
|
||||
{
|
||||
$realpath = realpath($file);
|
||||
|
||||
if ($realpath === false || is_file($realpath) === false) {
|
||||
throw new Exception(sprintf('The file does not exist at the given path: "%s"', $file));
|
||||
}
|
||||
|
||||
if ($in !== null) {
|
||||
$parent = realpath($in);
|
||||
|
||||
if ($parent === false || is_dir($parent) === false) {
|
||||
throw new Exception(sprintf('The parent directory does not exist: "%s"', $in));
|
||||
}
|
||||
|
||||
if (substr($realpath, 0, strlen($parent)) !== $parent) {
|
||||
throw new Exception('The file is not within the parent directory');
|
||||
}
|
||||
}
|
||||
|
||||
return $realpath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the relative path of the file
|
||||
* starting after $in
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.CountInLoopExpression)
|
||||
*/
|
||||
public static function relativepath(string $file, string|null $in = null): string
|
||||
{
|
||||
if (empty($in) === true) {
|
||||
return basename($file);
|
||||
}
|
||||
|
||||
// windows
|
||||
$file = str_replace('\\', '/', $file);
|
||||
$in = str_replace('\\', '/', $in);
|
||||
|
||||
// trim trailing slashes
|
||||
$file = rtrim($file, '/');
|
||||
$in = rtrim($in, '/');
|
||||
|
||||
if (Str::contains($file, $in . '/') === false) {
|
||||
// make the paths relative by stripping what they have
|
||||
// in common and adding `../` tokens at the start
|
||||
$fileParts = explode('/', $file);
|
||||
$inParts = explode('/', $in);
|
||||
while (count($fileParts) && count($inParts) && ($fileParts[0] === $inParts[0])) {
|
||||
array_shift($fileParts);
|
||||
array_shift($inParts);
|
||||
}
|
||||
|
||||
return str_repeat('../', count($inParts)) . implode('/', $fileParts);
|
||||
}
|
||||
|
||||
return '/' . Str::after($file, $in . '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a file
|
||||
*
|
||||
* <code>
|
||||
*
|
||||
* $remove = F::remove('test.txt');
|
||||
* if($remove) echo 'The file has been removed';
|
||||
*
|
||||
* </code>
|
||||
*
|
||||
* @param string $file The path for the file
|
||||
*/
|
||||
public static function remove(string $file): bool
|
||||
{
|
||||
if (strpos($file, '*') !== false) {
|
||||
foreach (glob($file) as $f) {
|
||||
static::remove($f);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$file = realpath($file);
|
||||
if (is_string($file) === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return static::unlink($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a file's full name (filename and extension)
|
||||
* to strip unwanted special characters
|
||||
*
|
||||
* <code>
|
||||
*
|
||||
* $safe = f::safeName('über genius.txt');
|
||||
* // safe will be ueber-genius.txt
|
||||
*
|
||||
* </code>
|
||||
*
|
||||
* @param string $string The file name
|
||||
*/
|
||||
public static function safeName(string $string): string
|
||||
{
|
||||
$basename = static::safeBasename($string);
|
||||
$extension = static::safeExtension($string);
|
||||
|
||||
if (empty($extension) === false) {
|
||||
$extension = '.' . $extension;
|
||||
}
|
||||
|
||||
return $basename . $extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a file's name (without extension)
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public static function safeBasename(
|
||||
string $string,
|
||||
bool $extract = true
|
||||
): string {
|
||||
// extract only the name part from whole filename string
|
||||
if ($extract === true) {
|
||||
$string = static::name($string);
|
||||
}
|
||||
|
||||
return Str::slug($string, '-', 'a-z0-9@._-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a file's extension
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public static function safeExtension(
|
||||
string $string,
|
||||
bool $extract = true
|
||||
): string {
|
||||
// extract only the extension part from whole filename string
|
||||
if ($extract === true) {
|
||||
$string = static::extension($string);
|
||||
}
|
||||
|
||||
return Str::slug($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find similar or the same file by
|
||||
* building a glob based on the path
|
||||
*/
|
||||
public static function similar(string $path, string $pattern = '*'): array
|
||||
{
|
||||
$dir = dirname($path);
|
||||
$name = static::name($path);
|
||||
$extension = static::extension($path);
|
||||
$glob = $dir . '/' . $name . $pattern . '.' . $extension;
|
||||
return glob($glob);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of a file or an array of files.
|
||||
*
|
||||
* @param string|array $file file path or array of paths
|
||||
*/
|
||||
public static function size(string|array $file): int
|
||||
{
|
||||
if (is_array($file) === true) {
|
||||
return array_reduce(
|
||||
$file,
|
||||
fn ($total, $file) => $total + F::size($file),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
if ($size = @filesize($file)) {
|
||||
return $size;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorize the file
|
||||
*
|
||||
* @param string $file Either the file path or extension
|
||||
*/
|
||||
public static function type(string $file): string|null
|
||||
{
|
||||
$length = strlen($file);
|
||||
|
||||
if ($length >= 2 && $length <= 4) {
|
||||
// use the file name as extension
|
||||
$extension = $file;
|
||||
} else {
|
||||
// get the extension from the filename
|
||||
$extension = pathinfo($file, PATHINFO_EXTENSION);
|
||||
}
|
||||
|
||||
if (empty($extension) === true) {
|
||||
// detect the mime type first to get the most reliable extension
|
||||
$mime = static::mime($file);
|
||||
$extension = static::mimeToExtension($mime);
|
||||
}
|
||||
|
||||
// sanitize extension
|
||||
$extension = strtolower($extension);
|
||||
|
||||
foreach (static::$types as $type => $extensions) {
|
||||
if (in_array($extension, $extensions) === true) {
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all extensions of a given file type
|
||||
* or `null` if the file type is unknown
|
||||
*/
|
||||
public static function typeToExtensions(string $type): array|null
|
||||
{
|
||||
return static::$types[$type] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that a file or link is deleted (with race condition handling)
|
||||
* @since 3.7.4
|
||||
*/
|
||||
public static function unlink(string $file): bool
|
||||
{
|
||||
return Helpers::handleErrors(
|
||||
fn (): bool => unlink($file),
|
||||
// if the file or link was already deleted (race condition),
|
||||
fn (int $errno, string $errstr): bool => Str::endsWith($errstr, 'No such file or directory'),
|
||||
// consider it a success
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unzips a zip file
|
||||
*/
|
||||
public static function unzip(string $file, string $to): bool
|
||||
{
|
||||
if (class_exists('ZipArchive') === false) {
|
||||
throw new Exception('The ZipArchive class is not available');
|
||||
}
|
||||
|
||||
$zip = new ZipArchive();
|
||||
|
||||
if ($zip->open($file) === true) {
|
||||
$zip->extractTo($to);
|
||||
$zip->close();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file as data uri
|
||||
*
|
||||
* @param string $file The path for the file
|
||||
*/
|
||||
public static function uri(string $file): string|false
|
||||
{
|
||||
if ($mime = static::mime($file)) {
|
||||
return 'data:' . $mime . ';base64,' . static::base64($file);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new file
|
||||
*
|
||||
* @param string $file The path for the new file
|
||||
* @param mixed $content Either a string, an object or an array. Arrays and objects will be serialized.
|
||||
* @param bool $append true: append the content to an existing file if available. false: overwrite.
|
||||
*/
|
||||
public static function write(string $file, $content, bool $append = false): bool
|
||||
{
|
||||
if (is_array($content) === true || is_object($content) === true) {
|
||||
$content = serialize($content);
|
||||
}
|
||||
|
||||
$mode = $append === true ? FILE_APPEND | LOCK_EX : LOCK_EX;
|
||||
|
||||
// if the parent directory does not exist, create it
|
||||
if (is_dir(dirname($file)) === false) {
|
||||
if (Dir::make(dirname($file)) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (static::isWritable($file) === false) {
|
||||
throw new Exception('The file "' . $file . '" is not writable');
|
||||
}
|
||||
|
||||
return file_put_contents($file, $content, $mode) !== false;
|
||||
}
|
||||
}
|
||||
566
kirby/src/Filesystem/File.php
Normal file
566
kirby/src/Filesystem/File.php
Normal file
@@ -0,0 +1,566 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Filesystem;
|
||||
|
||||
use IntlDateFormatter;
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Http\Response;
|
||||
use Kirby\Sane\Sane;
|
||||
use Kirby\Toolkit\Escape;
|
||||
use Kirby\Toolkit\Html;
|
||||
use Kirby\Toolkit\V;
|
||||
|
||||
/**
|
||||
* Flexible File object with a set of helpful
|
||||
* methods to inspect and work with files.
|
||||
*
|
||||
* @since 3.6.0
|
||||
*
|
||||
* @package Kirby Filesystem
|
||||
* @author Nico Hoffmann <nico@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class File
|
||||
{
|
||||
/**
|
||||
* Parent file model
|
||||
* The model object must use the `\Kirby\Filesystem\IsFile` trait
|
||||
*/
|
||||
protected object|null $model;
|
||||
|
||||
/**
|
||||
* Absolute file path
|
||||
*/
|
||||
protected string|null $root;
|
||||
|
||||
/**
|
||||
* Absolute file URL
|
||||
*/
|
||||
protected string|null $url;
|
||||
|
||||
/**
|
||||
* Validation rules to be used for `::match()`
|
||||
*/
|
||||
public static array $validations = [
|
||||
'maxsize' => ['size', 'max'],
|
||||
'minsize' => ['size', 'min']
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructor sets all file properties
|
||||
*
|
||||
* @param array|string|null $props Properties or deprecated `$root` string
|
||||
* @param string|null $url Deprecated argument, use `$props['url']` instead
|
||||
*
|
||||
* @throws \Kirby\Exception\InvalidArgumentException When the model does not use the `Kirby\Filesystem\IsFile` trait
|
||||
*/
|
||||
public function __construct(
|
||||
array|string $props = null,
|
||||
string $url = null
|
||||
) {
|
||||
// Legacy support for old constructor of
|
||||
// the `Kirby\Image\Image` class
|
||||
if (is_array($props) === false) {
|
||||
$props = [
|
||||
'root' => $props,
|
||||
'url' => $url
|
||||
];
|
||||
}
|
||||
|
||||
$this->root = $props['root'] ?? null;
|
||||
$this->url = $props['url'] ?? null;
|
||||
$this->model = $props['model'] ?? null;
|
||||
|
||||
if (
|
||||
$this->model !== null &&
|
||||
method_exists($this->model, 'hasIsFileTrait') !== true
|
||||
) {
|
||||
throw new InvalidArgumentException('The model object must use the "Kirby\Filesystem\IsFile" trait');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Improved `var_dump` output
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL for the file object
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->url() ?? $this->root() ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file content as base64 encoded string
|
||||
*/
|
||||
public function base64(): string
|
||||
{
|
||||
return base64_encode($this->read());
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a file to a new location.
|
||||
*/
|
||||
public function copy(string $target, bool $force = false): static
|
||||
{
|
||||
if (F::copy($this->root(), $target, $force) !== true) {
|
||||
throw new Exception('The file "' . $this->root() . '" could not be copied');
|
||||
}
|
||||
|
||||
return new static($target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file as data uri
|
||||
*
|
||||
* @param bool $base64 Whether the data should be base64 encoded or not
|
||||
*/
|
||||
public function dataUri(bool $base64 = true): string
|
||||
{
|
||||
if ($base64 === true) {
|
||||
return 'data:' . $this->mime() . ';base64,' . $this->base64();
|
||||
}
|
||||
|
||||
return 'data:' . $this->mime() . ',' . Escape::url($this->read());
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the file
|
||||
*/
|
||||
public function delete(): bool
|
||||
{
|
||||
if (F::remove($this->root()) !== true) {
|
||||
throw new Exception('The file "' . $this->root() . '" could not be deleted');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Automatically sends all needed headers
|
||||
* for the file to be downloaded and
|
||||
* echos the file's content
|
||||
*
|
||||
* @param string|null $filename Optional filename for the download
|
||||
*/
|
||||
public function download(string|null $filename = null): string
|
||||
{
|
||||
return Response::download($this->root(), $filename ?? $this->filename());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the file actually exists
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return file_exists($this->root()) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current lowercase extension (without .)
|
||||
*/
|
||||
public function extension(): string
|
||||
{
|
||||
return F::extension($this->root());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the filename
|
||||
*/
|
||||
public function filename(): string
|
||||
{
|
||||
return basename($this->root());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a md5 hash of the root
|
||||
*/
|
||||
public function hash(): string
|
||||
{
|
||||
return md5($this->root());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an appropriate header for the asset
|
||||
*/
|
||||
public function header(bool $send = true): Response|null
|
||||
{
|
||||
$response = new Response('', $this->mime());
|
||||
|
||||
if ($send !== true) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$response->send();
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the file to html
|
||||
*/
|
||||
public function html(array $attr = []): string
|
||||
{
|
||||
return Html::a($this->url() ?? '', $attr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a file is of a certain type
|
||||
*
|
||||
* @param string $value An extension or mime type
|
||||
*/
|
||||
public function is(string $value): bool
|
||||
{
|
||||
return F::is($this->root(), $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the file is readable
|
||||
*/
|
||||
public function isReadable(): bool
|
||||
{
|
||||
return is_readable($this->root()) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the file is a resizable image
|
||||
*/
|
||||
public function isResizable(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a preview can be displayed for the file
|
||||
* in the panel or in the frontend
|
||||
*/
|
||||
public function isViewable(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the file is writable
|
||||
*/
|
||||
public function isWritable(): bool
|
||||
{
|
||||
return F::isWritable($this->root());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the app instance if it exists
|
||||
*/
|
||||
public function kirby(): App|null
|
||||
{
|
||||
return App::instance(null, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a set of validations on the file object
|
||||
* (mainly for images).
|
||||
*
|
||||
* @throws \Kirby\Exception\Exception
|
||||
*/
|
||||
public function match(array $rules): bool
|
||||
{
|
||||
$rules = array_change_key_case($rules);
|
||||
|
||||
if (is_array($rules['mime'] ?? null) === true) {
|
||||
$mime = $this->mime();
|
||||
|
||||
// the MIME type could not be determined, but matching
|
||||
// to it was requested explicitly
|
||||
if ($mime === null) {
|
||||
throw new Exception([
|
||||
'key' => 'file.mime.missing',
|
||||
'data' => ['filename' => $this->filename()]
|
||||
]);
|
||||
}
|
||||
|
||||
// determine if any pattern matches the MIME type;
|
||||
// once any pattern matches, `$carry` is `true` and the rest is skipped
|
||||
$matches = array_reduce(
|
||||
$rules['mime'],
|
||||
fn ($carry, $pattern) => $carry || Mime::matches($mime, $pattern),
|
||||
false
|
||||
);
|
||||
|
||||
if ($matches !== true) {
|
||||
throw new Exception([
|
||||
'key' => 'file.mime.invalid',
|
||||
'data' => compact('mime')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_array($rules['extension'] ?? null) === true) {
|
||||
$extension = $this->extension();
|
||||
if (in_array($extension, $rules['extension']) !== true) {
|
||||
throw new Exception([
|
||||
'key' => 'file.extension.invalid',
|
||||
'data' => compact('extension')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_array($rules['type'] ?? null) === true) {
|
||||
$type = $this->type();
|
||||
if (in_array($type, $rules['type']) !== true) {
|
||||
throw new Exception([
|
||||
'key' => 'file.type.invalid',
|
||||
'data' => compact('type')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (static::$validations as $key => $arguments) {
|
||||
$rule = $rules[$key] ?? null;
|
||||
|
||||
if ($rule !== null) {
|
||||
$property = $arguments[0];
|
||||
$validator = $arguments[1];
|
||||
|
||||
if (V::$validator($this->$property(), $rule) === false) {
|
||||
throw new Exception([
|
||||
'key' => 'file.' . $key,
|
||||
'data' => [$property => $rule]
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the mime type of the file
|
||||
*/
|
||||
public function mime(): string|null
|
||||
{
|
||||
return Mime::type($this->root());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent file model, which uses this instance as proxied file asset
|
||||
*/
|
||||
public function model(): object|null
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file's last modification time
|
||||
*
|
||||
* @param 'date'|'intl'|'strftime'|null $handler Custom date handler or `null`
|
||||
* for the globally configured one
|
||||
*/
|
||||
public function modified(
|
||||
string|IntlDateFormatter|null $format = null,
|
||||
string|null $handler = null
|
||||
): string|int|false {
|
||||
return F::modified($this->root(), $format, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the file to a new location
|
||||
*
|
||||
* @param bool $overwrite Force overwriting any existing files
|
||||
*/
|
||||
public function move(string $newRoot, bool $overwrite = false): static
|
||||
{
|
||||
if (F::move($this->root(), $newRoot, $overwrite) !== true) {
|
||||
throw new Exception('The file: "' . $this->root() . '" could not be moved to: "' . $newRoot . '"');
|
||||
}
|
||||
|
||||
return new static($newRoot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the name of the file
|
||||
* without the extension
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return pathinfo($this->root(), PATHINFO_FILENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file size in a
|
||||
* human-readable format
|
||||
*
|
||||
* @param string|false|null $locale Locale for number formatting,
|
||||
* `null` for the current locale,
|
||||
* `false` to disable number formatting
|
||||
*/
|
||||
public function niceSize(string|false|null $locale = null): string
|
||||
{
|
||||
return F::niceSize($this->root(), $locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the file content and returns it.
|
||||
*/
|
||||
public function read(): string|false
|
||||
{
|
||||
return F::read($this->root());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the file
|
||||
*/
|
||||
public function realpath(): string
|
||||
{
|
||||
return realpath($this->root());
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the name of the file without
|
||||
* touching the extension
|
||||
*
|
||||
* @param bool $overwrite Force overwrite existing files
|
||||
*/
|
||||
public function rename(string $newName, bool $overwrite = false): static
|
||||
{
|
||||
$newRoot = F::rename($this->root(), $newName, $overwrite);
|
||||
|
||||
if ($newRoot === false) {
|
||||
throw new Exception('The file: "' . $this->root() . '" could not be renamed to: "' . $newName . '"');
|
||||
}
|
||||
|
||||
return new static($newRoot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the given file path
|
||||
*/
|
||||
public function root(): string|null
|
||||
{
|
||||
return $this->root ??= $this->model?->root();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute url for the file
|
||||
*/
|
||||
public function url(): string|null
|
||||
{
|
||||
// lazily determine the URL from the model object
|
||||
// only if it's needed to avoid breaking custom file::url
|
||||
// components that rely on `$cmsFile->asset()` methods
|
||||
return $this->url ??= $this->model?->url();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the file contents depending on the file type
|
||||
* by overwriting the file with the sanitized version
|
||||
* @since 3.6.0
|
||||
*
|
||||
* @param string|bool $typeLazy Explicit sane handler type string,
|
||||
* `true` for lazy autodetection or
|
||||
* `false` for normal autodetection
|
||||
*
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation
|
||||
* @throws \Kirby\Exception\LogicException If more than one handler applies
|
||||
* @throws \Kirby\Exception\NotFoundException If the handler was not found
|
||||
* @throws \Kirby\Exception\Exception On other errors
|
||||
*/
|
||||
public function sanitizeContents(string|bool $typeLazy = false): void
|
||||
{
|
||||
Sane::sanitizeFile($this->root(), $typeLazy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sha1 hash of the file
|
||||
* @since 3.6.0
|
||||
*/
|
||||
public function sha1(): string
|
||||
{
|
||||
return sha1_file($this->root());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw size of the file
|
||||
*/
|
||||
public function size(): int
|
||||
{
|
||||
return F::size($this->root());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the media object to a
|
||||
* plain PHP array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'extension' => $this->extension(),
|
||||
'filename' => $this->filename(),
|
||||
'hash' => $this->hash(),
|
||||
'isReadable' => $this->isReadable(),
|
||||
'isResizable' => $this->isResizable(),
|
||||
'isWritable' => $this->isWritable(),
|
||||
'mime' => $this->mime(),
|
||||
'modified' => $this->modified('c'),
|
||||
'name' => $this->name(),
|
||||
'niceSize' => $this->niceSize(),
|
||||
'root' => $this->root(),
|
||||
'safeName' => F::safeName($this->name()),
|
||||
'size' => $this->size(),
|
||||
'type' => $this->type(),
|
||||
'url' => $this->url()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the entire file array into
|
||||
* a json string
|
||||
*/
|
||||
public function toJson(): string
|
||||
{
|
||||
return json_encode($this->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file type.
|
||||
*/
|
||||
public function type(): string|null
|
||||
{
|
||||
return F::type($this->root());
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the file contents depending on the file type
|
||||
*
|
||||
* @param string|bool $typeLazy Explicit sane handler type string,
|
||||
* `true` for lazy autodetection or
|
||||
* `false` for normal autodetection
|
||||
*
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation
|
||||
* @throws \Kirby\Exception\NotFoundException If the handler was not found
|
||||
* @throws \Kirby\Exception\Exception On other errors
|
||||
*/
|
||||
public function validateContents(string|bool $typeLazy = false): void
|
||||
{
|
||||
Sane::validateFile($this->root(), $typeLazy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes content to the file
|
||||
*/
|
||||
public function write(string $content): bool
|
||||
{
|
||||
if (F::write($this->root(), $content) !== true) {
|
||||
throw new Exception('The file "' . $this->root() . '" could not be written');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
271
kirby/src/Filesystem/Filename.php
Normal file
271
kirby/src/Filesystem/Filename.php
Normal file
@@ -0,0 +1,271 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Filesystem;
|
||||
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* The `Filename` class handles complex
|
||||
* mapping of file attributes (i.e for thumbnails)
|
||||
* into human readable filenames.
|
||||
*
|
||||
* ```php
|
||||
* $filename = new Filename('some-file.jpg', '{{ name }}-{{ attributes }}.{{ extension }}', [
|
||||
* 'crop' => 'top left',
|
||||
* 'width' => 300,
|
||||
* 'height' => 200
|
||||
* 'quality' => 80
|
||||
* ]);
|
||||
*
|
||||
* echo $filename->toString();
|
||||
* // result: some-file-300x200-crop-top-left-q80.jpg
|
||||
*
|
||||
* @package Kirby Filesystem
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Filename
|
||||
{
|
||||
/**
|
||||
* List of all applicable attributes
|
||||
*/
|
||||
protected array $attributes;
|
||||
|
||||
/**
|
||||
* The sanitized file extension
|
||||
*/
|
||||
protected string $extension;
|
||||
|
||||
/**
|
||||
* The source original filename
|
||||
*/
|
||||
protected string $filename;
|
||||
|
||||
/**
|
||||
* The sanitized file name
|
||||
*/
|
||||
protected string $name;
|
||||
|
||||
/**
|
||||
* The template for the final name
|
||||
*/
|
||||
protected string $template;
|
||||
|
||||
/**
|
||||
* Creates a new Filename object
|
||||
*/
|
||||
public function __construct(string $filename, string $template, array $attributes = [])
|
||||
{
|
||||
$this->filename = $filename;
|
||||
$this->template = $template;
|
||||
$this->attributes = $attributes;
|
||||
$this->extension = $this->sanitizeExtension(
|
||||
$attributes['format'] ??
|
||||
pathinfo($filename, PATHINFO_EXTENSION)
|
||||
);
|
||||
$this->name = $this->sanitizeName($filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the entire object to a string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts all processed attributes
|
||||
* to an array. The array keys are already
|
||||
* the shortened versions for the filename
|
||||
*/
|
||||
public function attributesToArray(): array
|
||||
{
|
||||
$array = [
|
||||
'dimensions' => implode('x', $this->dimensions()),
|
||||
'crop' => $this->crop(),
|
||||
'blur' => $this->blur(),
|
||||
'bw' => $this->grayscale(),
|
||||
'q' => $this->quality(),
|
||||
'sharpen' => $this->sharpen(),
|
||||
];
|
||||
|
||||
$array = array_filter(
|
||||
$array,
|
||||
fn ($item) => $item !== null && $item !== false && $item !== ''
|
||||
);
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts all processed attributes
|
||||
* to a string, that can be used in the
|
||||
* new filename
|
||||
*
|
||||
* @param string|null $prefix The prefix will be used in the filename creation
|
||||
*/
|
||||
public function attributesToString(string|null $prefix = null): string
|
||||
{
|
||||
$array = $this->attributesToArray();
|
||||
$result = [];
|
||||
|
||||
foreach ($array as $key => $value) {
|
||||
if ($value === true) {
|
||||
$value = '';
|
||||
}
|
||||
|
||||
$result[] = match ($key) {
|
||||
'dimensions' => $value,
|
||||
'crop' => ($value === 'center') ? 'crop' : $key . '-' . $value,
|
||||
default => $key . $value
|
||||
};
|
||||
}
|
||||
|
||||
$result = array_filter($result);
|
||||
$attributes = implode('-', $result);
|
||||
|
||||
if (empty($attributes) === true) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $prefix . $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the blur option value
|
||||
*/
|
||||
public function blur(): int|false
|
||||
{
|
||||
$value = $this->attributes['blur'] ?? false;
|
||||
|
||||
if ($value === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (int)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the crop option value
|
||||
*/
|
||||
public function crop(): string|false
|
||||
{
|
||||
// get the crop value
|
||||
$crop = $this->attributes['crop'] ?? false;
|
||||
|
||||
if ($crop === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Str::slug($crop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a normalized array
|
||||
* with width and height values
|
||||
* if available
|
||||
*/
|
||||
public function dimensions(): array
|
||||
{
|
||||
if (empty($this->attributes['width']) === true && empty($this->attributes['height']) === true) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'width' => $this->attributes['width'] ?? null,
|
||||
'height' => $this->attributes['height'] ?? null
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sanitized extension
|
||||
*/
|
||||
public function extension(): string
|
||||
{
|
||||
return $this->extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the grayscale option value
|
||||
* and also the available ways to write
|
||||
* the option. You can use `grayscale`,
|
||||
* `greyscale` or simply `bw`. The function
|
||||
* will always return `grayscale`
|
||||
*/
|
||||
public function grayscale(): bool
|
||||
{
|
||||
// normalize options
|
||||
$value = $this->attributes['grayscale'] ?? $this->attributes['greyscale'] ?? $this->attributes['bw'] ?? false;
|
||||
|
||||
// turn anything into boolean
|
||||
return filter_var($value, FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the filename without extension
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the quality option value
|
||||
*/
|
||||
public function quality(): int|false
|
||||
{
|
||||
$value = $this->attributes['quality'] ?? false;
|
||||
|
||||
if ($value === false || $value === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (int)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the file extension.
|
||||
* It also replaces `jpeg` with `jpg`.
|
||||
*/
|
||||
protected function sanitizeExtension(string $extension): string
|
||||
{
|
||||
$extension = F::safeExtension('test.' . $extension);
|
||||
$extension = str_replace('jpeg', 'jpg', $extension);
|
||||
return $extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the file name
|
||||
*/
|
||||
protected function sanitizeName(string $name): string
|
||||
{
|
||||
return F::safeBasename($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the sharpen option value
|
||||
*/
|
||||
public function sharpen(): int|false
|
||||
{
|
||||
return match ($this->attributes['sharpen'] ?? false) {
|
||||
false => false,
|
||||
true => 50,
|
||||
default => (int)$this->attributes['sharpen']
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the converted filename as string
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return Str::template($this->template, [
|
||||
'name' => $this->name(),
|
||||
'attributes' => $this->attributesToString('-'),
|
||||
'extension' => $this->extension()
|
||||
], ['fallback' => '']);
|
||||
}
|
||||
}
|
||||
155
kirby/src/Filesystem/IsFile.php
Normal file
155
kirby/src/Filesystem/IsFile.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Filesystem;
|
||||
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Exception\BadMethodCallException;
|
||||
use Kirby\Image\Image;
|
||||
|
||||
/**
|
||||
* Trait for all objects that represent an asset file.
|
||||
* Adds `::asset()` method which returns either a
|
||||
* `Kirby\Filesystem\File` or `Kirby\Image\Image` object.
|
||||
* Proxies method calls to this object.
|
||||
* @since 3.6.0
|
||||
*
|
||||
* @package Kirby Filesystem
|
||||
* @author Nico Hoffmann <nico@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
trait IsFile
|
||||
{
|
||||
/**
|
||||
* File asset object
|
||||
*/
|
||||
protected File|null $asset = null;
|
||||
|
||||
/**
|
||||
* Absolute file path
|
||||
*/
|
||||
protected string|null $root;
|
||||
|
||||
/**
|
||||
* Absolute file URL
|
||||
*/
|
||||
protected string|null $url;
|
||||
|
||||
/**
|
||||
* Constructor sets all file properties
|
||||
*/
|
||||
public function __construct(array $props)
|
||||
{
|
||||
$this->root = $props['root'] ?? null;
|
||||
$this->url = $props['url'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic caller for asset methods
|
||||
*
|
||||
* @throws \Kirby\Exception\BadMethodCallException
|
||||
*/
|
||||
public function __call(string $method, array $arguments = []): mixed
|
||||
{
|
||||
// public property access
|
||||
if (isset($this->$method) === true) {
|
||||
return $this->$method;
|
||||
}
|
||||
|
||||
// asset method proxy
|
||||
if (method_exists($this->asset(), $method)) {
|
||||
return $this->asset()->$method(...$arguments);
|
||||
}
|
||||
|
||||
throw new BadMethodCallException('The method: "' . $method . '" does not exist');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the asset to a string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return (string)$this->asset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file asset object
|
||||
*/
|
||||
public function asset(array|string|null $props = null): File
|
||||
{
|
||||
if ($this->asset !== null) {
|
||||
return $this->asset;
|
||||
}
|
||||
|
||||
$props ??= [];
|
||||
|
||||
if (is_string($props) === true) {
|
||||
$props = ['root' => $props];
|
||||
}
|
||||
|
||||
$props['model'] ??= $this;
|
||||
|
||||
return $this->asset = match ($this->type()) {
|
||||
'image' => new Image($props),
|
||||
default => new File($props)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the file exists on disk
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
// Important to include this in the trait
|
||||
// to avoid infinite loops when trying
|
||||
// to proxy the method from the asset object
|
||||
return file_exists($this->root()) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* To check the existence of the IsFile trait
|
||||
*
|
||||
* @todo Switch to class constant in traits when min PHP version 8.2 required
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
protected function hasIsFileTrait(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the app instance
|
||||
*/
|
||||
public function kirby(): App
|
||||
{
|
||||
return App::instance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the given file path
|
||||
*/
|
||||
public function root(): string|null
|
||||
{
|
||||
return $this->root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file type
|
||||
*/
|
||||
public function type(): string|null
|
||||
{
|
||||
// Important to include this in the trait
|
||||
// to avoid infinite loops when trying
|
||||
// to proxy the method from the asset object
|
||||
return F::type($this->root() ?? $this->url());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute url for the file
|
||||
*/
|
||||
public function url(): string|null
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
}
|
||||
338
kirby/src/Filesystem/Mime.php
Normal file
338
kirby/src/Filesystem/Mime.php
Normal file
@@ -0,0 +1,338 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Filesystem;
|
||||
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Str;
|
||||
use SimpleXMLElement;
|
||||
|
||||
/**
|
||||
* The `Mime` class provides method
|
||||
* for MIME type detection or guessing
|
||||
* from different criteria like
|
||||
* extensions etc.
|
||||
*
|
||||
* @package Kirby Filesystem
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class Mime
|
||||
{
|
||||
/**
|
||||
* Extension to MIME type map
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $types = [
|
||||
'ai' => 'application/postscript',
|
||||
'aif' => 'audio/x-aiff',
|
||||
'aifc' => 'audio/x-aiff',
|
||||
'aiff' => 'audio/x-aiff',
|
||||
'avi' => 'video/x-msvideo',
|
||||
'avif' => 'image/avif',
|
||||
'bmp' => 'image/bmp',
|
||||
'css' => 'text/css',
|
||||
'csv' => ['text/csv', 'text/x-comma-separated-values', 'text/comma-separated-values', 'application/octet-stream'],
|
||||
'doc' => 'application/msword',
|
||||
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
|
||||
'dvi' => 'application/x-dvi',
|
||||
'eml' => 'message/rfc822',
|
||||
'eps' => 'application/postscript',
|
||||
'exe' => ['application/octet-stream', 'application/x-msdownload'],
|
||||
'gif' => 'image/gif',
|
||||
'gtar' => 'application/x-gtar',
|
||||
'gz' => 'application/x-gzip',
|
||||
'htm' => 'text/html',
|
||||
'html' => 'text/html',
|
||||
'ico' => 'image/x-icon',
|
||||
'ics' => 'text/calendar',
|
||||
'js' => ['application/javascript', 'application/x-javascript'],
|
||||
'json' => ['application/json', 'text/json'],
|
||||
'j2k' => ['image/jp2'],
|
||||
'jp2' => ['image/jp2'],
|
||||
'jpg' => ['image/jpeg', 'image/pjpeg'],
|
||||
'jpeg' => ['image/jpeg', 'image/pjpeg'],
|
||||
'jpe' => ['image/jpeg', 'image/pjpeg'],
|
||||
'log' => ['text/plain', 'text/x-log'],
|
||||
'm4a' => 'audio/mp4',
|
||||
'm4v' => 'video/mp4',
|
||||
'mid' => 'audio/midi',
|
||||
'midi' => 'audio/midi',
|
||||
'mif' => 'application/vnd.mif',
|
||||
'mjs' => 'text/javascript',
|
||||
'mov' => 'video/quicktime',
|
||||
'movie' => 'video/x-sgi-movie',
|
||||
'mp2' => 'audio/mpeg',
|
||||
'mp3' => ['audio/mpeg', 'audio/mpg', 'audio/mpeg3', 'audio/mp3'],
|
||||
'mp4' => 'video/mp4',
|
||||
'mpe' => 'video/mpeg',
|
||||
'mpeg' => 'video/mpeg',
|
||||
'mpg' => 'video/mpeg',
|
||||
'mpga' => 'audio/mpeg',
|
||||
'odc' => 'application/vnd.oasis.opendocument.chart',
|
||||
'odp' => 'application/vnd.oasis.opendocument.presentation',
|
||||
'odt' => 'application/vnd.oasis.opendocument.text',
|
||||
'pdf' => ['application/pdf', 'application/x-download'],
|
||||
'php' => ['text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'],
|
||||
'php3' => ['text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'],
|
||||
'phps' => ['text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'],
|
||||
'pht' => ['text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'],
|
||||
'phtml' => ['text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'],
|
||||
'png' => 'image/png',
|
||||
'ppt' => ['application/powerpoint', 'application/vnd.ms-powerpoint'],
|
||||
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
|
||||
'ps' => 'application/postscript',
|
||||
'psd' => 'application/x-photoshop',
|
||||
'qt' => 'video/quicktime',
|
||||
'rss' => 'application/rss+xml',
|
||||
'rtf' => 'text/rtf',
|
||||
'rtx' => 'text/richtext',
|
||||
'shtml' => 'text/html',
|
||||
'svg' => 'image/svg+xml',
|
||||
'swf' => 'application/x-shockwave-flash',
|
||||
'tar' => 'application/x-tar',
|
||||
'text' => 'text/plain',
|
||||
'txt' => 'text/plain',
|
||||
'tgz' => ['application/x-tar', 'application/x-gzip-compressed'],
|
||||
'tif' => 'image/tiff',
|
||||
'tiff' => 'image/tiff',
|
||||
'wav' => 'audio/x-wav',
|
||||
'wbxml' => 'application/wbxml',
|
||||
'webm' => 'video/webm',
|
||||
'webp' => 'image/webp',
|
||||
'word' => ['application/msword', 'application/octet-stream'],
|
||||
'xhtml' => 'application/xhtml+xml',
|
||||
'xht' => 'application/xhtml+xml',
|
||||
'xml' => 'text/xml',
|
||||
'xl' => 'application/excel',
|
||||
'xls' => ['application/excel', 'application/vnd.ms-excel', 'application/msexcel'],
|
||||
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
|
||||
'xsl' => 'text/xml',
|
||||
'yaml' => ['application/yaml', 'text/yaml'],
|
||||
'yml' => ['application/yaml', 'text/yaml'],
|
||||
'zip' => ['application/x-zip', 'application/zip', 'application/x-zip-compressed'],
|
||||
];
|
||||
|
||||
/**
|
||||
* Fixes an invalid MIME type guess for the given file
|
||||
*/
|
||||
public static function fix(
|
||||
string $file,
|
||||
string|null $mime = null,
|
||||
string|null $extension = null
|
||||
): string|null {
|
||||
// fixing map
|
||||
$map = [
|
||||
'text/html' => [
|
||||
'svg' => [Mime::class, 'fromSvg'],
|
||||
],
|
||||
'text/plain' => [
|
||||
'css' => 'text/css',
|
||||
'json' => 'application/json',
|
||||
'mjs' => 'text/javascript',
|
||||
'svg' => [Mime::class, 'fromSvg'],
|
||||
],
|
||||
'text/x-asm' => [
|
||||
'css' => 'text/css'
|
||||
],
|
||||
'text/x-java' => [
|
||||
'mjs' => 'text/javascript',
|
||||
],
|
||||
'image/svg' => [
|
||||
'svg' => 'image/svg+xml'
|
||||
],
|
||||
'application/octet-stream' => [
|
||||
'mjs' => 'text/javascript'
|
||||
]
|
||||
];
|
||||
|
||||
if ($mode = ($map[$mime][$extension] ?? null)) {
|
||||
if (is_callable($mode) === true) {
|
||||
return $mode($file, $mime, $extension);
|
||||
}
|
||||
|
||||
if (is_string($mode) === true) {
|
||||
return $mode;
|
||||
}
|
||||
}
|
||||
|
||||
return $mime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Guesses a MIME type by extension
|
||||
*/
|
||||
public static function fromExtension(string $extension): string|null
|
||||
{
|
||||
$mime = static::$types[$extension] ?? null;
|
||||
return is_array($mime) === true ? array_shift($mime) : $mime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the MIME type of a file
|
||||
*/
|
||||
public static function fromFileInfo(string $file): string|false
|
||||
{
|
||||
if (function_exists('finfo_file') === true && file_exists($file) === true) {
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mime = finfo_file($finfo, $file);
|
||||
finfo_close($finfo);
|
||||
return $mime;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the MIME type of a file
|
||||
*/
|
||||
public static function fromMimeContentType(string $file): string|false
|
||||
{
|
||||
if (
|
||||
function_exists('mime_content_type') === true &&
|
||||
file_exists($file) === true
|
||||
) {
|
||||
return mime_content_type($file);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to detect a valid SVG and returns the MIME type accordingly
|
||||
*/
|
||||
public static function fromSvg(string $file): string|false
|
||||
{
|
||||
if (file_exists($file) === true) {
|
||||
libxml_use_internal_errors(true);
|
||||
|
||||
$svg = new SimpleXMLElement(file_get_contents($file));
|
||||
|
||||
if ($svg !== false && $svg->getName() === 'svg') {
|
||||
return 'image/svg+xml';
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if a given MIME type is matched by an `Accept` header
|
||||
* pattern; returns true if the MIME type is contained at all
|
||||
*/
|
||||
public static function isAccepted(string $mime, string $pattern): bool
|
||||
{
|
||||
$accepted = Str::accepted($pattern);
|
||||
|
||||
foreach ($accepted as $m) {
|
||||
if (static::matches($mime, $m['value']) === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if a MIME wildcard pattern from an `Accept` header
|
||||
* matches a given type
|
||||
* @since 3.3.0
|
||||
*/
|
||||
public static function matches(string $test, string $wildcard): bool
|
||||
{
|
||||
return fnmatch($wildcard, $test, FNM_PATHNAME) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the extension for a given MIME type
|
||||
*/
|
||||
public static function toExtension(string $mime = null): string|false
|
||||
{
|
||||
foreach (static::$types as $key => $value) {
|
||||
if (is_array($value) === true && in_array($mime, $value) === true) {
|
||||
return $key;
|
||||
}
|
||||
|
||||
if ($value === $mime) {
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all available extensions for a given MIME type
|
||||
*/
|
||||
public static function toExtensions(string $mime = null, bool $matchWildcards = false): array
|
||||
{
|
||||
$extensions = [];
|
||||
$testMime = fn (string $v) => static::matches($v, $mime);
|
||||
|
||||
foreach (static::$types as $key => $value) {
|
||||
if (is_array($value) === true) {
|
||||
if ($matchWildcards === true) {
|
||||
if (A::some($value, $testMime)) {
|
||||
$extensions[] = $key;
|
||||
}
|
||||
} else {
|
||||
if (in_array($mime, $value) === true) {
|
||||
$extensions[] = $key;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($matchWildcards === true) {
|
||||
if ($testMime($value) === true) {
|
||||
$extensions[] = $key;
|
||||
}
|
||||
} else {
|
||||
if ($value === $mime) {
|
||||
$extensions[] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the MIME type of a file
|
||||
*/
|
||||
public static function type(
|
||||
string $file,
|
||||
string|null $extension = null
|
||||
): string|null {
|
||||
// use the standard finfo extension
|
||||
$mime = static::fromFileInfo($file);
|
||||
|
||||
// use the mime_content_type function
|
||||
if ($mime === false) {
|
||||
$mime = static::fromMimeContentType($file);
|
||||
}
|
||||
|
||||
// get the extension or extract it from the filename
|
||||
$extension ??= F::extension($file);
|
||||
|
||||
// try to guess the mime type at least
|
||||
if ($mime === false) {
|
||||
$mime = static::fromExtension($extension);
|
||||
}
|
||||
|
||||
// fix broken mime detection
|
||||
return static::fix($file, $mime, $extension);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all detectable MIME types
|
||||
*/
|
||||
public static function types(): array
|
||||
{
|
||||
return static::$types;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user