342 lines
9.9 KiB
PHP
342 lines
9.9 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Part of the Joomla Framework Filesystem Package
|
|
*
|
|
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
|
|
* @license GNU General Public License version 2 or later; see LICENSE
|
|
*/
|
|
|
|
namespace Joomla\Filesystem;
|
|
|
|
use Joomla\Filesystem\Exception\FilesystemException;
|
|
|
|
/**
|
|
* A File handling class
|
|
*
|
|
* @since 1.0
|
|
*/
|
|
class File
|
|
{
|
|
/**
|
|
* Gets the extension of a file name
|
|
*
|
|
* @param string $file The file name
|
|
*
|
|
* @return string The file extension
|
|
*
|
|
* @since 3.0.0
|
|
*/
|
|
public static function getExt($file)
|
|
{
|
|
// String manipulation should be faster than pathinfo() on newer PHP versions.
|
|
$dot = strrpos($file, '.');
|
|
|
|
if ($dot === false) {
|
|
return '';
|
|
}
|
|
|
|
$ext = substr($file, $dot + 1);
|
|
|
|
// Extension cannot contain slashes.
|
|
if (strpos($ext, '/') !== false || (DIRECTORY_SEPARATOR === '\\' && strpos($ext, '\\') !== false)) {
|
|
return '';
|
|
}
|
|
|
|
return $ext;
|
|
}
|
|
|
|
/**
|
|
* Strips the last extension off of a file name
|
|
*
|
|
* @param string $file The file name
|
|
*
|
|
* @return string The file name without the extension
|
|
*
|
|
* @since 1.0
|
|
*/
|
|
public static function stripExt($file)
|
|
{
|
|
return preg_replace('#\.[^.]*$#', '', $file);
|
|
}
|
|
|
|
/**
|
|
* Makes the file name safe to use
|
|
*
|
|
* @param string $file The name of the file [not full path]
|
|
* @param array $stripChars Array of regex (by default will remove any leading periods)
|
|
*
|
|
* @return string The sanitised string
|
|
*
|
|
* @since 1.0
|
|
*/
|
|
public static function makeSafe($file, array $stripChars = ['#^\.#'])
|
|
{
|
|
// Try transliterating the file name using the native php function
|
|
if (function_exists('transliterator_transliterate') && function_exists('iconv')) {
|
|
// Using iconv to ignore characters that can't be transliterated
|
|
$file = iconv("UTF-8", "ASCII//TRANSLIT//IGNORE", transliterator_transliterate('Any-Latin; Latin-ASCII', $file));
|
|
}
|
|
|
|
$regex = array_merge(['#(\.){2,}#', '#[^A-Za-z0-9\.\_\- ]#'], $stripChars);
|
|
$file = preg_replace($regex, '', $file);
|
|
|
|
// Remove any trailing dots, as those aren't ever valid file names.
|
|
$file = rtrim($file, '.');
|
|
|
|
return trim($file);
|
|
}
|
|
|
|
/**
|
|
* Copies a file
|
|
*
|
|
* @param string $src The path to the source file
|
|
* @param string $dest The path to the destination file
|
|
* @param string $path An optional base path to prefix to the file names
|
|
* @param boolean $useStreams True to use streams
|
|
*
|
|
* @return boolean True on success
|
|
*
|
|
* @since 1.0
|
|
* @throws FilesystemException
|
|
* @throws \UnexpectedValueException
|
|
*/
|
|
public static function copy($src, $dest, $path = null, $useStreams = false)
|
|
{
|
|
// Prepend a base path if it exists
|
|
if ($path) {
|
|
$src = Path::clean($path . '/' . $src);
|
|
$dest = Path::clean($path . '/' . $dest);
|
|
}
|
|
|
|
// Check src path
|
|
if (!is_readable($src)) {
|
|
throw new \UnexpectedValueException(
|
|
sprintf(
|
|
"%s: Cannot find or read file: %s",
|
|
__METHOD__,
|
|
Path::removeRoot($src)
|
|
)
|
|
);
|
|
}
|
|
|
|
if ($useStreams) {
|
|
$stream = Stream::getStream();
|
|
|
|
if (!$stream->copy($src, $dest, null, false)) {
|
|
throw new FilesystemException(sprintf('%1$s(%2$s, %3$s): %4$s', __METHOD__, $src, $dest, $stream->getError()));
|
|
}
|
|
|
|
self::invalidateFileCache($dest);
|
|
|
|
return true;
|
|
}
|
|
|
|
if (!@ copy($src, $dest)) {
|
|
throw new FilesystemException(__METHOD__ . ': Copy failed.');
|
|
}
|
|
|
|
self::invalidateFileCache($dest);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Delete a file or array of files
|
|
*
|
|
* @param mixed $file The file name or an array of file names
|
|
*
|
|
* @return boolean True on success
|
|
*
|
|
* @since 1.0
|
|
* @throws FilesystemException
|
|
*/
|
|
public static function delete($file)
|
|
{
|
|
$files = (array) $file;
|
|
|
|
foreach ($files as $file) {
|
|
$file = Path::clean($file);
|
|
$filename = basename($file);
|
|
|
|
// Try making the file writable first. If it's read-only, it can't be deleted
|
|
// on Windows, even if the parent folder is writable
|
|
@chmod($file, 0777);
|
|
|
|
// In case of restricted permissions we zap it one way or the other
|
|
// as long as the owner is either the webserver or the ftp
|
|
if (!@ unlink($file)) {
|
|
throw new FilesystemException(__METHOD__ . ': Failed deleting ' . $filename);
|
|
}
|
|
|
|
self::invalidateFileCache($file);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Moves a file
|
|
*
|
|
* @param string $src The path to the source file
|
|
* @param string $dest The path to the destination file
|
|
* @param string $path An optional base path to prefix to the file names
|
|
* @param boolean $useStreams True to use streams
|
|
*
|
|
* @return boolean True on success
|
|
*
|
|
* @since 1.0
|
|
* @throws FilesystemException
|
|
*/
|
|
public static function move($src, $dest, $path = '', $useStreams = false)
|
|
{
|
|
if ($path) {
|
|
$src = Path::clean($path . '/' . $src);
|
|
$dest = Path::clean($path . '/' . $dest);
|
|
}
|
|
|
|
// Check src path
|
|
if (!is_readable($src)) {
|
|
return 'Cannot find source file.';
|
|
}
|
|
|
|
if ($useStreams) {
|
|
$stream = Stream::getStream();
|
|
|
|
if (!$stream->move($src, $dest, null, false)) {
|
|
throw new FilesystemException(__METHOD__ . ': ' . $stream->getError());
|
|
}
|
|
|
|
self::invalidateFileCache($dest);
|
|
|
|
return true;
|
|
}
|
|
|
|
if (!@ rename($src, $dest)) {
|
|
throw new FilesystemException(__METHOD__ . ': Rename failed.');
|
|
}
|
|
|
|
self::invalidateFileCache($dest);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Write contents to a file
|
|
*
|
|
* @param string $file The full file path
|
|
* @param string $buffer The buffer to write
|
|
* @param boolean $useStreams Use streams
|
|
* @param boolean $appendToFile Append to the file and not overwrite it.
|
|
*
|
|
* @return boolean True on success
|
|
*
|
|
* @since 1.0
|
|
*/
|
|
public static function write($file, $buffer, $useStreams = false, $appendToFile = false)
|
|
{
|
|
if (\function_exists('set_time_limit')) {
|
|
set_time_limit(ini_get('max_execution_time'));
|
|
}
|
|
|
|
// If the destination directory doesn't exist we need to create it
|
|
if (!file_exists(\dirname($file))) {
|
|
Folder::create(\dirname($file));
|
|
}
|
|
|
|
if ($useStreams) {
|
|
$stream = Stream::getStream();
|
|
|
|
// Beef up the chunk size to a meg
|
|
$stream->set('chunksize', (1024 * 1024));
|
|
$stream->writeFile($file, $buffer, $appendToFile);
|
|
|
|
self::invalidateFileCache($file);
|
|
|
|
return true;
|
|
}
|
|
|
|
$file = Path::clean($file);
|
|
|
|
// Set the required flag to only append to the file and not overwrite it
|
|
if ($appendToFile === true) {
|
|
$res = \is_int(file_put_contents($file, $buffer, \FILE_APPEND));
|
|
} else {
|
|
$res = \is_int(file_put_contents($file, $buffer));
|
|
}
|
|
|
|
self::invalidateFileCache($file);
|
|
|
|
return $res;
|
|
}
|
|
|
|
/**
|
|
* Moves an uploaded file to a destination folder
|
|
*
|
|
* @param string $src The name of the php (temporary) uploaded file
|
|
* @param string $dest The path (including filename) to move the uploaded file to
|
|
* @param boolean $useStreams True to use streams
|
|
*
|
|
* @return boolean True on success
|
|
*
|
|
* @since 1.0
|
|
* @throws FilesystemException
|
|
*/
|
|
public static function upload($src, $dest, $useStreams = false)
|
|
{
|
|
// Ensure that the path is valid and clean
|
|
$dest = Path::clean($dest);
|
|
|
|
// Create the destination directory if it does not exist
|
|
$baseDir = \dirname($dest);
|
|
|
|
if (!is_dir($baseDir)) {
|
|
Folder::create($baseDir);
|
|
}
|
|
|
|
if ($useStreams) {
|
|
$stream = Stream::getStream();
|
|
|
|
if (!$stream->upload($src, $dest, null, false)) {
|
|
throw new FilesystemException(sprintf('%1$s(%2$s, %3$s): %4$s', __METHOD__, $src, $dest, $stream->getError()));
|
|
}
|
|
|
|
self::invalidateFileCache($dest);
|
|
|
|
return true;
|
|
}
|
|
|
|
if (is_writable($baseDir) && move_uploaded_file($src, $dest)) {
|
|
// Short circuit to prevent file permission errors
|
|
if (Path::setPermissions($dest)) {
|
|
self::invalidateFileCache($dest);
|
|
|
|
return true;
|
|
}
|
|
|
|
throw new FilesystemException(__METHOD__ . ': Failed to change file permissions.');
|
|
}
|
|
|
|
throw new FilesystemException(__METHOD__ . ': Failed to move file.');
|
|
}
|
|
|
|
/**
|
|
* Invalidate any opcache for a newly written file immediately, if opcache* functions exist and if this was a PHP file.
|
|
*
|
|
* @param string $file The path to the file just written to, to flush from opcache
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function invalidateFileCache($file)
|
|
{
|
|
if (function_exists('opcache_invalidate')) {
|
|
$info = pathinfo($file);
|
|
|
|
if (isset($info['extension']) && $info['extension'] === 'php') {
|
|
// Force invalidation to be absolutely sure the opcache is cleared for this file.
|
|
opcache_invalidate($file, true);
|
|
}
|
|
}
|
|
}
|
|
}
|