xref: /plugin/combo/action/metamanager.php (revision c3437056399326d621a01da73b649707fbb0ae69)
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                    $_POST = Json::createFromString($jsonString)->toArray();
155                }
156
157                if ($call === self::META_MANAGER_CALL_ID) {
158                    $this->handleManagerPost($event, $page, $_POST);
159                } else {
160                    $this->handleViewerPost($event, $page, $_POST);
161                }
162
163                return;
164            case "GET":
165
166                if ($call === self::META_MANAGER_CALL_ID) {
167                    $this->handleManagerGet($event, $page);
168                } else {
169                    $this->handleViewerGet($event, $page);
170                }
171                return;
172
173        }
174
175    }
176
177    public function handle_rail_bar(Doku_Event $event, $param)
178    {
179
180        if (!Identity::isWriter()) {
181            return;
182        }
183
184        /**
185         * The `view` property defines the menu that is currently built
186         * https://www.dokuwiki.org/devel:menus
187         * If this is not the page menu, return
188         */
189        if ($event->data['view'] != 'page') return;
190
191        global $INFO;
192        if (!$INFO['exists']) {
193            return;
194        }
195        array_splice($event->data['items'], -1, 0, array(new MetaManagerMenuItem()));
196
197    }
198
199    /**
200     * @param $event
201     * @param Page $page
202     * @param array $post
203     */
204    private function handleManagerPost($event, Page $page, array $post)
205    {
206
207        $formStore = MetadataFormDataStore::getOrCreateFromResource($page, $post);
208        $targetStore = MetadataDokuWikiStore::getOrCreateFromResource($page);
209
210        /**
211         * Boolean form field (default values)
212         * are not send back by the HTML form
213         */
214        $defaultBooleanMetadata = [
215            LowQualityPageOverwrite::PROPERTY_NAME,
216            QualityDynamicMonitoringOverwrite::PROPERTY_NAME
217        ];
218        $defaultBoolean = [];
219        foreach ($defaultBooleanMetadata as $booleanMeta) {
220            $metadata = Metadata::getForName($booleanMeta)
221                ->setResource($page)
222                ->setReadStore($formStore)
223                ->setWriteStore($targetStore);
224            $defaultBoolean[$metadata::getName()] = $metadata->toStoreDefaultValue();
225        }
226        $post = array_merge($defaultBoolean, $post);
227
228        /**
229         * Processing
230         */
231        $transfer = MetadataStoreTransfer::createForPage($page)
232            ->fromStore($formStore)
233            ->toStore($targetStore)
234            ->process($post);
235        $processingMessages = $transfer->getMessages();
236
237
238        $responseMessages = [];
239        $responseStatus = HttpResponse::STATUS_ALL_GOOD;
240        foreach ($processingMessages as $upsertMessages) {
241            $responseMessage = [ucfirst($upsertMessages->getType())];
242            $documentationHyperlink = $upsertMessages->getDocumentationHyperLink();
243            if ($documentationHyperlink !== null) {
244                $responseMessage[] = $documentationHyperlink;
245            }
246            $responseMessage[] = $upsertMessages->getContent(Mime::PLAIN_TEXT);
247            $responseMessages[] = implode(" - ", $responseMessage);
248            if ($upsertMessages->getType() === Message::TYPE_ERROR && $responseStatus !== HttpResponse::STATUS_BAD_REQUEST) {
249                $responseStatus = HttpResponse::STATUS_BAD_REQUEST;
250            }
251        }
252
253        if (sizeof($responseMessages) === 0) {
254            $responseMessages[] = self::SUCCESS_MESSAGE;
255        }
256
257        try {
258            $frontMatterMessage = MetadataFrontmatterStore::createFromPage($page)
259                ->sync();
260            $responseMessages[] = $frontMatterMessage->getPlainTextContent();
261        } catch (ExceptionCombo $e) {
262            $responseMessages[] = $e->getMessage();
263        }
264
265
266        /**
267         * Response
268         */
269        HttpResponse::create(HttpResponse::STATUS_ALL_GOOD)
270            ->setEvent($event)
271            ->sendMessage($responseMessages);
272
273
274    }
275
276    /**
277     * @param Doku_Event $event
278     * @param Page $page
279     * @throws ExceptionCombo
280     */
281    private
282    function handleManagerGet(Doku_Event $event, Page $page)
283    {
284        $formMeta = MetaManagerForm::createForPage($page)->toFormMeta();
285        $payload = json_encode($formMeta->toAssociativeArray());
286        HttpResponse::create(HttpResponse::STATUS_ALL_GOOD)
287            ->setEvent($event)
288            ->send($payload, Mime::JSON);
289    }
290
291    /**
292     * @param Doku_Event $event
293     * @param Page $page
294     */
295    private function handleViewerGet(Doku_Event $event, Page $page)
296    {
297        if (!Identity::isManager()) {
298            HttpResponse::create(HttpResponse::STATUS_NOT_AUTHORIZED)
299                ->setEvent($event)
300                ->setCanonical(self::CANONICAL)
301                ->sendMessage("Not Authorized (managers only)");
302            return;
303        }
304        $metadata = MetadataDokuWikiStore::getOrCreateFromResource($page)->getData();
305        $persistent = $metadata[MetadataDokuWikiStore::PERSISTENT_METADATA];
306        ksort($persistent);
307        $current = $metadata[MetadataDokuWikiStore::CURRENT_METADATA];
308        ksort($current);
309        $form = FormMeta::create("raw_metadata")
310            ->addField(
311                FormMetaField::create(MetadataDokuWikiStore::PERSISTENT_METADATA)
312                    ->setLabel("Persistent Metadata (User Metadata)")
313                    ->setTab("persistent")
314                    ->setDescription("The persistent metadata contains raw values. They contains the values set by the user and the fixed values such as page id.")
315                    ->addValue(json_encode($persistent))
316                    ->setType(DataType::JSON_TYPE_VALUE)
317            )
318            ->addField(FormMetaField::create(MetadataDokuWikiStore::CURRENT_METADATA)
319                ->setLabel("Current (Derived) Metadata")
320                ->setTab("current")
321                ->setDescription("The current metadata are the derived / calculated / runtime metadata values (extended with the persistent metadata).")
322                ->addValue(json_encode($current))
323                ->setType(DataType::JSON_TYPE_VALUE)
324                ->setMutable(false)
325            )
326            ->toAssociativeArray();
327
328        HttpResponse::create(HttpResponse::STATUS_ALL_GOOD)
329            ->setEvent($event)
330            ->setCanonical(self::CANONICAL)
331            ->send(json_encode($form), Mime::JSON);
332
333    }
334
335    private function handleViewerPost(Doku_Event $event, Page $page, array $post)
336    {
337
338        $metadataStore = MetadataDokuWikiStore::getOrCreateFromResource($page);
339        $metaData = $metadataStore->getData();
340
341        /**
342         * @var Message[]
343         */
344        $messages = [];
345        /**
346         * Only Persistent, current cannot be modified
347         */
348        $persistentMetadataType = MetadataDokuWikiStore::PERSISTENT_METADATA;
349        $postMeta = json_decode($post[$persistentMetadataType], true);
350        if ($postMeta === null) {
351            HttpResponse::create(HttpResponse::STATUS_BAD_REQUEST)
352                ->setEvent($event)
353                ->sendMessage("The metadata $persistentMetadataType should be in json format");
354            return;
355        }
356        $persistentPageMeta = &$metaData[$persistentMetadataType];
357
358
359        $managedMetaMessageSuffix = "is a managed metadata, you need to use the metadata manager to delete it";
360
361        /**
362         * Process the actual attribute
363         */
364        foreach ($persistentPageMeta as $key => $value) {
365            $postMetaValue = null;
366            if (isset($postMeta[$key])) {
367                $postMetaValue = $postMeta[$key];
368                unset($postMeta[$key]);
369            }
370
371            if ($postMetaValue === null) {
372                if (in_array($key, Metadata::MUTABLE_METADATA)) {
373                    $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) $managedMetaMessageSuffix");
374                    continue;
375                }
376                if (in_array($key, Metadata::NOT_MODIFIABLE_PERSISTENT_METADATA)) {
377                    $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) is a internal metadata, you can't delete it");
378                    continue;
379                }
380                unset($persistentPageMeta[$key]);
381                $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) with the value ($value) was deleted");
382            } else {
383                if ($value !== $postMetaValue) {
384                    if (in_array($key, Metadata::MUTABLE_METADATA)) {
385                        $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) $managedMetaMessageSuffix");
386                        continue;
387                    }
388                    if (in_array($key, Metadata::NOT_MODIFIABLE_PERSISTENT_METADATA)) {
389                        $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) is a internal metadata, you can't modify it");
390                        continue;
391                    }
392                    $persistentPageMeta[$key] = $postMetaValue;
393                    $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) was updated to the value ($postMetaValue) - Old value ($value)");
394                }
395            }
396        }
397        /**
398         * Process the new attribute
399         */
400        foreach ($postMeta as $key => $value) {
401            if (in_array($key, Metadata::MUTABLE_METADATA)) {
402                // This meta should be modified via the form
403                $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) can only be added via the meta manager");
404                continue;
405            }
406            if (in_array($key, Metadata::NOT_MODIFIABLE_PERSISTENT_METADATA)) {
407                // this meta are not modifiable
408                $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) is a internal metadata, you can't modify it");
409                continue;
410            }
411            $persistentPageMeta[$key] = $value;
412            $messages[] = Message::createInfoMessage("The $persistentMetadataType metadata ($key) was created with the value ($value)");
413        }
414
415        /**
416         * Delete the runtime if present
417         * (They were saved in persistent)
418         */
419        Metadata::deleteIfPresent($persistentPageMeta, Metadata::RUNTIME_META);
420
421        p_save_metadata($page->getDokuwikiId(), $metaData);
422        $metadataStore->setData($metaData);
423
424        if (sizeof($messages) !== 0) {
425            $messagesToSend = [];
426            foreach ($messages as $message) {
427                $messagesToSend[] = $message->getPlainTextContent();
428            }
429        } else {
430            $messagesToSend = "No metadata has been changed.";
431        }
432        HttpResponse::create(HttpResponse::STATUS_ALL_GOOD)
433            ->setEvent($event)
434            ->sendMessage($messagesToSend);
435
436    }
437
438
439}
440