1<?php 2 3namespace ComboStrap; 4 5 6use ComboStrap\Web\Url; 7 8/** 9 * Class DokuPath 10 * @package ComboStrap 11 * A dokuwiki path has the same structure than a windows path with a drive and a path 12 * 13 * The drive being a local path on the local file system 14 * 15 * Ultimately, this path is the application path and should be used everywhere. 16 * * for users input (ie in a link markup such as media and page link) 17 * * for output (ie creating the id for the url) 18 * 19 * Dokuwiki knows only two drives ({@link WikiPath::MARKUP_DRIVE} and {@link WikiPath::MEDIA_DRIVE} 20 * but we have added a couple more such as the {@link WikiPath::COMBO_DRIVE combo resources} 21 * and the {@link WikiPath::CACHE_DRIVE} to be able to serve resources 22 * 23 * TODO: because all {@link LocalPath} has at minium a drive (ie C:,D:, E: for windows or \ for linux) 24 * A Wiki Path can be just a wrapper around every local path) 25 * The {@link LocalPath::toWikiPath()} should not throw then but as not all drive 26 * may be public, we need to add a drive functionality to get this information. 27 */ 28class WikiPath extends PathAbs 29{ 30 31 const MEDIA_DRIVE = "media"; 32 const MARKUP_DRIVE = "markup"; 33 const UNKNOWN_DRIVE = "unknown"; 34 const NAMESPACE_SEPARATOR_DOUBLE_POINT = ":"; 35 36 // https://www.dokuwiki.org/config:useslash 37 const NAMESPACE_SEPARATOR_SLASH = "/"; 38 39 const SEPARATORS = [self::NAMESPACE_SEPARATOR_DOUBLE_POINT, self::NAMESPACE_SEPARATOR_SLASH]; 40 41 /** 42 * For whatever reason, dokuwiki uses also on windows 43 * the linux separator 44 */ 45 public const DIRECTORY_SEPARATOR = "/"; 46 public const SLUG_SEPARATOR = "-"; 47 48 49 /** 50 * Dokuwiki has a file system that starts at a page and/or media 51 * directory that depends on the used syntax. 52 * 53 * It's a little bit the same than as the icon library (we set it as library then) 54 * 55 * This parameters is an URL parameter 56 * that permits to set an another one 57 * when retrieving the file via HTTP 58 * For now, there is only one value: {@link WikiPath::COMBO_DRIVE} 59 */ 60 public const DRIVE_ATTRIBUTE = "drive"; 61 62 /** 63 * The interwiki scheme that points to the 64 * combo resources directory ie {@link WikiPath::COMBO_DRIVE} 65 * ie 66 * combo>library: 67 * combo>image: 68 */ 69 const COMBO_DRIVE = "combo"; 70 /** 71 * The home directory for all themes 72 */ 73 const COMBO_DATA_THEME_DRIVE = "combo-theme"; 74 const CACHE_DRIVE = "cache"; 75 const MARKUP_DEFAULT_TXT_EXTENSION = "txt"; 76 const MARKUP_MD_TXT_EXTENSION = "md"; 77 const REV_ATTRIBUTE = "rev"; 78 const CURRENT_PATH_CHARACTER = "."; 79 const CURRENT_PARENT_PATH_CHARACTER = ".."; 80 const CANONICAL = "wiki-path"; 81 const ALL_MARKUP_EXTENSIONS = [self::MARKUP_DEFAULT_TXT_EXTENSION, self::MARKUP_MD_TXT_EXTENSION]; 82 83 84 /** 85 * @var string[] 86 */ 87 private static $reservedWords; 88 89 /** 90 * @var string the path id passed to function (cleaned) 91 */ 92 private $id; 93 94 95 /** 96 * @var string 97 */ 98 private $drive; 99 /** 100 * @var string|null - ie mtime 101 */ 102 private $rev; 103 104 105 /** 106 * The separator from the {@link WikiPath::getDrive()} 107 */ 108 const DRIVE_SEPARATOR = ">"; 109 /** 110 * @var string - the absolute path (we use it for now to handle directory by adding a separator at the end) 111 */ 112 protected $absolutePath; 113 114 /** 115 * DokuPath constructor. 116 * 117 * A path for the Dokuwiki File System 118 * 119 * @param string $path - the path (may be relative) 120 * @param string $drive - the drive (media, page, combo) - same as in windows for the drive prefix (c, d, ...) 121 * @param string|null $rev - the revision (mtime) 122 * 123 * Thee path should be a qualified/absolute path because in Dokuwiki, a link to a {@link MarkupPath} 124 * that ends with the {@link WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT} points to a start page 125 * and not to a namespace. The qualification occurs in the transformation 126 * from ref to page. 127 * For a page: in {@link MarkupRef::getInternalPage()} 128 * For a media: in the {@link MediaLink::createMediaLinkFromId()} 129 * Because this class is mostly the file representation, it should be able to 130 * represents also a namespace 131 */ 132 protected function __construct(string $path, string $drive, string $rev = null) 133 { 134 135 $executionContext = ExecutionContext::getActualOrCreateFromEnv(); 136 137 /** 138 * Due to the fact that the request environment is set on the setup in test, 139 * the path may be not normalized 140 */ 141 $path = self::normalizeWikiPath($path); 142 143 if (trim($path) === "") { 144 try { 145 $path = WikiPath::getContextPath()->toAbsoluteId(); 146 } catch (ExceptionNotFound $e) { 147 throw new ExceptionRuntimeInternal("The context path is unknwon. The empty path string needs it."); 148 } 149 } 150 151 /** 152 * Relative Path ? 153 */ 154 $this->absolutePath = $path; 155 $firstCharacter = substr($path, 0, 1); 156 if ($drive === self::MARKUP_DRIVE && $firstCharacter !== WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT) { 157 $parts = preg_split('/' . WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT . '/', $path); 158 switch ($parts[0]) { 159 case WikiPath::CURRENT_PATH_CHARACTER: 160 // delete the relative character 161 $parts = array_splice($parts, 1); 162 try { 163 $rootRelativePath = $executionContext->getContextNamespacePath(); 164 } catch (ExceptionNotFound $e) { 165 // Root case: the relative path is in the root 166 // the root has no parent 167 LogUtility::error("The current relative path ({$this->absolutePath}) returns an error: {$e->getMessage()}", self::CANONICAL); 168 $rootRelativePath = WikiPath::createMarkupPathFromPath(WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT); 169 } 170 break; 171 case WikiPath::CURRENT_PARENT_PATH_CHARACTER: 172 // delete the relative character 173 $parts = array_splice($parts, 1); 174 175 $currentPagePath = $executionContext->getContextNamespacePath(); 176 try { 177 $rootRelativePath = $currentPagePath->getParent(); 178 } catch (ExceptionNotFound $e) { 179 LogUtility::error("The parent relative path ({$this->absolutePath}) returns an error: {$e->getMessage()}", self::CANONICAL); 180 $rootRelativePath = $executionContext->getContextNamespacePath(); 181 } 182 183 break; 184 default: 185 /** 186 * just a relative name path 187 * (ie hallo) 188 */ 189 $rootRelativePath = $executionContext->getContextNamespacePath(); 190 break; 191 } 192 // is relative directory path ? 193 // ie ..: or .: 194 $isRelativeDirectoryPath = false; 195 $countParts = sizeof($parts); 196 if ($countParts > 0 && $parts[$countParts - 1] === "") { 197 $isRelativeDirectoryPath = true; 198 $parts = array_splice($parts, 0, $countParts - 1); 199 } 200 foreach ($parts as $part) { 201 $rootRelativePath = $rootRelativePath->resolve($part); 202 } 203 $absolutePathString = $rootRelativePath->getAbsolutePath(); 204 if ($isRelativeDirectoryPath && !WikiPath::isNamespacePath($absolutePathString)) { 205 $absolutePathString = $absolutePathString . WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT; 206 } 207 $this->absolutePath = $absolutePathString; 208 } 209 210 211 /** 212 * ACL check does not care about the type of id 213 * https://www.dokuwiki.org/devel:event:auth_acl_check 214 * https://github.com/splitbrain/dokuwiki/issues/3476 215 * 216 * We check if there is an extension 217 * If this is the case, this is a media 218 */ 219 if ($drive === self::UNKNOWN_DRIVE) { 220 $lastPosition = StringUtility::lastIndexOf($path, "."); 221 if ($lastPosition === FALSE) { 222 $drive = self::MARKUP_DRIVE; 223 } else { 224 $drive = self::MEDIA_DRIVE; 225 } 226 } 227 $this->drive = $drive; 228 229 230 /** 231 * We use interwiki to define the combo resources 232 * (Internal use only) 233 */ 234 $comboInterWikiScheme = "combo>"; 235 if (strpos($this->absolutePath, $comboInterWikiScheme) === 0) { 236 $pathPart = substr($this->absolutePath, strlen($comboInterWikiScheme)); 237 $this->id = $this->toDokuWikiIdDriveContextual($pathPart); 238 $this->drive = self::COMBO_DRIVE; 239 } else { 240 WikiPath::addRootSeparatorIfNotPresent($this->absolutePath); 241 $this->id = $this->toDokuWikiIdDriveContextual($this->absolutePath); 242 } 243 244 245 $this->rev = $rev; 246 247 } 248 249 250 /** 251 * For a Markup drive path, a file path should have an extension 252 * if it's not a namespace 253 * 254 * This function checks that 255 * 256 * @param string $parameterPath - the path in a wiki form that may be relative - if the path is blank, it's the current markup (the requested markup) 257 * @param string|null $rev - the revision (ie timestamp in number format) 258 * @return WikiPath - the wiki path 259 * @throws ExceptionBadArgument - if a relative path is given and the context path does not have any parent 260 */ 261 public static function createMarkupPathFromPath(string $parameterPath, string $rev = null): WikiPath 262 { 263 $executionContext = ExecutionContext::getActualOrCreateFromEnv(); 264 265 if ($parameterPath == "") { 266 return $executionContext->getContextPath(); 267 } 268 if (WikiPath::isNamespacePath($parameterPath)) { 269 270 if ($parameterPath[0] !== self::CURRENT_PATH_CHARACTER) { 271 /** 272 * Not a relative path 273 */ 274 return new WikiPath($parameterPath, self::MARKUP_DRIVE, $rev); 275 } 276 /** 277 * A relative path 278 */ 279 $contextPath = $executionContext->getContextPath(); 280 if ($parameterPath === self::CURRENT_PARENT_PATH_CHARACTER . self::NAMESPACE_SEPARATOR_DOUBLE_POINT) { 281 /** 282 * ie processing `..:` 283 */ 284 try { 285 return $contextPath->getParent()->getParent(); 286 } catch (ExceptionNotFound $e) { 287 throw new ExceptionBadArgument("The context path ($contextPath) does not have a grand parent, therefore the relative path ($parameterPath) is invalid.", $e); 288 } 289 } 290 /** 291 * ie processing `.:` 292 */ 293 try { 294 return $contextPath->getParent(); 295 } catch (ExceptionNotFound $e) { 296 LogUtility::internalError("A context path is a page and should therefore have a parent", $e); 297 } 298 299 } 300 301 /** 302 * Default Path 303 * (we add the txt extension if not present) 304 */ 305 $defaultPath = $parameterPath; 306 $lastName = $parameterPath; 307 $lastSeparator = strrpos($parameterPath, self::NAMESPACE_SEPARATOR_DOUBLE_POINT); 308 if ($lastSeparator !== false) { 309 $lastName = substr($parameterPath, $lastSeparator); 310 } 311 $lastPoint = strpos($lastName, "."); 312 if ($lastPoint === false) { 313 $defaultPath = $defaultPath . '.' . self::MARKUP_DEFAULT_TXT_EXTENSION; 314 } else { 315 /** 316 * Case such as file `1.22` 317 */ 318 $parameterPathExtension = substr($lastName, $lastPoint + 1); 319 if (!in_array($parameterPathExtension, self::ALL_MARKUP_EXTENSIONS)) { 320 $defaultPath = $defaultPath . '.' . self::MARKUP_DEFAULT_TXT_EXTENSION; 321 } 322 } 323 $defaultWikiPath = new WikiPath($defaultPath, self::MARKUP_DRIVE, $rev); 324 if (FileSystems::exists($defaultWikiPath)) { 325 return $defaultWikiPath; 326 } 327 328 /** 329 * Markup extension (Markdown, ...) 330 */ 331 if (!isset($parameterPathExtension)) { 332 foreach (self::ALL_MARKUP_EXTENSIONS as $markupExtension) { 333 if ($markupExtension == self::MARKUP_DEFAULT_TXT_EXTENSION) { 334 continue; 335 } 336 $markupWikiPath = new WikiPath($parameterPath . '.' . $markupExtension, self::MARKUP_DRIVE, $rev); 337 if (FileSystems::exists($markupWikiPath)) { 338 return $markupWikiPath; 339 } 340 } 341 } 342 343 /** 344 * Return the non-existen default wiki path 345 */ 346 return $defaultWikiPath; 347 348 } 349 350 351 public 352 static function createMediaPathFromPath($path, $rev = null): WikiPath 353 { 354 return new WikiPath($path, WikiPath::MEDIA_DRIVE, $rev); 355 } 356 357 /** 358 * If the media may come from the 359 * dokuwiki media or combo resources media, 360 * you should use this function 361 * 362 * The constructor will determine the type based on 363 * the id structure. 364 * @param $id 365 * @return WikiPath 366 */ 367 public 368 static function createFromUnknownRoot($id): WikiPath 369 { 370 return new WikiPath($id, WikiPath::UNKNOWN_DRIVE); 371 } 372 373 /** 374 * @param $url - a URL path http://whatever/hello/my/lord (The canonical) 375 * @return WikiPath - a dokuwiki Id hello:my:lord 376 * @deprecated for {@link FetcherPage::createPageFragmentFetcherFromUrl()} 377 */ 378 public 379 static function createFromUrl($url): WikiPath 380 { 381 // Replace / by : and suppress the first : because the global $ID does not have it 382 $parsedQuery = parse_url($url, PHP_URL_QUERY); 383 $parsedQueryArray = []; 384 parse_str($parsedQuery, $parsedQueryArray); 385 $queryId = 'id'; 386 if (array_key_exists($queryId, $parsedQueryArray)) { 387 // Doku form (ie doku.php?id=) 388 $id = $parsedQueryArray[$queryId]; 389 } else { 390 // Slash form ie (/my/id) 391 $urlPath = parse_url($url, PHP_URL_PATH); 392 $id = substr(str_replace("/", ":", $urlPath), 1); 393 } 394 return self::createMarkupPathFromPath(":$id"); 395 } 396 397 /** 398 * Static don't ask why 399 * @param $pathId 400 * @return false|string 401 */ 402 public 403 static function getLastPart($pathId) 404 { 405 $endSeparatorLocation = StringUtility::lastIndexOf($pathId, WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT); 406 if ($endSeparatorLocation === false) { 407 $endSeparatorLocation = StringUtility::lastIndexOf($pathId, WikiPath::NAMESPACE_SEPARATOR_SLASH); 408 } 409 if ($endSeparatorLocation === false) { 410 $lastPathPart = $pathId; 411 } else { 412 $lastPathPart = substr($pathId, $endSeparatorLocation + 1); 413 } 414 return $lastPathPart; 415 } 416 417 /** 418 * @param $id 419 * @return string 420 * Return an path from a id 421 */ 422 public 423 static function IdToAbsolutePath($id) 424 { 425 if (is_null($id)) { 426 LogUtility::msg("The id passed should not be null"); 427 } 428 return WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT . $id; 429 } 430 431 function toDokuWikiIdDriveContextual($path): string 432 { 433 /** 434 * Delete the first separator 435 */ 436 $id = self::removeRootSepIfPresent($path); 437 438 /** 439 * If this is a markup, we delete the txt extension if any 440 */ 441 if ($this->getDrive() === self::MARKUP_DRIVE) { 442 StringUtility::rtrim($id, '.' . self::MARKUP_DEFAULT_TXT_EXTENSION); 443 } 444 return $id; 445 446 } 447 448 public 449 static function createMediaPathFromId($id, $rev = null): WikiPath 450 { 451 WikiPath::addRootSeparatorIfNotPresent($id); 452 return self::createMediaPathFromPath($id, $rev); 453 } 454 455 public static function getComboCustomThemeHomeDirectory(): WikiPath 456 { 457 return new WikiPath(self::NAMESPACE_SEPARATOR_DOUBLE_POINT, self::COMBO_DATA_THEME_DRIVE); 458 } 459 460 /** 461 * @throws ExceptionBadArgument 462 */ 463 public 464 static function createFromUri(string $uri): WikiPath 465 { 466 467 $schemeQualified = WikiFileSystem::SCHEME . "://"; 468 $lengthSchemeQualified = strlen($schemeQualified); 469 $uriScheme = substr($uri, 0, $lengthSchemeQualified); 470 if ($uriScheme !== $schemeQualified) { 471 throw new ExceptionBadArgument("The uri ($uri) is not a wiki uri"); 472 } 473 $uriWithoutScheme = substr($uri, $lengthSchemeQualified); 474 $locationQuestionMark = strpos($uriWithoutScheme, "?"); 475 if ($locationQuestionMark === false) { 476 $pathAndDrive = $uriWithoutScheme; 477 $rev = ''; 478 } else { 479 $pathAndDrive = substr($uriWithoutScheme, 0, $locationQuestionMark); 480 $query = substr($uriWithoutScheme, $locationQuestionMark + 1); 481 parse_str($query, $queryKeys); 482 $queryKeys = new ArrayCaseInsensitive($queryKeys); 483 $rev = $queryKeys['rev']; 484 } 485 $locationGreaterThan = strpos($pathAndDrive, ">"); 486 if ($locationGreaterThan === false) { 487 $path = $pathAndDrive; 488 $locationLastPoint = strrpos($pathAndDrive, "."); 489 if ($locationLastPoint === false) { 490 $drive = WikiPath::MARKUP_DRIVE; 491 } else { 492 $extension = substr($pathAndDrive, $locationLastPoint + 1); 493 if (in_array($extension, WikiPath::ALL_MARKUP_EXTENSIONS)) { 494 $drive = WikiPath::MARKUP_DRIVE; 495 } else { 496 $drive = WikiPath::MEDIA_DRIVE; 497 } 498 } 499 } else { 500 $drive = substr($pathAndDrive, 0, $locationGreaterThan); 501 $path = substr($pathAndDrive, $locationGreaterThan + 1); 502 } 503 return new WikiPath(":$path", $drive, $rev); 504 } 505 506 507 public 508 static function createMarkupPathFromId($id, $rev = null): WikiPath 509 { 510 if (strpos($id, WikiFileSystem::SCHEME . "://") !== false) { 511 return WikiPath::createFromUri($id); 512 } 513 WikiPath::addRootSeparatorIfNotPresent($id); 514 return self::createMarkupPathFromPath($id); 515 } 516 517 /** 518 * If the id does not have a root separator, 519 * it's added (ie to transform an id to a path) 520 * @param string $path 521 */ 522 public 523 static function addRootSeparatorIfNotPresent(string &$path) 524 { 525 $firstCharacter = substr($path, 0, 1); 526 if (!in_array($firstCharacter, [WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT, WikiPath::CURRENT_PATH_CHARACTER])) { 527 $path = WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT . $path; 528 } 529 } 530 531 /** 532 * @param string $relativePath 533 * @return string - a dokuwiki path (replacing the windows or linux path separator to the dokuwiki separator) 534 */ 535 public 536 static function toDokuWikiSeparator(string $relativePath): string 537 { 538 return preg_replace('/[\\\\\/]/', ":", $relativePath); 539 } 540 541 542 /** 543 * @param $path - a manual path value 544 * @return string - a valid path 545 */ 546 public 547 static function toValidAbsolutePath($path): string 548 { 549 $path = cleanID($path); 550 WikiPath::addRootSeparatorIfNotPresent($path); 551 return $path; 552 } 553 554 /** 555 */ 556 public 557 static function createComboResource($stringPath): WikiPath 558 { 559 return new WikiPath($stringPath, self::COMBO_DRIVE); 560 } 561 562 563 /** 564 * @param $path - relative or absolute path 565 * @param $drive - the drive 566 * @param string $rev - the revision 567 * @return WikiPath 568 */ 569 public 570 static function createWikiPath($path, $drive, string $rev = ''): WikiPath 571 { 572 return new WikiPath($path, $drive, $rev); 573 } 574 575 /** 576 * The executing markup 577 * @throws ExceptionNotFound 578 */ 579 public 580 static function createExecutingMarkupWikiPath(): WikiPath 581 { 582 return ExecutionContext::getActualOrCreateFromEnv() 583 ->getExecutingWikiPath(); 584 585 } 586 587 588 /** 589 * @throws ExceptionNotFound 590 */ 591 public 592 static function createRequestedPagePathFromRequest(): WikiPath 593 { 594 return ExecutionContext::getActualOrCreateFromEnv()->getRequestedPath(); 595 } 596 597 /** 598 * @throws ExceptionBadArgument - if the path is not a local path or is not in a known drive 599 */ 600 public 601 static function createFromPathObject(Path $path): WikiPath 602 { 603 if ($path instanceof WikiPath) { 604 return $path; 605 } 606 if (!($path instanceof LocalPath)) { 607 throw new ExceptionBadArgument("The path ($path) is not a local path and cannot be converted to a wiki path"); 608 } 609 $driveRoots = WikiPath::getDriveRoots(); 610 611 foreach ($driveRoots as $driveRoot => $drivePath) { 612 613 try { 614 $relativePath = $path->relativize($drivePath); 615 } catch (ExceptionBadArgument $e) { 616 /** 617 * The drive may be a symlink link 618 * (not the path) 619 */ 620 if (!$drivePath->isSymlink()) { 621 continue; 622 } 623 try { 624 $drivePath = $drivePath->toCanonicalAbsolutePath(); 625 $relativePath = $path->relativize($drivePath); 626 } catch (ExceptionBadArgument $e) { 627 // not a relative path 628 continue; 629 } 630 } 631 $wikiId = $relativePath->toAbsoluteId(); 632 if (FileSystems::isDirectory($path)) { 633 WikiPath::addNamespaceEndSeparatorIfNotPresent($wikiId); 634 } 635 WikiPath::addRootSeparatorIfNotPresent($wikiId); 636 return WikiPath::createWikiPath($wikiId, $driveRoot); 637 638 } 639 throw new ExceptionBadArgument("The local path ($path) is not inside a wiki path drive"); 640 641 } 642 643 /** 644 * @return LocalPath[] 645 */ 646 public 647 static function getDriveRoots(): array 648 { 649 return [ 650 self::MEDIA_DRIVE => Site::getMediaDirectory(), 651 self::MARKUP_DRIVE => Site::getPageDirectory(), 652 self::COMBO_DRIVE => DirectoryLayout::getComboResourcesDirectory(), 653 self::COMBO_DATA_THEME_DRIVE => Site::getDataDirectory()->resolve("combo")->resolve("theme"), 654 self::CACHE_DRIVE => Site::getCacheDirectory() 655 ]; 656 } 657 658 /** 659 * 660 * Wiki path system cannot make the difference between a txt file 661 * and a directory natively because there is no extension. 662 * 663 * ie `ns:name` is by default the file `ns:name.txt` 664 * 665 * To make this distinction, we add a `:` at the end 666 * 667 * TODO: May be ? We may also just check if the txt file exists 668 * and if not if the directory exists 669 * 670 * Also related {@link WikiPath::addNamespaceEndSeparatorIfNotPresent()} 671 * 672 * @param string $namespacePath 673 * @return bool 674 */ 675 public 676 static function isNamespacePath(string $namespacePath): bool 677 { 678 if (substr($namespacePath, -1) !== WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT) { 679 return false; 680 } 681 return true; 682 683 } 684 685 /** 686 * @throws ExceptionBadSyntax 687 */ 688 public 689 static function checkNamespacePath(string $namespacePath) 690 { 691 if (!self::isNamespacePath($namespacePath)) { 692 throw new ExceptionBadSyntax("The path ($namespacePath) is not a namespace path"); 693 } 694 } 695 696 /** 697 * Add a end separator to the wiki path to pass the fact that this is a directory/namespace 698 * See {@link WikiPath::isNamespacePath()} for more info 699 * 700 * @param string $namespaceAttribute 701 * @return void 702 */ 703 public 704 static function addNamespaceEndSeparatorIfNotPresent(string &$namespaceAttribute) 705 { 706 if (substr($namespaceAttribute, -1) !== WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT) { 707 $namespaceAttribute = $namespaceAttribute . WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT; 708 } 709 } 710 711 /** 712 * @param string $path - path or id 713 * @param string $drive 714 * @param string|null $rev 715 * @return WikiPath 716 */ 717 public 718 static function createFromPath(string $path, string $drive, string $rev = null): WikiPath 719 { 720 return new WikiPath($path, $drive, $rev); 721 } 722 723 724 public 725 static function getContextPath(): WikiPath 726 { 727 return ExecutionContext::getActualOrCreateFromEnv()->getContextPath(); 728 } 729 730 /** 731 * Normalize a valid id 732 * (ie from / to :) 733 * 734 * @param string $id 735 * @return array|string|string[] 736 * 737 * This is not the same than {@link MarkupRef::normalizePath()} 738 * because there is no relativity or any reserved character in a id 739 * 740 * as an {@link WikiPath::getWikiId() id} is a validated absolute path without root character 741 */ 742 public 743 static function normalizeWikiPath(string $id) 744 { 745 return str_replace(WikiPath::NAMESPACE_SEPARATOR_SLASH, WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT, $id); 746 } 747 748 public 749 static function createRootNamespacePathOnMarkupDrive(): WikiPath 750 { 751 return WikiPath::createMarkupPathFromPath(self::NAMESPACE_SEPARATOR_DOUBLE_POINT); 752 } 753 754 /** 755 * @param $path 756 * @return string with the root path 757 */ 758 public 759 static function removeRootSepIfPresent($path): string 760 { 761 $id = $path; 762 if ($id[0] === WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT) { 763 return substr($id, 1); 764 } 765 return $id; 766 } 767 768 769 /** 770 * The last part of the path 771 * @throws ExceptionNotFound 772 */ 773 public 774 function getLastName(): string 775 { 776 /** 777 * See also {@link noNSorNS} 778 */ 779 $names = $this->getNames(); 780 $lastName = $names[sizeOf($names) - 1] ?? null; 781 if ($lastName === null) { 782 throw new ExceptionNotFound("This path ($this) does not have any last name"); 783 } 784 return $lastName; 785 } 786 787 public 788 function getNames(): array 789 { 790 791 $actualNames = explode(self::NAMESPACE_SEPARATOR_DOUBLE_POINT, $this->absolutePath); 792 793 /** 794 * First element can be an empty string 795 * Case of only one string without path separator 796 * the first element returned is an empty string 797 * Last element can be empty (namespace split, ie :ns:) 798 */ 799 $names = []; 800 foreach ($actualNames as $name) { 801 /** 802 * Don't use the {@link empty()} function 803 * In the cache, we may have the directory '0' 804 * and it's empty but is valid name 805 */ 806 if ($name !== "") { 807 $names[] = $name; 808 } 809 } 810 811 return $names; 812 } 813 814 /** 815 * @return bool true if this id represents a page 816 */ 817 public 818 function isPage(): bool 819 { 820 821 if ( 822 $this->drive === self::MARKUP_DRIVE 823 && 824 !$this->isGlob() 825 ) { 826 return true; 827 } else { 828 return false; 829 } 830 831 } 832 833 834 public 835 function isGlob(): bool 836 { 837 /** 838 * {@link search_universal} triggers ACL check 839 * with id of the form :path:* 840 * (for directory ?) 841 */ 842 return StringUtility::endWiths($this->getWikiId(), ":*"); 843 } 844 845 public 846 function __toString() 847 { 848 return $this->toUriString(); 849 } 850 851 /** 852 * 853 * 854 * @return string - the wiki id is the absolute path 855 * without the root separator (ie normalized) 856 * 857 * The index stores needs this value 858 * And most of the function that are not links related 859 * use this format (What fucked up is fucked up) 860 * 861 * The id is a validated absolute path without any root character. 862 * 863 * Heavily used inside Dokuwiki 864 */ 865 public 866 function getWikiId(): string 867 { 868 869 return $this->id; 870 871 } 872 873 public 874 function getPath(): string 875 { 876 877 return $this->absolutePath; 878 879 } 880 881 882 public 883 function getScheme(): string 884 { 885 886 return WikiFileSystem::SCHEME; 887 888 } 889 890 /** 891 * The wiki revision value 892 * as seen in the {@link basicinfo()} function 893 * is the {@link File::getModifiedTime()} of the file 894 * 895 * Let op passing a revision to Dokuwiki will 896 * make it search to the history 897 * The actual file will then not be found 898 * 899 * @return string|null 900 * @throws ExceptionNotFound 901 */ 902 public 903 function getRevision(): string 904 { 905 /** 906 * Empty because the value may be null or empty string 907 */ 908 if (empty($this->rev)) { 909 throw new ExceptionNotFound("The rev was not set"); 910 } 911 return $this->rev; 912 } 913 914 /** 915 * 916 * @throws ExceptionNotFound - if the revision is not set and the path does not exist 917 */ 918 public 919 function getRevisionOrDefault() 920 { 921 try { 922 return $this->getRevision(); 923 } catch (ExceptionNotFound $e) { 924 // same as $INFO['lastmod']; 925 return FileSystems::getModifiedTime($this)->getTimestamp(); 926 } 927 928 } 929 930 931 /** 932 * @return string 933 * 934 * This is the local absolute path WITH the root separator. 935 * It's used in ref present in {@link MarkupRef link} or {@link MediaLink} 936 * when creating test, otherwise the ref is considered as relative 937 * 938 * 939 * Otherwise everywhere in Dokuwiki, they use the {@link WikiPath::getWikiId()} absolute value that does not have any root separator 940 * and is absolute (internal index, function, ...) 941 * 942 */ 943 public 944 function getAbsolutePath(): string 945 { 946 947 return $this->absolutePath; 948 949 } 950 951 /** 952 * @return array the pages where the wiki file (page or media) is used 953 * * backlinks for page 954 * * page with media for media 955 */ 956 public 957 function getReferencedBy(): array 958 { 959 $absoluteId = $this->getWikiId(); 960 if ($this->drive == self::MEDIA_DRIVE) { 961 return idx_get_indexer()->lookupKey('relation_media', $absoluteId); 962 } else { 963 return idx_get_indexer()->lookupKey('relation_references', $absoluteId); 964 } 965 } 966 967 968 /** 969 * Return the path relative to the base directory 970 * (ie $conf[basedir]) 971 * @return string 972 */ 973 public 974 function toRelativeFileSystemPath(): string 975 { 976 $relativeSystemPath = "."; 977 if (!empty($this->getWikiId())) { 978 $relativeSystemPath .= "/" . utf8_encodeFN(str_replace(':', '/', $this->getWikiId())); 979 } 980 return $relativeSystemPath; 981 982 } 983 984 public 985 function isPublic(): bool 986 { 987 return $this->getAuthAclValue() >= AUTH_READ; 988 } 989 990 /** 991 * @return int - An AUTH_ value for this page for the current logged user 992 * See the file defines.php 993 * 994 */ 995 public 996 function getAuthAclValue(): int 997 { 998 return auth_quickaclcheck($this->getWikiId()); 999 } 1000 1001 1002 public 1003 static function getReservedWords(): array 1004 { 1005 if (self::$reservedWords == null) { 1006 self::$reservedWords = array_merge(Url::RESERVED_WORDS, LocalPath::RESERVED_WINDOWS_CHARACTERS); 1007 } 1008 return self::$reservedWords; 1009 } 1010 1011 1012 /** 1013 * The absolute path for a wiki path 1014 * @return string - the wiki id with a root separator 1015 */ 1016 function toAbsoluteId(): string 1017 { 1018 return self::NAMESPACE_SEPARATOR_DOUBLE_POINT . $this->getWikiId(); 1019 } 1020 1021 1022 function toAbsolutePath(): Path 1023 { 1024 return new WikiPath($this->absolutePath, $this->drive, $this->rev); 1025 } 1026 1027 /** 1028 * The parent path is a directory (namespace) 1029 * The root path throw an errors 1030 * 1031 * @return WikiPath 1032 * @throws ExceptionNotFound when the root 1033 */ 1034 function getParent(): Path 1035 { 1036 /** 1037 * Same as {@link getNS()} 1038 */ 1039 $names = $this->getNames(); 1040 switch (sizeof($names)) { 1041 case 0: 1042 throw new ExceptionNotFound("The path `{$this}` does not have any parent"); 1043 case 1: 1044 return new WikiPath(WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT, $this->drive, $this->rev); 1045 default: 1046 $names = array_slice($names, 0, sizeof($names) - 1); 1047 $path = implode(WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT, $names); 1048 /** 1049 * Because DokuPath does not have the notion of extension 1050 * if this is a page, we don't known if this is a directory 1051 * or a page. To make the difference, we add a separator at the end 1052 */ 1053 $sep = self::NAMESPACE_SEPARATOR_DOUBLE_POINT; 1054 $path = "$sep$path$sep"; 1055 return new WikiPath($path, $this->drive, $this->rev); 1056 } 1057 1058 } 1059 1060 /** 1061 * @throws ExceptionNotFound 1062 */ 1063 function getMime(): Mime 1064 { 1065 if ($this->drive === self::MARKUP_DRIVE) { 1066 return new Mime(Mime::PLAIN_TEXT); 1067 } 1068 return FileSystems::getMime($this); 1069 1070 } 1071 1072 1073 public 1074 function getDrive(): string 1075 { 1076 return $this->drive; 1077 } 1078 1079 public 1080 function resolve(string $name): WikiPath 1081 { 1082 1083 // Directory path have already separator at the end, don't add it 1084 if ($this->absolutePath[strlen($this->absolutePath) - 1] !== WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT) { 1085 $path = $this->absolutePath . WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT . $name; 1086 } else { 1087 $path = $this->absolutePath . $name; 1088 } 1089 return new WikiPath($path, $this->getDrive()); 1090 1091 } 1092 1093 1094 function toUriString(): string 1095 { 1096 $driveSep = self::DRIVE_SEPARATOR; 1097 $absolutePath = self::removeRootSepIfPresent($this->absolutePath); 1098 $uri = "{$this->getScheme()}://$this->drive$driveSep$absolutePath"; 1099 if (!empty($this->rev)) { 1100 $uri = "$uri?rev={$this->rev}"; 1101 } 1102 return $uri; 1103 1104 } 1105 1106 function getUrl(): Url 1107 { 1108 return $this->toLocalPath()->getUrl(); 1109 } 1110 1111 function getHost(): string 1112 { 1113 return "localhost"; 1114 } 1115 1116 public 1117 function resolveId($markupId): WikiPath 1118 { 1119 if ($this->getDrive() !== self::MARKUP_DRIVE) { 1120 return $this->resolve($markupId); 1121 } 1122 if (!WikiPath::isNamespacePath($this->absolutePath)) { 1123 try { 1124 $contextId = $this->getParent()->getWikiId() . self::NAMESPACE_SEPARATOR_DOUBLE_POINT; 1125 } catch (ExceptionNotFound $e) { 1126 $contextId = ""; 1127 } 1128 } else { 1129 $contextId = $this->getWikiId(); 1130 } 1131 return WikiPath::createMarkupPathFromId($contextId . $markupId); 1132 1133 } 1134 1135 /** 1136 * @return LocalPath 1137 * TODO: change it for a constructor on LocalPath 1138 * @throws ExceptionCast 1139 */ 1140 public 1141 function toLocalPath(): LocalPath 1142 { 1143 /** 1144 * File path 1145 */ 1146 $isNamespacePath = self::isNamespacePath($this->absolutePath); 1147 if ($isNamespacePath) { 1148 /** 1149 * Namespace 1150 * (Fucked up is fucked up) 1151 * We qualify for the namespace here 1152 * because there is no link or media for a namespace 1153 */ 1154 global $conf; 1155 switch ($this->drive) { 1156 case self::MEDIA_DRIVE: 1157 $localPath = LocalPath::createFromPathString($conf['mediadir']); 1158 break; 1159 case self::MARKUP_DRIVE: 1160 $localPath = LocalPath::createFromPathString($conf['datadir']); 1161 break; 1162 default: 1163 $localPath = WikiPath::getDriveRoots()[$this->drive]; 1164 break; 1165 } 1166 1167 foreach ($this->getNames() as $name) { 1168 $localPath = $localPath->resolve($name); 1169 } 1170 return $localPath; 1171 } 1172 1173 // File 1174 switch ($this->drive) { 1175 case self::MEDIA_DRIVE: 1176 if (!empty($rev)) { 1177 $filePathString = mediaFN($this->id, $rev); 1178 } else { 1179 $filePathString = mediaFN($this->id); 1180 } 1181 break; 1182 case self::MARKUP_DRIVE: 1183 /** 1184 * Adaptation of {@link WikiFN} 1185 */ 1186 global $conf; 1187 try { 1188 $extension = $this->getExtension(); 1189 } catch (ExceptionNotFound $e) { 1190 LogUtility::internalError("For a markup path file, the extension should have been set. This is not the case for ($this)"); 1191 $extension = self::MARKUP_DEFAULT_TXT_EXTENSION; 1192 } 1193 $idFileSystem = str_replace(':', '/', $this->id); 1194 if (empty($this->rev)) { 1195 $filePathString = Site::getPageDirectory()->resolve(utf8_encodeFN($idFileSystem) . '.' . $extension)->toAbsoluteId(); 1196 } else { 1197 $filePathString = Site::getOldDirectory()->resolve(utf8_encodeFN($idFileSystem) . '.' . $this->rev . '.' . $extension)->toAbsoluteId(); 1198 if ($conf['compression']) { 1199 //test for extensions here, we want to read both compressions 1200 if (file_exists($filePathString . '.gz')) { 1201 $filePathString .= '.gz'; 1202 } elseif (file_exists($filePathString . '.bz2')) { 1203 $filePathString .= '.bz2'; 1204 } else { 1205 // File doesnt exist yet, so we take the configured extension 1206 $filePathString .= '.' . $conf['compression']; 1207 } 1208 } 1209 } 1210 1211 break; 1212 default: 1213 $baseDirectory = WikiPath::getDriveRoots()[$this->drive]; 1214 if ($baseDirectory === null) { 1215 // We don't throw, the file will just not exist 1216 // this is metadata 1217 throw new ExceptionCast("The drive ($this->drive) is unknown, the local file system path could not be found"); 1218 } 1219 $filePath = $baseDirectory; 1220 foreach ($this->getNames() as $name) { 1221 $filePath = $filePath->resolve($name); 1222 } 1223 $filePathString = $filePath->toAbsoluteId(); 1224 break; 1225 } 1226 return LocalPath::createFromPathString($filePathString); 1227 1228 } 1229 1230 public function hasRevision(): bool 1231 { 1232 try { 1233 $this->getRevision(); 1234 return true; 1235 } catch (ExceptionNotFound $e) { 1236 return false; 1237 } 1238 } 1239 1240} 1241