PluginProbe ʕ •ᴥ•ʔ
VikAppointments Services Booking Calendar / 1.2.20
VikAppointments Services Booking Calendar v1.2.20
1.2.21 1.2.20 trunk 1.2.17 1.2.18 1.2.19
vikappointments / site / helpers / libraries / webhook / queue.php
vikappointments / site / helpers / libraries / webhook Last commit date
classes 1 month ago index.html 1 month ago queue.php 1 month ago webhook.php 1 month ago
queue.php
354 lines
1 <?php
2 /**
3 * @package VikAppointments
4 * @subpackage core
5 * @author E4J s.r.l.
6 * @copyright Copyright (C) 2021 E4J s.r.l. All Rights Reserved.
7 * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
8 * @link https://vikwp.com
9 */
10
11 // No direct access
12 defined('ABSPATH') or die('No script kiddies please!');
13
14 VAPLoader::import('libraries.webhook.webhook');
15
16 /**
17 * Web hooks queue manager class.
18 *
19 * @since 1.7
20 */
21 class VAPWebHookQueue
22 {
23 /**
24 * Singleton reference.
25 *
26 * @var self
27 */
28 protected static $instance = null;
29
30 /**
31 * A list of supported web hooks.
32 *
33 * @var array
34 */
35 protected $hooks = array();
36
37 /**
38 * A queue of payloads.
39 *
40 * @var array
41 */
42 protected $queue = array();
43
44 /**
45 * Returns the queue instance by creating it only once.
46 *
47 * @return self
48 */
49 public static function getInstance()
50 {
51 if (static::$instance === null)
52 {
53 static::$instance = new static();
54 }
55
56 return static::$instance;
57 }
58
59 /**
60 * Constructs the object by pre-loading all the supported web hooks
61 * created by the administrator.
62 */
63 public function __construct()
64 {
65 $dbo = JFactory::getDbo();
66
67 $q = $dbo->getQuery(true)
68 ->select('*')
69 ->from($dbo->qn('#__vikappointments_webhook'))
70 ->where($dbo->qn('published') . ' = 1');
71
72 $dbo->setQuery($q);
73
74 foreach ($dbo->loadObjectList() as $hook)
75 {
76 // decode hook params
77 $hook->params = $hook->params ? json_decode($hook->params) : array();
78 // register web hook record
79 $this->hooks[] = $hook;
80 }
81 }
82
83 /**
84 * Auto-deliver the pending requests while this instance gets
85 * destructed by the garbage collector.
86 */
87 public function __destruct()
88 {
89 $this->deliver();
90 }
91
92 /**
93 * Registers a new hook within the queue.
94 *
95 * @param string $hook The hook name.
96 * @param mixed $payload The payload to deliver.
97 *
98 * @return boolean True on success, false otherwise.
99 */
100 public function register($hook, $payload)
101 {
102 // make sure the hook is supported
103 if (!$this->getHooks($hook))
104 {
105 // hook not observed
106 return false;
107 }
108
109 // create web hook instance
110 $webhook = VAPWebHook::getInstance($hook, $payload);
111
112 // iterate all previously registered web hooks to make
113 // sure they should be merged together instead of being
114 // send with separated requests
115 $inside = $this->search($webhook);
116
117 if ($inside)
118 {
119 // web hook already registered, merge payloads
120 $inside->extend($webhook);
121 }
122 else
123 {
124 // web hook not found, register within the queue
125 $this->queue[] = $webhook;
126 }
127
128 return true;
129 }
130
131 /**
132 * Proxy for `deliver`.
133 *
134 * @deprecated 1.8 Use deliver() instead.
135 */
136 public function delivery()
137 {
138 return $this->deliver();
139 }
140
141 /**
142 * Delivers the web hooks currently registered within the queue.
143 *
144 * @return boolean True on success, false otherwise.
145 *
146 * @since 1.7.4 Renamed from delivery.
147 */
148 public function deliver()
149 {
150 if (!$this->queue)
151 {
152 // nothing to notify
153 return false;
154 }
155
156 // Close connection to speed up the page load.
157 // Nothing processed from now on needs to be presented to the user.
158 $this->closeConnection();
159
160 // get event dispatcher
161 $dispatcher = VAPFactory::getEventDispatcher();
162
163 $status = false;
164
165 $http = new JHttp();
166
167 // get web hook model
168 $model = JModelVAP::getInstance('webhook');
169
170 // iterate all the elements registered within the queue
171 foreach ($this->queue as $job)
172 {
173 // get all matching web hooks
174 $hooks = $this->getHooks($job->getHook());
175
176 foreach ($hooks as $hook)
177 {
178 // prepare headers
179 $headers = array(
180 'Content-Type' => 'application/json',
181 'X-VAP-WEBHOOK-ID' => $hook->id,
182 'X-VAP-WEBHOOK-ACTION' => $hook->hook,
183 'X-VAP-WEBHOOK-SECURE' => md5($hook->secret),
184 );
185
186 try
187 {
188 // fetch request payload
189 $payload = $job->getPayload($hook->params);
190 }
191 catch (Exception $e)
192 {
193 // extract exception details and send them as payload, so that we can
194 // track the error through the log files
195 $payload = array(
196 'code' => $e->getCode(),
197 'message' => $e->getMessage(),
198 'trace' => $e->getTrace(),
199 );
200 }
201
202 /**
203 * Plugins can use this event to manipulate the payload to post and the
204 * request headers. By setting the payload to "false", the dispatcher
205 * will automatically skip the web hook.
206 *
207 * @param mixed &$payload The payload to send.
208 * @param array &$headers The request headers.
209 * @param object $hook The web hook details.
210 * @param VAPWebHook $job The web hook job.
211 *
212 * @return void
213 *
214 * @since 1.7
215 */
216 $dispatcher->trigger('onBeforeDispatchWebhook', array(&$payload, &$headers, $hook, $job));
217
218 // ignore payload in case the webhook explicitly returned false
219 if ($payload !== false)
220 {
221 // JSON encode payload (if not already encoded)
222 $payload = is_string($payload) ? $payload : json_encode($payload);
223
224 // dispatch request to the specified end-point
225 $response = $http->post($hook->url, $payload, $headers);
226
227 /**
228 * Plugins can use this event to manipulate the response received after
229 * deploying the web hook.
230 *
231 * @param object $response The HTTP response object.
232 * @param object $hook The web hook details.
233 *
234 * @return void
235 *
236 * @since 1.7
237 */
238 $dispatcher->trigger('onAfterDispatchWebhook', array($response, $hook));
239
240 // prepare save data
241 $saveData = array(
242 'id' => $hook->id,
243 'lastping' => true,
244 'logkey' => $hook->logkey,
245 'log' => array(
246 'headers' => $headers,
247 'payload' => json_decode($payload),
248 'response' => $response->body,
249 ),
250 );
251
252 // validate HTTP response code
253 if ($response->code >= 200 && $response->code < 300)
254 {
255 $status = true;
256
257 // reset failure counter on success
258 $saveData['failed'] = 0;
259 }
260 else
261 {
262 // increase failure counter
263 $saveData['failed'] = $hook->failed + 1;
264 }
265
266 // update web hook record
267 $model->save($saveData);
268 }
269 }
270 }
271
272 return $status;
273 }
274
275 /**
276 * Checks whether the specified web hook should be triggered
277 * and returns a list of matching records.
278 *
279 * @param string $action The action to look for.
280 *
281 * @return array An array of matching web hooks.
282 */
283 public function getHooks($action)
284 {
285 // filter the array of supported web hooks and return only the
286 // ones with the same action
287 return array_filter($this->hooks, function($hook) use ($action)
288 {
289 return $hook->hook == $action;
290 });
291 }
292
293 /**
294 * Searches inside the queue whether we have a matching web hook.
295 *
296 * @param VAPWebHook $hook
297 *
298 * @return mixed The matching web hook on success, null otherwise.
299 */
300 protected function search(VAPWebHook $hook)
301 {
302 foreach ($this->queue as $job)
303 {
304 if ($job->equalsTo($hook))
305 {
306 // matching element, return it
307 return $job;
308 }
309 }
310
311 // no matching element
312 return null;
313 }
314
315 /**
316 * Tries to immediately terminate the HTTP connection before dispatching the queue.
317 * This way we can present the response to the user without having to wait for the
318 * queue completion.
319 *
320 * @return void
321 */
322 protected function closeConnection()
323 {
324 // fastcgi_finish_request is the cleanest way to send the response and keep the script running,
325 // but not every server has it
326 if (!is_callable('fastcgi_finish_request'))
327 {
328 return;
329 }
330
331 /**
332 * Trigger hook to safely prevent the closure of the HTTP connection at runtime.
333 * This way we can prevent any errors that might occur with certain server
334 * configurations.
335 *
336 * @return boolean False to avoid closing the connection.
337 *
338 * @since 1.7
339 */
340 if (VAPFactory::getEventDispatcher()->false('onBeforeWebhookCloseHttpConnection'))
341 {
342 // do not close the connection, wait for the queue completion
343 return;
344 }
345
346 set_time_limit(0);
347
348 // ignore user abort to prevent the behavior that terminates the process by calling flush
349 // or any other similar function
350 ignore_user_abort(true);
351 fastcgi_finish_request();
352 }
353 }
354