Files
espocrm/application/Espo/Core/Utils/File/Manager.php
bsiggel 127fa6503b chore: Update copyright year from 2025 to 2026 across core files
- Updated copyright headers in 3,055 core application files
- Changed 'Copyright (C) 2014-2025' to 'Copyright (C) 2014-2026'
- Added 123 new files from EspoCRM core updates
- Removed 4 deprecated files
- Total changes: 61,637 insertions, 54,283 deletions

This is a routine maintenance update for the new year 2026.
2026-02-07 16:05:21 +01:00

1164 lines
32 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://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 Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero 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\Json;
use Espo\Core\Utils\Util;
use Espo\Core\Utils\File\Exceptions\FileError;
use Espo\Core\Utils\File\Exceptions\PermissionError;
use stdClass;
use Throwable;
use const PHP_OS;
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;
}
$dirItems = scandir($path) ?: [];
foreach ($dirItems as $value) {
if (in_array($value, [".", ".."])) {
continue;
}
$add = false;
if (is_dir($path . Util::getSeparator() . $value)) {
/** @var mixed $recursively */
if (
!is_int($recursively) && $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) {
continue;
}
if (!empty($filter)) {
if (preg_match('/'.$filter.'/i', $value)) {
$result[] = $value;
}
continue;
}
$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(string $path): string
{
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<int|string, 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 $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 $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<string|int, 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 $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 $this->putJsonContents($path, $unsettedData);
}
/**
* 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);
}
if (is_dir($path)) {
return true;
}
$result = mkdir($path, $permission);
if (!$result && is_dir($path)) {
// Dir can be created by a concurrent process.
return true;
}
// Needs if umask was applied.
@chmod($path, $permission);
if (!empty($defaultPermissions['user'])) {
$this->getPermissionUtils()->chown($path);
}
if (!empty($defaultPermissions['group'])) {
$this->getPermissionUtils()->chgrp($path);
}
return $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);
/** @var string $dirname */
$dirname = $pathParts['dirname'] ?? null;
if (!file_exists($dirname)) {
$dirPermissionOriginal = $defaultPermissions['dir'];
$dirPermission = is_string($dirPermissionOriginal) ?
(int) base_convert($dirPermissionOriginal, 8, 10) :
$dirPermissionOriginal;
if (!$this->mkdir($dirname, $dirPermission)) {
throw new PermissionError('Permission denied: unable to create a folder on the server ' . $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 */
$fileList = $this->getFileList($path);
$result = true;
// @todo Remove the if statement.
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)) {
$this->opcacheInvalidate($fullPath, true);
$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.
*
* @phpstan-impure
*/
public function isDir(string $dirPath): bool
{
return is_dir($dirPath);
}
/**
* Check whether a file.
*
* @phpstan-impure
*/
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 it 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 (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 (!str_starts_with($extension, '.')) {
$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 */
$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<string|int, 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
{
$tabElement = ' ';
$tab = str_repeat($tabElement, $level + 1);
$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 obtained
* 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) {}
}
}