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()); 110c3437056SNickeau if ($value !== null) { 111*47a8d2b6Sgerardnico /** 112*47a8d2b6Sgerardnico * Due to duplicate in canonical, the row returned may be from another resource 113*47a8d2b6Sgerardnico */ 114*47a8d2b6Sgerardnico $pathDbValue = $dbStore->getFromPersistentName(PagePath::getPersistentName()); 115*47a8d2b6Sgerardnico $resourcePath = $resource->getPath()->toString(); 116*47a8d2b6Sgerardnico if ($pathDbValue === $resourcePath) { 117c3437056SNickeau return parent::buildFromStoreValue($value); 118c3437056SNickeau } 119c3437056SNickeau } 120*47a8d2b6Sgerardnico } 121c3437056SNickeau 122c3437056SNickeau // Value is still null, not in the the frontmatter, not in the database 123c3437056SNickeau // generate and store 124c3437056SNickeau $actualValue = self::generateUniquePageId(); 125c3437056SNickeau parent::buildFromStoreValue($actualValue); 126c3437056SNickeau try { 127c3437056SNickeau // Store the page id on the file system 128c3437056SNickeau MetadataDokuWikiStore::getOrCreateFromResource($resource) 129c3437056SNickeau ->set($this); 130c3437056SNickeau /** 131c3437056SNickeau * Create the row in the database (to allow permanent url redirection {@link PageUrlType}) 132c3437056SNickeau */ 133c3437056SNickeau (new DatabasePageRow()) 134c3437056SNickeau ->setPage($resource) 135c3437056SNickeau ->upsertAttributes([PageId::getPersistentName() => $actualValue]); 136c3437056SNickeau } catch (ExceptionCombo $e) { 137c3437056SNickeau LogUtility::msg("Unable to store the page id generated. Message:" . $e->getMessage()); 138c3437056SNickeau } 139c3437056SNickeau 140c3437056SNickeau return $this; 141c3437056SNickeau 142c3437056SNickeau } 143c3437056SNickeau 144c3437056SNickeau 145c3437056SNickeau public function getTab(): string 146c3437056SNickeau { 147c3437056SNickeau return MetaManagerForm::TAB_INTEGRATION_VALUE; 148c3437056SNickeau } 149c3437056SNickeau 150c3437056SNickeau public function getDescription(): string 151c3437056SNickeau { 152c3437056SNickeau return "An unique identifier for the page"; 153c3437056SNickeau } 154c3437056SNickeau 155c3437056SNickeau public function getLabel(): string 156c3437056SNickeau { 157c3437056SNickeau return "Page Id"; 158c3437056SNickeau } 159c3437056SNickeau 160c3437056SNickeau static public function getName(): string 161c3437056SNickeau { 162c3437056SNickeau return self::PROPERTY_NAME; 163c3437056SNickeau } 164c3437056SNickeau 165c3437056SNickeau public function getPersistenceType(): string 166c3437056SNickeau { 167c3437056SNickeau return Metadata::PERSISTENT_METADATA; 168c3437056SNickeau } 169c3437056SNickeau 170c3437056SNickeau public function getMutable(): bool 171c3437056SNickeau { 172c3437056SNickeau return false; 173c3437056SNickeau } 174c3437056SNickeau 175c3437056SNickeau /** 176c3437056SNickeau * @return string|null 177c3437056SNickeau */ 178c3437056SNickeau public function getDefaultValue(): ?string 179c3437056SNickeau { 180c3437056SNickeau return null; 181c3437056SNickeau } 182c3437056SNickeau 183c3437056SNickeau public function getCanonical(): string 184c3437056SNickeau { 185c3437056SNickeau return $this->getName(); 186c3437056SNickeau } 187c3437056SNickeau 188c3437056SNickeau 189c3437056SNickeau /** 190c3437056SNickeau * For, there is no real replication between website. 191c3437056SNickeau * 192c3437056SNickeau * Therefore, the source of truth is the value in the {@link syntax_plugin_combo_frontmatter} 193c3437056SNickeau * Therefore, the page id generation should happen after the rendering of the page 194c3437056SNickeau * at the database level 195c3437056SNickeau * 196c3437056SNickeau * Return a page id collision free 197c3437056SNickeau * for the page already {@link DatabasePageRow::replicatePage() replicated} 198c3437056SNickeau * 199c3437056SNickeau * https://zelark.github.io/nano-id-cc/ 200c3437056SNickeau * 201c3437056SNickeau * 1000 id / hour = ~35 years needed, in order to have a 1% probability of at least one collision. 202c3437056SNickeau * 203c3437056SNickeau * We don't rely on a sequence because 204c3437056SNickeau * - the database may be refreshed 205c3437056SNickeau * - sqlite does have only auto-increment support 206c3437056SNickeau * https://www.sqlite.org/autoinc.html 207c3437056SNickeau * 208c3437056SNickeau * @return string 209c3437056SNickeau */ 210c3437056SNickeau static function generateUniquePageId(): string 211c3437056SNickeau { 212c3437056SNickeau /** 213c3437056SNickeau * Collision detection happens just after the use of this function on the 214c3437056SNickeau * creation of the {@link DatabasePageRow::getDatabaseRowFromPage() databasePage object} 215c3437056SNickeau * 216c3437056SNickeau */ 217c3437056SNickeau $nanoIdClient = new Client(); 218c3437056SNickeau $pageId = ($nanoIdClient)->formattedId(self::PAGE_ID_ALPHABET, self::PAGE_ID_LENGTH); 219c3437056SNickeau while (DatabasePageRow::createFromPageId($pageId)->exists()) { 220c3437056SNickeau $pageId = ($nanoIdClient)->formattedId(self::PAGE_ID_ALPHABET, self::PAGE_ID_LENGTH); 221c3437056SNickeau } 222c3437056SNickeau return $pageId; 223c3437056SNickeau } 224c3437056SNickeau 225c3437056SNickeau /** 226c3437056SNickeau * Overwrite the page id even if it exists already 227c3437056SNickeau * It should not be possible - used for now in case of conflict in page move 228c3437056SNickeau * @throws ExceptionCombo 229c3437056SNickeau */ 230c3437056SNickeau public function setValueForce(?string $value): PageId 231c3437056SNickeau { 232c3437056SNickeau return $this->setValueWithOrWithoutForce($value, true); 233c3437056SNickeau } 234c3437056SNickeau 235c3437056SNickeau 236c3437056SNickeau /** 237c3437056SNickeau * 238c3437056SNickeau * @param bool $force - It should not be possible - used for now in case of conflict in page move 239c3437056SNickeau * @throws ExceptionCombo 240c3437056SNickeau */ 241c3437056SNickeau private function setValueWithOrWithoutForce(?string $value, bool $force = false): PageId 242c3437056SNickeau { 243c3437056SNickeau if ($value === null) { 244c3437056SNickeau throw new ExceptionCombo("A page id can not be set with a null value (Page: {$this->getResource()})", $this->getCanonical()); 245c3437056SNickeau } 246c3437056SNickeau if (!is_string($value) || !preg_match("/[" . self::PAGE_ID_ALPHABET . "]/", $value)) { 247c3437056SNickeau throw new ExceptionCombo("The page id value to set ($value) is not an alphanumeric string (Page: {$this->getResource()})", $this->getCanonical()); 248c3437056SNickeau } 249c3437056SNickeau $actualId = $this->getValue(); 250c3437056SNickeau 251c3437056SNickeau if ($force !== true) { 252c3437056SNickeau if ($actualId !== null && $actualId !== $value) { 253c3437056SNickeau 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()); 254c3437056SNickeau } 255c3437056SNickeau if ($actualId !== null) { 256c3437056SNickeau throw new ExceptionCombo("The page id cannot be changed, the page ({$this->getResource()}) has already an id ($actualId})", $this->getCanonical()); 257c3437056SNickeau } 258c3437056SNickeau } else { 259c3437056SNickeau if (PluginUtility::isDevOrTest()) { 260c3437056SNickeau // this should never happened (exception in test/dev) 261c3437056SNickeau throw new ExceptionComboRuntime("Forcing of the page id should not happen in dev/test", $this->getCanonical()); 262c3437056SNickeau } 263c3437056SNickeau } 264c3437056SNickeau return parent::setValue($value); 265c3437056SNickeau } 266c3437056SNickeau 267c3437056SNickeau public function sendToWriteStore(): Metadata 268c3437056SNickeau { 269c3437056SNickeau /** 270c3437056SNickeau * If the data was built with one store 271c3437056SNickeau * and send to another store 272c3437056SNickeau * We prevent the overwriting of a page id 273c3437056SNickeau */ 274c3437056SNickeau $actualStoreValue = $this->getReadStore()->get($this); 275c3437056SNickeau $value = $this->getValue(); 276c3437056SNickeau if ($actualStoreValue !== null && $actualStoreValue !== $value) { 277c3437056SNickeau 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"); 278c3437056SNickeau } 279c3437056SNickeau parent::sendToWriteStore(); 280c3437056SNickeau return $this; 281c3437056SNickeau 282c3437056SNickeau } 283c3437056SNickeau 284c3437056SNickeau 285c3437056SNickeau public function getValueFromStore() 286c3437056SNickeau { 287c3437056SNickeau return $this->getReadStore()->get($this); 288c3437056SNickeau } 289c3437056SNickeau 290c3437056SNickeau 291c3437056SNickeau} 292