1<?php
2
3require_once(__DIR__ . '/../ComboStrap/PluginUtility.php');
4
5use ComboStrap\DataType;
6use ComboStrap\ExceptionCombo;
7use ComboStrap\FormMeta;
8use ComboStrap\FormMetaField;
9use ComboStrap\HttpResponse;
10use ComboStrap\Identity;
11use ComboStrap\Json;
12use ComboStrap\LowQualityPageOverwrite;
13use ComboStrap\Message;
14use ComboStrap\Metadata;
15use ComboStrap\MetadataDokuWikiStore;
16use ComboStrap\MetadataFormDataStore;
17use ComboStrap\MetadataFrontmatterStore;
18use ComboStrap\MetadataStoreTransfer;
19use ComboStrap\MetaManagerForm;
20use ComboStrap\MetaManagerMenuItem;
21use ComboStrap\Mime;
22use ComboStrap\Page;
23use ComboStrap\PluginUtility;
24use ComboStrap\QualityDynamicMonitoringOverwrite;
25
26if (!defined('DOKU_INC')) die();
27
28/**
29 *
30 * Save metadata that were send by ajax
31 */
32class action_plugin_combo_metamanager extends DokuWiki_Action_Plugin
33{
34
35
36    const META_MANAGER_CALL_ID = "combo-meta-manager";
37    const META_VIEWER_CALL_ID = "combo-meta-viewer";
38
39    const CANONICAL = "meta-manager";
40
41
42
43    /**
44     * The canonical for the metadata page
45     */
46    const METADATA_CANONICAL = "metadata";
47
48    const SUCCESS_MESSAGE = "The data were updated without errors.";
49
50
51    public function register(Doku_Event_Handler $controller)
52    {
53
54        /**
55         * The ajax api to return data
56         */
57        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, '_ajax_call');
58
59        /**
60         * Add a icon in the page tools menu
61         * https://www.dokuwiki.org/devel:event:menu_items_assembly
62         */
63        $controller->register_hook('MENU_ITEMS_ASSEMBLY', 'AFTER', $this, 'handle_rail_bar');
64    }
65
66    /**
67     * Handle Metadata HTTP ajax requests
68     * @param $event Doku_Event
69     *
70     *
71     * https://www.dokuwiki.org/devel:plugin_programming_tips#handle_json_ajax_request
72     *
73     * CSRF checks are only for logged in users
74     * This is public ({@link getSecurityToken()}
75     */
76    function _ajax_call(Doku_Event &$event): void
77    {
78
79        $call = $event->data;
80        if (!in_array($call, [self::META_MANAGER_CALL_ID, self::META_VIEWER_CALL_ID])) {
81            return;
82        }
83        //no other ajax call handlers needed
84        $event->stopPropagation();
85        $event->preventDefault();
86
87        /**
88         * Shared check between post and get HTTP method
89         */
90        $id = $_GET["id"];
91        if ($id === null) {
92            /**
93             * With {@link TestRequest}
94             * for instance
95             */
96            $id = $_REQUEST["id"];
97        }
98
99        if (empty($id)) {
100            HttpResponse::create(HttpResponse::STATUS_BAD_REQUEST)
101                ->setEvent($event)
102                ->setCanonical(self::CANONICAL)
103                ->sendMessage("The page path (id form) is empty");
104            return;
105        }
106        $page = Page::createPageFromId($id);
107        if (!$page->exists()) {
108            HttpResponse::create(HttpResponse::STATUS_DOES_NOT_EXIST)
109                ->setEvent($event)
110                ->setCanonical(self::CANONICAL)
111                ->sendMessage("The page ($id) does not exist");
112            return;
113        }
114
115        /**
116         * Security
117         */
118        if (!$page->canBeUpdatedByCurrentUser()) {
119            $user = Identity::getUser();
120            HttpResponse::create(HttpResponse::STATUS_NOT_AUTHORIZED)
121                ->setEvent($event)
122                ->setCanonical(self::CANONICAL)
123                ->sendMessage("Not Authorized: The user ($user) has not the `write` permission for the page (:$id).");
124            return;
125        }
126
127        /**
128         * Functional code
129         */
130
131        $requestMethod = $_SERVER['REQUEST_METHOD'];
132        switch ($requestMethod) {
133            case 'POST':
134
135                if ($_SERVER["CONTENT_TYPE"] !== "application/json") {
136                    /**
137                     * We can't set the mime content in a {@link TestRequest}
138                     */
139                    if (!PluginUtility::isTest()) {
140                        HttpResponse::create(HttpResponse::STATUS_UNSUPPORTED_MEDIA_TYPE)
141                            ->setEvent($event)
142                            ->setCanonical(self::CANONICAL)
143                            ->sendMessage("The post content should be in json format");
144                        return;
145                    }
146                }
147
148                /**
149                 * We can't simulate a php://input in a {@link TestRequest}
150                 * We set therefore the post
151                 */
152                if (!PluginUtility::isTest()) {
153                    $jsonString = file_get_contents('php://input');
154                    try {
155                        $_POST = Json::createFromString($jsonString)->toArray();
156                    } catch (ExceptionCombo $e) {
157                        HttpResponse::create(HttpResponse::STATUS_BAD_REQUEST)
158                            ->setEvent($event)
159                            ->setCanonical(self::CANONICAL)
160                            ->sendMessage("The json payload could not decoded. Error: {$e->getMessage()}");
161                        return;
162                    }
163                }
164
165                if ($call === self::META_MANAGER_CALL_ID) {
166                    $this->handleManagerPost($event, $page, $_POST);
167                } else {
168                    $this->handleViewerPost($event, $page, $_POST);
169                }
170
171                return;
172            case "GET":
173
174                if ($call === self::META_MANAGER_CALL_ID) {
175                    $this->handleManagerGet($event, $page);
176                } else {
177                    $this->handleViewerGet($event, $page);
178                }
179                return;
180
181        }
182
183    }
184
185    public function handle_rail_bar(Doku_Event $event, $param)
186    {
187
188        if (!Identity::isWriter()) {
189            return;
190        }
191
192        /**
193         * The `view` property defines the menu that is currently built
194         * https://www.dokuwiki.org/devel:menus
195         * If this is not the page menu, return
196         */
197        if ($event->data['view'] != 'page') return;
198
199        global $INFO;
200        if (!$INFO['exists']) {
201            return;
202        }
203        array_splice($event->data['items'], -1, 0, array(new MetaManagerMenuItem()));
204
205    }
206
207    /**
208     * @param $event
209     * @param Page $page
210     * @param array $post
211     */
212    private function handleManagerPost($event, Page $page, array $post)
213    {
214
215        $formStore = MetadataFormDataStore::getOrCreateFromResource($page, $post);
216        $targetStore = MetadataDokuWikiStore::getOrCreateFromResource($page);
217
218        /**
219         * Boolean form field (default values)
220         * are not send back by the HTML form
221         */
222        $defaultBooleanMetadata = [
223            LowQualityPageOverwrite::PROPERTY_NAME,
224            QualityDynamicMonitoringOverwrite::PROPERTY_NAME
225        ];
226        $defaultBoolean = [];
227        foreach ($defaultBooleanMetadata as $booleanMeta) {
228            $metadata = Metadata::getForName($booleanMeta)
229                ->setResource($page)
230                ->setReadStore($formStore)
231                ->setWriteStore($targetStore);
232            $defaultBoolean[$metadata::getName()] = $metadata->toStoreDefaultValue();
233        }
234        $post = array_merge($defaultBoolean, $post);
235
236        /**
237         * Processing
238         */
239        $transfer = MetadataStoreTransfer::createForPage($page)
240            ->fromStore($formStore)
241            ->toStore($targetStore)
242            ->process($post);
243        $processingMessages = $transfer->getMessages();
244
245
246        $responseMessages = [];
247        $responseStatus = HttpResponse::STATUS_ALL_GOOD;
248        foreach ($processingMessages as $upsertMessages) {
249            $responseMessage = [ucfirst($upsertMessages->getType())];
250            $documentationHyperlink = $upsertMessages->getDocumentationHyperLink();
251            if ($documentationHyperlink !== null) {
252                $responseMessage[] = $documentationHyperlink;
253            }
254            $responseMessage[] = $upsertMessages->getContent(Mime::PLAIN_TEXT);
255            $responseMessages[] = implode(" - ", $responseMessage);
256            if ($upsertMessages->getType() === Message::TYPE_ERROR && $responseStatus !== HttpResponse::STATUS_BAD_REQUEST) {
257                $responseStatus = HttpResponse::STATUS_BAD_REQUEST;
258            }
259        }
260
261        if (sizeof($responseMessages) === 0) {
262            $responseMessages[] = self::SUCCESS_MESSAGE;
263        }
264
265        try {
266            $frontMatterMessage = MetadataFrontmatterStore::createFromPage($page)
267                ->sync();
268            $responseMessages[] = $frontMatterMessage->getPlainTextContent();
269        } catch (ExceptionCombo $e) {
270            $responseMessages[] = $e->getMessage();
271        }
272
273
274        /**
275         * Response
276         */
277        HttpResponse::create(HttpResponse::STATUS_ALL_GOOD)
278            ->setEvent($event)
279            ->sendMessage($responseMessages);
280
281
282    }
283
284    /**
285     * @param Doku_Event $event
286     * @param Page $page
287     */
288    private
289    function handleManagerGet(Doku_Event $event, Page $page)
290    {
291        $formMeta = MetaManagerForm::createForPage($page)->toFormMeta();
292        $payload = json_encode($formMeta->toAssociativeArray());
293        HttpResponse::create(HttpResponse::STATUS_ALL_GOOD)
294            ->setEvent($event)
295            ->send($payload, Mime::JSON);
296    }
297
298    /**
299     * @param Doku_Event $event
300     * @param Page $page
301     */
302    private function handleViewerGet(Doku_Event $event, Page $page)
303    {
304        if (!Identity::isManager()) {
305            HttpResponse::create(HttpResponse::STATUS_NOT_AUTHORIZED)
306                ->setEvent($event)
307                ->setCanonical(self::CANONICAL)
308                ->sendMessage("Not Authorized (managers only)");
309            return;
310        }
311        $metadata = MetadataDokuWikiStore::getOrCreateFromResource($page)->getData();
312        $persistent = $metadata[MetadataDokuWikiStore::PERSISTENT_METADATA];
313        ksort($persistent);
314        $current = $metadata[MetadataDokuWikiStore::CURRENT_METADATA];
315        ksort($current);
316        $form = FormMeta::create("raw_metadata")
317            ->addField(
318                FormMetaField::create(MetadataDokuWikiStore::PERSISTENT_METADATA)
319                    ->setLabel("Persistent Metadata (User Metadata)")
320                    ->setTab("persistent")
321                    ->setDescription("The persistent metadata contains raw values. They contains the values set by the user and the fixed values such as page id.")
322                    ->addValue(json_encode($persistent))
323                    ->setType(DataType::JSON_TYPE_VALUE)
324            )
325            ->addField(FormMetaField::create(MetadataDokuWikiStore::CURRENT_METADATA)
326                ->setLabel("Current (Derived) Metadata")
327                ->setTab("current")
328                ->setDescription("The current metadata are the derived / calculated / runtime metadata values (extended with the persistent metadata).")
329                ->addValue(json_encode($current))
330                ->setType(DataType::JSON_TYPE_VALUE)
331                ->setMutable(false)
332            )
333            ->toAssociativeArray();
334
335        HttpResponse::create(HttpResponse::STATUS_ALL_GOOD)
336            ->setEvent($event)
337            ->setCanonical(self::CANONICAL)
338            ->send(json_encode($form), Mime::JSON);
339
340    }
341
342    private function handleViewerPost(Doku_Event $event, Page $page, array $post)
343    {
344
345        $metadataStore = MetadataDokuWikiStore::getOrCreateFromResource($page);
346        $metaData = $metadataStore->getData();
347
348        /**
349         * @var Message[]
350         */
351        $messages = [];
352        /**
353         * Only Persistent, current cannot be modified
354         */
355        $persistentMetadataType = MetadataDokuWikiStore::PERSISTENT_METADATA;
356        $postMeta = json_decode($post[$persistentMetadataType], true);
357        if ($postMeta === null) {
358            HttpResponse::create(HttpResponse::STATUS_BAD_REQUEST)
359                ->setEvent($event)
360                ->sendMessage("The metadata $persistentMetadataType should be in json format");
361            return;
362        }
363        $persistentPageMeta = &$metaData[$persistentMetadataType];
364
365
366        $managedMetaMessageSuffix = "is a managed metadata, you need to use the metadata manager to delete it";
367
368        /**
369         * Process the actual attribute
370         */
371        foreach ($persistentPageMeta as $key => $value) {
372            $postMetaValue = null;
373            if (isset($postMeta[$key])) {
374                $postMetaValue = $postMeta[$key];
375                unset($postMeta[$key]);
376            }
377
378            if ($postMetaValue === null) {
379                if (in_array($key, Metadata::MUTABLE_METADATA)) {
380                    $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) $managedMetaMessageSuffix");
381                    continue;
382                }
383                if (in_array($key, Metadata::NOT_MODIFIABLE_PERSISTENT_METADATA)) {
384                    $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) is a internal metadata, you can't delete it");
385                    continue;
386                }
387                unset($persistentPageMeta[$key]);
388                $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) with the value ($value) was deleted");
389            } else {
390                if ($value !== $postMetaValue) {
391                    if (in_array($key, Metadata::MUTABLE_METADATA)) {
392                        $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) $managedMetaMessageSuffix");
393                        continue;
394                    }
395                    if (in_array($key, Metadata::NOT_MODIFIABLE_PERSISTENT_METADATA)) {
396                        $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) is a internal metadata, you can't modify it");
397                        continue;
398                    }
399                    $persistentPageMeta[$key] = $postMetaValue;
400                    $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) was updated to the value ($postMetaValue) - Old value ($value)");
401                }
402            }
403        }
404        /**
405         * Process the new attribute
406         */
407        foreach ($postMeta as $key => $value) {
408            if (in_array($key, Metadata::MUTABLE_METADATA)) {
409                // This meta should be modified via the form
410                $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) can only be added via the meta manager");
411                continue;
412            }
413            if (in_array($key, Metadata::NOT_MODIFIABLE_PERSISTENT_METADATA)) {
414                // this meta are not modifiable
415                $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) is a internal metadata, you can't modify it");
416                continue;
417            }
418            $persistentPageMeta[$key] = $value;
419            $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) was created with the value ($value)");
420        }
421
422        /**
423         * Delete the runtime if present
424         * (They were saved in persistent)
425         */
426        Metadata::deleteIfPresent($persistentPageMeta, Metadata::RUNTIME_META);
427
428        p_save_metadata($page->getDokuwikiId(), $metaData);
429        $metadataStore->setData($metaData);
430
431        if (sizeof($messages) !== 0) {
432            $messagesToSend = [];
433            foreach ($messages as $message) {
434                $messagesToSend[] = $message->getPlainTextContent();
435            }
436        } else {
437            $messagesToSend = "No metadata has been changed.";
438        }
439        HttpResponse::create(HttpResponse::STATUS_ALL_GOOD)
440            ->setEvent($event)
441            ->sendMessage($messagesToSend);
442
443    }
444
445
446}
447