xref: /plugin/combo/ComboStrap/PageId.php (revision c3437056399326d621a01da73b649707fbb0ae69)
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