1<?php
2
3require_once(__DIR__ . '/../ComboStrap/PluginUtility.php');
4
5use ComboStrap\DatabasePageRow;
6use ComboStrap\ExceptionCompile;
7use ComboStrap\ExceptionNotFound;
8use ComboStrap\ExecutionContext;
9use ComboStrap\FileSystems;
10use ComboStrap\LinkMarkup;
11use ComboStrap\LogUtility;
12use ComboStrap\MarkupPath;
13use ComboStrap\MarkupRef;
14use ComboStrap\Meta\Field\Aliases;
15use ComboStrap\Meta\Store\MetadataDbStore;
16use ComboStrap\Meta\Store\MetadataDokuWikiStore;
17use ComboStrap\PageId;
18use ComboStrap\PluginUtility;
19use ComboStrap\Site;
20
21
22/**
23 * Class action_plugin_combo_move
24 * Handle the move of a page in order to update:
25 *   * the link
26 *   * the data in the database
27 */
28class action_plugin_combo_linkmove extends DokuWiki_Action_Plugin
29{
30
31
32    const CANONICAL = "move";
33    const FILE_MOVE_OPERATION = "move";
34
35    public static function isMoveOperation(): bool
36    {
37        try {
38            $executionContext = ExecutionContext::getActualOrCreateFromEnv();
39            $executionContext->getRuntimeObject(action_plugin_combo_linkmove::FILE_MOVE_OPERATION);
40            return true;
41        } catch (ExceptionNotFound $e) {
42            return false;
43        }
44    }
45
46    private static function checkAndSendAMessageIfLockFilePresent(): bool
47    {
48        $lockFile = Site::getDataDirectory()->resolve("locks_plugin_move.lock");
49        if (!FileSystems::exists($lockFile)) {
50            return false;
51        }
52        $lockFileDateTimeModified = FileSystems::getModifiedTime($lockFile);
53        $lockFileModifiedTimestamp = $lockFileDateTimeModified->getTimestamp();
54        $now = time();
55
56        $distance = $now - $lockFileModifiedTimestamp;
57        $lockFileAgeInMinute = ($distance) / 60;
58        if ($lockFileAgeInMinute > 5) {
59            LogUtility::msg("The move lockfile ($lockFile) exists and is older than 5 minutes (exactly $lockFileAgeInMinute minutes). If you are no more in a move, you should delete this file otherwise it will disable the move of page and the cache.");
60            return true;
61        }
62        return false;
63    }
64
65    /**
66     * As explained https://www.dokuwiki.org/plugin:move#support_for_other_plugins
67     * @param Doku_Event_Handler $controller
68     */
69    function register(Doku_Event_Handler $controller)
70    {
71
72        /**
73         * To rewrite the page meta in the database
74         */
75        $controller->register_hook('PLUGIN_MOVE_PAGE_RENAME', 'BEFORE', $this, 'handle_rename_before', array());
76        $controller->register_hook('PLUGIN_MOVE_PAGE_RENAME', 'AFTER', $this, 'handle_rename_after', array());
77
78        /**
79         * To rewrite the link
80         */
81        $controller->register_hook('PLUGIN_MOVE_HANDLERS_REGISTER', 'BEFORE', $this, 'handle_link', array());
82
83
84        /**
85         * Check for the presence of a lock file
86         */
87        $controller->register_hook('PARSER_WIKITEXT_PREPROCESS', 'BEFORE', $this, 'check_lock_file_age', array());
88
89
90    }
91
92    /**
93     * @param Doku_Event $event
94     * @param $params
95     *
96     * When a lock file is present,
97     * the move plugin will purge the data in {@link action_plugin_move_rewrite::handle_cache()}
98     * making the rendering fucking slow
99     * We check that the lock file is not
100     */
101    function check_lock_file_age(Doku_Event $event, $params)
102    {
103        self::checkAndSendAMessageIfLockFilePresent();
104
105    }
106
107    /**
108     * Handle the path modification of a page
109     * @param Doku_Event $event - https://www.dokuwiki.org/plugin:move#for_plugin_authors
110     * @param $params
111     *
112     */
113    function handle_rename_before(Doku_Event $event, $params)
114    {
115
116
117        /**
118         * Check that the lock file is not older
119         * Lock file bigger than 5 minutes
120         * Is not really possible
121         */
122        $result = self::checkAndSendAMessageIfLockFilePresent();
123        if ($result === true) {
124            $event->preventDefault();
125            LogUtility::warning("The move lock file is present, the move was canceled.");
126            return;
127        }
128
129        /**
130         * The move plugin just delete the file
131         * and recreate it.
132         * There is no move operation
133         * Therefore, because a delete in the database is a soft delete (ie if not exists)
134         * We move it now
135         */
136        $sourceId = $event->data["src_id"];
137        $targetId = $event->data["dst_id"];
138        try {
139
140            /**
141             * Update the dokuwiki id and path
142             */
143            try {
144                $databasePage = DatabasePageRow::getFromDokuWikiId($sourceId);
145            } catch (ExceptionNotFound $e) {
146                LogUtility::warning("The source database row $sourceId was not found in the database");
147                return;
148            }
149
150            $databasePage->updatePathAndDokuwikiId($targetId);
151
152            /**
153             * Advertise the move
154             * Because otherwise the file is deleted
155             * in {@link action_plugin_combo_pagesystemmutation}
156             */
157            ExecutionContext::getActualOrCreateFromEnv()
158                ->setRuntimeObject(self::FILE_MOVE_OPERATION, $sourceId);
159
160        } catch (Exception $exception) {
161            $this->reportError($exception);
162        }
163
164    }
165
166    /**
167     * Handle the path modification of a page after
168     *
169     * The metadata file should also have been moved
170     *
171     * @param Doku_Event $event - https://www.dokuwiki.org/plugin:move#for_plugin_authors
172     * @param $params
173     *
174     */
175    function handle_rename_after(Doku_Event $event, $params)
176    {
177        /**
178         *
179         * $event->data
180         * src_id ⇒ string – the original ID of the page
181         * dst_id ⇒ string – the new ID of the page
182         */
183        $sourceId = $event->data["src_id"];
184        $targetId = $event->data["dst_id"];
185        try {
186
187            /**
188             * Update the dokuwiki id and path
189             */
190            try {
191                $databasePage = DatabasePageRow::getFromDokuWikiId($targetId);
192            } catch (ExceptionNotFound $e) {
193                LogUtility::warning("The target database row with the id ($targetId) was not found in the database");
194                return;
195            }
196
197            /**
198             * Check page id
199             */
200            $targetPage = MarkupPath::createMarkupFromId($targetId);
201            $targetPageId = PageId::createForPage($targetPage);
202            $targetPageIdValue = $targetPageId->getValueFromStore();
203            $databasePageIdValue = $databasePage->getPageId();
204
205            if ($targetPageIdValue === null) {
206                $targetPageId
207                    ->setValue($databasePageIdValue)
208                    ->persist();
209            } else {
210                /**
211                 * {@link helper_plugin_move_op::movePage()}
212                 * delete and save the file with log
213                 */
214                $targetPageId
215                    ->setValueForce($databasePageIdValue)
216                    ->persist();
217            }
218
219            /**
220             * Add the alias
221             */
222            Aliases::createForPage($targetPage)
223                ->addAlias($sourceId)
224                ->setWriteStore(MetadataDokuWikiStore::class)
225                ->sendToWriteStore()
226                ->persist()
227                ->setReadStore(MetadataDbStore::class)
228                ->sendToWriteStore()
229                ->persist();
230
231            /**
232             * Stop advertising the move
233             */
234            ExecutionContext::getActualOrCreateFromEnv()
235                ->closeAndRemoveRuntimeVariableIfExists(self::FILE_MOVE_OPERATION);
236
237        } catch (Exception $exception) {
238            $this->reportError($exception);
239        }
240
241    }
242
243
244    /**
245     * Handle the move of a page
246     * @param Doku_Event $event
247     * @param $params
248     */
249    function handle_link(Doku_Event $event, $params)
250    {
251        /**
252         * The handlers is the name of the component (ie refers to the {@link syntax_plugin_combo_link} handler)
253         * and 'rewrite_combo' to the below method
254         */
255        $event->data['handlers'][syntax_plugin_combo_link::COMPONENT] = array($this, 'rewrite_combo');
256    }
257
258    /**
259     *
260     * @param $match
261     * @param $state
262     * @param $pos
263     * @param $plugin
264     * @param helper_plugin_move_handler $handler
265     */
266    public function rewrite_combo($match, $state, $pos, $plugin, helper_plugin_move_handler $handler)
267    {
268        /**
269         * The goal is to recreate the document
270         * in the {@link helper_plugin_move_handler::$calls}
271         * variable (this is a string, not an array of calls)
272         *
273         * We got the {@link syntax_plugin_combo_link::handle() render match of the handle function}
274         *
275         * Unfortunately, all environemnt propertes of {@link helper_plugin_move_handler}
276         * such as {@link helper_plugin_move_handler::$id} are private
277         *
278         * The code below calls then the rewrite function {@link helper_plugin_move_handler::internallink()}
279         * and change the content modified in the {@link helper_plugin_move_handler::$calls} variable
280         *
281         */
282        if ($state !== DOKU_LEXER_ENTER) {
283            // Description and ending
284            $handler->calls .= $match;
285            return;
286        }
287
288        /**
289         * All environment on the {@link helper_plugin_move_handler handler} are private
290         * We can't get it, we just hack around the move of the handler then
291         */
292        $parseAttributes = syntax_plugin_combo_link::parse($match);
293        $ref = $parseAttributes[syntax_plugin_combo_link::MARKUP_REF_ATTRIBUTE];
294        try {
295            $link = LinkMarkup::createFromRef($ref);
296            $isWikiUri = $link->getMarkupRef()->getSchemeType() === MarkupRef::WIKI_URI;
297        } catch (ExceptionCompile $e) {
298            LogUtility::error("Unable to rewrite the markup reference for a link move. The markup ref ($ref) could not be parsed. Error: {$e->getMessage()}");
299            $handler->calls .= $match;
300            return;
301        }
302
303        if (!$isWikiUri) {
304            // Other type of links
305            $handler->calls .= $match;
306            return;
307        }
308
309        /**
310         * This function will modify and add the link to the new output (ie calls)
311         * {@link helper_plugin_move_handler::$calls}
312         */
313        $handler->internallink($match, $state, $pos);
314
315        /**
316         * Internal Link Calls Hack
317         * that delete the ]]
318         */
319        $suffix = "]]";
320        if (substr($handler->calls, -strlen($suffix)) === $suffix) {
321            $handler->calls = substr($handler->calls, 0, strlen($handler->calls) - strlen($suffix));
322        }
323
324
325    }
326
327    private function reportError(Exception $exception)
328    {
329        // We catch the errors if any to not stop the move
330        // There is no transaction feature (it happens or not)
331        $message = "An error occurred during the move replication to the database. Error message was: " . $exception->getMessage();
332        if (PluginUtility::isDevOrTest()) {
333            throw new RuntimeException($exception);
334        } else {
335            LogUtility::error($message, self::CANONICAL, $exception);
336        }
337    }
338
339
340}
341