1c3437056SNickeau<?php /** @noinspection SpellCheckingInspection */ 2c3437056SNickeau 3c3437056SNickeau 4c3437056SNickeaunamespace ComboStrap; 5c3437056SNickeau 6c3437056SNickeau 7*04fd306cSNickeauuse action_plugin_combo_linkmove; 8*04fd306cSNickeauuse ComboStrap\Meta\Api\Metadata; 9*04fd306cSNickeauuse ComboStrap\Meta\Api\MetadataText; 10*04fd306cSNickeauuse ComboStrap\Meta\Store\MetadataDbStore; 11*04fd306cSNickeauuse ComboStrap\Meta\Store\MetadataDokuWikiStore; 12c3437056SNickeauuse Hidehalo\Nanoid\Client; 13*04fd306cSNickeau 14c3437056SNickeau 15c3437056SNickeauclass PageId extends MetadataText 16c3437056SNickeau{ 17c3437056SNickeau 18c3437056SNickeau public const PROPERTY_NAME = "page_id"; 19c3437056SNickeau 20c3437056SNickeau /** 21c3437056SNickeau * No separator, no uppercase to be consistent on the whole url 22c3437056SNickeau */ 23c3437056SNickeau public const PAGE_ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'; 24c3437056SNickeau 25c3437056SNickeau /** 26c3437056SNickeau * Length to get the same probability than uuid v4. Too much ? 27c3437056SNickeau */ 28c3437056SNickeau public const PAGE_ID_LENGTH = 21; 29*04fd306cSNickeau /** 30*04fd306cSNickeau * 31*04fd306cSNickeau * The page id abbreviation is used in the url to make them unique. 32*04fd306cSNickeau * 33*04fd306cSNickeau * A website is not git but an abbreviation of 7 34*04fd306cSNickeau * is enough for a website. 35*04fd306cSNickeau * 36*04fd306cSNickeau * 7 is also the initial length of the git has abbreviation 37*04fd306cSNickeau * 38*04fd306cSNickeau * It gives a probability of collision of 1 percent 39*04fd306cSNickeau * for 24 pages creation by day over a period of 100 year 40*04fd306cSNickeau * (You need to create 876k pages). 41*04fd306cSNickeau * with the 36 alphabet 42*04fd306cSNickeau * Furthermore, we test on creation the uniqueness on the 7 page id abbreviation 43*04fd306cSNickeau * 44*04fd306cSNickeau * more ... https://datacadamia.com/crypto/hash/collision 45*04fd306cSNickeau */ 46c3437056SNickeau public const PAGE_ID_ABBREV_LENGTH = 7; 47c3437056SNickeau public const PAGE_ID_ABBR_ATTRIBUTE = "page_id_abbr"; 48c3437056SNickeau 49c3437056SNickeau public static function createForPage(ResourceCombo $resource): PageId 50c3437056SNickeau { 51c3437056SNickeau return (new PageId()) 52c3437056SNickeau ->setResource($resource); 53c3437056SNickeau } 54c3437056SNickeau 55*04fd306cSNickeau public static function getAbbreviated(string $pageId) 56*04fd306cSNickeau { 57*04fd306cSNickeau return substr($pageId, 0, PageId::PAGE_ID_ABBREV_LENGTH); 58*04fd306cSNickeau } 59*04fd306cSNickeau 60*04fd306cSNickeau /** 61*04fd306cSNickeau * Generate and store 62*04fd306cSNickeau * Store the page id on the file system 63*04fd306cSNickeau */ 64*04fd306cSNickeau public static function generateAndStorePageId(MarkupPath $markupPath): string 65*04fd306cSNickeau { 66*04fd306cSNickeau $pageId = self::generateUniquePageId(); 67*04fd306cSNickeau MetadataDokuWikiStore::getOrCreateFromResource($markupPath) 68*04fd306cSNickeau ->setFromPersistentName(PageId::getPersistentName(), $pageId); 69*04fd306cSNickeau return $pageId; 70*04fd306cSNickeau } 71*04fd306cSNickeau 72c3437056SNickeau 73c3437056SNickeau /** 74c3437056SNickeau * 75c3437056SNickeau * 76c3437056SNickeau * @param string|null $value 77c3437056SNickeau * @return MetadataText 78*04fd306cSNickeau * @throws ExceptionCompile 79c3437056SNickeau */ 80c3437056SNickeau public function setValue($value): Metadata 81c3437056SNickeau { 82c3437056SNickeau return $this->setValueWithOrWithoutForce($value); 83c3437056SNickeau } 84c3437056SNickeau 85c3437056SNickeau /** 86c3437056SNickeau * Page Id cannot be null when build 87c3437056SNickeau * 88c3437056SNickeau * Check how to handle a move id to avoid creating an id for a page that is moving with the 89c3437056SNickeau * move plugin {@link \action_plugin_combo_linkmove::handle_rename_after()} 90c3437056SNickeau * 91c3437056SNickeau * @param $value 92c3437056SNickeau * @return Metadata 93c3437056SNickeau */ 94*04fd306cSNickeau public function setFromStoreValueWithoutException($value): Metadata 95c3437056SNickeau { 96c3437056SNickeau 97c3437056SNickeau if ($value !== null) { 98*04fd306cSNickeau return parent::setFromStoreValueWithoutException($value); 99c3437056SNickeau } 100c3437056SNickeau 101c3437056SNickeau 102c3437056SNickeau $resource = $this->getResource(); 103*04fd306cSNickeau if (!($resource instanceof MarkupPath)) { 104c3437056SNickeau LogUtility::msg("Page Id is for now only for the page, this is not a page but {$this->getResource()->getType()}"); 105c3437056SNickeau return $this; 106c3437056SNickeau } 107c3437056SNickeau 108c3437056SNickeau // null for non-existing page 109*04fd306cSNickeau if (!FileSystems::exists($resource->getPathObject())) { 110*04fd306cSNickeau return parent::setFromStoreValueWithoutException($value); 111c3437056SNickeau } 112c3437056SNickeau 113c3437056SNickeau 114c3437056SNickeau /** 115c3437056SNickeau * If the store is not the file system store 116c3437056SNickeau * check that it does not exist already on the file system 117c3437056SNickeau * and save it 118c3437056SNickeau */ 119c3437056SNickeau $readStore = $this->getReadStore(); 120c3437056SNickeau if (!($readStore instanceof MetadataDokuWikiStore)) { 121c3437056SNickeau $metadataFileSystemStore = MetadataDokuWikiStore::getOrCreateFromResource($resource); 122*04fd306cSNickeau $value = $metadataFileSystemStore->getFromName(self::getPersistentName()); 123c3437056SNickeau if ($value !== null) { 124*04fd306cSNickeau return parent::setFromStoreValueWithoutException($value); 125c3437056SNickeau } 126c3437056SNickeau } 127c3437056SNickeau 128c3437056SNickeau // The page Id can be into the frontmatter 129c3437056SNickeau // if the instructions are old, render them to parse the frontmatter 130c3437056SNickeau // frontmatter is the first element that is processed during a run 131c3437056SNickeau try { 132c3437056SNickeau $frontmatter = MetadataFrontmatterStore::createFromPage($resource); 133*04fd306cSNickeau $value = $frontmatter->getFromName(self::getPersistentName()); 134c3437056SNickeau if ($value !== null) { 135*04fd306cSNickeau return parent::setFromStoreValueWithoutException($value); 136c3437056SNickeau } 137*04fd306cSNickeau } catch (ExceptionCompile $e) { 138c3437056SNickeau LogUtility::msg("Error while reading the frontmatter"); 139c3437056SNickeau return $this; 140c3437056SNickeau } 141c3437056SNickeau 142c3437056SNickeau // datastore 143c3437056SNickeau if (!($readStore instanceof MetadataDbStore)) { 144*04fd306cSNickeau try { 145c3437056SNickeau $dbStore = MetadataDbStore::getOrCreateFromResource($resource); 146*04fd306cSNickeau $value = $dbStore->getFromName(self::getPersistentName()); 147468152aeSgerardnico if ($value !== null && $value !== "") { 1480e43c1dbSgerardnico 149*04fd306cSNickeau $pathDbValue = $dbStore->getFromName(PagePath::getPersistentName()); 1500e43c1dbSgerardnico 1510e43c1dbSgerardnico /** 1520e43c1dbSgerardnico * If the page in the database does not exist, 1530e43c1dbSgerardnico * We think that the page was moved from the file system 1540e43c1dbSgerardnico * and we return the page id 1550e43c1dbSgerardnico */ 156*04fd306cSNickeau $pageDbValue = MarkupPath::createPageFromAbsoluteId($pathDbValue); 157*04fd306cSNickeau if (!FileSystems::exists($pageDbValue->getPathObject())) { 158*04fd306cSNickeau return parent::setFromStoreValueWithoutException($value); 1590e43c1dbSgerardnico } 1600e43c1dbSgerardnico 1610e43c1dbSgerardnico /** 1620e43c1dbSgerardnico * The page path in the database exists 1630e43c1dbSgerardnico * If they are the same, we return the page id 1640e43c1dbSgerardnico * (because due to duplicate in canonical, the row returned may be from another resource) 1650e43c1dbSgerardnico */ 166*04fd306cSNickeau $resourcePath = $resource->getPathObject()->toAbsoluteId(); 16747a8d2b6Sgerardnico if ($pathDbValue === $resourcePath) { 168*04fd306cSNickeau return parent::setFromStoreValueWithoutException($value); 169c3437056SNickeau } 170c3437056SNickeau } 171*04fd306cSNickeau } catch (ExceptionNotExists|ExceptionSqliteNotAvailable $e) { 172*04fd306cSNickeau // no page id or not in the store or whatever 17347a8d2b6Sgerardnico } 174c3437056SNickeau 175c3437056SNickeau } 176c3437056SNickeau 177*04fd306cSNickeau // null ? 178*04fd306cSNickeau return parent::setFromStoreValueWithoutException($value); 179c3437056SNickeau 180c3437056SNickeau } 181c3437056SNickeau 182c3437056SNickeau 183*04fd306cSNickeau static public function getTab(): string 184c3437056SNickeau { 185c3437056SNickeau return MetaManagerForm::TAB_INTEGRATION_VALUE; 186c3437056SNickeau } 187c3437056SNickeau 188*04fd306cSNickeau static public function getDescription(): string 189c3437056SNickeau { 190c3437056SNickeau return "An unique identifier for the page"; 191c3437056SNickeau } 192c3437056SNickeau 193*04fd306cSNickeau static public function getLabel(): string 194c3437056SNickeau { 195c3437056SNickeau return "Page Id"; 196c3437056SNickeau } 197c3437056SNickeau 198c3437056SNickeau static public function getName(): string 199c3437056SNickeau { 200c3437056SNickeau return self::PROPERTY_NAME; 201c3437056SNickeau } 202c3437056SNickeau 203*04fd306cSNickeau static public function getPersistenceType(): string 204c3437056SNickeau { 205c3437056SNickeau return Metadata::PERSISTENT_METADATA; 206c3437056SNickeau } 207c3437056SNickeau 208*04fd306cSNickeau static public function isMutable(): bool 209c3437056SNickeau { 210c3437056SNickeau return false; 211c3437056SNickeau } 212c3437056SNickeau 213c3437056SNickeau /** 214c3437056SNickeau * @return string|null 215c3437056SNickeau */ 216c3437056SNickeau public function getDefaultValue(): ?string 217c3437056SNickeau { 218c3437056SNickeau return null; 219c3437056SNickeau } 220c3437056SNickeau 221c3437056SNickeau 222c3437056SNickeau /** 223c3437056SNickeau * For, there is no real replication between website. 224c3437056SNickeau * 225c3437056SNickeau * Therefore, the source of truth is the value in the {@link syntax_plugin_combo_frontmatter} 226c3437056SNickeau * Therefore, the page id generation should happen after the rendering of the page 227c3437056SNickeau * at the database level 228c3437056SNickeau * 229c3437056SNickeau * Return a page id collision free 230c3437056SNickeau * for the page already {@link DatabasePageRow::replicatePage() replicated} 231c3437056SNickeau * 232c3437056SNickeau * https://zelark.github.io/nano-id-cc/ 233c3437056SNickeau * 234c3437056SNickeau * 1000 id / hour = ~35 years needed, in order to have a 1% probability of at least one collision. 235c3437056SNickeau * 236c3437056SNickeau * We don't rely on a sequence because 237c3437056SNickeau * - the database may be refreshed 238c3437056SNickeau * - sqlite does have only auto-increment support 239c3437056SNickeau * https://www.sqlite.org/autoinc.html 240c3437056SNickeau * 241c3437056SNickeau * @return string 242c3437056SNickeau */ 243c3437056SNickeau static function generateUniquePageId(): string 244c3437056SNickeau { 245c3437056SNickeau /** 246c3437056SNickeau * Collision detection happens just after the use of this function on the 247c3437056SNickeau * creation of the {@link DatabasePageRow::getDatabaseRowFromPage() databasePage object} 248c3437056SNickeau * 249c3437056SNickeau */ 250c3437056SNickeau $nanoIdClient = new Client(); 251c3437056SNickeau $pageId = ($nanoIdClient)->formattedId(self::PAGE_ID_ALPHABET, self::PAGE_ID_LENGTH); 252*04fd306cSNickeau /** 253*04fd306cSNickeau * The real id is the abbreviated one 254*04fd306cSNickeau * Test if there is not yet a page with this value 255*04fd306cSNickeau */ 256*04fd306cSNickeau while ( 257*04fd306cSNickeau DatabasePageRow::createFromPageIdAbbr(self::getAbbreviated($pageId))->exists() 258*04fd306cSNickeau ) { 259c3437056SNickeau $pageId = ($nanoIdClient)->formattedId(self::PAGE_ID_ALPHABET, self::PAGE_ID_LENGTH); 260c3437056SNickeau } 261*04fd306cSNickeau 262c3437056SNickeau return $pageId; 263c3437056SNickeau } 264c3437056SNickeau 265c3437056SNickeau /** 266c3437056SNickeau * Overwrite the page id even if it exists already 267c3437056SNickeau * It should not be possible - used for now in case of conflict in page move 268*04fd306cSNickeau * @throws ExceptionCompile 269c3437056SNickeau */ 270c3437056SNickeau public function setValueForce(?string $value): PageId 271c3437056SNickeau { 272c3437056SNickeau return $this->setValueWithOrWithoutForce($value, true); 273c3437056SNickeau } 274c3437056SNickeau 275c3437056SNickeau 276c3437056SNickeau /** 277c3437056SNickeau * 278c3437056SNickeau * @param bool $force - It should not be possible - used for now in case of conflict in page move 279*04fd306cSNickeau * @throws ExceptionCompile 280c3437056SNickeau */ 281c3437056SNickeau private function setValueWithOrWithoutForce(?string $value, bool $force = false): PageId 282c3437056SNickeau { 283c3437056SNickeau if ($value === null) { 284*04fd306cSNickeau throw new ExceptionCompile("A page id can not be set with a null value (Page: {$this->getResource()})", $this->getCanonical()); 285c3437056SNickeau } 286c3437056SNickeau if (!is_string($value) || !preg_match("/[" . self::PAGE_ID_ALPHABET . "]/", $value)) { 287*04fd306cSNickeau throw new ExceptionCompile("The page id value to set ($value) is not an alphanumeric string (Page: {$this->getResource()})", $this->getCanonical()); 288c3437056SNickeau } 289*04fd306cSNickeau try { 290c3437056SNickeau $actualId = $this->getValue(); 291*04fd306cSNickeau } catch (ExceptionNotFound $e) { 292*04fd306cSNickeau $actualId = null; 293*04fd306cSNickeau } 294c3437056SNickeau 295c3437056SNickeau if ($force !== true) { 296c3437056SNickeau if ($actualId !== null && $actualId !== $value) { 297*04fd306cSNickeau throw new ExceptionCompile("The page id cannot be changed, the page ({$this->getResource()}) has already an id ($actualId}) that has not the same value ($value})", $this->getCanonical()); 298c3437056SNickeau } 299c3437056SNickeau if ($actualId !== null) { 300*04fd306cSNickeau throw new ExceptionCompile("The page id cannot be changed, the page ({$this->getResource()}) has already an id ($actualId})", $this->getCanonical()); 301c3437056SNickeau } 302c3437056SNickeau } else { 303*04fd306cSNickeau 304*04fd306cSNickeau /** 305*04fd306cSNickeau * This should never happened (exception in test/dev) 306*04fd306cSNickeau * Unfortunately, it does not happen in test 307*04fd306cSNickeau * but in real life 308*04fd306cSNickeau */ 309*04fd306cSNickeau if (!(action_plugin_combo_linkmove::isMoveOperation())) { 310*04fd306cSNickeau LogUtility::internalError("Forcing of the page id should not happen in dev/test", $this->getCanonical()); 311c3437056SNickeau } 312c3437056SNickeau } 313c3437056SNickeau return parent::setValue($value); 314c3437056SNickeau } 315c3437056SNickeau 316*04fd306cSNickeau /** 317*04fd306cSNickeau * @throws ExceptionBadArgument 318*04fd306cSNickeau * 319*04fd306cSNickeau */ 320c3437056SNickeau public function sendToWriteStore(): Metadata 321c3437056SNickeau { 322c3437056SNickeau /** 323c3437056SNickeau * If the data was built with one store 324c3437056SNickeau * and send to another store 325c3437056SNickeau * We prevent the overwriting of a page id 326c3437056SNickeau */ 327c3437056SNickeau $actualStoreValue = $this->getReadStore()->get($this); 328*04fd306cSNickeau try { 329c3437056SNickeau $value = $this->getValue(); 330*04fd306cSNickeau } catch (ExceptionNotFound $e) { 331*04fd306cSNickeau throw new ExceptionBadArgument("No value to store"); 332*04fd306cSNickeau } 333c3437056SNickeau if ($actualStoreValue !== null && $actualStoreValue !== $value) { 334*04fd306cSNickeau throw new ExceptionBadArgument("The page id can not be modified once generated. The value in the store is $actualStoreValue while the new value is $value"); 335c3437056SNickeau } 336c3437056SNickeau parent::sendToWriteStore(); 337c3437056SNickeau return $this; 338c3437056SNickeau 339c3437056SNickeau } 340c3437056SNickeau 341c3437056SNickeau 342c3437056SNickeau public function getValueFromStore() 343c3437056SNickeau { 344c3437056SNickeau return $this->getReadStore()->get($this); 345c3437056SNickeau } 346c3437056SNickeau 347*04fd306cSNickeau /** 348*04fd306cSNickeau * @throws ExceptionNotFound 349*04fd306cSNickeau */ 350*04fd306cSNickeau public function getValue(): string 351*04fd306cSNickeau { 352c3437056SNickeau 353*04fd306cSNickeau return parent::getValue(); 354*04fd306cSNickeau 355*04fd306cSNickeau } 356*04fd306cSNickeau 357*04fd306cSNickeau 358*04fd306cSNickeau static public function isOnForm(): bool 359*04fd306cSNickeau { 360*04fd306cSNickeau return true; 361*04fd306cSNickeau } 362c3437056SNickeau} 363