PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.7.2
VikBooking Hotel Booking Engine & PMS v1.7.2
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.7.2, at admin/helpers/src/notification/center.php

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