1<?php
2/**
3 * DokuWiki Plugin Slack Notifier (Action Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 */
7
8use dokuwiki\Extension\ActionPlugin;
9use dokuwiki\Extension\Event;
10use dokuwiki\Extension\EventHandler;
11use dokuwiki\HTTP\DokuHTTPClient;
12use dokuwiki\Logger;
13use dokuwiki\plugin\slacknotifier\event\PageMoveEvent;
14use dokuwiki\plugin\slacknotifier\event\PageSaveEvent;
15use dokuwiki\plugin\slacknotifier\helper\Config;
16use dokuwiki\plugin\slacknotifier\helper\Context;
17use dokuwiki\plugin\slacknotifier\helper\Formatter;
18
19class action_plugin_slacknotifier extends ActionPlugin
20{
21    /** @var Event[] */
22    private $changes = [];
23    /** @var PageMoveEvent[] */
24    private $created = [];
25    /** @var PageMoveEvent[] */
26    private $deleted = [];
27    private $inRename = false;
28    /** @var Config */
29    private $config;
30
31    public function __construct() {
32        $this->config = new Config($this);
33    }
34
35    public function register(EventHandler $controller): void
36    {
37        if (!$this->config->webhook) {
38            return;
39        }
40
41        $controller->register_hook('COMMON_WIKIPAGE_SAVE', 'AFTER', $this, 'handleSave');
42        $controller->register_hook('PLUGIN_MOVE_PAGE_RENAME', 'BEFORE', $this, 'handleRenameBefore', 'BEFORE');
43        $controller->register_hook('PLUGIN_MOVE_PAGE_RENAME', 'AFTER', $this, 'handleRenameAfter', 'AFTER');
44    }
45
46    public function handleRenameBefore(Event $rawEvent): void
47    {
48        $this->inRename = true;
49        $event = new PageMoveEvent($rawEvent);
50        $this->created[$event->dst_id] = $event;
51        $this->deleted[$event->src_id] = $event;
52    }
53
54    public function handleRenameAfter(): void
55    {
56        if (!$this->inRename) {
57            // Sanity check
58            throw new RuntimeException('Logic error: in rename is false after rename');
59        }
60        $this->inRename = false;
61
62        foreach ($this->getEvents() as $event) {
63            $this->processEvent($event);
64        }
65    }
66
67    public function handleSave(Event $event): void
68    {
69        if ($this->inRename) {
70            $this->changes[] = $event;
71            return;
72        }
73
74        $this->processEvent(new PageSaveEvent($event));
75    }
76
77    /**
78     * @return PageSaveEvent[]
79     */
80    private function getEvents(): array
81    {
82        $events = [];
83        foreach ($this->changes as $rawEvent) {
84            $event = new PageSaveEvent($rawEvent);
85            $pageId = $event->id;
86
87            if ($event->isCreate() && isset($this->created[$pageId])) {
88                $moveEvent = $this->created[$pageId];
89                $moveEvent->setCreatedPageEvent($event);
90                unset($this->created[$pageId]);
91            } elseif ($event->isDelete() && isset($this->deleted[$pageId])) {
92                $moveEvent = $this->deleted[$pageId];
93                $createdEvent = $moveEvent->getCreatedPageEvent();
94                $createdEvent->convertToRename($event);
95                unset($this->deleted[$pageId]);
96                // Skip delete event itself
97                continue;
98            }
99
100            $events[] = $event;
101        }
102        $this->changes = [];
103
104        return $events;
105    }
106
107    private function processEvent(PageSaveEvent $event): void
108    {
109        if (!$this->config->isValidNamespace($event->getNamespace())) {
110            return;
111        }
112
113        if (!$this->isValidEvent($event->getEventType())) {
114            return;
115        }
116
117        $formatter = new Formatter($this->config);
118        $formatted = $formatter->format($event, new Context());
119        $this->submitPayload($this->config->webhook, $formatted);
120    }
121
122    private function isValidEvent(?string $eventType): bool
123    {
124        if ($eventType === 'create' && $this->config->notify_create) {
125            return true;
126        } elseif ($eventType === 'edit' && $this->config->notify_edit) {
127            return true;
128        } elseif ($eventType === 'edit minor' && $this->config->notify_edit && $this->config->notify_edit_minor) {
129            return true;
130        } elseif ($eventType === 'delete' && $this->config->notify_delete) {
131            return true;
132        } elseif ($eventType === 'rename' && $this->config->notify_create && $this->config->notify_delete) {
133            return true;
134        }
135
136        return false;
137    }
138
139    private function submitPayload(string $url, array $payload): void
140    {
141        $http = new DokuHTTPClient();
142        $http->headers['Content-Type'] = 'application/json';
143        // we do single ops here, no need for keep-alive
144        $http->keep_alive = false;
145
146        $result = $http->post($url, ['payload' => json_encode($payload)]);
147        if ($result !== 'ok') {
148            $ctx = [
149                'resp_body' => $http->resp_body,
150                'result' => $result,
151                'http_error' => $http->error,
152            ];
153            Logger::error('Error posting to Slack', $ctx, __FILE__, __LINE__);
154        }
155    }
156}
157