Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
78.41% covered (warning)
78.41%
69 / 88
78.57% covered (warning)
78.57%
11 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
WebhookEventsService
78.41% covered (warning)
78.41%
69 / 88
78.57% covered (warning)
78.57%
11 / 14
34.34
0.00% covered (danger)
0.00%
0 / 1
 registerEvent
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 unregisterEvent
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 isEventRegistered
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getRegisteredEvents
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 handleEvent
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 checkEventIsSupported
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 prepareTasksParams
72.41% covered (warning)
72.41%
21 / 29
0.00% covered (danger)
0.00%
0 / 1
6.76
 createWebhookTasks
16.67% covered (danger)
16.67%
2 / 12
0.00% covered (danger)
0.00%
0 / 1
8.21
 generateEventId
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getEventHandlerCallback
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getWebhookRegistry
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getWebhookTaskService
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getEventManager
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 extendPayload
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2019 (original work) Open Assessment Technologies SA;
19 */
20
21namespace oat\tao\model\webhooks;
22
23use oat\oatbox\event\Event;
24use oat\oatbox\event\EventManager;
25use oat\oatbox\log\LoggerAwareTrait;
26use oat\oatbox\service\ConfigurableService;
27use oat\tao\model\exceptions\WebhookConfigMissingException;
28use oat\tao\model\webhooks\configEntity\WebhookInterface;
29use oat\tao\model\webhooks\task\WebhookTaskParams;
30
31class WebhookEventsService extends ConfigurableService implements WebhookEventsServiceInterface
32{
33    use LoggerAwareTrait;
34
35    /**
36     * Option value is array of ['eventName' => true, ... ] of supported events
37     * Such array structure is needed to perform quick search by key
38     */
39    public const OPTION_SUPPORTED_EVENTS = 'supportedEvents';
40
41    /**
42     * @inheritDoc
43     */
44    public function registerEvent($eventName)
45    {
46        $supportedEvents = $this->getRegisteredEvents();
47        $supportedEvents[$eventName] = true;
48        $this->setOption(self::OPTION_SUPPORTED_EVENTS, $supportedEvents);
49
50        $this->getEventManager()->attach($eventName, $this->getEventHandlerCallback());
51    }
52
53    /**
54     * @inheritDoc
55     */
56    public function unregisterEvent($eventName)
57    {
58        $supportedEvents = $this->getRegisteredEvents();
59        unset($supportedEvents[$eventName]);
60        $this->setOption(self::OPTION_SUPPORTED_EVENTS, $supportedEvents);
61
62        $this->getEventManager()->detach($eventName, $this->getEventHandlerCallback());
63    }
64
65    /**
66     * @inheritDoc
67     */
68    public function isEventRegistered($eventName)
69    {
70        $supportedEvents = $this->getRegisteredEvents();
71        return isset($supportedEvents[$eventName]);
72    }
73
74    /**
75     * @return string[]
76     */
77    public function getRegisteredEvents()
78    {
79        $events = $this->getOption(self::OPTION_SUPPORTED_EVENTS);
80        return $events !== null
81            ? $events
82            : [];
83    }
84
85    public function handleEvent(Event $event)
86    {
87        if (!$this->checkEventIsSupported($event)) {
88            return;
89        }
90
91        /** @var WebhookSerializableEventInterface $event */
92
93        $webhookConfigIds = $this->getWebhookRegistry()->getWebhookConfigIds($event->getName());
94        if (count($webhookConfigIds) === 0) {
95            return;
96        }
97
98        $tasksParams = $this->prepareTasksParams($event, $webhookConfigIds);
99        $this->createWebhookTasks($tasksParams);
100    }
101
102    /**
103     * @param Event $event
104     * @return bool
105     */
106    private function checkEventIsSupported(Event $event)
107    {
108        $eventName = $event->getName();
109
110        if (!$this->isEventRegistered($eventName)) {
111            $this->logError("Event '$eventName' is not supported by " . self::class);
112            return false;
113        }
114
115        if (!($event instanceof WebhookSerializableEventInterface)) {
116            $this->logError(sprintf(
117                'Event "%s" passed to "%s" is not implementing "%s"',
118                $eventName,
119                self::class,
120                WebhookSerializableEventInterface::class
121            ));
122            return false;
123        }
124
125        return true;
126    }
127
128    /**
129     * @param WebhookSerializableEventInterface $event
130     * @param string[] $webhookConfigIds
131     * @return WebhookTaskParams[]
132     * @throws WebhookConfigMissingException
133     */
134    private function prepareTasksParams(WebhookSerializableEventInterface $event, $webhookConfigIds)
135    {
136        try {
137            $eventData = $event->serializeForWebhook();
138        } catch (\Exception $exception) {
139            $this->logError(sprintf(
140                'Error during "%s" event serialization for webhook. %s',
141                $event->getName(),
142                $exception->getMessage()
143            ));
144            return [];
145        }
146
147        $triggeredTimestamp = time();
148        $eventId = $this->generateEventId($event->getName());
149
150        $result = [];
151
152        foreach ($webhookConfigIds as $webhookConfigId) {
153            if (($webhookConfig = $this->getWebhookRegistry()->getWebhookConfig($webhookConfigId)) === null) {
154                throw new WebhookConfigMissingException(
155                    sprintf('Webhook config for id %s not found', $webhookConfigId)
156                );
157            }
158
159            if (($event instanceof WebhookConditionalEventInterface) && !$event->isSatisfiedBy($webhookConfig)) {
160                continue;
161            }
162
163            $result[] = new WebhookTaskParams([
164                WebhookTaskParams::EVENT_NAME => $event->getWebhookEventName(),
165                WebhookTaskParams::EVENT_ID => $eventId,
166                WebhookTaskParams::TRIGGERED_TIMESTAMP => $triggeredTimestamp,
167                WebhookTaskParams::EVENT_DATA => $this->extendPayload($webhookConfig, $eventData),
168                WebhookTaskParams::WEBHOOK_CONFIG_ID => $webhookConfigId,
169                WebhookTaskParams::RETRY_COUNT => 1,
170                WebhookTaskParams::RETRY_MAX => $webhookConfig->getMaxRetries(),
171                WebhookTaskParams::RESPONSE_VALIDATION => $webhookConfig->getResponseValidationEnable(),
172            ]);
173        }
174
175        return $result;
176    }
177
178    /**
179     * @param WebhookTaskParams[] $tasksParams
180     */
181    private function createWebhookTasks($tasksParams)
182    {
183        foreach ($tasksParams as $taskParams) {
184            try {
185                $this->getWebhookTaskService()->createTask($taskParams);
186            } catch (\Exception $exception) {
187                $this->logError(
188                    sprintf(
189                        "Can't create webhook task for %s. %s",
190                        $taskParams[WebhookTaskParams::EVENT_ID],
191                        $exception->getMessage()
192                    ),
193                    (array) $taskParams
194                );
195                continue;
196            }
197        }
198    }
199
200    /**
201     * @param string $eventName
202     * @return string
203     */
204    private function generateEventId($eventName)
205    {
206        return md5(
207            microtime() .
208            mt_rand() .
209            $eventName .
210            gethostname()
211        );
212    }
213
214    /**
215     * @return array|callable
216     */
217    private function getEventHandlerCallback()
218    {
219        return [self::SERVICE_ID, 'handleEvent'];
220    }
221
222    private function getWebhookRegistry(): WebhookRegistryInterface
223    {
224        /** @noinspection PhpIncompatibleReturnTypeInspection */
225        return $this->getServiceLocator()->getContainer()->get(WebhookRegistryInterface::class);
226    }
227
228    /**
229     * @return WebhookTaskServiceInterface
230     */
231    private function getWebhookTaskService()
232    {
233        /** @noinspection PhpIncompatibleReturnTypeInspection */
234        return $this->getServiceLocator()->get(WebhookTaskServiceInterface::SERVICE_ID);
235    }
236
237    /**
238     * @return EventManager
239     */
240    private function getEventManager()
241    {
242        /** @noinspection PhpIncompatibleReturnTypeInspection */
243        return $this->getServiceLocator()->get(EventManager::SERVICE_ID);
244    }
245
246    private function extendPayload(WebhookInterface $webhookConfig, array $eventData): array
247    {
248        if (empty($webhookConfig->getExtraPayload())) {
249            return $eventData;
250        }
251
252        return array_merge($webhookConfig->getExtraPayload(), $eventData);
253    }
254}