PluginProbe
VikBooking Hotel Booking Engine & PMS / trunk
VikBooking Hotel Booking Engine & PMS vtrunk
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 1.7.3 All 36 releases
vikbooking / admin / helpers / src / chat / mediator.php

mediator.php in VikBooking Hotel Booking Engine & PMS trunk, at admin/helpers/src/chat/mediator.php

504 lines 14.4 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 E4J s.r.l.
6 * @copyright Copyright (C) 2026 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 * Chat mediator class.
16 *
17 * @since 1.8
18 */
19 class VBOChatMediator
20 {
21 /**
22 * The storage engine used for input/output purposes.
23 *
24 * @var VBOChatStorage
25 */
26 protected $storage;
27
28 /**
29 * The currently authenticated user.
30 *
31 * @var VBOChatUser
32 */
33 private $user;
34
35 /**
36 * The path where the attachments are internally stored.
37 *
38 * @var string
39 * @since 1.8.8 Changed visibility to public.
40 */
41 public $attachmentsPath;
42
43 /**
44 * A string holding all the supported file extensions, separated by a comma.
45 *
46 * @var string
47 * @since 1.8.8 Changed visibility to public.
48 */
49 public $supportedFiles;
50
51 /**
52 * Class constructor.
53 *
54 * @param VBOChatStorage $storage
55 */
56 public function __construct(VBOChatStorage $storage)
57 {
58 $this->storage = $storage;
59
60 // create default attachments folder
61 $this->attachmentsPath = (defined('VBO_MEDIA_PATH') ? VBO_MEDIA_PATH : '') . DIRECTORY_SEPARATOR . 'attachments';
62
63 // create default attachments extension filters
64 $this->supportedFiles = implode(',', [
65 // images
66 'png,apng,bmp,gif,ico,jpg,jpeg,heic,webp',
67 // videos
68 'mp4,mov,ogm,webm,3gp,asf,avi,divx,flv,mkv,mpg,mpeg,wmv,xvid',
69 // audios
70 'aac,m4a,mp3,ogg,opus,wav,wave,ac3,aiff,flac,mid,midi,wma',
71 // archives
72 'zip,tar,rar,gz,bzip2',
73 // documents
74 'pdf,doc,docx,rtf,odt,pages,txt,md,markdown',
75 // spreedsheets
76 'xls,xlsx,csv,ods,numbers',
77 // presentations
78 'pps,ppsx,odp,keynote',
79 ]);
80 }
81
82 /**
83 * Authenticates as the provided user.
84 * When no user is passed, the system will attempt to auto-login according
85 * to the client and session data.
86 *
87 * @param VBOChatUser|null $user
88 *
89 * @return self
90 */
91 public function authenticate(?VBOChatUser $user = null)
92 {
93 if ($user === null) {
94 // auto-bind the sender only if not provided
95 if (JFactory::getApplication()->isClient('administrator')) {
96 // authenticate as administrator
97 $user = new VBOChatUserAdmin;
98 } else {
99 // fetch details of the logged in operator
100 $operator = VikBooking::getOperatorInstance()->getOperatorAccount();
101
102 if ($operator) {
103 // authenticate as operator
104 $user = new VBOChatUserOperator($operator);
105 } else {
106 // authenticate as guest user
107 $user = new VBOChatUserGuest;
108 }
109 }
110 }
111
112 $this->user = $user;
113
114 return $this;
115 }
116
117 /**
118 * Returns the currently logged in user.
119 * In case of missing authentication, the system will attempt to
120 * perform an auto-login.
121 *
122 * @return VBOChatUser
123 */
124 public function getUser()
125 {
126 if (!$this->user) {
127 // no authenticated user, auto-login now
128 $this->authenticate();
129 }
130
131 return $this->user;
132 }
133
134 /**
135 * Returns the messages matching the specified search query.
136 *
137 * @param VBOChatSearch $search
138 *
139 * @return VBOChatMessage[]
140 */
141 public function getMessages(VBOChatSearch $search)
142 {
143 $messages = [];
144
145 if (!$search->hasReader()) {
146 // forces the current user as the reader
147 $search->reader($this->getUser()->getID());
148 }
149
150 // pull the messages from the storage
151 $rows = $this->storage->getMessages($search);
152
153 foreach ($rows as $raw) {
154 // wrap raw record within a message object
155 $messages[] = $this->createMessage($raw);
156 }
157
158 return $messages;
159 }
160
161 /**
162 * Sends a new message to all the recipients of the context.
163 *
164 * @param VBOChatMessage $message
165 *
166 * @return void
167 */
168 public function send(VBOChatMessage $message)
169 {
170 $user = $this->getUser();
171
172 // force the sender name and ID according to the details of the logged in user
173 $message->setSender($user->getName(), $user->getID());
174
175 // attempt to save the message
176 $this->saveMessage($message);
177
178 // iterate all the users that should receive a notification
179 foreach ($message->getContext()->getRecipients() as $recipient) {
180 if ($message->getSenderID() == $recipient->getID()) {
181 // do not notify myself
182 continue;
183 }
184
185 if ($recipient instanceof VBOChatNotifiable) {
186 // schedule message notification
187 $recipient->scheduleNotification($message, $user);
188 }
189 }
190 }
191
192 /**
193 * Saves the provided message.
194 *
195 * @param VBOChatMessage $message The message to save.
196 *
197 * @return void
198 *
199 * @throws Exception
200 *
201 * @since 1.8.8
202 */
203 public function saveMessage(VBOChatMessage $message)
204 {
205 // save the message
206 $this->storage->saveMessage($message);
207 }
208
209 /**
210 * Removes the provided message.
211 *
212 * @param VBOChatMessage $message The message to delete.
213 *
214 * @return void
215 *
216 * @throws Exception
217 *
218 * @since 1.8.8
219 */
220 public function deleteMessage(VBOChatMessage $message)
221 {
222 // delete the message
223 $this->storage->deleteMessage($message);
224 }
225
226 /**
227 * Moves the uploaded temporary file onto the server and creates a new attachment.
228 *
229 * @param array $file The temporary file under $_FILES.
230 *
231 * @return VBOChatAttachment
232 *
233 * @throws Exception
234 */
235 public function uploadAttachment(array $file)
236 {
237 // assert attachments folder first
238 if (!JFolder::exists($this->attachmentsPath) && !JFolder::create($this->attachmentsPath)) {
239 throw new \RuntimeException('Unable to create the attachments folder: ' . $this->attachmentsPath, 403);
240 }
241
242 // create upload attachment
243 $attachment = new VBOChatAttachmentUpload($file, $this->attachmentsPath);
244
245 // make sure the file extension is supported
246 if (!VikBooking::isFileTypeCompatible($attachment->getExtension(), $this->supportedFiles)) {
247 throw new \RuntimeException('File type not supported: ' . $attachment->getExtension(), 400);
248 }
249
250 if (!$attachment->upload()) {
251 throw new \RuntimeException('Impossible to upload the file: ' . $attachment->getName(), 403);
252 }
253
254 return $attachment;
255 }
256
257 /**
258 * Removes the specified attachment from the server.
259 *
260 * @param VBOChatAttachment $attachment
261 *
262 * @return bool
263 */
264 public function removeAttachment(VBOChatAttachment $attachment)
265 {
266 if (!$attachment->exists()) {
267 return false;
268 }
269
270 $filePath = $attachment->getPath();
271
272 /**
273 * Resolve traversal segments before comparing the received path
274 * against the allowed folder.
275 *
276 * @since 1.18.11 (J) - 1.8.11 (WP)
277 */
278 $filePath = realpath($filePath);
279
280 /**
281 * Make sure the file is located under the registered attachments path
282 * to prevent arbitrary file deletion.
283 *
284 * @since 1.18.10 (J) - 1.8.10 (WP)
285 */
286 if (!$filePath || strpos($filePath, $this->attachmentsPath) !== 0) {
287 return false;
288 }
289
290 return JFile::delete($filePath);
291 }
292
293 /**
294 * Reads all the messages under the specified context for the currently logged in user.
295 *
296 * @param VBOChatContext $context The chat context.
297 * @param string|null $date When specified, only the messages with creation date equal or
298 * lower than this value will be read.
299 *
300 * @return int[] A list of read message IDs.
301 */
302 public function readMessages(VBOChatContext $context, ?string $date = null)
303 {
304 $search = (new VBOChatSearch)
305 // take the latest 50 unread messages
306 ->start(0)->limit(50)->unread()
307 // under the specified context
308 ->withContext($context)
309 // created before the specified threshold date
310 ->date($date ?: JFactory::getDate('now')->toSql(), '<=')
311 // unread by the currently logged in user
312 ->reader($this->getUser()->getID());
313
314 $read = [];
315
316 // iterate all unread messages
317 foreach ($this->storage->getMessages($search) as $message) {
318 try {
319 // read the message
320 $this->storage->readMessage($message->id, $this->getUser()->getID());
321
322 // mark message as read
323 $read[] = $message->id;
324 } catch (Exception $error) {
325 // go ahead silently
326 }
327 }
328
329 return $read;
330 }
331
332 /**
333 * Read the provided message.
334 *
335 * @param VBOChatMessage $message The message to read.
336 *
337 * @return void
338 *
339 * @throws Exception
340 *
341 * @since 1.8.8
342 */
343 public function readMessage(VBOChatMessage $message)
344 {
345 // read the message
346 $this->storage->readMessage($message->getID(), $this->getUser()->getID());
347 }
348
349 /**
350 * Creates a new message object.
351 *
352 * @param object|array $message The raw message record.
353 *
354 * @return VBOChatMessage
355 */
356 public function createMessage($message)
357 {
358 if (!is_array($message) && !is_object($message)) {
359 throw new \InvalidArgumentException('Cannot bind chat message! Array or object expected, ' . gettype($message) . ' given.', 400);
360 }
361
362 $message = (object) $message;
363
364 // hold raw data into a message object
365 return new VBOChatMessage(
366 // create proper context handler
367 $this->createContext($message->context ?? '', $message->id_context ?? 0),
368 // bind raw information
369 $message
370 );
371 }
372
373 /**
374 * Creates a new context object.
375 *
376 * @param string $alias The context alias identifier.
377 * @param int $id The context foreign key.
378 *
379 * @return VBOChatContext
380 */
381 public function createContext(string $alias, int $id)
382 {
383 if ($alias === '') {
384 throw new InvalidArgumentException('The context alias cannot be empty.', 400);
385 }
386
387 if ($id <= 0) {
388 throw new InvalidArgumentException('Invalid context foreign key provided.', 400);
389 }
390
391 // build context class name
392 $classname = 'VBOChatContext' . ucfirst(strtolower($alias));
393
394 if (!class_exists($classname)) {
395 throw new RuntimeException('The class [' . $classname . '] does not exist.', 404);
396 }
397
398 // instantiate class by inject the provided ID
399 return new $classname($id);
400 }
401
402 /**
403 * Forces the pre-loading of the resources to make the chat scripts work.
404 *
405 * @return self
406 */
407 public function useAssets()
408 {
409 static $loaded = false;
410
411 if ($loaded) {
412 // do not load assets again
413 return $this;
414 }
415
416 $loaded = true;
417
418 // load dependencies first
419 JHtml::fetch('jquery.framework');
420 VikBooking::getVboApplication()->loadContextMenuAssets();
421
422 // make translations available also for JS scripts
423 JText::script('VBO_CHAT_YOU');
424 JText::script('VBTODAY');
425 JText::script('VBOYESTERDAY');
426 JText::script('VBO_CHAT_SENDING_ERR');
427 JText::script('VBO_CHAT_TEXTAREA_PLACEHOLDER');
428 JText::script('VBO_ATTACH');
429
430 $document = JFactory::getDocument();
431 $document->addScript(VBO_SITE_URI . 'resources/chat.js');
432 $document->addStyleSheet(VBO_SITE_URI . 'resources/chat.css');
433
434 /** @var VBOChatUser */
435 $user = $this->getUser();
436
437 // load assets for each supported context
438 (new VBOChatContextTask(0))->useAssets($user);
439 (new VBOChatContextSession(0))->useAssets($user);
440
441 return $this;
442 }
443
444 /**
445 * Renders the chat interface.
446 *
447 * @param VBOChatContext $context The conversation context.
448 * @param array $options A configuration array.
449 *
450 * List of supported configuration options.
451 * @var bool assets Whether the resources should be loaded (true by default).
452 *
453 * @return string The chat interface output.
454 */
455 public function render(VBOChatContext $context, array $options = [])
456 {
457 if ($options['assets'] ?? true) {
458 $this->useAssets();
459 }
460
461 /** @var VBOChatUser */
462 $user = $this->getUser();
463
464 // load the latest 20 messages of the specified context
465 $messages = $this->getMessages(
466 (new VBOChatSearch)->limit($options['limit'] ?? 20)->withContext($context)
467 );
468
469 $users = [];
470
471 // get all involved users
472 foreach ($context->getRecipients() as $recipient) {
473 $users[$recipient->getID()] = $recipient;
474 }
475
476 // detect AJAX base URI environment depending on the platform
477 $ajaxUri = VBOFactory::getPlatform()->getUri()->ajax('index.php?option=com_vikbooking');
478
479 // generate a random suffix in case it has been specified
480 $options['suffix'] = $options['suffix'] ?? uniqid();
481
482 // create layout file
483 $layout = new JLayoutFile('chat.chat', null, [
484 'component' => 'com_vikbooking',
485 'client' => 'admin',
486 ]);
487
488 // render template
489 return $layout->render([
490 'uri' => $ajaxUri,
491 'messages' => $messages,
492 'users' => $users,
493 'user' => $user,
494 'options' => $options,
495 'context' => [
496 'id' => $context->getID(),
497 'alias' => $context->getAlias(),
498 'actions' => $context->getActions($user),
499 'metadata' => $context->getMetadata(true),
500 ],
501 ]);
502 }
503 }
504