1<?php 2 3namespace ComboStrap; 4 5require_once(__DIR__ . '/PluginUtility.php'); 6 7/** 8 * Class DokuPath 9 * @package ComboStrap 10 * A dokuwiki path has the same structure than a windows path 11 * with a drive and a path 12 * 13 * The drive is just a local path on the local file system 14 * 15 * Dokuwiki knows only two drives ({@link DokuPath::PAGE_DRIVE} and {@link DokuPath::MEDIA_DRIVE} 16 * but we have added a couple more such as the {@link DokuPath::COMBO_DRIVE combo resources} 17 * and the {@link DokuPath::CACHE_DRIVE} 18 * 19 */ 20class DokuPath extends PathAbs 21{ 22 const MEDIA_DRIVE = "media"; 23 const PAGE_DRIVE = "page"; 24 const UNKNOWN_DRIVE = "unknown"; 25 const PATH_SEPARATOR = ":"; 26 27 // https://www.dokuwiki.org/config:useslash 28 const SEPARATOR_SLASH = "/"; 29 30 const SEPARATORS = [self::PATH_SEPARATOR, self::SEPARATOR_SLASH]; 31 32 /** 33 * For whatever reason, dokuwiki uses also on windows 34 * the linux separator 35 */ 36 public const DIRECTORY_SEPARATOR = "/"; 37 public const SLUG_SEPARATOR = "-"; 38 39 40 /** 41 * Dokuwiki has a file system that starts at a page and/or media 42 * directory that depends on the used syntax. 43 * 44 * It's a little bit the same than as the icon library (we set it as library then) 45 * 46 * This parameters is an URL parameter 47 * that permits to set an another one 48 * when retrieving the file via HTTP 49 * For now, there is only one value: {@link DokuPath::COMBO_DRIVE} 50 */ 51 public const DRIVE_ATTRIBUTE = "drive"; 52 53 /** 54 * The interwiki scheme that points to the 55 * combo resources directory ie {@link DokuPath::COMBO_DRIVE} 56 * ie 57 * combo>library: 58 * combo>image: 59 */ 60 const COMBO_DRIVE = "combo"; 61 const CACHE_DRIVE = "cache"; 62 const DRIVES = [self::COMBO_DRIVE, self::CACHE_DRIVE, self::MEDIA_DRIVE]; 63 64 /** 65 * @var string[] 66 */ 67 private static $reservedWords; 68 69 /** 70 * @var string the path id passed to function (cleaned) 71 */ 72 private $id; 73 74 /** 75 * @var string the absolute id with the root separator 76 * See {@link $id} for the absolute id without root separator for the index 77 */ 78 private $absolutePath; 79 80 /** 81 * @var string 82 */ 83 private $drive; 84 /** 85 * @var string|null - ie mtime 86 */ 87 private $rev; 88 89 90 /** 91 * @var string the path scheme one constant that starts with SCHEME 92 * ie 93 * {@link DokuFs::SCHEME} 94 */ 95 private $scheme; 96 private $filePath; 97 98 /** 99 * The separator from the {@link DokuPath::getDrive()} 100 * Same as {@link InterWikiPath} 101 */ 102 const DRIVE_SEPARATOR = ">"; 103 104 /** 105 * DokuPath constructor. 106 * 107 * A path for the Dokuwiki File System 108 * 109 * @param string $path - the dokuwiki absolute path (may not be relative but may be a namespace) 110 * @param string $drive - the drive (media, page, combo) - same as in windows for the drive prefix (c, d, ...) 111 * @param string|null $rev - the revision (mtime) 112 * 113 * Thee path should be a qualified/absolute path because in Dokuwiki, a link to a {@link Page} 114 * that ends with the {@link DokuPath::PATH_SEPARATOR} points to a start page 115 * and not to a namespace. The qualification occurs in the transformation 116 * from ref to page. 117 * For a page: in {@link MarkupRef::getInternalPage()} 118 * For a media: in the {@link MediaLink::createMediaLinkFromId()} 119 * Because this class is mostly the file representation, it should be able to 120 * represents also a namespace 121 */ 122 protected function __construct(string $path, string $drive, string $rev = null) 123 { 124 125 if (empty($path)) { 126 LogUtility::msg("A null path was given", LogUtility::LVL_MSG_WARNING); 127 } 128 129 130 /** 131 * Scheme determination 132 */ 133 $this->scheme = $this->schemeDetermination($path); 134 135 switch ($this->scheme) { 136 case InterWikiPath::scheme: 137 /** 138 * We use interwiki to define the combo resources 139 * (Internal use only) 140 */ 141 $comboInterWikiScheme = "combo>"; 142 if (strpos($path, $comboInterWikiScheme) === 0) { 143 $this->scheme = DokuFs::SCHEME; 144 $this->id = substr($path, strlen($comboInterWikiScheme)); 145 $drive = self::COMBO_DRIVE; 146 }; 147 break; 148 case DokuFs::SCHEME: 149 default: 150 DokuPath::addRootSeparatorIfNotPresent($path); 151 $this->id = DokuPath::toDokuwikiId($path); 152 153 } 154 $this->absolutePath = $path; 155 156 157 /** 158 * ACL check does not care about the type of id 159 * https://www.dokuwiki.org/devel:event:auth_acl_check 160 * https://github.com/splitbrain/dokuwiki/issues/3476 161 * 162 * We check if there is an extension 163 * If this is the case, this is a media 164 */ 165 if ($drive == self::UNKNOWN_DRIVE) { 166 $lastPosition = StringUtility::lastIndexOf($path, "."); 167 if ($lastPosition === FALSE) { 168 $drive = self::PAGE_DRIVE; 169 } else { 170 $drive = self::MEDIA_DRIVE; 171 } 172 } 173 $this->drive = $drive; 174 $this->rev = $rev; 175 176 /** 177 * File path 178 */ 179 $filePath = $this->absolutePath; 180 if ($this->scheme == DokuFs::SCHEME) { 181 182 $isNamespacePath = false; 183 if (\mb_substr($this->absolutePath, -1) == self::PATH_SEPARATOR) { 184 $isNamespacePath = true; 185 } 186 187 global $ID; 188 189 if (!$isNamespacePath) { 190 191 switch ($drive) { 192 193 case self::MEDIA_DRIVE: 194 if (!empty($rev)) { 195 $filePath = mediaFN($this->id, $rev); 196 } else { 197 $filePath = mediaFN($this->id); 198 } 199 break; 200 case self::PAGE_DRIVE: 201 if (!empty($rev)) { 202 $filePath = wikiFN($this->id, $rev); 203 } else { 204 $filePath = wikiFN($this->id); 205 } 206 break; 207 default: 208 $baseDirectory = DokuPath::getDriveRoots()[$drive]; 209 if ($baseDirectory === null) { 210 // We don't throw, the file will just not exist 211 // this is metadata 212 LogUtility::msg("The drive ($drive) is unknown, the local file system path could not be found"); 213 } else { 214 $relativeFsPath = DokuPath::toFileSystemSeparator($this->id); 215 $filePath = $baseDirectory->resolve($relativeFsPath)->toString(); 216 } 217 break; 218 } 219 } else { 220 /** 221 * Namespace 222 * (Fucked up is fucked up) 223 * We qualify for the namespace here 224 * because there is no link or media for a namespace 225 */ 226 $this->id = resolve_id(getNS($ID), $this->id, true); 227 global $conf; 228 if ($drive == self::MEDIA_DRIVE) { 229 $filePath = $conf['mediadir'] . '/' . utf8_encodeFN($this->id); 230 } else { 231 $filePath = $conf['datadir'] . '/' . utf8_encodeFN($this->id); 232 } 233 } 234 } 235 $this->filePath = $filePath; 236 } 237 238 239 /** 240 * 241 * @param $absolutePath 242 * @return DokuPath 243 */ 244 public static function createPagePathFromPath($absolutePath): DokuPath 245 { 246 return new DokuPath($absolutePath, DokuPath::PAGE_DRIVE); 247 } 248 249 public static function createMediaPathFromAbsolutePath($absolutePath, $rev = ''): DokuPath 250 { 251 return new DokuPath($absolutePath, DokuPath::MEDIA_DRIVE, $rev); 252 } 253 254 /** 255 * If the media may come from the 256 * dokuwiki media or combo resources media, 257 * you should use this function 258 * 259 * The constructor will determine the type based on 260 * the id structure. 261 * @param $id 262 * @return DokuPath 263 */ 264 public static function createFromUnknownRoot($id): DokuPath 265 { 266 return new DokuPath($id, DokuPath::UNKNOWN_DRIVE); 267 } 268 269 /** 270 * @param $url - a URL path http://whatever/hello/my/lord (The canonical) 271 * @return DokuPath - a dokuwiki Id hello:my:lord 272 */ 273 public static function createFromUrl($url): DokuPath 274 { 275 // Replace / by : and suppress the first : because the global $ID does not have it 276 $parsedQuery = parse_url($url, PHP_URL_QUERY); 277 $parsedQueryArray = []; 278 parse_str($parsedQuery, $parsedQueryArray); 279 $queryId = 'id'; 280 if (array_key_exists($queryId, $parsedQueryArray)) { 281 // Doku form (ie doku.php?id=) 282 $id = $parsedQueryArray[$queryId]; 283 } else { 284 // Slash form ie (/my/id) 285 $urlPath = parse_url($url, PHP_URL_PATH); 286 $id = substr(str_replace("/", ":", $urlPath), 1); 287 } 288 return self::createPagePathFromPath(":$id"); 289 } 290 291 /** 292 * Static don't ask why 293 * @param $pathId 294 * @return false|string 295 */ 296 public static function getLastPart($pathId) 297 { 298 $endSeparatorLocation = StringUtility::lastIndexOf($pathId, DokuPath::PATH_SEPARATOR); 299 if ($endSeparatorLocation === false) { 300 $endSeparatorLocation = StringUtility::lastIndexOf($pathId, DokuPath::SEPARATOR_SLASH); 301 } 302 if ($endSeparatorLocation === false) { 303 $lastPathPart = $pathId; 304 } else { 305 $lastPathPart = substr($pathId, $endSeparatorLocation + 1); 306 } 307 return $lastPathPart; 308 } 309 310 /** 311 * @param $id 312 * @return string 313 * Return an path from a id 314 */ 315 public static function IdToAbsolutePath($id) 316 { 317 if (is_null($id)) { 318 LogUtility::msg("The id passed should not be null"); 319 } 320 return DokuPath::PATH_SEPARATOR . $id; 321 } 322 323 public 324 static function toDokuwikiId($absolutePath) 325 { 326 // Root ? 327 if ($absolutePath == DokuPath::PATH_SEPARATOR) { 328 return ""; 329 } 330 if ($absolutePath[0] === DokuPath::PATH_SEPARATOR) { 331 return substr($absolutePath, 1); 332 } 333 return $absolutePath; 334 335 } 336 337 public static function createMediaPathFromId($id, $rev = ''): DokuPath 338 { 339 DokuPath::addRootSeparatorIfNotPresent($id); 340 return self::createMediaPathFromAbsolutePath($id, $rev); 341 } 342 343 344 public static function createPagePathFromId($id): DokuPath 345 { 346 return new DokuPath(DokuPath::PATH_SEPARATOR . $id, self::PAGE_DRIVE); 347 } 348 349 /** 350 * If the path does not have a root separator, 351 * it's added (ie to transform an id to a path) 352 * @param string $path 353 */ 354 public static function addRootSeparatorIfNotPresent(string &$path) 355 { 356 if (substr($path, 0, 1) !== ":") { 357 $path = DokuPath::PATH_SEPARATOR . $path; 358 } 359 } 360 361 /** 362 * @param string $relativePath 363 * @return string - a dokuwiki path (replacing the windows or linux path separator to the dokuwiki separator) 364 */ 365 public static function toDokuWikiSeparator(string $relativePath): string 366 { 367 return preg_replace('/[\\\\\/]/', ":", $relativePath); 368 } 369 370 public static function toFileSystemSeparator($dokuPath) 371 { 372 return str_replace(":", self::DIRECTORY_SEPARATOR, $dokuPath); 373 } 374 375 /** 376 * @param $path - a manual path value 377 * @return string - a valid path 378 */ 379 public static function toValidAbsolutePath($path): string 380 { 381 $path = cleanID($path); 382 DokuPath::addRootSeparatorIfNotPresent($path); 383 return $path; 384 } 385 386 /** 387 */ 388 public static function createComboResource($dokuwikiId): DokuPath 389 { 390 return new DokuPath($dokuwikiId, self::COMBO_DRIVE); 391 } 392 393 /** 394 */ 395 public static function createDokuPath($path, $drive, $rev = ''): DokuPath 396 { 397 return new DokuPath($path, $drive, $rev); 398 } 399 400 public static function getDriveRoots(): array 401 { 402 return [ 403 self::MEDIA_DRIVE => Site::getMediaDirectory(), 404 self::PAGE_DRIVE => Site::getPageDirectory(), 405 self::COMBO_DRIVE => Site::getComboResourcesDirectory(), 406 self::CACHE_DRIVE => Site::getCacheDirectory() 407 ]; 408 } 409 410 411 /** 412 * The last part of the path 413 */ 414 public 415 function getLastName() 416 { 417 /** 418 * See also {@link noNSorNS} 419 */ 420 $names = $this->getNames(); 421 return $names[sizeOf($names) - 1]; 422 } 423 424 /** 425 * @return null|string 426 */ 427 public function getLastNameWithoutExtension(): ?string 428 { 429 /** 430 * A page doku path has no extension for now 431 */ 432 if ($this->drive === self::PAGE_DRIVE) { 433 return $this->getLastName(); 434 } 435 return parent::getLastNameWithoutExtension(); 436 437 } 438 439 440 public 441 function getNames() 442 { 443 444 $names = explode(self::PATH_SEPARATOR, $this->getDokuwikiId()); 445 446 if ($names[0] === "") { 447 /** 448 * Case of only one string without path separator 449 * the first element returned is an empty string 450 */ 451 $names = array_splice($names, 1); 452 } 453 return $names; 454 } 455 456 /** 457 * @return bool true if this id represents a page 458 */ 459 public function isPage(): bool 460 { 461 462 if ( 463 $this->drive === self::PAGE_DRIVE 464 && 465 !$this->isGlob() 466 ) { 467 return true; 468 } else { 469 return false; 470 } 471 472 } 473 474 475 public function isGlob(): bool 476 { 477 /** 478 * {@link search_universal} triggers ACL check 479 * with id of the form :path:* 480 * (for directory ?) 481 */ 482 return StringUtility::endWiths($this->getDokuwikiId(), ":*"); 483 } 484 485 public 486 function __toString() 487 { 488 return $this->toUriString(); 489 } 490 491 /** 492 * 493 * 494 * @return string - the id of dokuwiki is the absolute path 495 * without the root separator (ie normalized) 496 * 497 * The index stores needs this value 498 * And most of the function that are not links related 499 * use this format (What fucked up is fucked up) 500 * /** 501 * The absolute path without root separator 502 * Heavily used inside Dokuwiki 503 */ 504 public 505 function getDokuwikiId(): string 506 { 507 508 if ($this->getScheme() == DokuFs::SCHEME) { 509 return $this->id; 510 } else { 511 // the url (it's stored as id in the metadata) 512 return $this->getPath(); 513 } 514 515 } 516 517 public 518 function getPath(): string 519 { 520 521 return $this->absolutePath; 522 523 } 524 525 public 526 function getScheme() 527 { 528 529 return $this->scheme; 530 531 } 532 533 /** 534 * The dokuwiki revision value 535 * as seen in the {@link basicinfo()} function 536 * is the {@link File::getModifiedTime()} of the file 537 * 538 * Let op passing a revision to Dokuwiki will 539 * make it search to the history 540 * The actual file will then not be found 541 * 542 * @return string|null 543 */ 544 public 545 function getRevision(): ?string 546 { 547 if ($this->rev === null) { 548 $localPath = $this->toLocalPath(); 549 if (FileSystems::exists($localPath)) { 550 return FileSystems::getModifiedTime($localPath)->getTimestamp(); 551 } 552 } 553 return $this->rev; 554 } 555 556 557 /** 558 * @return string 559 * 560 * This is the local absolute path WITH the root separator. 561 * It's used in ref present in {@link MarkupRef link} or {@link MediaLink} 562 * when creating test, otherwise the ref is considered as relative 563 * 564 * 565 * Otherwise everywhere in Dokuwiki, they use the {@link DokuPath::getDokuwikiId()} absolute value that does not have any root separator 566 * and is absolute (internal index, function, ...) 567 * 568 */ 569 public 570 function getAbsolutePath(): string 571 { 572 573 return $this->absolutePath; 574 575 } 576 577 /** 578 * @return array the pages where the dokuwiki file (page or media) is used 579 * * backlinks for page 580 * * page with media for media 581 */ 582 public 583 function getReferencedBy(): array 584 { 585 $absoluteId = $this->getDokuwikiId(); 586 if ($this->drive == self::MEDIA_DRIVE) { 587 return idx_get_indexer()->lookupKey('relation_media', $absoluteId); 588 } else { 589 return idx_get_indexer()->lookupKey('relation_references', $absoluteId); 590 } 591 } 592 593 594 /** 595 * Return the path relative to the base directory 596 * (ie $conf[basedir]) 597 * @return string 598 */ 599 public 600 function toRelativeFileSystemPath() 601 { 602 $relativeSystemPath = "."; 603 if (!empty($this->getDokuwikiId())) { 604 $relativeSystemPath .= "/" . utf8_encodeFN(str_replace(':', '/', $this->getDokuwikiId())); 605 } 606 return $relativeSystemPath; 607 608 } 609 610 public function isPublic(): bool 611 { 612 return $this->getAuthAclValue() >= AUTH_READ; 613 } 614 615 /** 616 * @return int - An AUTH_ value for this page for the current logged user 617 * See the file defines.php 618 * 619 */ 620 public function getAuthAclValue(): int 621 { 622 return auth_quickaclcheck($this->getDokuwikiId()); 623 } 624 625 626 public static function getReservedWords(): array 627 { 628 if (self::$reservedWords == null) { 629 self::$reservedWords = array_merge(Url::RESERVED_WORDS, LocalPath::RESERVED_WINDOWS_CHARACTERS); 630 } 631 return self::$reservedWords; 632 } 633 634 /** 635 * @return string - a label from a path (used in link when their is no label available) 636 * The path is separated in words and every word gets an uppercase letter 637 */ 638 public function toLabel(): string 639 { 640 $words = preg_split("/\s/", preg_replace("/-|_|:/", " ", $this->getPath())); 641 $wordsUc = []; 642 foreach ($words as $word) { 643 $wordsUc[] = ucfirst($word); 644 } 645 return implode(" ", $wordsUc); 646 } 647 648 public function toLocalPath(): LocalPath 649 { 650 return LocalPath::create($this->filePath); 651 } 652 653 /** 654 * @return string - Returns the string representation of this path (to be able to use it in url) 655 * To get the full string version see {@link DokuPath::toUriString()} 656 */ 657 function toString(): string 658 { 659 return $this->absolutePath; 660 } 661 662 function toUriString(): string 663 { 664 $driveSep = self::DRIVE_SEPARATOR; 665 $string = "{$this->scheme}://$this->drive$driveSep$this->id"; 666 if ($this->rev !== null) { 667 return "$string?rev={$this->rev}"; 668 } 669 return $string; 670 } 671 672 function toAbsolutePath(): Path 673 { 674 return new DokuPath($this->absolutePath, $this->drive, $this->rev); 675 } 676 677 /** 678 * The parent path is a directory (namespace) 679 * The parent of page in the root does return null. 680 * 681 * @return DokuPath|null 682 */ 683 function getParent(): ?Path 684 { 685 686 /** 687 * Same as {@link getNS()} 688 */ 689 $names = $this->getNames(); 690 switch (sizeof($names)) { 691 case 0: 692 return null; 693 case 1: 694 return new DokuPath(DokuPath::PATH_SEPARATOR, $this->drive, $this->rev); 695 default: 696 $names = array_slice($names, 0, sizeof($names) - 1); 697 $path = implode(DokuPath::PATH_SEPARATOR, $names); 698 return new DokuPath($path, $this->drive, $this->rev); 699 } 700 701 } 702 703 function getMime(): ?Mime 704 { 705 if ($this->drive === self::PAGE_DRIVE) { 706 return new Mime(Mime::PLAIN_TEXT); 707 } 708 return parent::getMime(); 709 710 } 711 712 public function getLibrary(): string 713 { 714 return $this->drive; 715 } 716 717 private function schemeDetermination($absolutePath): string 718 { 719 720 if (media_isexternal($absolutePath)) { 721 /** 722 * This code should not be here 723 * Because it should be another path (ie http path) 724 * but for historical reason due to compatibility with 725 * dokuwiki, it's here. 726 */ 727 return InternetPath::scheme; 728 729 } 730 if (link_isinterwiki($absolutePath)) { 731 732 return InterWikiPath::scheme; 733 734 } 735 736 DokuPath::addRootSeparatorIfNotPresent($absolutePath); 737 $this->absolutePath = $absolutePath; 738 739 if (substr($absolutePath, 1, 1) === DokuPath::PATH_SEPARATOR) { 740 /** 741 * path given is `::path` 742 */ 743 if (PluginUtility::isDevOrTest()) { 744 LogUtility::msg("The path given ($absolutePath) has too much separator", LogUtility::LVL_MSG_ERROR); 745 } 746 } 747 return DokuFs::SCHEME; 748 749 750 } 751 752 public function getDrive(): string 753 { 754 return $this->drive; 755 } 756 757 public function resolve(string $name): DokuPath 758 { 759 return new DokuPath($this->absolutePath . self::PATH_SEPARATOR . $name, $this->getDrive()); 760 } 761} 762