", "|"]; const RELATIVE_CURRENT = "."; const RELATIVE_PARENT = ".."; const LINUX_SEPARATOR = "/"; const WINDOWS_SEPARATOR = '\\'; const CANONICAL = "support"; /** * @throws ExceptionBadArgument */ public static function createFromUri($uri): LocalPath { if (strpos($uri, LocalFileSystem::SCHEME) !== 0) { throw new ExceptionBadArgument("$uri is not a local path uri"); } return new LocalPath($uri); } /** * @throws ExceptionBadArgument * @throws ExceptionCast */ public static function createFromPathObject(Path $path): LocalPath { if ($path instanceof LocalPath) { return $path; } if ($path instanceof WikiPath) { return $path->toLocalPath(); } throw new ExceptionBadArgument("The path is not a local path nor a wiki path, we can't transform it"); } /** * * @throws ExceptionNotFound - if the env directory is not found */ public static function createDesktopDirectory(): LocalPath { return LocalPath::createHomeDirectory()->resolve("Desktop"); } public function toUriString(): string { return $this->getUrl()->toString(); } private $path; /** * @var mixed */ private $sep = DIRECTORY_SEPARATOR; private ?string $host = null; /** * LocalPath constructor. * @param string $path - relative or absolute, or a locale file uri * @param string|null $sep - the directory separator - it permits to test linux path on windows, and vice-versa */ public function __construct(string $path, string $sep = null) { /** * php mon amour, * if we pass a {@link LocalPath}, no error, * it just pass the {@link PathAbs::__toString()} */ if (strpos($path, LocalFileSystem::SCHEME.'://') === 0) { try { $path = Url::createFromString($path)->getPath(); LogUtility::errorIfDevOrTest("The path given as constructor should not be an uri or a path object"); } catch (ExceptionBadArgument|ExceptionBadSyntax|ExceptionNotFound $e) { LogUtility::internalError("The uri path could not be created", self::CANONICAL, $e); } } if ($sep != null) { $this->sep = $sep; } // The network share windows/wiki styles with with two \\ and not // $networkShare = "\\\\"; if (substr($path, 0, 2) === $networkShare) { // window share $pathWithoutNetworkShare = substr($path, 2); $pathWithoutNetworkShare = str_replace("\\", "/", $pathWithoutNetworkShare); [$this->host, $relativePath] = explode("/", $pathWithoutNetworkShare, 2); $this->path = "/$relativePath"; return; } $this->path = self::normalizeToOsSeparator($path); } /** * @param string $filePath * @return LocalPath * @deprecated for {@link LocalPath::createFromPathString()} */ public static function create(string $filePath): LocalPath { return new LocalPath($filePath); } /** * @param $path * @return array|string|string[] * * For whatever reason, it seems that php/dokuwiki uses always the / separator on windows also * but not always (ie https://www.php.net/manual/en/function.realpath.php output \ on windows) * * Because we want to be able to copy the path value and to be able to use * it directly, we normalize it to the OS separator at build time */ private function normalizeToOsSeparator($path) { if ($path === self::RELATIVE_CURRENT || $path === self::RELATIVE_PARENT) { return realpath($path); } $directorySeparator = $this->getDirectorySeparator(); if ($directorySeparator === self::WINDOWS_SEPARATOR) { return str_replace(self::LINUX_SEPARATOR, self::WINDOWS_SEPARATOR, $path); } else { return str_replace(self::WINDOWS_SEPARATOR, self::LINUX_SEPARATOR, $path); } } /** * @throws ExceptionNotFound */ public static function createHomeDirectory(): LocalPath { $home = getenv("HOME"); if ($home === false) { $home = getenv("USERPROFILE"); } if ($home === false) { throw new ExceptionNotFound(" The home directory variable could not be found"); } return LocalPath::createFromPathString($home); } public static function createFromPathString(string $string, string $sep = null): LocalPath { return new LocalPath($string, $sep); } function getScheme(): string { return LocalFileSystem::SCHEME; } function getLastName(): string { $names = $this->getNames(); $sizeof = sizeof($names); if ($sizeof === 0) { throw new ExceptionNotFound("No last name for the path ($this)"); } return $names[$sizeof - 1]; } public function getExtension(): string { $extension = pathinfo($this->path, PATHINFO_EXTENSION); if ($extension === "") { throw new ExceptionNotFound("No extension found for the path ($this)"); } return $extension; } function getNames() { $directorySeparator = $this->getDirectorySeparator(); return explode($directorySeparator, $this->path); } function toAbsoluteId(): string { return $this->path; } public function getParent(): Path { $absolutePath = pathinfo($this->path, PATHINFO_DIRNAME); if ($absolutePath === $this->path || empty($absolutePath)) { // the directory on windows of the root (ie C:\) is (C:\), yolo ! throw new ExceptionNotFound("No parent"); } return new LocalPath($absolutePath); } function toAbsolutePath(): Path { if ($this->isAbsolute()) { return $this; } return $this->toCanonicalAbsolutePath(); } /** * @throws ExceptionBadArgument - if the path is not inside a drive */ public function toWikiPath(): WikiPath { return WikiPath::createFromPathObject($this); } public function resolve(string $name): LocalPath { $newPath = $this->toCanonicalAbsolutePath()->toAbsoluteId() . $this->getDirectorySeparator() . utf8_encodeFN($name); return self::createFromPathString($newPath); } /** * @throws ExceptionBadArgument - if the path cannot be relativized */ public function relativize(LocalPath $localPath): LocalPath { /** * One of the problem of relativization is * that it may be: * * logical (when using a symlink) * * physical */ if (!$this->isAbsolute() || $this->isShortName()) { /** * This is not a logical resolution * (if the path is logically not absolute and is a symlink, * we have a problem) */ $actualPath = $this->toCanonicalAbsolutePath(); } else { $actualPath = $this; } if (!$localPath->isAbsolute() || $localPath->isShortName()) { $localPath = $localPath->toCanonicalAbsolutePath(); } if (strpos($actualPath->toAbsoluteId(), $localPath->toAbsoluteId()) === 0) { if ($actualPath->toAbsoluteId() === $localPath->toAbsoluteId()) { return LocalPath::createFromPathString(""); } $sepCharacter = 1; // delete the sep characters $relativePath = substr($actualPath->toAbsoluteId(), strlen($localPath->toAbsoluteId()) + $sepCharacter); $relativePath = str_replace($this->getDirectorySeparator(), WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT, $relativePath); return LocalPath::createFromPathString($relativePath); } /** * May be a symlink link */ if ($this->isSymlink()) { $realPath = $this->toCanonicalAbsolutePath(); return $realPath->relativize($localPath); } if ($localPath->isSymlink()) { $localPath = $localPath->toCanonicalAbsolutePath(); $this->relativize($localPath); } throw new ExceptionBadArgument("The path ($localPath) is not a parent path of the actual path ($actualPath)"); } public function isAbsolute(): bool { /** * / * or a-z:\ */ if (preg_match("/^(\/|[a-z]:\\\\?).*/i", $this->path)) { return true; } return false; } /** * An absolute path may not be canonical * (ie windows short name or the path separator is not consistent (ie / in place of \ on windows) * * This function makes the path canonical meaning that two canonical path can be compared. * This is also needed when you path a path string to a php function such as `clearstatcache` * * If this is a symlink, it will resolve it to the real path */ public function toCanonicalAbsolutePath(): LocalPath { /** * realpath() is just a system/library call to actual realpath() function supported by OS. * real path handle also the windows name ie USERNAME~ */ $isSymlink = $this->isSymlink(); $realPath = realpath($this->path); if($isSymlink){ /** * * What fucked is fucked up * * With the symlink * D:/dokuwiki-animals/combo.nico.lan/data/pages * if you pass it to realpath: * ``` * realpath("D:/dokuwiki-animals/combo.nico.lan/data/pages") * ``` * you get: `d:\dokuwiki\website\pages` * if you pass the result again in realpath * ``` * realpath(d:\dokuwiki\website\pages) * ``` * we get another result `D:\dokuwiki\website\pages` * */ $realPath = realpath($realPath); } if ($realPath !== false) { return LocalPath::createFromPathString($realPath); } /** * It returns false on on file that does not exists. * The suggestion on the realpath man page * is to look for an existing parent directory. * https://man7.org/linux/man-pages/man3/realpath.3.html */ $parts = null; $isRoot = false; $counter = 0; // breaker $workingPath = $this->path; while ($realPath === false) { $counter++; $parent = dirname($workingPath); /** * From the doc: https://www.php.net/manual/en/function.dirname.php * dirname('.'); // Will return '.'. * dirname('/'); // Will return `\` on Windows and '/' on *nix systems. * dirname('\\'); // Will return `\` on Windows and '.' on *nix systems. * dirname('C:\\'); // Will return 'C:\' on Windows and '.' on *nix systems. * dirname('\'); // Will return `C:\` on Windows and ??? on *nix systems. */ if (preg_match("/^(\.|\/|\\\\|[a-z]:\\\\)$/i", $parent) || $parent === $workingPath || $parent === "\\" // bug on regexp ) { $isRoot = true; } // root, no need to delete the last sep $lastSep = 1; if ($isRoot) { $lastSep = 0; } $parts[] = substr($workingPath, strlen($parent) + $lastSep); $realPath = realpath($parent); if ($isRoot) { break; } if ($counter > 200) { $message = "Bad absolute local path file ($this->path)"; if (PluginUtility::isDevOrTest()) { throw new ExceptionRuntime($message); } else { LogUtility::msg($message); } return $this; } if ($realPath === false) { // loop $workingPath = $parent; } } if ($parts !== null) { if (!$isRoot) { $realPath .= $this->getDirectorySeparator(); } $parts = array_reverse($parts); $realPath .= implode($this->getDirectorySeparator(), $parts); } return LocalPath::createFromPathString($realPath); } public function getDirectorySeparator() { return $this->sep; } function getUrl(): Url { /** * file://host/path */ $uri = LocalFileSystem::SCHEME . '://'; try { // Windows share host $uri = "$uri{$this->getHost()}"; } catch (ExceptionNotFound $e) { // ok } $pathNormalized = str_replace(self::WINDOWS_SEPARATOR, self::LINUX_SEPARATOR, $this->path); if ($pathNormalized[0] !== "/") { $uri = $uri . "/" . $pathNormalized; } else { $uri = $uri . $pathNormalized; } try { return Url::createFromString($uri); } catch (ExceptionBadSyntax|ExceptionBadArgument $e) { $message = "Local Uri Path has a bad syntax ($uri)"; // should not happen LogUtility::internalError($message); throw new ExceptionRuntime($message); } } /** * @throws ExceptionNotFound */ function getHost(): string { if ($this->host === null) { throw new ExceptionNotFound("No host. Localhost should be the default"); } return $this->host; } public function isSymlink(): bool { return is_link($this->path); } private function isShortName(): bool { /** * See short name in windows * https://datacadamia.com/os/windows/path#pathname */ return strpos($this->path, "~1") !== false; } }