1c3437056SNickeau<?php /** @noinspection SpellCheckingInspection */ 2c3437056SNickeau 3c3437056SNickeau 4c3437056SNickeaunamespace ComboStrap; 5c3437056SNickeau 6c3437056SNickeau 7c3437056SNickeauuse Hidehalo\Nanoid\Client; 8c3437056SNickeauuse RuntimeException; 9c3437056SNickeau 10c3437056SNickeauclass PageId extends MetadataText 11c3437056SNickeau{ 12c3437056SNickeau 13c3437056SNickeau public const PROPERTY_NAME = "page_id"; 14c3437056SNickeau 15c3437056SNickeau /** 16c3437056SNickeau * No separator, no uppercase to be consistent on the whole url 17c3437056SNickeau */ 18c3437056SNickeau public const PAGE_ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'; 19c3437056SNickeau 20c3437056SNickeau /** 21c3437056SNickeau * Length to get the same probability than uuid v4. Too much ? 22c3437056SNickeau */ 23c3437056SNickeau public const PAGE_ID_LENGTH = 21; 24c3437056SNickeau public const PAGE_ID_ABBREV_LENGTH = 7; 25c3437056SNickeau public const PAGE_ID_ABBR_ATTRIBUTE = "page_id_abbr"; 26c3437056SNickeau 27c3437056SNickeau public static function createForPage(ResourceCombo $resource): PageId 28c3437056SNickeau { 29c3437056SNickeau return (new PageId()) 30c3437056SNickeau ->setResource($resource); 31c3437056SNickeau } 32c3437056SNickeau 33c3437056SNickeau 34c3437056SNickeau /** 35c3437056SNickeau * 36c3437056SNickeau * 37c3437056SNickeau * @param string|null $value 38c3437056SNickeau * @return MetadataText 39c3437056SNickeau * @throws ExceptionCombo 40c3437056SNickeau */ 41c3437056SNickeau public function setValue($value): Metadata 42c3437056SNickeau { 43c3437056SNickeau return $this->setValueWithOrWithoutForce($value); 44c3437056SNickeau } 45c3437056SNickeau 46c3437056SNickeau /** 47c3437056SNickeau * Page Id cannot be null when build 48c3437056SNickeau * 49c3437056SNickeau * Check how to handle a move id to avoid creating an id for a page that is moving with the 50c3437056SNickeau * move plugin {@link \action_plugin_combo_linkmove::handle_rename_after()} 51c3437056SNickeau * 52c3437056SNickeau * @param $value 53c3437056SNickeau * @return Metadata 54c3437056SNickeau */ 55c3437056SNickeau public function buildFromStoreValue($value): Metadata 56c3437056SNickeau { 57c3437056SNickeau 58c3437056SNickeau if ($value !== null) { 59c3437056SNickeau return parent::buildFromStoreValue($value); 60c3437056SNickeau } 61c3437056SNickeau 62c3437056SNickeau 63c3437056SNickeau $resource = $this->getResource(); 64c3437056SNickeau if (!($resource instanceof Page)) { 65c3437056SNickeau LogUtility::msg("Page Id is for now only for the page, this is not a page but {$this->getResource()->getType()}"); 66c3437056SNickeau return $this; 67c3437056SNickeau } 68c3437056SNickeau 69c3437056SNickeau // null for non-existing page 70c3437056SNickeau if (!FileSystems::exists($resource->getPath())) { 71c3437056SNickeau if (PluginUtility::isDevOrTest()) { 72c3437056SNickeau LogUtility::msg("You can't ask a `page id`, the page ({$this->getResource()}) does not exist", LogUtility::LVL_MSG_INFO, $this->getCanonical()); 73c3437056SNickeau } 74c3437056SNickeau return parent::buildFromStoreValue($value); 75c3437056SNickeau } 76c3437056SNickeau 77c3437056SNickeau 78c3437056SNickeau /** 79c3437056SNickeau * If the store is not the file system store 80c3437056SNickeau * check that it does not exist already on the file system 81c3437056SNickeau * and save it 82c3437056SNickeau */ 83c3437056SNickeau $readStore = $this->getReadStore(); 84c3437056SNickeau if (!($readStore instanceof MetadataDokuWikiStore)) { 85c3437056SNickeau $metadataFileSystemStore = MetadataDokuWikiStore::getOrCreateFromResource($resource); 86c3437056SNickeau $value = $metadataFileSystemStore->getFromPersistentName(self::getPersistentName()); 87c3437056SNickeau if ($value !== null) { 88c3437056SNickeau return parent::buildFromStoreValue($value); 89c3437056SNickeau } 90c3437056SNickeau } 91c3437056SNickeau 92c3437056SNickeau // The page Id can be into the frontmatter 93c3437056SNickeau // if the instructions are old, render them to parse the frontmatter 94c3437056SNickeau // frontmatter is the first element that is processed during a run 95c3437056SNickeau try { 96c3437056SNickeau $frontmatter = MetadataFrontmatterStore::createFromPage($resource); 97c3437056SNickeau $value = $frontmatter->getFromPersistentName(self::getPersistentName()); 98c3437056SNickeau if ($value !== null) { 99c3437056SNickeau return parent::buildFromStoreValue($value); 100c3437056SNickeau } 101c3437056SNickeau } catch (ExceptionCombo $e) { 102c3437056SNickeau LogUtility::msg("Error while reading the frontmatter"); 103c3437056SNickeau return $this; 104c3437056SNickeau } 105c3437056SNickeau 106c3437056SNickeau // datastore 107c3437056SNickeau if (!($readStore instanceof MetadataDbStore)) { 108c3437056SNickeau $dbStore = MetadataDbStore::getOrCreateFromResource($resource); 109c3437056SNickeau $value = $dbStore->getFromPersistentName(self::getPersistentName()); 110*468152aeSgerardnico if ($value !== null && $value !== "") { 1110e43c1dbSgerardnico 11247a8d2b6Sgerardnico $pathDbValue = $dbStore->getFromPersistentName(PagePath::getPersistentName()); 1130e43c1dbSgerardnico 1140e43c1dbSgerardnico /** 1150e43c1dbSgerardnico * If the page in the database does not exist, 1160e43c1dbSgerardnico * We think that the page was moved from the file system 1170e43c1dbSgerardnico * and we return the page id 1180e43c1dbSgerardnico */ 1190e43c1dbSgerardnico $pageDbValue = Page::createPageFromQualifiedPath($pathDbValue); 1200e43c1dbSgerardnico if(!FileSystems::exists($pageDbValue->getPath())){ 1210e43c1dbSgerardnico return parent::buildFromStoreValue($value); 1220e43c1dbSgerardnico } 1230e43c1dbSgerardnico 1240e43c1dbSgerardnico /** 1250e43c1dbSgerardnico * The page path in the database exists 1260e43c1dbSgerardnico * If they are the same, we return the page id 1270e43c1dbSgerardnico * (because due to duplicate in canonical, the row returned may be from another resource) 1280e43c1dbSgerardnico */ 12947a8d2b6Sgerardnico $resourcePath = $resource->getPath()->toString(); 13047a8d2b6Sgerardnico if ($pathDbValue === $resourcePath) { 131c3437056SNickeau return parent::buildFromStoreValue($value); 132c3437056SNickeau } 133c3437056SNickeau } 13447a8d2b6Sgerardnico } 135c3437056SNickeau 136c3437056SNickeau // Value is still null, not in the the frontmatter, not in the database 137c3437056SNickeau // generate and store 138c3437056SNickeau $actualValue = self::generateUniquePageId(); 139c3437056SNickeau parent::buildFromStoreValue($actualValue); 140c3437056SNickeau try { 141c3437056SNickeau // Store the page id on the file system 142c3437056SNickeau MetadataDokuWikiStore::getOrCreateFromResource($resource) 143c3437056SNickeau ->set($this); 144c3437056SNickeau /** 145c3437056SNickeau * Create the row in the database (to allow permanent url redirection {@link PageUrlType}) 146c3437056SNickeau */ 147c3437056SNickeau (new DatabasePageRow()) 148c3437056SNickeau ->setPage($resource) 149c3437056SNickeau ->upsertAttributes([PageId::getPersistentName() => $actualValue]); 150c3437056SNickeau } catch (ExceptionCombo $e) { 151c3437056SNickeau LogUtility::msg("Unable to store the page id generated. Message:" . $e->getMessage()); 152c3437056SNickeau } 153c3437056SNickeau 154c3437056SNickeau return $this; 155c3437056SNickeau 156c3437056SNickeau } 157c3437056SNickeau 158c3437056SNickeau 159c3437056SNickeau public function getTab(): string 160c3437056SNickeau { 161c3437056SNickeau return MetaManagerForm::TAB_INTEGRATION_VALUE; 162c3437056SNickeau } 163c3437056SNickeau 164c3437056SNickeau public function getDescription(): string 165c3437056SNickeau { 166c3437056SNickeau return "An unique identifier for the page"; 167c3437056SNickeau } 168c3437056SNickeau 169c3437056SNickeau public function getLabel(): string 170c3437056SNickeau { 171c3437056SNickeau return "Page Id"; 172c3437056SNickeau } 173c3437056SNickeau 174c3437056SNickeau static public function getName(): string 175c3437056SNickeau { 176c3437056SNickeau return self::PROPERTY_NAME; 177c3437056SNickeau } 178c3437056SNickeau 179c3437056SNickeau public function getPersistenceType(): string 180c3437056SNickeau { 181c3437056SNickeau return Metadata::PERSISTENT_METADATA; 182c3437056SNickeau } 183c3437056SNickeau 184c3437056SNickeau public function getMutable(): bool 185c3437056SNickeau { 186c3437056SNickeau return false; 187c3437056SNickeau } 188c3437056SNickeau 189c3437056SNickeau /** 190c3437056SNickeau * @return string|null 191c3437056SNickeau */ 192c3437056SNickeau public function getDefaultValue(): ?string 193c3437056SNickeau { 194c3437056SNickeau return null; 195c3437056SNickeau } 196c3437056SNickeau 197c3437056SNickeau public function getCanonical(): string 198c3437056SNickeau { 199c3437056SNickeau return $this->getName(); 200c3437056SNickeau } 201c3437056SNickeau 202c3437056SNickeau 203c3437056SNickeau /** 204c3437056SNickeau * For, there is no real replication between website. 205c3437056SNickeau * 206c3437056SNickeau * Therefore, the source of truth is the value in the {@link syntax_plugin_combo_frontmatter} 207c3437056SNickeau * Therefore, the page id generation should happen after the rendering of the page 208c3437056SNickeau * at the database level 209c3437056SNickeau * 210c3437056SNickeau * Return a page id collision free 211c3437056SNickeau * for the page already {@link DatabasePageRow::replicatePage() replicated} 212c3437056SNickeau * 213c3437056SNickeau * https://zelark.github.io/nano-id-cc/ 214c3437056SNickeau * 215c3437056SNickeau * 1000 id / hour = ~35 years needed, in order to have a 1% probability of at least one collision. 216c3437056SNickeau * 217c3437056SNickeau * We don't rely on a sequence because 218c3437056SNickeau * - the database may be refreshed 219c3437056SNickeau * - sqlite does have only auto-increment support 220c3437056SNickeau * https://www.sqlite.org/autoinc.html 221c3437056SNickeau * 222c3437056SNickeau * @return string 223c3437056SNickeau */ 224c3437056SNickeau static function generateUniquePageId(): string 225c3437056SNickeau { 226c3437056SNickeau /** 227c3437056SNickeau * Collision detection happens just after the use of this function on the 228c3437056SNickeau * creation of the {@link DatabasePageRow::getDatabaseRowFromPage() databasePage object} 229c3437056SNickeau * 230c3437056SNickeau */ 231c3437056SNickeau $nanoIdClient = new Client(); 232c3437056SNickeau $pageId = ($nanoIdClient)->formattedId(self::PAGE_ID_ALPHABET, self::PAGE_ID_LENGTH); 233c3437056SNickeau while (DatabasePageRow::createFromPageId($pageId)->exists()) { 234c3437056SNickeau $pageId = ($nanoIdClient)->formattedId(self::PAGE_ID_ALPHABET, self::PAGE_ID_LENGTH); 235c3437056SNickeau } 236c3437056SNickeau return $pageId; 237c3437056SNickeau } 238c3437056SNickeau 239c3437056SNickeau /** 240c3437056SNickeau * Overwrite the page id even if it exists already 241c3437056SNickeau * It should not be possible - used for now in case of conflict in page move 242c3437056SNickeau * @throws ExceptionCombo 243c3437056SNickeau */ 244c3437056SNickeau public function setValueForce(?string $value): PageId 245c3437056SNickeau { 246c3437056SNickeau return $this->setValueWithOrWithoutForce($value, true); 247c3437056SNickeau } 248c3437056SNickeau 249c3437056SNickeau 250c3437056SNickeau /** 251c3437056SNickeau * 252c3437056SNickeau * @param bool $force - It should not be possible - used for now in case of conflict in page move 253c3437056SNickeau * @throws ExceptionCombo 254c3437056SNickeau */ 255c3437056SNickeau private function setValueWithOrWithoutForce(?string $value, bool $force = false): PageId 256c3437056SNickeau { 257c3437056SNickeau if ($value === null) { 258c3437056SNickeau throw new ExceptionCombo("A page id can not be set with a null value (Page: {$this->getResource()})", $this->getCanonical()); 259c3437056SNickeau } 260c3437056SNickeau if (!is_string($value) || !preg_match("/[" . self::PAGE_ID_ALPHABET . "]/", $value)) { 261c3437056SNickeau throw new ExceptionCombo("The page id value to set ($value) is not an alphanumeric string (Page: {$this->getResource()})", $this->getCanonical()); 262c3437056SNickeau } 263c3437056SNickeau $actualId = $this->getValue(); 264c3437056SNickeau 265c3437056SNickeau if ($force !== true) { 266c3437056SNickeau if ($actualId !== null && $actualId !== $value) { 267c3437056SNickeau throw new ExceptionCombo("The page id cannot be changed, the page ({$this->getResource()}) has already an id ($actualId}) that has not the same value ($value})", $this->getCanonical()); 268c3437056SNickeau } 269c3437056SNickeau if ($actualId !== null) { 270c3437056SNickeau throw new ExceptionCombo("The page id cannot be changed, the page ({$this->getResource()}) has already an id ($actualId})", $this->getCanonical()); 271c3437056SNickeau } 272c3437056SNickeau } else { 273c3437056SNickeau if (PluginUtility::isDevOrTest()) { 274c3437056SNickeau // this should never happened (exception in test/dev) 275c3437056SNickeau throw new ExceptionComboRuntime("Forcing of the page id should not happen in dev/test", $this->getCanonical()); 276c3437056SNickeau } 277c3437056SNickeau } 278c3437056SNickeau return parent::setValue($value); 279c3437056SNickeau } 280c3437056SNickeau 281c3437056SNickeau public function sendToWriteStore(): Metadata 282c3437056SNickeau { 283c3437056SNickeau /** 284c3437056SNickeau * If the data was built with one store 285c3437056SNickeau * and send to another store 286c3437056SNickeau * We prevent the overwriting of a page id 287c3437056SNickeau */ 288c3437056SNickeau $actualStoreValue = $this->getReadStore()->get($this); 289c3437056SNickeau $value = $this->getValue(); 290c3437056SNickeau if ($actualStoreValue !== null && $actualStoreValue !== $value) { 291c3437056SNickeau throw new ExceptionComboRuntime("The page id can not be modified once generated. The value in the store is $actualStoreValue while the new value is $value"); 292c3437056SNickeau } 293c3437056SNickeau parent::sendToWriteStore(); 294c3437056SNickeau return $this; 295c3437056SNickeau 296c3437056SNickeau } 297c3437056SNickeau 298c3437056SNickeau 299c3437056SNickeau public function getValueFromStore() 300c3437056SNickeau { 301c3437056SNickeau return $this->getReadStore()->get($this); 302c3437056SNickeau } 303c3437056SNickeau 304c3437056SNickeau 305c3437056SNickeau} 306