PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.6.8
VikBooking Hotel Booking Engine & PMS v1.6.8
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 All 37 releases
vikbooking / admin / helpers / src / notification / center.php

center.php in VikBooking Hotel Booking Engine & PMS 1.6.8, at admin/helpers/src/notification/center.php

629 lines 16.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage core
5 * @author Alessio Gaggii - E4J s.r.l.
6 * @copyright Copyright (C) 2024 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 /**
15 * Notification Center class handler.
16 *
17 * @since 1.16.8 (J) - 1.6.8 (WP)
18 */
19 final class VBONotificationCenter
20 {
21 /** @var array */
22 private static $count_cache = [];
23
24 /** @var bool */
25 private static $lang_loaded = false;
26
27 /** @var int */
28 private $last_found_notifications = 0;
29
30 /**
31 * Class constructor.
32 */
33 public function __construct()
34 {}
35
36 /**
37 * Returns a list of notification groups.
38 *
39 * @param bool $global whether the "All" global group should be included.
40 *
41 * @return array
42 */
43 public function getGroups($global = true)
44 {
45 $dbo = JFactory::getDbo();
46
47 $groups = [];
48
49 $dbo->setQuery(
50 $dbo->getQuery(true)
51 ->select($dbo->qn('group'))
52 ->select('SUM((' . $dbo->qn('read') . ' + 1) % 2) AS ' . $dbo->qn('badge_count'))
53 ->from($dbo->qn('#__vikbooking_notifications'))
54 ->group($dbo->qn('group'))
55 ->order($dbo->qn('badge_count') . ' DESC')
56 );
57
58 foreach ($dbo->loadAssocList() as $group) {
59 $name_key = 'VBO_NOTIFS_GROUP_' . strtoupper($group['group']);
60 $name_lang = JText::translate($name_key);
61 $group_name = $name_key != $name_lang ? $name_lang : ucwords(str_replace('_', ' ', strtolower($group['group'])));
62 $groups[] = [
63 'id' => $group['group'],
64 'name' => $group_name,
65 'badge_count' => $group['badge_count'],
66 ];
67 }
68
69 if ($global) {
70 // prepend the "global" notifications group
71 array_unshift($groups, [
72 'id' => '',
73 'name' => JText::translate('VBNEWCOUPONEIGHT'),
74 'badge_count' => array_sum(array_column($groups, 'badge_count')),
75 ]);
76 }
77
78 return $groups;
79 }
80
81 /**
82 * Counts the number of unread notifications.
83 *
84 * @param string $group optional group identifier.
85 *
86 * @return int
87 */
88 public function countUnread(string $group = '')
89 {
90 $group_key = $group ?: 0;
91
92 if (isset(static::$count_cache[$group_key])) {
93 return static::$count_cache[$group_key];
94 }
95
96 $dbo = JFactory::getDbo();
97
98 $q = $dbo->getQuery(true)
99 ->select('COUNT(*)')
100 ->from($dbo->qn('#__vikbooking_notifications'))
101 ->where($dbo->qn('read') . ' = 0');
102
103 if ($group) {
104 $q->where($dbo->qn('group') . ' = ' . $dbo->q($group));
105 }
106
107 $dbo->setQuery($q);
108 $tot_unread = (int) $dbo->loadResult();
109
110 static::$count_cache[$group_key] = $tot_unread;
111
112 return static::$count_cache[$group_key];
113 }
114
115 /**
116 * Loads a list of notifications.
117 *
118 * @param int $start query limit start.
119 * @param int $lim query limit count.
120 * @param array $filters optional list of column filters.
121 * @param int $min_id optional minimum notification ID.
122 */
123 public function loadNotifications(int $start = 0, int $lim = null, array $filters = [], int $min_id = 0)
124 {
125 $dbo = JFactory::getDbo();
126
127 $q = $dbo->getQuery(true)
128 ->select('SQL_CALC_FOUND_ROWS *')
129 ->from($dbo->qn('#__vikbooking_notifications'));
130
131 foreach ($filters as $column => $filter) {
132 if (is_scalar($filter)) {
133 $q->where($dbo->qn($column) . ' = ' . $dbo->q($filter));
134 } else {
135 foreach ($filter as $filter_data) {
136 if (!is_array($filter_data) || !isset($filter_data['operand']) || !isset($filter_data['value'])) {
137 continue;
138 }
139 $q->where($dbo->qn($column) . ' ' . $filter_data['operand'] . ' ' . $dbo->q($filter_data['value']));
140 }
141 }
142 }
143
144 if ($min_id && empty($filters['id'])) {
145 $q->where($dbo->qn('id') . ' > ' . $min_id);
146 }
147
148 $q->order($dbo->qn('createdon') . ' DESC');
149 $q->order($dbo->qn('read') . ' ASC');
150
151 $dbo->setQuery($q, $start, $lim);
152 $notifications = $dbo->loadObjectList();
153
154 // count the total number of found rows
155 $this->last_found_notifications = 0;
156 if ($notifications) {
157 $dbo->setQuery(
158 $dbo->getQuery(true)
159 ->select('FOUND_ROWS()')
160 );
161 $this->last_found_notifications = (int) $dbo->loadResult();
162 }
163
164 // grab the non-empty booking IDs from the list of objects
165 $booking_ids = array_filter(array_column($notifications, 'idorder'));
166
167 // load the customer names and profile pictures for all the involved booking IDs
168 $customer_infos = [];
169 if ($booking_ids) {
170 $booking_ids = array_unique(array_map(function($id) {
171 return (int) $id;
172 }, $booking_ids));
173
174 $dbo->setQuery(
175 $dbo->getQuery(true)
176 ->select([
177 $dbo->qn('co.idorder'),
178 $dbo->qn('c.first_name'),
179 $dbo->qn('c.last_name'),
180 $dbo->qn('c.pic'),
181 ])
182 ->from($dbo->qn('#__vikbooking_customers_orders', 'co'))
183 ->leftJoin($dbo->qn('#__vikbooking_customers', 'c') . ' ON ' . $dbo->qn('co.idcustomer') . ' = ' . $dbo->qn('c.id'))
184 ->where($dbo->qn('co.idorder') . ' IN (' . implode(', ', $booking_ids) . ')')
185 );
186
187 $customer_infos = $dbo->loadObjectList();
188 }
189
190 // append the customer information properties to each notification and adjust properties
191 foreach ($notifications as &$notification) {
192 // check for call-to-action data
193 if ($notification->cta_data) {
194 $cta_data = json_decode($notification->cta_data);
195 if (is_object($cta_data)) {
196 // overwrite property with the decoded payload
197 $notification->cta_data = $cta_data;
198 }
199 }
200
201 // set default null values
202 $notification->customer_name = null;
203 $notification->customer_pic = null;
204
205 // parse all customer information objects
206 foreach ($customer_infos as $customer_info) {
207 if (!$notification->idorder || $customer_info->idorder != $notification->idorder) {
208 // not the booking ID we want to look for
209 continue;
210 }
211
212 if (!empty($customer_info->first_name) || !empty($customer_info->last_name)) {
213 // populate customer name
214 $notification->customer_name = trim($customer_info->first_name . ' ' . $customer_info->last_name);
215 }
216
217 if (!empty($customer_info->pic)) {
218 // populate customer profile picture
219 $notification->customer_pic = $customer_info->pic;
220 }
221
222 // go to the next notification
223 break;
224 }
225 }
226
227 // unset last reference
228 unset($notification);
229
230 // return the list of notification objects
231 return $notifications;
232 }
233
234 /**
235 * Counts the last found notification rows.
236 *
237 * @return int
238 *
239 * @see loadNotifications()
240 */
241 public function countFoundNotifications()
242 {
243 return $this->last_found_notifications;
244 }
245
246 /**
247 * Reads some or all notifications (if none given), and returns
248 * a list of groups involved for the given notification IDs.
249 *
250 * @param array $notification_ids list of IDs or empty list.
251 *
252 * @return array list of group identifiers involved.
253 */
254 public function readNotifications(array $notification_ids)
255 {
256 $dbo = JFactory::getDbo();
257
258 // sanitize the list
259 $notification_ids = array_filter(array_map('intval', $notification_ids));
260
261 // read the requested notifications
262 $q = $dbo->getQuery(true)
263 ->update($dbo->qn('#__vikbooking_notifications'))
264 ->set($dbo->qn('read') . ' = 1');
265
266 if ($notification_ids) {
267 $q->where($dbo->qn('id') . ' IN (' . implode(', ', $notification_ids) . ')');
268 }
269
270 $dbo->setQuery($q);
271 $dbo->execute();
272
273 // get the groups involved
274 $q = $dbo->getQuery(true)
275 ->select($dbo->qn('group'))
276 ->from($dbo->qn('#__vikbooking_notifications'))
277 ->group($dbo->qn('group'));
278
279 if ($notification_ids) {
280 $q->where($dbo->qn('id') . ' IN (' . implode(', ', $notification_ids) . ')');
281 }
282
283 $dbo->setQuery($q);
284
285 // build a list of groups involved (if any notification is saved, hence was updated)
286 $involved = [];
287 foreach ($dbo->loadObjectList() as $record) {
288 $involved[] = $record->group;
289 }
290
291 if ($involved) {
292 // prepend the "global" notifications group as an empty value
293 array_unshift($involved, 0);
294 }
295
296 return $involved;
297 }
298
299 /**
300 * Reads the notifications matching the given criterias. Useful for reading
301 * the notifications when opening a specific context, like a chat thread.
302 *
303 * @param array $criteria associative list of column-value criterias.
304 *
305 * @return int list of group identifiers involved.
306 */
307 public function readMatchingNotifications(array $criteria)
308 {
309 if (!$criteria) {
310 // do not proceed
311 return 0;
312 }
313
314 $dbo = JFactory::getDbo();
315
316 // start building the query for reading the matching notifications
317 $q = $dbo->getQuery(true)
318 ->update($dbo->qn('#__vikbooking_notifications'))
319 ->set($dbo->qn('read') . ' = 1');
320
321 foreach ($criteria as $col => $value) {
322 $q->where($dbo->qn($col) . ' = ' . $dbo->q($value));
323 }
324
325 try {
326 $dbo->setQuery($q);
327 $dbo->execute();
328
329 return (int) $dbo->getAffectedRows();
330 } catch (Exception $e) {
331 // silently catch any database error
332 }
333
334 return 0;
335 }
336
337 /**
338 * Processes a list of notification objects to store.
339 *
340 * @param array $notifications list of notification objects.
341 *
342 * @return array associative list of data stored.
343 *
344 * @throws Exception
345 */
346 public function store(array $notifications)
347 {
348 $result = [
349 'new_notifications' => 0,
350 ];
351
352 foreach ($notifications as $notification) {
353 // wrap the notification array/object into a registry
354 $notif_registry = new VBONotificationElements($notification);
355
356 if ($this->storeNotification($notif_registry)) {
357 // record was stored successfully
358 $result['new_notifications']++;
359
360 // parse the next one
361 continue;
362 }
363
364 if ($notif_registry->getError()) {
365 // abort only in case of error
366 throw new Exception($notif_registry->getError(), $notif_registry->getErrorCode());
367 }
368 }
369
370 return $result;
371 }
372
373 /**
374 * Parses and stores a notification registry, if compliant.
375 *
376 * @param VBONotificationElements $notification the notification registry.
377 *
378 * @return bool
379 */
380 private function storeNotification(VBONotificationElements $notification)
381 {
382 if (!$notification->get('sender') || !$notification->get('type')) {
383 $notification->setError('Notification sender and type are mandatory');
384 return false;
385 }
386
387 if (!$notification->get('title') && !$notification->get('summary')) {
388 $notification->setError('Either title or summary is mandatory');
389 return false;
390 }
391
392 if (strcasecmp($notification->get('sender', ''), 'website')) {
393 // ensure language definitions are loaded for non-website notifications
394 $this->loadLanguageDefs();
395 }
396
397 // ensure the same notification does not exist already
398 if ($this->signatureExists($notification->getSignature())) {
399 // abort without raising any errors to skip a duplicate notification
400 return false;
401 }
402
403 $dbo = JFactory::getDbo();
404
405 // build notification record to store
406 $record = new stdClass;
407
408 $record->signature = $notification->getSignature();
409 $record->group = $notification->getGroup();
410 $record->type = $notification->getType();
411 $record->title = $notification->getTitle();
412 $record->summary = $notification->getSummary();
413 $record->cta_data = $notification->getCallToActionData();
414 $record->idorder = $notification->getReservationID();
415 $record->idorderota = $notification->getOTAReservationID();
416 $record->channel = $notification->getChannel();
417 $record->createdon = $notification->getDate();
418
419 if (!$dbo->insertObject('#__vikbooking_notifications', $record, 'id')) {
420 $notification->setError('Query failed when inserting the record');
421 return false;
422 }
423
424 return true;
425 }
426
427 /**
428 * Parses a new guest message received through the Channel Manager from an OTA.
429 *
430 * @param object $thread the thread record in VCM.
431 * @param object $message the message record in VCM.
432 *
433 * @return array associative list of data stored.
434 *
435 * @throws Exception
436 */
437 public function parseNewGuestMessage($thread, $message)
438 {
439 if (!is_object($thread) || !is_object($message)) {
440 throw new Exception('Invalid message object provided', 400);
441 }
442
443 // ensure language definitions are loaded
444 $this->loadLanguageDefs();
445
446 // build guest message notification payload
447 $notification = [
448 'sender' => 'guests',
449 'type' => 'guest_message',
450 'title' => JText::sprintf('VBO_MESSAGE_FROM', $message->sender_name ?? 'guest'),
451 'summary' => $message->content ?? '',
452 'idorder' => $thread->idorder ?? null,
453 'idorderota' => $thread->idorderota ?? null,
454 'channel' => $thread->channel ?? null,
455 ];
456
457 // store the guest message notification(s)
458 return $this->store([$notification]);
459 }
460
461 /**
462 * Checks if new notifications should be downloaded or generated, at most once per day.
463 * The method is usually invoked through the administrator section of VikBooking.
464 *
465 * @return void
466 */
467 public function downloadNotifications()
468 {
469 $session = JFactory::getSession();
470
471 if ($session->get('vbo_nc_download')) {
472 // check was made already
473 return;
474 }
475
476 // turn session flag on
477 $session->set('vbo_nc_download', 1);
478
479 $config = VBOFactory::getConfig();
480
481 $last_download = $config->get('nc_last_download', '');
482 $today_dt = date('Y-m-d');
483
484 if ($last_download == $today_dt) {
485 // check was made already
486 return;
487 }
488
489 // turn db flag on
490 $config->set('nc_last_download', $today_dt);
491
492 // check if automatic notifications should be generated.
493 if (!empty($last_download)) {
494 $last_info = getdate(strtotime($last_download));
495 $prev_m_info = getdate(strtotime('-1 month'));
496 if ($last_info['mon'] == $prev_m_info['mon'] && $last_info['year'] == $prev_m_info['year']) {
497 // we are on a new month, generate a finance notification for the past month
498 $this->generatePastMonthNotification($last_info);
499 }
500 }
501
502 // check if the Channel Manager can download new notifications
503
504 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
505 // channel manager not installed
506 return;
507 }
508
509 if (!class_exists('VCMNotificationsHelper')) {
510 // channel manager is outdated
511 return;
512 }
513
514 // let the CM request to download new notifications, if any eligible channel is configured
515 try {
516 (new VCMNotificationsHelper)
517 ->downloadNotifications();
518 } catch (Throwable $e) {
519 // silently catch the error
520 }
521 }
522
523 /**
524 * Checks if a notification exists from the given signature.
525 *
526 * @param string $signature the notification elements signature.
527 *
528 * @return bool
529 */
530 private function signatureExists(string $signature)
531 {
532 $dbo = JFactory::getDbo();
533
534 $dbo->setQuery(
535 $dbo->getQuery(true)
536 ->select($dbo->qn('id'))
537 ->from($dbo->qn('#__vikbooking_notifications'))
538 ->where($dbo->qn('signature') . ' = ' . $dbo->q($signature))
539 );
540
541 if ($dbo->loadResult()) {
542 return true;
543 }
544
545 return false;
546 }
547
548 /**
549 * Generates a financial notification for the stats of the past month.
550 *
551 * @param array $month the past month date information.
552 *
553 * @return void
554 */
555 private function generatePastMonthNotification(array $month)
556 {
557 $months_map = [
558 JText::translate('VBMONTHONE'),
559 JText::translate('VBMONTHTWO'),
560 JText::translate('VBMONTHTHREE'),
561 JText::translate('VBMONTHFOUR'),
562 JText::translate('VBMONTHFIVE'),
563 JText::translate('VBMONTHSIX'),
564 JText::translate('VBMONTHSEVEN'),
565 JText::translate('VBMONTHEIGHT'),
566 JText::translate('VBMONTHNINE'),
567 JText::translate('VBMONTHTEN'),
568 JText::translate('VBMONTHELEVEN'),
569 JText::translate('VBMONTHTWELVE'),
570 ];
571
572 // build past month finance notification payload
573 $notification = [
574 'sender' => 'website',
575 'type' => 'info',
576 'title' => $months_map[$month['mon'] - 1] . ' ' . $month['year'],
577 'summary' => JText::translate('VBO_CHECK_HOW_WENT'),
578 'label' => JText::translate('VBO_W_FINANCE_TITLE'),
579 'widget' => 'finance',
580 'widget_options' => [
581 'fromdate' => date('Y-m-01', $month[0]),
582 'todate' => date('Y-m-t', $month[0]),
583 'type' => 'month',
584 ],
585 ];
586
587 try {
588 // store the notification(s)
589 $this->store([$notification]);
590 } catch (Throwable $e) {
591 // silently catch the error
592 }
593 }
594
595 /**
596 * Attempts to load the language definitions in case the client/CMS requires so.
597 *
598 * @return void
599 */
600 private function loadLanguageDefs()
601 {
602 if (static::$lang_loaded) {
603 return;
604 }
605
606 // turn flag on
607 static::$lang_loaded = true;
608
609 $lang = JFactory::getLanguage();
610
611 if (VBOPlatformDetection::isJoomla()) {
612 $lang->load('com_vikbooking', JPATH_ADMINISTRATOR, $lang->getTag(), true);
613 $lang->load('joomla', JPATH_ADMINISTRATOR, $lang->getTag(), true);
614 } else {
615 $lang->load('com_vikbooking', VIKBOOKING_LANG, $lang->getTag(), true);
616
617 // make sure to register the language handler
618 $lib_base_path = defined('VIKBOOKING_LIBRARIES') ? VIKBOOKING_LIBRARIES : '';
619 if (!$lib_base_path && defined('VIKCHANNELMANAGER_LIBRARIES')) {
620 $lib_base_path = str_replace('vikchannelmanager' . DIRECTORY_SEPARATOR . 'libraries', 'vikbooking' . DIRECTORY_SEPARATOR . 'libraries', VIKCHANNELMANAGER_LIBRARIES);
621 }
622
623 if ($lib_base_path) {
624 $lang->attachHandler($lib_base_path . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR . 'admin.php', 'vikbooking');
625 }
626 }
627 }
628 }
629