xref: /template/strap/ComboStrap/PageId.php (revision 04fd306c7c155fa133ebb3669986875d65988276)
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