xref: /template/strap/ComboStrap/PageId.php (revision 0e43c1db59ebeeac06e9a22249bf3806a94553d4)
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*0e43c1dbSgerardnico
11247a8d2b6Sgerardnico                $pathDbValue = $dbStore->getFromPersistentName(PagePath::getPersistentName());
113*0e43c1dbSgerardnico
114*0e43c1dbSgerardnico                /**
115*0e43c1dbSgerardnico                 * If the page in the database does not exist,
116*0e43c1dbSgerardnico                 * We think that the page was moved from the file system
117*0e43c1dbSgerardnico                 * and we return the page id
118*0e43c1dbSgerardnico                 */
119*0e43c1dbSgerardnico                $pageDbValue = Page::createPageFromQualifiedPath($pathDbValue);
120*0e43c1dbSgerardnico                if(!FileSystems::exists($pageDbValue->getPath())){
121*0e43c1dbSgerardnico                    return parent::buildFromStoreValue($value);
122*0e43c1dbSgerardnico                }
123*0e43c1dbSgerardnico
124*0e43c1dbSgerardnico                /**
125*0e43c1dbSgerardnico                 * The page path in the database exists
126*0e43c1dbSgerardnico                 * If they are the same, we return the page id
127*0e43c1dbSgerardnico                 * (because due to duplicate in canonical, the row returned may be from another resource)
128*0e43c1dbSgerardnico                 */
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