<?php
/**
*
* @package Duplicator
* @copyright (c) 2022, Snap Creek LLC
*/
namespace Duplicator\Libs\Snap;
use Error;
use Exception;
class SnapIO
{
// Real upper bound of a signed int is 214748364.
// The value chosen below, makes sure we have a buffer of ~4.7 million.
const FILE_SIZE_LIMIT_32BIT = 1900000000;
const FWRITE_CHUNK_SIZE = 4096; // bytes
/**
* Return include file in a string
*
* @param string $path inclue path
* @param array<string, mixed> $args array key/val where key is the var name in include
* @param bool $required if true is required
*
* @return string
*
* @throws Exception // thorw exception if is $required and file can't be read
*/
public static function getInclude($path, $args = array(), $required = true)
{
if (!is_readable($path)) {
if ($required) {
throw new Exception('Can\'t read required file ' . $path);
} else {
return '';
}
}
foreach ($args as $var => $value) {
${$var} = $value;
}
ob_start();
if ($required) {
require($path);
} else {
include($path);
}
return ob_get_clean();
}
/**
* Copy file
*
* @param string $source source path
* @param string $dest detination path
* @param boolean $overwriteIfExists if true and file exists the file is overwritten
*
* @return boolean Returns true on success or false on failure.
*/
public static function copy($source, $dest, $overwriteIfExists = true)
{
if (file_exists($dest)) {
if ($overwriteIfExists) {
self::rm($dest);
} else {
return false;
}
}
return copy($source, $dest);
}
/**
* Copy part of file, if offset is 0 anf to file exists is truncated
*
* @param string|resource $from file name or resource
* @param string|resource $to file name or resource
* @param int<0, max> $offset copy offset
* @param int<-1, max> $length copy if -1 copy ot the end of file
*
* @return bool true on success or false on fail.
*/
public static function copyFilePart($from, $to, $offset = 0, $length = -1)
{
$closeFrom = false;
$closeTo = false;
$fromStream = null;
$toStream = null;
if (is_resource($from)) {
$fromStream = $from;
} else {
if (!is_file((string) $from)) {
return false;
}
if (($fromStream = self::fopen($from, 'r')) === false) {
return false;
}
$closeFrom = true;
}
if (is_resource($to)) {
$toStream = $to;
} else {
$mode = ($offset == 0 ? 'w+' : 'c+');
if (($toStream = SnapIO::fopen($to, $mode)) === false) {
return false;
}
$closeTo = true;
}
if ($offset === 0) {
if (ftruncate($toStream, 0) === false) {
return false;
}
}
if (fseek($toStream, $offset) === -1) {
return false;
}
if ($closeFrom && is_resource($fromStream)) {
fclose($fromStream);
}
if ($closeTo && is_resource($toStream)) {
fclose($toStream);
}
return (stream_copy_to_stream($fromStream, $toStream, ($length < 0 ? null : $length), $offset) !== false);
}
/**
* Copy recursive folder content
*
* @param string $source source path
* @param string $dest detination path
*
* @return boolean Returns true on success or false on failure.
*/
public static function rcopy($source, $dest)
{
if (!is_readable($source)) {
return false;
}
if (is_dir($source)) {
if (!file_exists($dest)) {
if (!self::mkdir($dest)) {
return false;
}
}
if (($handle = opendir($source)) == false) {
return false;
}
while ($file = readdir($handle)) {
if ($file == "." || $file == "..") {
continue;
}
if (!self::rcopy($source . '/' . $file, $dest . '/' . $file)) {
closedir($handle);
return false;
}
}
closedir($handle);
return true;
} else {
return copy($source, $dest);
}
}
/**
* Untrailingslashit path
*
* @param string $path file path
*
* @return string
*/
public static function untrailingslashit($path)
{
return rtrim($path, '/\\');
}
/**
* Trailingslashit path
*
* @param string $path file path
*
* @return string
*/
public static function trailingslashit($path)
{
return self::untrailingslashit($path) . '/';
}
/**
* Normalize path
*
* @param string $path file path
* @param boolean $real if true apply realpath function
*
* @return string
*/
public static function safePath($path, $real = false)
{
if ($real) {
if (($res = realpath($path)) === false) {
$res = $path;
}
} else {
$res = $path;
}
return self::normalizePath($res);
}
/**
* Untrailingslashit and normalize path
*
* @param string $path file path
* @param boolean $real if true apply realpath function
*
* @return string
*/
public static function safePathUntrailingslashit($path, $real = false)
{
if ($real) {
if (($res = realpath($path)) === false) {
$res = $path;
}
} else {
$res = $path;
}
return rtrim(self::normalizePath($res), '/');
}
/**
* Trailingslashit and normalize path
*
* @param string $path file path
* @param boolean $real if true apply realpath function
*
* @return string
*/
public static function safePathTrailingslashit($path, $real = false)
{
return self::safePathUntrailingslashit($path, $real) . '/';
}
/**
* Remove file path
*
* @param string $file path
*
* @return bool Returns TRUE on success or FALSE on failure.
*/
public static function unlink($file)
{
try {
if (!file_exists($file)) {
return true;
}
if (!function_exists('unlink') || is_dir($file)) {
return false;
}
self::chmod($file, 'u+rw');
return @unlink($file);
} catch (Exception $e) {
return false;
} catch (Error $e) {
return false;
}
}
/**
* Rename file from old name to new name
*
* @param string $oldname path
* @param string $newname path
* @param bool $removeIfExists if true remove exists file
*
* @return bool Returns TRUE on success or FALSE on failure.
*/
public static function rename($oldname, $newname, $removeIfExists = false)
{
try {
if (!file_exists($oldname) || !function_exists('rename')) {
return false;
}
if ($removeIfExists && file_exists($newname)) {
if (!self::rrmdir($newname)) {
return false;
}
}
return @rename($oldname, $newname);
} catch (Exception $e) {
return false;
} catch (Error $e) {
return false;
}
}
/**
* Open file
*
* @param string $filepath File path
* @param string $mode The mode parameter specifies the type of access you require to the stream.
* @param boolean $throwOnError thorw exception on error
*
* @return boolean|resource Returns a file pointer resource on success, or false on failure
*/
public static function fopen($filepath, $mode, $throwOnError = true)
{
if (strlen($filepath) > PHP_MAXPATHLEN) {
throw new Exception('Skipping a file that exceeds allowed max path length [' . PHP_MAXPATHLEN . ']. File: ' . $filepath);
}
if (SnapString::startsWith($mode, 'w') || SnapString::startsWith($mode, 'c') || file_exists($filepath)) {
$file_handle = @fopen($filepath, $mode);
} else {
if ($throwOnError) {
throw new Exception("$filepath doesn't exist");
} else {
return false;
}
}
if (!is_resource($file_handle)) {
if ($throwOnError) {
throw new Exception("Error opening $filepath");
} else {
return false;
}
} else {
return $file_handle;
}
}
/**
* Touch file
*
* @param string $filepath File path
* @param int $time The touch time. If time is not supplied, the current system time is used.
*
* @return bool Returns true on success or false on failure.
*/
public static function touch($filepath, $time = null)
{
if (!function_exists('touch')) {
return false;
}
if ($time === null) {
$time = time();
}
return @touch($filepath, $time);
}
/**
* Remove folder
*
* @param string $dirname dir path
* @param boolean $mustExist if true and folder don't esist thorw error
*
* @return void
*/
public static function rmdir($dirname, $mustExist = false)
{
if (file_exists($dirname)) {
self::chmod($dirname, 'u+rwx');
if (self::rrmdir($dirname) === false) {
throw new Exception("Couldn't remove {$dirname}");
}
} elseif ($mustExist) {
throw new Exception("{$dirname} doesn't exist");
}
}
/**
* Remove file
*
* @param string $filepath file path
* @param boolean $mustExist if true and folder don't esist thorw error
*
* @return void
*/
public static function rm($filepath, $mustExist = false)
{
if (file_exists($filepath)) {
self::chmod($filepath, 'u+rw');
if (@unlink($filepath) === false) {
throw new Exception("Couldn't remove {$filepath}");
}
} elseif ($mustExist) {
throw new Exception("{$filepath} doesn't exist");
}
}
/**
* string string in file
*
* @param resource $handle file handle
* @param string $string fwrite string
*
* @return int bytes written
*/
public static function fwrite($handle, $string)
{
$bytes_written = @fwrite($handle, $string);
if ($bytes_written != strlen($string)) {
throw new Exception('Error writing all bytes to file.');
} else {
return $bytes_written;
}
}
/**
* Wrinte file in chunk mode. For big data.
*
* @param resource $handle file handle
* @param string $content fwrite string
*
* @return int bytes written
*
* @throws Exception
*/
public static function fwriteChunked($handle, $content)
{
if (strlen($content) == 0) {
return 0;
}
$pieces = str_split($content, self::FWRITE_CHUNK_SIZE);
$written = 0;
foreach ($pieces as $piece) {
if (($fwResult = @fwrite($handle, $piece, self::FWRITE_CHUNK_SIZE)) === false) {
throw new Exception('Error writing to file.');
}
$written += $fwResult;
}
if ($written != strlen($content)) {
throw new Exception('Error writing all bytes to file.');
}
return $written;
}
/**
* Append file $from to file $to, if $to file don't exits create it.
* In case of error throw exceptions.
*
* @param string $from file path
* @param string $to file path
*
* @return int writte bytes
*/
public static function appendFileToFile($from, $to)
{
try {
$written = 0;
$fromHd = false;
$toHd = false;
if (!file_exists($from) || !is_readable($from)) {
throw new Exception('File: ' . $from . ' don\'t exists os isn\'t readable');
}
if (file_exists($to) && !is_writable($to)) {
throw new Exception('File: ' . $to . ' isn\'t writeable');
}
if (($fromHd = @fopen($from, "rb")) === false) {
throw new Exception('Could not open file: ' . $from);
}
if (($toHd = @fopen($to, "ab")) === false) {
throw new Exception('Could not open file: ' . $to);
}
if (($fromStat = fstat($fromHd)) == false) {
throw new Exception('Can\t stat file: ' . $from);
}
while ($buffer = fread($fromHd, self::FWRITE_CHUNK_SIZE)) {
if (($fwResult = @fwrite($toHd, $buffer)) === false) {
throw new Exception('Error writing to file ' . $to);
}
$written += $fwResult;
}
if ($written != $fromStat['size']) {
throw new Exception('Error on file append, written bytes ' . $written . ' expected ' . $fromStat['size']);
}
} catch (Exception $e) {
if ($fromHd !== false) {
fclose($fromHd);
}
if ($toHd !== false) {
fclose($toHd);
}
throw $e;
}
fclose($fromHd);
fclose($toHd);
return $written;
}
/**
* File get
*
* @param resource $handle file handle
* @param int $length max num bytes
*
* @return string
*/
public static function fgets($handle, $length)
{
$line = fgets($handle, $length);
if ($line === false) {
throw new Exception('Error reading line.');
}
return $line;
}
/**
* File close
*
* @param resource $handle file handle
* @param boolean $exception_on_fail if true thorw exception on fail
*
* @return void
*/
public static function fclose($handle, $exception_on_fail = true)
{
if ((@fclose($handle) === false) && $exception_on_fail) {
throw new Exception("Error closing file");
}
}
/**
* Exec a flock, thow exception on failure
*
* @param resource $handle file handle
* @param int $operation flock openration
*
* @return void
*/
public static function flock($handle, $operation)
{
if (@flock($handle, $operation) === false) {
throw new Exception("Error locking file");
}
}
/**
* Returns the current position of the file read/write pointer
* throw exception on failure
*
* @param resource $file_handle file handle
*
* @return int
*/
public static function ftell($file_handle)
{
$position = @ftell($file_handle);
if ($position === false) {
throw new Exception("Couldn't retrieve file offset.");
} else {
return $position;
}
}
/**
* Safely remove a directory and recursively files and directory upto multiple sublevels
*
* @param string $path The full path to the directory to remove
*
* @return bool Returns true if all content was removed
*/
public static function rrmdir($path)
{
if (is_dir($path)) {
if (($dh = opendir($path)) === false) {
return false;
}
while (($object = readdir($dh)) !== false) {
if ($object == "." || $object == "..") {
continue;
}
if (!self::rrmdir($path . "/" . $object)) {
closedir($dh);
return false;
}
}
closedir($dh);
return @rmdir($path);
} else {
if (is_writable($path)) {
return @unlink($path);
} else {
return false;
}
}
}
/**
* Return files size, throw eception on failure
*
* @param string $filename file path
*
* @return int
*/
public static function filesize($filename)
{
$file_size = @filesize($filename);
if ($file_size === false) {
throw new Exception("Error retrieving file size of $filename");
}
return $file_size;
}
/**
* Fseek on file, throw exception on failure
*
* @param resource $handle file handle
* @param int $offset The offset.
* @param int $whence whence values are: SEEK_SET
* - Set position equal to offset bytes. SEEK_CUR
* - Set position to current location plus offset. SEEK_END
* - Set position to end-of-file plus offset.
*
* @return void
*/
public static function fseek($handle, $offset, $whence = SEEK_SET)
{
$ret_val = @fseek($handle, $offset, $whence);
if ($ret_val !== 0) {
$filepath = stream_get_meta_data($handle);
$filepath = $filepath["uri"];
$filesize = self::filesize($filepath);
if (
abs($offset) > self::FILE_SIZE_LIMIT_32BIT ||
$filesize > self::FILE_SIZE_LIMIT_32BIT ||
($offset <= 0 && ($whence == SEEK_SET || $whence == SEEK_END))
) {
//This check is not strict, but in most cases 32 Bit PHP will be the issue
throw new Snap32BitSizeLimitException("Trying to seek on a file beyond the capability of 32 bit PHP. offset=$offset filesize=$filesize");
} else {
throw new Exception("Error seeking to file offset $offset. Retval = $ret_val");
}
}
}
/**
* Gets file modification time
*
* @param string $filename file path
*
* @return int|false the time the file was last modified, or false on failure.
* The time is returned as a Unix timestamp, which is suitable for the date function
*/
public static function filemtime($filename)
{
$mtime = filemtime($filename);
if ($mtime === false) {
throw new Exception("Cannot retrieve last modified time of $filename");
}
return $mtime;
}
/**
* File put content, thorw exception on failure
*
* @param string $filename file path
* @param mixed $data The data to write. Can be either a string, an array or a stream resource.
* @return bool
*/
public static function filePutContents($filename, $data)
{
if (($dirFile = realpath(dirname($filename))) === false) {
throw new Exception('FILE ERROR: put_content for file ' . $filename . ' failed [realpath fail]');
}
if (!is_dir($dirFile)) {
throw new Exception('FILE ERROR: put_content for file ' . $filename . ' failed [dir ' . $dirFile . ' doesn\'t exist]');
}
if (!is_writable($dirFile)) {
throw new Exception('FILE ERROR: put_content for file ' . $filename . ' failed [dir ' . $dirFile . ' exists but isn\'t writable]');
}
$realFileName = $dirFile . basename($filename);
if (file_exists($realFileName) && !is_writable($realFileName)) {
throw new Exception('FILE ERROR: put_content for file ' . $filename . ' failed [file exist ' . $realFileName . ' but isn\'t writable');
}
if (file_put_contents($filename, $data) === false) {
throw new Exception('FILE ERROR: put_content for file ' . $filename . ' failed [Couldn\'t write data to ' . $realFileName . ']');
}
return true;
}
/**
* this function make a chmod only if the are different from perms input and if chmod function is enabled
*
* this function handles the variable MODE in a way similar to the chmod of lunux
* So the MODE variable can be
* 1) an octal number (0755)
* 2) a string that defines an octal number ("644")
* 3) a string with the following format [ugoa]*([-+=]([rwx]*)+
*
* examples
* u+rw add read and write at the user
* u+rw,uo-wx add read and write ad the user and remove wx at groupd and other
* a=rw is equal at 666
* u=rwx,go-rwx is equal at 700
*
* @param string $file file path
* @param int|string $mode permission mode
*
* @return boolean
*/
public static function chmod($file, $mode)
{
if (!file_exists($file)) {
return false;
}
$octalMode = 0;
if (is_int($mode)) {
$octalMode = $mode;
} elseif (is_numeric($mode)) {
$octalMode = intval((($mode[0] === '0' ? '' : '0') . $mode), 8);
} elseif (is_string($mode) && preg_match_all('/(a|[ugo]{1,3})([-=+])([rwx]{1,3})/', $mode, $gMatch, PREG_SET_ORDER)) {
if (!function_exists('fileperms')) {
return false;
}
// start by file permission
$octalMode = (fileperms($file) & 0777);
foreach ($gMatch as $matches) {
// [ugo] or a = ugo
$group = $matches[1];
if ($group === 'a') {
$group = 'ugo';
}
// can be + - =
$action = $matches[2];
// [rwx]
$gPerms = $matches[3];
// reset octal group perms
$octalGroupMode = 0;
// Init sub perms
$subPerm = 0;
$subPerm += strpos($gPerms, 'x') !== false ? 1 : 0; // mask 001
$subPerm += strpos($gPerms, 'w') !== false ? 2 : 0; // mask 010
$subPerm += strpos($gPerms, 'r') !== false ? 4 : 0; // mask 100
$ugoLen = strlen($group);
if ($action === '=') {
// generate octal group permsissions and ugo mask invert
$ugoMaskInvert = 0777;
for ($i = 0; $i < $ugoLen; $i++) {
switch ($group[$i]) {
case 'u':
$octalGroupMode = $octalGroupMode | $subPerm << 6; // mask xxx000000
$ugoMaskInvert = $ugoMaskInvert & 077;
break;
case 'g':
$octalGroupMode = $octalGroupMode | $subPerm << 3; // mask 000xxx000
$ugoMaskInvert = $ugoMaskInvert & 0707;
break;
case 'o':
$octalGroupMode = $octalGroupMode | $subPerm; // mask 000000xxx
$ugoMaskInvert = $ugoMaskInvert & 0770;
break;
}
}
// apply = action
$octalMode = $octalMode & ($ugoMaskInvert | $octalGroupMode);
} else {
// generate octal group permsissions
for ($i = 0; $i < $ugoLen; $i++) {
switch ($group[$i]) {
case 'u':
$octalGroupMode = $octalGroupMode | $subPerm << 6; // mask xxx000000
break;
case 'g':
$octalGroupMode = $octalGroupMode | $subPerm << 3; // mask 000xxx000
break;
case 'o':
$octalGroupMode = $octalGroupMode | $subPerm; // mask 000000xxx
break;
}
}
// apply + or - action
switch ($action) {
case '+':
$octalMode = $octalMode | $octalGroupMode;
break;
case '-':
$octalMode = $octalMode & ~$octalGroupMode;
break;
}
}
}
} else {
return true;
}
// if input permissions are equal at file permissions return true without performing chmod
if (function_exists('fileperms') && $octalMode === (fileperms($file) & 0777)) {
return true;
}
if (!function_exists('chmod')) {
return false;
}
return @chmod($file, $octalMode);
}
/**
* Return file perms in string
*
* @param int|string $perms permssions
*
* @return string|false false if fail
*/
public static function permsToString($perms)
{
if (is_int($perms)) {
return decoct($perms);
} elseif (is_numeric($perms)) {
return ($perms[0] === '0' ? '' : '0') . $perms;
} elseif (is_string($perms)) {
return $perms;
} else {
return false;
}
}
/**
* this function creates a folder if it does not exist and performs a chmod.
* it is different from the normal mkdir function to which an umask is applied to the input permissions.
*
* this function handles the variable MODE in a way similar to the chmod of lunux
* So the MODE variable can be
* 1) an octal number (0755)
* 2) a string that defines an octal number ("644")
* 3) a string with the following format [ugoa]*([-+=]([rwx]*)+
*
* @param string $path folder path
* @param int|string $mode mode permissions
* @param bool $recursive Allows the creation of nested directories specified in the pathname. Default to false.
* @param resource $context not used for windows bug
*
* @return boolean bool TRUE on success or FALSE on failure.
*
* @todo check recursive true and multiple chmod
*/
public static function mkdir($path, $mode = 0777, $recursive = false, $context = null)
{
if (strlen($path) > PHP_MAXPATHLEN) {
throw new Exception('Skipping a file that exceeds allowed max path length [' . PHP_MAXPATHLEN . ']. File: ' . $path);
}
if (!file_exists($path)) {
if (!function_exists('mkdir')) {
return false;
}
if (!@mkdir($path, 0777, $recursive)) {
return false;
}
}
return self::chmod($path, $mode);
}
/**
* this function call snap mkdir if te folder don't exists od don't have write or exec permissions
*
* this function handles the variable MODE in a way similar to the chmod of lunux
* The mode variable can be set to have more flexibility but not giving the user write and read and exec permissions doesn't make much sense
*
* @param string $path folder path
* @param int|string $mode mode permissions
* @param bool $recursive Allows the creation of nested directories specified in the pathname. Default to false.
* @param resource $context not used for windows bug
*
* @return boolean bool TRUE on success or FALSE on failure.
*/
public static function dirWriteCheckOrMkdir($path, $mode = 'u+rwx', $recursive = false, $context = null)
{
if (!file_exists($path)) {
return self::mkdir($path, $mode, $recursive, $context);
} elseif (!is_writable($path) || (function_exists('is_executable') && !is_executable($path))) {
return self::chmod($path, $mode);
} else {
return true;
}
}
/**
* Create an empty index.php file in dir.
*
* @param string $dir Dir where create index.php
*
* @return bool true on success, false on failure
*/
public static function createSilenceIndex($dir)
{
if (!is_dir($dir)) {
return false;
}
$path = self::trailingslashit($dir) . 'index.php';
if (!file_exists($path)) {
$fileContent = <<<INDEXPHP
<?php
// silence
INDEXPHP;
return (file_put_contents($path, $fileContent) !== false);
}
return true;
}
/**
* from wordpress function wp_is_stream
*
* @param string $path The resource path or URL.
*
* @return bool True if the path is a stream URL.
*/
public static function isStream($path)
{
$scheme_separator = strpos($path, '://');
if (false === $scheme_separator) {
// $path isn't a stream
return false;
}
$stream = substr($path, 0, $scheme_separator);
return in_array($stream, stream_get_wrappers(), true);
}
/**
* From Wordpress function: wp_mkdir_p
*
* Recursive directory creation based on full path.
*
* Will attempt to set permissions on folders.
*
* @param string $target Full path to attempt to create.
*
* @return bool Whether the path was created. True if path already exists.
*/
public static function mkdirP($target)
{
$wrapper = null;
// Strip the protocol.
if (self::isStream($target)) {
list( $wrapper, $target ) = explode('://', $target, 2);
}
// From php.net/mkdir user contributed notes.
$target = str_replace('//', '/', $target);
// Put the wrapper back on the target.
if ($wrapper !== null) {
$target = $wrapper . '://' . $target;
}
/*
* Safe mode fails with a trailing slash under certain PHP versions.
* Use rtrim() instead of untrailingslashit to avoid formatting.php dependency.
*/
$target = rtrim($target, '/');
if (empty($target)) {
$target = '/';
}
if (file_exists($target)) {
return @is_dir($target);
}
// We need to find the permissions of the parent folder that exists and inherit that.
$target_parent = dirname($target);
while ('.' != $target_parent && !is_dir($target_parent) && dirname($target_parent) !== $target_parent) {
$target_parent = dirname($target_parent);
}
// Get the permission bits.
if ($stat = @stat($target_parent)) {
$dir_perms = $stat['mode'] & 0007777;
} else {
$dir_perms = 0777;
}
if (@mkdir($target, $dir_perms, true)) {
/*
* If a umask is set that modifies $dir_perms, we'll have to re-set
* the $dir_perms correctly with chmod()
*/
if ($dir_perms != ( $dir_perms & ~umask() )) {
$folder_parts = explode('/', substr($target, strlen($target_parent) + 1));
for ($i = 1, $c = count($folder_parts); $i <= $c; $i++) {
@chmod($target_parent . '/' . implode('/', array_slice($folder_parts, 0, $i)), $dir_perms);
}
}
return true;
}
return false;
}
/**
* This function returns the relative path to mainPath
*
* @param string $path file path
* @param string $mainPath main path
* @param bool $real if true check real path
*
* @return bool|string false if path isn't a sub path of main path or return the relative path
*/
public static function getRelativePath($path, $mainPath, $real = false)
{
if (strlen($mainPath) == 0) {
return ltrim(self::safePathUntrailingslashit($path, $real), '/');
}
$safePath = self::safePathUntrailingslashit($path, $real);
$safeMainPath = self::safePathUntrailingslashit($mainPath, $real);
if ($safePath === $safeMainPath) {
return '';
} elseif (strpos($safePath, self::trailingslashit($safeMainPath)) === 0) {
return ltrim(substr($safePath, strlen($safeMainPath)), '/');
} else {
return false;
}
}
/**
* Check if path is child of mainPath
*
* @param string $path file path
* @param string $mainPath main path
* @param boolean $reverseCheck if true check if path is child of mainpath and if mainPash is child of path
* @param boolean $trueIfEquals if paths are equals and is true return true else false
*
* @return boolean
*/
public static function isChildPath($path, $mainPath, $reverseCheck = false, $trueIfEquals = true)
{
if (strlen($mainPath) == 0) {
return true;
}
if ($reverseCheck && strlen($path) == 0) {
return true;
}
$safePath = self::safePathUntrailingslashit($path);
$safeMainPath = self::safePathUntrailingslashit($mainPath);
if ($safePath === $safeMainPath) {
return $trueIfEquals;
} elseif (strpos($safePath, self::trailingslashit($safeMainPath)) === 0) {
return true;
} elseif ($reverseCheck && strpos($safeMainPath, self::trailingslashit($safePath)) === 0) {
return true;
} else {
return false;
}
}
/**
* Return sorted array by subfolders count
*
* @param string[] $paths paths lists
* @param boolean $childsFirst if true put childs before parents
* @param boolean $maintainIndex if true maintain array indexes
* @param boolean $sortKeys if true sort by keys
*
* @return string[]
*/
public static function sortBySubfoldersCount($paths, $childsFirst = false, $maintainIndex = false, $sortKeys = false)
{
if ($sortKeys) {
$function = 'uksort';
} elseif ($maintainIndex) {
$function = 'uasort';
} else {
$function = 'usort';
}
$function($paths, function ($a, $b) use ($childsFirst) {
$lenA = count(preg_split('/[\\\\\/]+/', $a));
$lenB = count(preg_split('/[\\\\\/]+/', $b));
if ($lenA === $lenB) {
return strcmp($a, $b) * ($childsFirst ? -1 : 1);
} elseif ($lenA > $lenB) {
return ($childsFirst ? -1 : 1);
} else {
return ($childsFirst ? 1 : -1);
}
});
return $paths;
}
/**
* from wp_normalize_path
*
* @param string $path Path to normalize.
*
* @return string Normalized path.
*/
public static function normalizePath($path)
{
$wrapper = '';
if (self::isStream($path)) {
list( $wrapper, $path ) = explode('://', $path, 2);
$wrapper .= '://';
}
// Standardise all paths to use /
$path = str_replace('\\', '/', $path);
// Replace multiple slashes down to a singular, allowing for network shares having two slashes.
$path = preg_replace('|(?<=.)/+|', '/', $path);
if (strpos($path, '//') === 0) {
$path = substr($path, 1);
}
// Windows paths should uppercase the drive letter
if (':' === substr($path, 1, 1)) {
$path = ucfirst($path);
}
return $wrapper . $path;
}
/**
* Get common parent path from given paths
*
* @param string[] $paths list of paths
*
* @return string common parent path
*/
public static function getCommonPath($paths = array())
{
if (empty($paths)) {
return '';
} if (!is_array($paths)) {
$paths = array($paths);
} else {
$paths = array_values($paths);
}
$pathAssoc = array();
$numPaths = count($paths);
$minPathCouts = PHP_INT_MAX;
for ($i = 0; $i < $numPaths; $i++) {
$pathAssoc[$i] = explode('/', self::safePathUntrailingslashit($paths[$i]));
$pathCount = count($pathAssoc[$i]);
if ($minPathCouts > $pathCount) {
$minPathCouts = $pathCount;
}
}
for ($partIndex = 0; $partIndex < $minPathCouts; $partIndex++) {
$currentPart = $pathAssoc[0][$partIndex];
for ($currentPath = 1; $currentPath < $numPaths; $currentPath++) {
if ($pathAssoc[$currentPath][$partIndex] != $currentPart) {
break 2;
}
}
}
$resultParts = array_slice($pathAssoc[0], 0, $partIndex);
return implode('/', $resultParts);
}
/**
* remove root path transforming the current path into a relative path
*
* ex. /aaa/bbb become aaa/bbb
* ex. C:\aaa\bbb become aaa\bbb
*
* @param string $path file path
*
* @return string
*/
public static function removeRootPath($path)
{
return preg_replace('/^(?:[A-Za-z]:)?[\/](.*)/', '$1', $path);
}
/**
* Returns the last N lines of a file. Simular to tail command
*
* @param string $filepath The full path to the file to be tailed
* @param int $lines The number of lines to return with each tail call
*
* @return false|string The last N parts of the file, flse on failure
*/
public static function tailFile($filepath, $lines = 2)
{
// Open file
$f = @fopen($filepath, "rb");
if ($f === false) {
return false;
}
// Sets buffer size
$buffer = 256;
// Jump to last character
fseek($f, -1, SEEK_END);
// Read it and adjust line number if necessary
// (Otherwise the result would be wrong if file doesn't end with a blank line)
if (fread($f, 1) != "\n") {
$lines -= 1;
}
// Start reading
$output = '';
$chunk = '';
// While we would like more
while (ftell($f) > 0 && $lines >= 0) {
// Figure out how far back we should jump
$seek = min(ftell($f), $buffer);
// Do the jump (backwards, relative to where we are)
fseek($f, -$seek, SEEK_CUR);
// Read a chunk and prepend it to our output
$output = ($chunk = fread($f, $seek)) . $output;
// Jump back to where we started reading
fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);
// Decrease our line counter
$lines -= substr_count($chunk, "\n");
}
// While we have too many lines
// (Because of buffer size we might have read too many)
while ($lines++ < 0) {
// Find first newline and remove all text before that
$output = substr($output, strpos($output, "\n") + 1);
}
fclose($f);
return trim($output);
}
/**
* @param string $filepath path to file to be downloaded
* @param string $downloadName name to be downloaded as
* @param int $bufferSize file chunks to be served
* @param bool $limitRate if set to true the download rate will be limited to $bufferSize/seconds
*
* @return void
*/
public static function serveFileForDownload($filepath, $downloadName, $bufferSize = 0, $limitRate = false)
{
// Process download
if (!file_exists($filepath)) {
throw new Exception("File does not exist!");
}
if (!is_file($filepath)) {
throw new Exception("'$filepath' is not a file!");
}
// Clean output buffers
SnapUtil::obCleanAll(false);
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $downloadName . '"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($filepath));
flush(); // Flush system output buffer
if ($bufferSize <= 0) {
readfile($filepath);
exit;
}
$fp = @fopen($filepath, 'r');
if (!is_resource($fp)) {
throw new Exception('Fail to open the file ' . $filepath);
}
while (!feof($fp) && ($data = fread($fp, $bufferSize)) !== false) {
echo $data;
if ($limitRate) {
sleep(1);
}
}
@fclose($fp);
exit;
}
/**
* Serve error 500 and exit
*
* @return never
*/
public static function serverError500()
{
header('HTTP/1.1 500 Internal Server Error');
exit;
}
/**
* Return lasts fine of file
*
* @param string $path Path to the file
* @param int $n Number of lines to get
* @param int $charLimit Number of chars to include in each line
*
* @return bool|string[] Last $n lines of file
*/
public static function getLastLinesOfFile($path, $n, $charLimit = null)
{
if (!is_readable($path)) {
return false;
}
if (($handle = self::fopen($path, 'r', false)) === false) {
return false;
}
$result = array();
$pos = -1;
$currentLine = '';
$counter = 0;
while ($counter < $n && -1 !== fseek($handle, $pos, SEEK_END)) {
$char = fgetc($handle);
if (PHP_EOL == $char) {
$trimmedValue = trim($currentLine);
if (is_null($charLimit)) {
$currentLine = substr($currentLine, 0);
} else {
$currentLine = substr($currentLine, 0, (int) $charLimit);
if (strlen($currentLine) == $charLimit) {
$currentLine .= '...';
}
}
if (!empty($trimmedValue)) {
$result[] = $currentLine;
$counter++;
}
$currentLine = '';
} else {
$currentLine = $char . $currentLine;
}
$pos--;
}
self::fclose($handle, false);
return array_reverse($result);
}
/**
* Thif function scan a folder filter by regex
*
* Options
* regexFile: [bool|string|array] if is bool alrays or never match, if is string o array of string check if rexeses match file name
* regexFolder: [bool|string|array] if is bool alrays or never match, if is string o array of string check if rexeses match file name
* checkFullPath: bool if false only current file/folder name is passed at regex if true is passed the full path
* recursive: bool if false check only passed folder or all sub folder recursively
* invert: bool if false pass invert the result
* childFirst: bool if false is parsed parent folters first or child folders first
*
* @param string $dir dir to scan
* @param array<string, bool|string|string[]> $options array{
* regexFile?: bool|string,
* regexFolder?: bool|string,
* checkFullPath?: bool,
* recursive?: bool,
* invert?: bool,
* childFirst?: bool
* }
*
* @return string[] paths lists
*/
public static function regexGlob($dir, $options)
{
$result = array();
self::regexGlobCallback($dir, function ($path) use (&$result) {
$result[] = $path;
}, $options);
return $result;
}
/**
* Execute the callback function foreach right element, private function for optimization
*
* Options
* regexFile: [bool|string|array] if is bool alrays or never match, if is string o array of string check if rexeses match file name
* regexFolder: [bool|string|array] if is bool alrays or never match, if is string o array of string check if rexeses match file name
* checkFullPath: bool if false only current file/folder name is passed at regex if true is passed the full path
* recursive: bool if false check only passed folder or all sub folder recursively
* invert: bool if false pass invert the result
* childFirst: bool if false is parsed parent folters first or child folders first
* symlinks: string[] list a symblink parsed
*
* @param string $dir dir to scan
* @param callable $callback callback function
* @param array<string, bool|string|string[]> $options array{
* regexFile?: bool|string,
* regexFolder?: bool|string,
* checkFullPath?: bool,
* recursive?: bool,
* invert?: bool,
* childFirst?: bool,
* symlinks?: string[]
* }
*
* @return boolean Returns true on success or false on failure.
*/
protected static function regexGlobCallbackPrivate($dir, $callback, &$options)
{
if (($dh = opendir($dir)) == false) {
return false;
}
while (($elem = readdir($dh)) !== false) {
if ($elem === '.' || $elem === '..') {
continue;
}
$fullPath = $dir . $elem;
$isDir = is_dir($fullPath);
if (($regex = $isDir ? $options['regexFolder'] : $options['regexFile']) === false) {
continue;
}
if ($isDir && is_link($fullPath)) {
$realPath = self::safePathUntrailingslashit($fullPath, true);
if (in_array($realPath, $options['symlinks'])) {
continue;
}
$options['symlinks'][] = $realPath;
}
if (is_bool($regex)) {
$match = $regex;
} else {
$match = false;
$pathCheck = $options['checkFullPath'] ? $fullPath : $elem;
foreach ($regex as $currentRegex) {
if (preg_match($currentRegex, $pathCheck) === 1) {
$match = true;
break;
}
}
if ($options['invert']) {
$match = !$match;
}
}
if ($match) {
if ($isDir && $options['execChildFirst']) {
self::regexGlobCallbackPrivate($fullPath . '/', $callback, $options);
}
call_user_func($callback, $fullPath);
if ($isDir && $options['execChildAfter']) {
self::regexGlobCallbackPrivate($fullPath . '/', $callback, $options);
}
}
}
closedir($dh);
return true;
}
/**
* Execute the callback function foreach right element (folder or files)
*
* Options
* regexFile: [bool|string|array] if is bool alrays or never match, if is string o array of string check if rexeses match file name
* regexFolder: [bool|string|array] if is bool alrays or never match, if is string o array of string check if rexeses match file name
* checkFullPath: bool if false only current file/folder name is passed at regex if true is passed the full path
* recursive: bool if false check only passed folder or all sub folder recursively
* invert: bool if false pass invert the result
* childFirst: bool if false is parsed parent folters first or child folders first
*
* @param string $dir dir to scan
* @param callable $callback callback function
* @param array<string, bool|string|string[]> $options array{
* regexFile?: bool|string,
* regexFolder?: bool|string,
* checkFullPath?: bool,
* recursive?: bool,
* invert?: bool,
* childFirst?: bool
* }
*
* @return boolean Returns true on success or false on failure.
*/
public static function regexGlobCallback($dir, $callback, $options = array())
{
$dir = self::safePathTrailingslashit($dir);
if (!is_dir($dir) || !is_readable($dir)) {
return false;
}
if (!is_callable($callback)) {
return false;
}
$options = array_merge(array(
'regexFile' => true,
'regexFolder' => true,
'checkFullPath' => false,
'recursive' => false,
'invert' => false,
'childFirst' => false,
'symlinks' => array()
), (array) $options);
if (is_bool($options['regexFile'])) {
$options['regexFile'] = ($options['regexFile'] xor $options['invert']);
} elseif (is_scalar($options['regexFile'])) {
$options['regexFile'] = array($options['regexFile']);
}
if (is_bool($options['regexFolder'])) {
$options['regexFolder'] = ($options['regexFolder'] xor $options['invert']);
} elseif (is_scalar($options['regexFolder'])) {
$options['regexFolder'] = array($options['regexFolder']);
}
// optimizization
$options['execChildFirst'] = ($options['recursive'] && $options['childFirst'] === true);
$options['execChildAfter'] = ($options['recursive'] && $options['childFirst'] === false);
return self::regexGlobCallbackPrivate($dir, $callback, $options);
}
/**
* Empty passed dir
*
* @param string $dir folder to empty
* @param string[] $filter childs name to skip
*
* @return boolean Returns true on success or false on failure.
*/
public static function emptyDir($dir, $filter = array())
{
$dir = self::safePathTrailingslashit($dir);
if (!is_dir($dir) || !is_readable($dir)) {
return false;
}
if (($dh = opendir($dir)) == false) {
return false;
}
$listToDelete = array();
while (($elem = readdir($dh)) !== false) {
if ($elem === '.' || $elem === '..') {
continue;
}
if (in_array($elem, $filter)) {
continue;
}
$fullPath = $dir . $elem;
if (self::chmod($fullPath, 'ugo+rwx')) {
$listToDelete[] = $fullPath;
}
}
closedir($dh);
foreach ($listToDelete as $path) {
self::rrmdir($path);
}
return true;
}
/**
* Returns a path to the base root folder of path taking into account the
* open_basedir setting.
*
* @param string $path file path
*
* @return bool|string Base root path of $path if it's accessible, otherwise false;
*/
public static function getMaxAllowedRootOfPath($path)
{
$path = self::safePathUntrailingslashit($path, true);
if (!self::isOpenBaseDirEnabled()) {
$parts = explode("/", $path);
return $parts[0] . "/";
} else {
return self::getOpenBaseDirRootOfPath($path);
}
}
/**
* Check if php.ini open_basedir is enabled
*
* @return bool true if open_basedir is set
*/
public static function isOpenBaseDirEnabled()
{
$iniVar = ini_get("open_basedir");
return (strlen($iniVar) > 0);
}
/**
* Get open_basedir list paths
*
* @return string[] Paths contained in the open_basedir setting. Empty array if the setting is not enabled.
*/
public static function getOpenBaseDirPaths()
{
if (!($openBase = ini_get("open_basedir"))) {
return array();
}
return explode(PATH_SEPARATOR, $openBase);
}
/**
* Get open base dir root path of path
*
* @param string $path file path
*
* @return bool|string Path to the base dir of $path if it exists, otherwise false
*/
public static function getOpenBaseDirRootOfPath($path)
{
foreach (self::getOpenBaseDirPaths() as $allowedPath) {
$allowedPath = $allowedPath !== "/" ? self::safePathUntrailingslashit($allowedPath) : "/";
if (strpos($path, $allowedPath) === 0) {
return $allowedPath;
}
}
return false;
}
/**
* this function is similar at dirname but if empty path return empty value not .
* and is SO indipendent so work on not normalized path
*
* @param string $path file path
*
* @return string
*/
public static function getRelativeDirname($path)
{
if (preg_match('/^(.*)[\/]+/', $path, $matches) !== 1) {
return '';
}
return $matches[1];
}
/**
* Set full user permissions on folder (rwx)
*
* @param string $path dir path
*
* @return boolean // return false if folder don't have read write permission on folder
*/
public static function dirAddFullPermsAndCheckResult($path)
{
if (!SnapIO::chmod($path, 'u+rwx')) {
return false;
}
if (!is_readable($path) || !is_writable($path)) {
return false;
}
if (function_exists('is_executable') && !is_executable($path) && !SnapOS::isWindows()) {
return false;
}
return true;
}
/**
* set full user permissions on file (rwx)
*
* @param string $path file path
*
* @return boolean // return false if folder don't have read write permission on folder
*/
public static function fileAddFullPermsAndCheckResult($path)
{
if (!SnapIO::chmod($path, 'u+rw')) {
return false;
}
if (!is_readable($path) || !is_writable($path)) {
return false;
}
return true;
}
/**
* Returns the total size of a filesystem or disk partition in bytes
*
* @param string $directory path
*
* @return int rturn number of bytes or -1 on failure
*/
public static function diskTotalSpace($directory)
{
if (!function_exists('disk_total_space')) {
return -1;
}
if (($space = disk_total_space($directory)) === false) {
return -1;
}
return (int) round($space);
}
/**
* Returns available space in directory in bytes
*
* @param string $directory path
*
* @return int rturn number of bytes or -1 on failure
*/
public static function diskFreeSpace($directory)
{
if (!function_exists('disk_free_space')) {
return -1;
}
if (($space = disk_free_space($directory)) === false) {
return -1;
}
return (int) round($space);
}
}
|