Files
espocrm/application/Espo/Core/Utils/File/Manager.php
Yuri Kuznetsov 955a489e21 exception changes
2022-06-23 11:45:50 +03:00

1201 lines
32 KiB
PHP

<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Utils\File;
use Espo\Core\{
Utils\Util,
Utils\Json,
};
use Espo\Core\Utils\File\Exceptions\FileError;
use Espo\Core\Utils\File\Exceptions\PermissionError;
use stdClass;
use Throwable;
use InvalidArgumentException;
use const E_USER_DEPRECATED;
class Manager
{
private Permission $permission;
/**
* @var string[]
*/
private $permissionDeniedList = [];
protected string $tmpDir = 'data/tmp';
protected const RENAME_RETRY_NUMBER = 10;
protected const RENAME_RETRY_INTERVAL = 0.1;
protected const GET_SAFE_CONTENTS_RETRY_NUMBER = 10;
protected const GET_SAFE_CONTENTS_RETRY_INTERVAL = 0.1;
/**
* @param ?array{
* dir: string|int|null,
* file: string|int|null,
* user: string|int|null,
* group: string|int|null,
* } $defaultPermissions
*/
public function __construct(?array $defaultPermissions = null)
{
$params = null;
if ($defaultPermissions) {
$params = [
'defaultPermissions' => $defaultPermissions,
];
}
$this->permission = new Permission($this, $params);
}
public function getPermissionUtils(): Permission
{
return $this->permission;
}
/**
* Get a list of directories in a specified directory.
*
* @return string[]
*/
public function getDirList(string $path): array
{
/** @var string[] */
return $this->getFileList($path, false, '', false);
}
/**
* Get a list of files in a specified directory.
*
* @param string $path A folder path.
* @param bool|int $recursively Find files in sub-folders.
* @param string $filter Filter for files. Use regular expression, Example: `\.json$`.
* @param bool|null $onlyFileType Filter for type of files/directories.
* If TRUE - returns only file list, if FALSE - only directory list.
* @param bool $returnSingleArray Return a single array.
*
* @return string[]|array<string,string[]>
*/
public function getFileList(
string $path,
$recursively = false,
$filter = '',
$onlyFileType = null,
bool $returnSingleArray = false
): array {
$result = [];
if (!file_exists($path) || !is_dir($path)) {
return $result;
}
$cdir = scandir($path) ?: [];
foreach ($cdir as $key => $value) {
if (!in_array($value, [".", ".."])) {
$add = false;
if (is_dir($path . Util::getSeparator() . $value)) {
if ($recursively || (is_int($recursively) && $recursively!=0)) {
$nextRecursively = is_int($recursively) ? ($recursively-1) : $recursively;
$result[$value] = $this->getFileList(
$path . Util::getSeparator() . $value,
$nextRecursively,
$filter,
$onlyFileType
);
}
else if (!isset($onlyFileType) || !$onlyFileType){ /*save only directories*/
$add = true;
}
}
else if (!isset($onlyFileType) || $onlyFileType) { /*save only files*/
$add = true;
}
if ($add) {
if (!empty($filter)) {
if (preg_match('/'.$filter.'/i', $value)) {
$result[] = $value;
}
}
else {
$result[] = $value;
}
}
}
}
if ($returnSingleArray) {
/** @var string[] $result */
return $this->getSingleFileList($result, $onlyFileType, $path);
}
/** @var array<string,string[]> */
return $result;
}
/**
* @param string[] $fileList
* @param ?bool $onlyFileType
* @param ?string $basePath
* @param string $parentDirName
* @return string[]
*/
private function getSingleFileList(
array $fileList,
$onlyFileType = null,
$basePath = null,
$parentDirName = ''
): array {
$singleFileList = [];
foreach ($fileList as $dirName => $fileName) {
if (is_array($fileName)) {
$currentDir = Util::concatPath($parentDirName, $dirName);
if (
!isset($onlyFileType) ||
$onlyFileType == $this->isFilenameIsFile($basePath . '/' . $currentDir)
) {
$singleFileList[] = $currentDir;
}
$singleFileList = array_merge(
$singleFileList, $this->getSingleFileList($fileName, $onlyFileType, $basePath, $currentDir)
);
}
else {
$currentFileName = Util::concatPath($parentDirName, $fileName);
if (
!isset($onlyFileType) ||
$onlyFileType == $this->isFilenameIsFile($basePath . '/' . $currentFileName)
) {
$singleFileList[] = $currentFileName;
}
}
}
return $singleFileList;
}
/**
* Get file contents.
*
* @param string $path
* @throws FileError If the file could not be read.
*/
public function getContents($path): string
{
/** @var mixed $path */
if (is_array($path)) {
// For backward compatibility.
// @todo Remove support of arrays in v7.3.
trigger_error(
'Array parameter is deprecated for FileManager::getContents.',
E_USER_DEPRECATED
);
$path = $this->concatPaths($path);
}
else if (!is_string($path)) {
throw new InvalidArgumentException();
}
if (!file_exists($path)) {
throw new FileError("File '{$path}' does not exist.");
}
$contents = file_get_contents($path);
if ($contents === false) {
throw new FileError("Could not open file '{$path}'.");
}
return $contents;
}
/**
* Get data from a PHP file.
*
* @return mixed
* @throws FileError
*/
public function getPhpContents(string $path)
{
if (!file_exists($path)) {
throw new FileError("File '$path' does not exist.");
}
if (strtolower(substr($path, -4)) !== '.php') {
throw new FileError("File '$path' is not PHP.");
}
return include($path);
}
/**
* Get array or stdClass data from PHP file.
* If a file is not yet written, it will wait until it's ready.
*
* @return array<mixed,mixed>|stdClass
* @throws FileError
*/
public function getPhpSafeContents(string $path)
{
if (!file_exists($path)) {
throw new FileError("Can't get contents from non-existing file '{$path}'.");
}
if (!strtolower(substr($path, -4)) == '.php') {
throw new FileError("Only PHP file are allowed for getting contents.");
}
$counter = 0;
while ($counter < self::GET_SAFE_CONTENTS_RETRY_NUMBER) {
$data = include($path);
if (is_array($data) || $data instanceof stdClass) {
return $data;
}
usleep((int) (self::GET_SAFE_CONTENTS_RETRY_INTERVAL * 1000000));
$counter ++;
}
throw new FileError("Bad data stored in file '{$path}'.");
}
/**
* Write contents to a file.
*
* @param mixed $data
* @throws PermissionError
*/
public function putContents(string $path, $data, int $flags = 0, bool $useRenaming = false): bool
{
if ($this->checkCreateFile($path) === false) {
throw new PermissionError('Permission denied for '. $path);
}
$result = false;
if ($useRenaming) {
$result = $this->putContentsUseRenaming($path, $data);
}
if (!$result) {
$result = (file_put_contents($path, $data, $flags) !== false);
}
if ($result) {
$this->opcacheInvalidate($path);
}
return (bool) $result;
}
/**
* @param string $data
*/
private function putContentsUseRenaming(string $path, $data): bool
{
$tmpDir = $this->tmpDir;
if (!$this->isDir($tmpDir)) {
$this->mkdir($tmpDir);
}
if (!$this->isDir($tmpDir)) {
return false;
}
$tmpPath = tempnam($tmpDir, 'tmp');
if ($tmpPath === false) {
return false;
}
$tmpPath = $this->getRelativePath($tmpPath);
if (!$tmpPath) {
return false;
}
if (!$this->isFile($tmpPath)) {
return false;
}
if (!$this->isWritable($tmpPath)) {
return false;
}
$h = fopen($tmpPath, 'w');
if ($h === false) {
return false;
}
fwrite($h, $data);
fclose($h);
$this->getPermissionUtils()->setDefaultPermissions($tmpPath);
if (!$this->isReadable($tmpPath)) {
return false;
}
$result = rename($tmpPath, $path);
if (!$result && stripos(\PHP_OS, 'WIN') === 0) {
$result = $this->renameInLoop($tmpPath, $path);
}
if ($this->isFile($tmpPath)) {
$this->removeFile($tmpPath);
}
return (bool) $result;
}
private function renameInLoop(string $source, string $destination): bool
{
$counter = 0;
while ($counter < self::RENAME_RETRY_NUMBER) {
if (!$this->isWritable($destination)) {
break;
}
$result = rename($source, $destination);
if ($result !== false) {
return true;
}
usleep((int) (self::RENAME_RETRY_INTERVAL * 1000000));
$counter++;
}
return false;
}
/**
* Save PHP contents to a file.
*
* @param mixed $data
*/
public function putPhpContents(string $path, $data, bool $withObjects = false, bool $useRenaming = false): bool
{
return $this->putContents($path, $this->wrapForDataExport($data, $withObjects), LOCK_EX, $useRenaming);
}
/**
* Save JSON content to a file.
* @param mixed $data
*/
public function putJsonContents(string $path, $data): bool
{
$options = JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
$contents = Json::encode($data, $options);
return $this->putContents($path, $contents, LOCK_EX);
}
/**
* Merge JSON file contents with existing and override the file.
*
* @param array<mixed,mixed> $data
*/
public function mergeJsonContents(string $path, array $data): bool
{
$currentData = [];
if ($this->isFile($path)) {
$currentContents = $this->getContents($path);
$currentData = Json::decode($currentContents, true);
}
if (!is_array($currentData)) {
throw new FileError("Neither array nor object in '{$path}'.");
}
$mergedData = Util::merge($currentData, $data);
$stringData = Json::encode($mergedData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
return (bool) $this->putContents($path, $stringData);
}
/**
* Append contents to a file.
*
* @param string $data
*/
public function appendContents(string $path, $data): bool
{
return $this->putContents($path, $data, FILE_APPEND | LOCK_EX);
}
/**
* Unset specific items in a JSON file and override the file.
* Items are specified as an array of JSON paths.
*
* @param array<mixed,string> $unsets
*/
public function unsetJsonContents(string $path, array $unsets): bool
{
if (!$this->isFile($path)) {
return true;
}
$currentContents = $this->getContents($path);
$currentData = Json::decode($currentContents, true);
$unsettedData = Util::unsetInArray($currentData, $unsets, true);
if (empty($unsettedData)) {
return $this->unlink($path);
}
return (bool) $this->putJsonContents($path, $unsettedData);
}
/**
* @deprecated
* @param string|string[] $paths
* @return string
*/
private function concatPaths($paths)
{
if (is_string($paths)) {
return Util::fixPath($paths);
}
$fullPath = '';
foreach ($paths as $path) {
$fullPath = Util::concatPath($fullPath, $path);
}
return $fullPath;
}
/**
* Create a new dir.
*
* @param string $path
* @param int $permission Example: `0755`.
*/
public function mkdir(string $path, $permission = null): bool
{
if (file_exists($path) && is_dir($path)) {
return true;
}
$parentDirPath = dirname($path);
if (!file_exists($parentDirPath)) {
$this->mkdir($parentDirPath, $permission);
}
$defaultPermissions = $this->getPermissionUtils()->getRequiredPermissions($path);
if (!isset($permission)) {
$permission = (int) base_convert((string) $defaultPermissions['dir'], 8, 10);
}
$umask = umask(0);
$result = mkdir($path, $permission);
if ($umask) {
umask($umask);
}
if (!empty($defaultPermissions['user'])) {
$this->getPermissionUtils()->chown($path);
}
if (!empty($defaultPermissions['group'])) {
$this->getPermissionUtils()->chgrp($path);
}
return (bool) $result;
}
/**
* Copy files from one directory to another.
* Example: $sourcePath = 'data/uploads/extensions/file.json',
* $destPath = 'data/uploads/backup', result will be data/uploads/backup/data/uploads/backup/file.json.
*
* @param string $sourcePath
* @param string $destPath
* @param bool $recursively
* @param string[] $fileList List of files that should be copied.
* @param bool $copyOnlyFiles Copy only files, instead of full path with directories.
* Example:
* $sourcePath = 'data/uploads/extensions/file.json',
* $destPath = 'data/uploads/backup', result will be 'data/uploads/backup/file.json'.
*
* @throws PermissionError
*/
public function copy(
string $sourcePath,
string $destPath,
bool $recursively = false,
array $fileList = null,
bool $copyOnlyFiles = false
): bool {
if (!isset($fileList)) {
$fileList = is_file($sourcePath) ?
(array) $sourcePath :
$this->getFileList($sourcePath, $recursively, '', true, true);
}
$permissionDeniedList = [];
/** @var string[] $fileList */
foreach ($fileList as $file) {
if ($copyOnlyFiles) {
$file = pathinfo($file, PATHINFO_BASENAME);
}
$destFile = Util::concatPath($destPath, $file);
$isFileExists = file_exists($destFile);
if ($this->checkCreateFile($destFile) === false) {
$permissionDeniedList[] = $destFile;
}
else if (!$isFileExists) {
$this->removeFile($destFile);
}
}
if (!empty($permissionDeniedList)) {
$betterPermissionList = $this->getPermissionUtils()->arrangePermissionList($permissionDeniedList);
throw new PermissionError("Permission denied for <br>". implode(", <br>", $betterPermissionList));
}
$res = true;
foreach ($fileList as $file) {
if ($copyOnlyFiles) {
$file = pathinfo($file, PATHINFO_BASENAME);
}
$sourceFile = is_file($sourcePath) ?
$sourcePath :
Util::concatPath($sourcePath, $file);
$destFile = Util::concatPath($destPath, $file);
if (file_exists($sourceFile) && is_file($sourceFile)) {
$res &= copy($sourceFile, $destFile);
$this->getPermissionUtils()->setDefaultPermissions($destFile);
$this->opcacheInvalidate($destFile);
}
}
return (bool) $res;
}
/**
* Checks whether a new file can be created. It will also create all needed directories.
*
* @throws PermissionError
*/
public function checkCreateFile(string $filePath): bool
{
$defaultPermissions = $this->getPermissionUtils()->getRequiredPermissions($filePath);
if (file_exists($filePath)) {
if (
!is_writable($filePath) &&
!in_array(
$this->getPermissionUtils()->getCurrentPermission($filePath),
[$defaultPermissions['file'], $defaultPermissions['dir']]
)
) {
return $this->getPermissionUtils()->setDefaultPermissions($filePath);
}
return true;
}
$pathParts = pathinfo($filePath);
if (!file_exists($pathParts['dirname'])) {
$dirPermissionOriginal = $defaultPermissions['dir'];
$dirPermission = is_string($dirPermissionOriginal) ?
(int) base_convert($dirPermissionOriginal, 8, 10) :
$dirPermissionOriginal;
if (!$this->mkdir($pathParts['dirname'], $dirPermission)) {
throw new PermissionError(
'Permission denied: unable to create a folder on the server ' . $pathParts['dirname']
);
}
}
$touchResult = touch($filePath);
if (!$touchResult) {
return false;
}
$setPermissionsResult = $this->getPermissionUtils()->setDefaultPermissions($filePath);
if (!$setPermissionsResult) {
$this->unlink($filePath);
/**
* Returning true will cause situations when files are created with
* a wrong ownership. This is a trade-off for being able to run
* Espo under a user that is neither webserver-user nor root. A file
* will be created owned by a user running the process.
*/
return true;
}
return true;
}
/**
* Remove a file or multiples files.
*
* @param string[]|string $filePaths File paths or a single path.
*/
public function unlink($filePaths): bool
{
return $this->removeFile($filePaths);
}
/**
* @deprecated Use removeDir.
* @param string[]|string $dirPaths
*/
public function rmdir($dirPaths): bool
{
if (!is_array($dirPaths)) {
$dirPaths = (array) $dirPaths;
}
$result = true;
foreach ($dirPaths as $dirPath) {
if (is_dir($dirPath) && is_writable($dirPath)) {
$result &= rmdir($dirPath);
}
}
return (bool) $result;
}
/**
* Remove a directory or multiple directories.
*
* @param string[]|string $dirPaths
*/
public function removeDir($dirPaths): bool
{
return $this->rmdir($dirPaths);
}
/**
* Remove file or multiples files.
*
* @param string[]|string $filePaths File paths or a single path.
* @param ?string $dirPath A directory path.
*/
public function removeFile($filePaths, $dirPath = null): bool
{
if (!is_array($filePaths)) {
$filePaths = (array) $filePaths;
}
$result = true;
foreach ($filePaths as $filePath) {
if (isset($dirPath)) {
$filePath = Util::concatPath($dirPath, $filePath);
}
if (file_exists($filePath) && is_file($filePath)) {
$this->opcacheInvalidate($filePath, true);
$result &= unlink($filePath);
}
}
return (bool) $result;
}
/**
* Remove all files inside a given directory.
*/
public function removeInDir(string $path, bool $removeWithDir = false): bool
{
/** @var string[] */
$fileList = $this->getFileList($path, false);
$result = true;
if (is_array($fileList)) {
foreach ($fileList as $file) {
$fullPath = Util::concatPath($path, $file);
if (is_dir($fullPath)) {
$result &= $this->removeInDir($fullPath, true);
}
else if (file_exists($fullPath)) {
$result &= unlink($fullPath);
}
}
}
if ($removeWithDir && $this->isDirEmpty($path)) {
$result &= $this->rmdir($path);
}
return (bool) $result;
}
/**
* Remove items (files or directories).
*
* @param string|string[] $items
* @param ?string $dirPath
*/
public function remove($items, $dirPath = null, bool $removeEmptyDirs = false): bool
{
if (!is_array($items)) {
$items = (array) $items;
}
$removeList = [];
$permissionDeniedList = [];
foreach ($items as $item) {
if (isset($dirPath)) {
$item = Util::concatPath($dirPath, $item);
}
if (!file_exists($item)) {
continue;
}
$removeList[] = $item;
if (!is_writable($item)) {
$permissionDeniedList[] = $item;
}
else if (!is_writable(dirname($item))) {
$permissionDeniedList[] = dirname($item);
}
}
if (!empty($permissionDeniedList)) {
$betterPermissionList = $this->getPermissionUtils()->arrangePermissionList($permissionDeniedList);
throw new PermissionError("Permission denied for <br>". implode(", <br>", $betterPermissionList));
}
$result = true;
foreach ($removeList as $item) {
if (is_dir($item)) {
$result &= $this->removeInDir($item, true);
}
else {
$result &= $this->removeFile($item);
}
if ($removeEmptyDirs) {
$result &= $this->removeEmptyDirs($item);
}
}
return (bool) $result;
}
/**
* Remove empty parent directories if they are empty.
*/
private function removeEmptyDirs(string $path): bool
{
$parentDirName = $this->getParentDirName($path);
$res = true;
if ($this->isDirEmpty($parentDirName)) {
$res &= $this->rmdir($parentDirName);
$res &= $this->removeEmptyDirs($parentDirName);
}
return (bool) $res;
}
/**
* Check whether a path is a directory.
*/
public function isDir(string $dirPath): bool
{
return is_dir($dirPath);
}
/**
* Check whether a file.
*/
public function isFile(string $path): bool
{
return is_file($path);
}
/**
* Get a file size in bytes.
*
* @throws FileError
*/
public function getSize(string $path): int
{
$size = filesize($path);
if ($size === false) {
throw new FileError("Could not get file size for `{$path}`.");
}
return $size;
}
/**
* Check whether a file or directory exists.
*/
public function exists(string $path): bool
{
return file_exists($path);
}
/**
* Check whether a file. If doesn't exist, check by path info.
*/
private function isFilenameIsFile(string $path): bool
{
if (file_exists($path)) {
return is_file($path);
}
$fileExtension = pathinfo($path, PATHINFO_EXTENSION);
if (!empty($fileExtension)) {
return true;
}
return false;
}
/**
* Check whether a directory is empty.
*/
public function isDirEmpty(string $path): bool
{
if (is_dir($path)) {
$fileList = $this->getFileList($path, true);
if (is_array($fileList) && empty($fileList)) {
return true;
}
}
return false;
}
/**
* Get a filename without a file extension.
*
* @param string $fileName
* @param string $extension Extension, example: `.json`.
*/
public function getFileName(string $fileName, string $extension = ''): string
{
if (empty($extension)) {
$dotIndex = strrpos($fileName, '.', -1);
if ($dotIndex === false) {
$dotIndex = strlen($fileName);
}
$fileName = substr($fileName, 0, $dotIndex);
}
else {
if (substr($extension, 0, 1) != '.') {
$extension = '.' . $extension;
}
if (substr($fileName, -(strlen($extension))) == $extension) {
$fileName = substr($fileName, 0, -(strlen($extension)));
}
}
$array = explode('/', Util::toFormat($fileName, '/'));
return end($array);
}
/**
* Get a directory name from the path.
*/
public function getDirName(string $path, bool $isFullPath = true, bool $useIsDir = true): string
{
/** @var string */
$dirName = preg_replace('/\/$/i', '', $path);
$dirName = ($useIsDir && is_dir($dirName)) ?
$dirName :
pathinfo($dirName, PATHINFO_DIRNAME);
if (!$isFullPath) {
$pieces = explode('/', $dirName);
$dirName = $pieces[count($pieces)-1];
}
return $dirName;
}
/**
* Get parent dir name/path.
*
* @param string $path
* @param boolean $isFullPath
* @return string
*/
public function getParentDirName(string $path, bool $isFullPath = true): string
{
return $this->getDirName($path, $isFullPath, false);
}
/**
* Wrap data for export to PHP file.
*
* @param array<mixed,mixed>|object|null $data
* @return string|false
*/
public function wrapForDataExport($data, bool $withObjects = false)
{
if (!isset($data)) {
return false;
}
if (!$withObjects) {
return "<?php\n" .
"return " . var_export($data, true) . ";\n";
}
return "<?php\n" .
"return " . $this->varExport($data) . ";\n";
}
/**
* @param mixed $variable
*/
private function varExport($variable, int $level = 0): string
{
$tab = '';
$tabElement = ' ';
for ($i = 0; $i <= $level; $i++) {
$tab .= $tabElement;
}
$prevTab = substr($tab, 0, strlen($tab) - strlen($tabElement));
if ($variable instanceof stdClass) {
return "(object) " . $this->varExport(get_object_vars($variable), $level);
}
if (is_array($variable)) {
$array = [];
foreach ($variable as $key => $value) {
$array[] = var_export($key, true) . " => " . $this->varExport($value, $level + 1);
}
if (count($array) === 0) {
return "[]";
}
return "[\n" . $tab . implode(",\n" . $tab, $array) . "\n" . $prevTab . "]";
}
return var_export($variable, true);
}
/**
* Check if $paths are writable. Permission denied list can be ontained
* with getLastPermissionDeniedList().
*
* @param string[] $paths
*/
public function isWritableList(array $paths): bool
{
$permissionDeniedList = [];
$result = true;
foreach ($paths as $path) {
$rowResult = $this->isWritable($path);
if (!$rowResult) {
$permissionDeniedList[] = $path;
}
$result &= $rowResult;
}
if (!empty($permissionDeniedList)) {
$this->permissionDeniedList =
$this->getPermissionUtils()->arrangePermissionList($permissionDeniedList);
}
return (bool) $result;
}
/**
* Get last permission denied list.
*
* @return string[]
*/
public function getLastPermissionDeniedList(): array
{
return $this->permissionDeniedList;
}
/**
* Check if $path is writable.
*/
public function isWritable(string $path): bool
{
$existFile = $this->getExistsPath($path);
return is_writable($existFile);
}
/**
* Check if $path is writable.
*/
public function isReadable(string $path): bool
{
$existFile = $this->getExistsPath($path);
return is_readable($existFile);
}
/**
* Get exists path.
* Example: If `/var/www/espocrm/custom/someFile.php` file doesn't exist,
* result will be `/var/www/espocrm/custom`.
*/
private function getExistsPath(string $path): string
{
if (!file_exists($path)) {
return $this->getExistsPath(pathinfo($path, PATHINFO_DIRNAME));
}
return $path;
}
/**
* @deprecated
* @todo Make private or move to `File\Util`.
*/
public function getRelativePath(string $path, ?string $basePath = null, ?string $dirSeparator = null): string
{
if (!$basePath) {
$basePath = getcwd();
}
if ($basePath === false) {
return '';
}
$path = Util::fixPath($path);
$basePath = Util::fixPath($basePath);
if (!$dirSeparator) {
$dirSeparator = Util::getSeparator();
}
if (substr($basePath, -1) != $dirSeparator) {
$basePath .= $dirSeparator;
}
/** @var string */
return preg_replace('/^'. preg_quote($basePath, $dirSeparator) . '/', '', $path);
}
private function opcacheInvalidate(string $filepath, bool $force = false): void
{
if (!function_exists('opcache_invalidate')) {
return;
}
try {
opcache_invalidate($filepath, $force);
}
catch (Throwable $e) {}
}
}