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