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