1<?php 2 3 4namespace ComboStrap; 5 6use ComboStrap\Web\Url; 7 8/** 9 * Class LocalPath 10 * @package ComboStrap 11 * A local file system path 12 * 13 * File protocol Uri: 14 * 15 * file://[HOST]/[PATH] 16 */ 17class LocalPath extends PathAbs 18{ 19 20 21 /** 22 * The characters that cannot be in the path for windows 23 * @var string[] 24 */ 25 public const RESERVED_WINDOWS_CHARACTERS = ["\\", "/", ":", "*", "?", "\"", "<", ">", "|"]; 26 27 const RELATIVE_CURRENT = "."; 28 const RELATIVE_PARENT = ".."; 29 const LINUX_SEPARATOR = "/"; 30 const WINDOWS_SEPARATOR = '\\'; 31 const CANONICAL = "support"; 32 33 /** 34 * @throws ExceptionBadArgument 35 */ 36 public static function createFromUri($uri): LocalPath 37 { 38 if (strpos($uri, LocalFileSystem::SCHEME) !== 0) { 39 throw new ExceptionBadArgument("$uri is not a local path uri"); 40 } 41 return new LocalPath($uri); 42 } 43 44 45 /** 46 * @throws ExceptionBadArgument 47 * @throws ExceptionCast 48 */ 49 public static function createFromPathObject(Path $path): LocalPath 50 { 51 if ($path instanceof LocalPath) { 52 return $path; 53 } 54 if ($path instanceof WikiPath) { 55 return $path->toLocalPath(); 56 } 57 throw new ExceptionBadArgument("The path is not a local path nor a wiki path, we can't transform it"); 58 } 59 60 /** 61 * 62 * @throws ExceptionNotFound - if the env directory is not found 63 */ 64 public static function createDesktopDirectory(): LocalPath 65 { 66 return LocalPath::createHomeDirectory()->resolve("Desktop"); 67 } 68 69 70 public function toUriString(): string 71 { 72 return $this->getUrl()->toString(); 73 } 74 75 private $path; 76 /** 77 * @var mixed 78 */ 79 private $sep = DIRECTORY_SEPARATOR; 80 81 private ?string $host = null; 82 83 /** 84 * LocalPath constructor. 85 * @param string $path - relative or absolute, or a locale file uri 86 * @param string|null $sep - the directory separator - it permits to test linux path on windows, and vice-versa 87 */ 88 public function __construct(string $path, string $sep = null) 89 { 90 /** 91 * php mon amour, 92 * if we pass a {@link LocalPath}, no error, 93 * it just pass the {@link PathAbs::__toString()} 94 */ 95 if (strpos($path, LocalFileSystem::SCHEME.'://') === 0) { 96 try { 97 $path = Url::createFromString($path)->getPath(); 98 LogUtility::errorIfDevOrTest("The path given as constructor should not be an uri or a path object"); 99 } catch (ExceptionBadArgument|ExceptionBadSyntax|ExceptionNotFound $e) { 100 LogUtility::internalError("The uri path could not be created", self::CANONICAL, $e); 101 } 102 } 103 if ($sep != null) { 104 $this->sep = $sep; 105 } 106 // The network share windows/wiki styles with with two \\ and not // 107 $networkShare = "\\\\"; 108 if (substr($path, 0, 2) === $networkShare) { 109 // window share 110 $pathWithoutNetworkShare = substr($path, 2); 111 $pathWithoutNetworkShare = str_replace("\\", "/", $pathWithoutNetworkShare); 112 [$this->host, $relativePath] = explode("/", $pathWithoutNetworkShare, 2); 113 $this->path = "/$relativePath"; 114 return; 115 } 116 $this->path = self::normalizeToOsSeparator($path); 117 } 118 119 120 /** 121 * @param string $filePath 122 * @return LocalPath 123 * @deprecated for {@link LocalPath::createFromPathString()} 124 */ 125 public static function create(string $filePath): LocalPath 126 { 127 return new LocalPath($filePath); 128 } 129 130 /** 131 * @param $path 132 * @return array|string|string[] 133 * 134 * For whatever reason, it seems that php/dokuwiki uses always the / separator on windows also 135 * but not always (ie https://www.php.net/manual/en/function.realpath.php output \ on windows) 136 * 137 * Because we want to be able to copy the path value and to be able to use 138 * it directly, we normalize it to the OS separator at build time 139 */ 140 private function normalizeToOsSeparator($path) 141 { 142 if ($path === self::RELATIVE_CURRENT || $path === self::RELATIVE_PARENT) { 143 return realpath($path); 144 } 145 $directorySeparator = $this->getDirectorySeparator(); 146 if ($directorySeparator === self::WINDOWS_SEPARATOR) { 147 return str_replace(self::LINUX_SEPARATOR, self::WINDOWS_SEPARATOR, $path); 148 } else { 149 return str_replace(self::WINDOWS_SEPARATOR, self::LINUX_SEPARATOR, $path); 150 } 151 } 152 153 /** 154 * @throws ExceptionNotFound 155 */ 156 public static function createHomeDirectory(): LocalPath 157 { 158 $home = getenv("HOME"); 159 if ($home === false) { 160 $home = getenv("USERPROFILE"); 161 } 162 if ($home === false) { 163 throw new ExceptionNotFound(" The home directory variable could not be found"); 164 } 165 return LocalPath::createFromPathString($home); 166 } 167 168 169 public static function createFromPathString(string $string, string $sep = null): LocalPath 170 { 171 return new LocalPath($string, $sep); 172 } 173 174 function getScheme(): string 175 { 176 return LocalFileSystem::SCHEME; 177 } 178 179 function getLastName(): string 180 { 181 $names = $this->getNames(); 182 $sizeof = sizeof($names); 183 if ($sizeof === 0) { 184 throw new ExceptionNotFound("No last name for the path ($this)"); 185 } 186 return $names[$sizeof - 1]; 187 188 } 189 190 191 public function getExtension(): string 192 { 193 $extension = pathinfo($this->path, PATHINFO_EXTENSION); 194 if ($extension === "") { 195 throw new ExceptionNotFound("No extension found for the path ($this)"); 196 } 197 return $extension; 198 } 199 200 function getNames() 201 { 202 $directorySeparator = $this->getDirectorySeparator(); 203 return explode($directorySeparator, $this->path); 204 } 205 206 207 function toAbsoluteId(): string 208 { 209 return $this->path; 210 } 211 212 public function getParent(): Path 213 { 214 $absolutePath = pathinfo($this->path, PATHINFO_DIRNAME); 215 if ($absolutePath === $this->path || empty($absolutePath)) { 216 // the directory on windows of the root (ie C:\) is (C:\), yolo ! 217 throw new ExceptionNotFound("No parent"); 218 } 219 return new LocalPath($absolutePath); 220 } 221 222 function toAbsolutePath(): Path 223 { 224 225 if ($this->isAbsolute()) { 226 return $this; 227 } 228 229 return $this->toCanonicalAbsolutePath(); 230 231 } 232 233 234 /** 235 * @throws ExceptionBadArgument - if the path is not inside a drive 236 */ 237 public function toWikiPath(): WikiPath 238 { 239 return WikiPath::createFromPathObject($this); 240 } 241 242 public function resolve(string $name): LocalPath 243 { 244 245 $newPath = $this->toCanonicalAbsolutePath()->toAbsoluteId() . $this->getDirectorySeparator() . utf8_encodeFN($name); 246 return self::createFromPathString($newPath); 247 248 } 249 250 /** 251 * @throws ExceptionBadArgument - if the path cannot be relativized 252 */ 253 public function relativize(LocalPath $localPath): LocalPath 254 { 255 256 /** 257 * One of the problem of relativization is 258 * that it may be: 259 * * logical (when using a symlink) 260 * * physical 261 */ 262 if (!$this->isAbsolute() || $this->isShortName()) { 263 /** 264 * This is not a logical resolution 265 * (if the path is logically not absolute and is a symlink, 266 * we have a problem) 267 */ 268 $actualPath = $this->toCanonicalAbsolutePath(); 269 } else { 270 $actualPath = $this; 271 } 272 if (!$localPath->isAbsolute() || $localPath->isShortName()) { 273 $localPath = $localPath->toCanonicalAbsolutePath(); 274 } 275 276 if (strpos($actualPath->toAbsoluteId(), $localPath->toAbsoluteId()) === 0) { 277 if ($actualPath->toAbsoluteId() === $localPath->toAbsoluteId()) { 278 return LocalPath::createFromPathString(""); 279 } 280 $sepCharacter = 1; // delete the sep characters 281 $relativePath = substr($actualPath->toAbsoluteId(), strlen($localPath->toAbsoluteId()) + $sepCharacter); 282 $relativePath = str_replace($this->getDirectorySeparator(), WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT, $relativePath); 283 return LocalPath::createFromPathString($relativePath); 284 } 285 /** 286 * May be a symlink link 287 */ 288 if ($this->isSymlink()) { 289 $realPath = $this->toCanonicalAbsolutePath(); 290 return $realPath->relativize($localPath); 291 } 292 if ($localPath->isSymlink()) { 293 $localPath = $localPath->toCanonicalAbsolutePath(); 294 $this->relativize($localPath); 295 } 296 throw new ExceptionBadArgument("The path ($localPath) is not a parent path of the actual path ($actualPath)"); 297 298 } 299 300 public function isAbsolute(): bool 301 { 302 /** 303 * / 304 * or a-z:\ 305 */ 306 if (preg_match("/^(\/|[a-z]:\\\\?).*/i", $this->path)) { 307 return true; 308 } 309 return false; 310 311 } 312 313 /** 314 * An absolute path may not be canonical 315 * (ie windows short name or the path separator is not consistent (ie / in place of \ on windows) 316 * 317 * This function makes the path canonical meaning that two canonical path can be compared. 318 * This is also needed when you path a path string to a php function such as `clearstatcache` 319 * 320 * If this is a symlink, it will resolve it to the real path 321 */ 322 public function toCanonicalAbsolutePath(): LocalPath 323 { 324 325 /** 326 * realpath() is just a system/library call to actual realpath() function supported by OS. 327 * real path handle also the windows name ie USERNAME~ 328 */ 329 $isSymlink = $this->isSymlink(); 330 $realPath = realpath($this->path); 331 if($isSymlink){ 332 /** 333 * 334 * What fucked is fucked up 335 * 336 * With the symlink 337 * D:/dokuwiki-animals/combo.nico.lan/data/pages 338 * if you pass it to realpath: 339 * ``` 340 * realpath("D:/dokuwiki-animals/combo.nico.lan/data/pages") 341 * ``` 342 * you get: `d:\dokuwiki\website\pages` 343 * if you pass the result again in realpath 344 * ``` 345 * realpath(d:\dokuwiki\website\pages) 346 * ``` 347 * we get another result `D:\dokuwiki\website\pages` 348 * 349 */ 350 $realPath = realpath($realPath); 351 } 352 if ($realPath !== false) { 353 return LocalPath::createFromPathString($realPath); 354 } 355 356 /** 357 * It returns false on on file that does not exists. 358 * The suggestion on the realpath man page 359 * is to look for an existing parent directory. 360 * https://man7.org/linux/man-pages/man3/realpath.3.html 361 */ 362 $parts = null; 363 $isRoot = false; 364 $counter = 0; // breaker 365 $workingPath = $this->path; 366 while ($realPath === false) { 367 $counter++; 368 $parent = dirname($workingPath); 369 /** 370 * From the doc: https://www.php.net/manual/en/function.dirname.php 371 * dirname('.'); // Will return '.'. 372 * dirname('/'); // Will return `\` on Windows and '/' on *nix systems. 373 * dirname('\\'); // Will return `\` on Windows and '.' on *nix systems. 374 * dirname('C:\\'); // Will return 'C:\' on Windows and '.' on *nix systems. 375 * dirname('\'); // Will return `C:\` on Windows and ??? on *nix systems. 376 */ 377 if (preg_match("/^(\.|\/|\\\\|[a-z]:\\\\)$/i", $parent) 378 || $parent === $workingPath 379 || $parent === "\\" // bug on regexp 380 ) { 381 $isRoot = true; 382 } 383 // root, no need to delete the last sep 384 $lastSep = 1; 385 if ($isRoot) { 386 $lastSep = 0; 387 } 388 $parts[] = substr($workingPath, strlen($parent) + $lastSep); 389 390 $realPath = realpath($parent); 391 if ($isRoot) { 392 break; 393 } 394 if ($counter > 200) { 395 $message = "Bad absolute local path file ($this->path)"; 396 if (PluginUtility::isDevOrTest()) { 397 throw new ExceptionRuntime($message); 398 } else { 399 LogUtility::msg($message); 400 } 401 return $this; 402 } 403 if ($realPath === false) { 404 // loop 405 $workingPath = $parent; 406 } 407 } 408 if ($parts !== null) { 409 if (!$isRoot) { 410 $realPath .= $this->getDirectorySeparator(); 411 } 412 $parts = array_reverse($parts); 413 $realPath .= implode($this->getDirectorySeparator(), $parts); 414 } 415 return LocalPath::createFromPathString($realPath); 416 } 417 418 public function getDirectorySeparator() 419 { 420 return $this->sep; 421 } 422 423 424 function getUrl(): Url 425 { 426 427 /** 428 * file://host/path 429 */ 430 $uri = LocalFileSystem::SCHEME . '://'; 431 try { 432 // Windows share host 433 $uri = "$uri{$this->getHost()}"; 434 } catch (ExceptionNotFound $e) { 435 // ok 436 } 437 $pathNormalized = str_replace(self::WINDOWS_SEPARATOR, self::LINUX_SEPARATOR, $this->path); 438 if ($pathNormalized[0] !== "/") { 439 $uri = $uri . "/" . $pathNormalized; 440 } else { 441 $uri = $uri . $pathNormalized; 442 } 443 try { 444 return Url::createFromString($uri); 445 } catch (ExceptionBadSyntax|ExceptionBadArgument $e) { 446 $message = "Local Uri Path has a bad syntax ($uri)"; 447 // should not happen 448 LogUtility::internalError($message); 449 throw new ExceptionRuntime($message); 450 } 451 452 } 453 454 /** 455 * @throws ExceptionNotFound 456 */ 457 function getHost(): string 458 { 459 if ($this->host === null) { 460 throw new ExceptionNotFound("No host. Localhost should be the default"); 461 } 462 return $this->host; 463 } 464 465 public function isSymlink(): bool 466 { 467 return is_link($this->path); 468 } 469 470 private function isShortName(): bool 471 { 472 /** 473 * See short name in windows 474 * https://datacadamia.com/os/windows/path#pathname 475 */ 476 return strpos($this->path, "~1") !== false; 477 } 478} 479