PluginProbe ʕ •ᴥ•ʔ
VikAppointments Services Booking Calendar / trunk
VikAppointments Services Booking Calendar vtrunk
trunk 1.2.17 1.2.18 1.2.19
vikappointments / admin / models / usernote.php
vikappointments / admin / models Last commit date
apiban.php 4 years ago apilog.php 2 years ago apiplugin.php 4 years ago apiuser.php 2 years ago apiuseroptions.php 2 years ago backup.php 5 months ago caldays.php 1 month ago calendar.php 1 month ago city.php 4 years ago closure.php 1 month ago configapp.php 4 years ago configcldays.php 4 years ago configcron.php 4 years ago configemp.php 4 years ago configsmsapi.php 4 years ago configuration.php 5 months ago conversion.php 4 years ago country.php 2 years ago coupon.php 2 years ago couponemployee.php 4 years ago coupongroup.php 2 years ago couponservice.php 4 years ago cronjob.php 2 years ago cronjoblog.php 4 years ago customer.php 5 months ago customf.php 2 years ago customfservice.php 4 years ago customizer.php 4 years ago empgroup.php 2 years ago employee.php 2 years ago empsettings.php 4 years ago file.php 4 years ago findreservation.php 1 month ago group.php 2 years ago import.php 4 years ago index.html 4 years ago invoice.php 1 month ago langcustomf.php 4 years ago langempgroup.php 4 years ago langemployee.php 4 years ago langgroup.php 4 years ago langmedia.php 4 years ago langoption.php 2 years ago langoptiongroup.php 4 years ago langoptionvar.php 4 years ago langpackage.php 4 years ago langpackgroup.php 4 years ago langpayment.php 4 years ago langservice.php 4 years ago langstatuscode.php 4 years ago langsubscr.php 4 years ago langtax.php 2 years ago langtaxrule.php 4 years ago location.php 2 years ago mailtext.php 2 years ago makerecurrence.php 1 month ago media.php 2 years ago multiorder.php 1 month ago option.php 1 year ago optiongroup.php 2 years ago optionvar.php 1 year ago orderstatus.php 2 years ago package.php 2 years ago packageservice.php 4 years ago packgroup.php 2 years ago packorder.php 2 years ago packorderitem.php 1 month ago payment.php 2 years ago rate.php 2 years ago reportsemp.php 5 months ago reportsser.php 5 months ago reservation.php 1 month ago resoptassoc.php 2 years ago restriction.php 2 years ago review.php 3 years ago serempassoc.php 1 year ago seroptassoc.php 4 years ago serrateassoc.php 4 years ago serrestrassoc.php 4 years ago service.php 2 years ago state.php 2 years ago statswidget.php 2 years ago statuscode.php 2 years ago subscription.php 1 month ago subscrorder.php 1 month ago tag.php 4 years ago tax.php 2 years ago taxrule.php 2 years ago updateprogram.php 4 years ago usernote.php 2 years ago waitinglist.php 1 month ago webhook.php 1 year ago worktime.php 1 month ago
usernote.php
528 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.mvc.model');
15
16 /**
17 * VikAppointments user note model.
18 *
19 * @since 1.7
20 */
21 class VikAppointmentsModelUsernote extends JModelVAP
22 {
23 /**
24 * Basic save implementation.
25 *
26 * @param mixed $data Either an array or an object of data to save.
27 *
28 * @return mixed The ID of the record on success, false otherwise.
29 */
30 public function save($data)
31 {
32 $data = (array) $data;
33
34 /**
35 * Avoid to save the note in case of duplicate.
36 *
37 * @since 1.7.5
38 */
39 if ($this->isDuplicate($data))
40 {
41 $this->setError('Duplicate note.');
42 return false;
43 }
44
45 // check if we are updating an existing
46 if (isset($data['attachments']) && !empty($data['id']))
47 {
48 // get previous attachments
49 $prevAttachments = $this->getItem($data['id'])->attachments;
50
51 if (!is_array($data['attachments']))
52 {
53 // decode JSON
54 $data['attachments'] = json_decode($data['attachments']);
55 }
56
57 // fetch items that differ between the previous attachments
58 // and the one we are updating
59 $diff = array_diff($prevAttachments, $data['attachments']);
60
61 if ($diff)
62 {
63 // delete all missing files
64 JModelVAP::getInstance('media')->delete($diff);
65 }
66
67 // NOTE: in case the user uploads and delete a file during the
68 // creation of a new element, the deleted file won't be unlinked
69 // from the file system, because the process will never enter
70 // here, as we are creating a new record.
71 }
72
73 if (isset($data['tags']))
74 {
75 // commit tags
76 $data['tags'] = JModelVAP::getInstance('tag')->writeTags($data['tags'], 'usernotes');
77 }
78
79 // attempt to save the record
80 $id = parent::save($data);
81
82 if (!$id)
83 {
84 return false;
85 }
86
87 if (!empty($data['notifycust']))
88 {
89 // send e-mail notification to customer
90 $this->sendEmailNotification($id);
91 }
92
93 return $id;
94 }
95
96 /**
97 * Saves a DRAFT.
98 *
99 * @param mixed $data Either an array or an object of data to save.
100 *
101 * @return mixed The ID of the record on success, false otherwise.
102 */
103 public function saveDraft($data)
104 {
105 // split paragraphs
106 $chunks = preg_split("/\R{2,2}/", @$data['content']);
107
108 if (count($chunks) > 1)
109 {
110 // extract title from chunks (first paragraph)
111 $data['title'] = array_shift($chunks);
112
113 // title has a maximum length of 128 chars, make
114 // sure the specified paragraph doesn't exceed it
115 if (strlen((string) $data['title']) > 128)
116 {
117 // re-push the title within the list of paragraphs
118 array_unshift($chunks, $data['title']);
119 // unset title
120 $data['title'] = '';
121 }
122
123 // merge remaining contents
124 $data['content'] = implode("\n", array_map(function($p)
125 {
126 return '<p>' . $p . '</p>';
127 }, $chunks));
128 }
129
130 return $this->save($data);
131 }
132
133 /**
134 * Basic item loading implementation.
135 *
136 * @param mixed $pk An optional primary key value to load the row by, or an array of fields to match.
137 * If not set the instance property value is used.
138 * @param boolean $new True to return an empty object if missing.
139 *
140 * @return mixed The record object on success, null otherwise.
141 */
142 public function getItem($pk, $new = false)
143 {
144 // load item through parent
145 $item = parent::getItem($pk, $new);
146
147 if ($item)
148 {
149 // decode encoded parameters
150 $item->attachments = $item->attachments ? (array) json_decode($item->attachments, true) : array();
151
152 // add base path to attachments list
153 $item->attachments = array_map(function($file)
154 {
155 return VAPCUSTOMERS_DOCUMENTS . DIRECTORY_SEPARATOR . $file;
156 }, $item->attachments);
157 }
158
159 return $item;
160 }
161
162 /**
163 * Sends an e-mail notification to target of the user note.
164 *
165 * @param integer $id The user note ID.
166 *
167 * @return boolean True on success, false otherwise.
168 */
169 public function sendEmailNotification($id)
170 {
171 // load note details
172 $item = $this->getItem((int) $id);
173
174 // make sure the note is public
175 if (!$item->status)
176 {
177 $this->setError('Cannot notify private notes');
178 return false;
179 }
180
181 if ($item->group == 'appointments')
182 {
183 // send e-mail to the owner of the appointment
184 return JModelVAP::getInstance('reservation')->sendEmailNotification($item->id_parent);
185 }
186 else if ($item->id_user)
187 {
188 // load customer details
189 $customer = VikAppointments::getCustomer($item->id_user);
190
191 // make sure the customer exists
192 if (!$customer)
193 {
194 $this->setError('Customer not found');
195 return false;
196 }
197
198 // make sure the user owns an e-mail address
199 if (!$customer->billing_mail)
200 {
201 // billing e-mail not found, try to use the account e-mail
202 $customer->billing_mail = $customer->user->email;
203
204 if (!$customer->billing_mail)
205 {
206 $this->setError('Customer did not specify an e-mail address');
207 return false;
208 }
209 }
210
211 $sendername = VAPFactory::getConfig()->get('agencyname');
212
213 // fetch e-mail subject and content
214 $subject = JText::sprintf('VAPUSERNOTEMAILSUBJECT', $sendername, $item->title);
215 // append user note title and subject
216 $content = $item->content;
217
218 // import mail factory to implement subject/content extendability
219 VAPLoader::import('libraries.mail.factory');
220
221 // trigger hook to allow subject manipulation: onBeforeSendMailSubjectUsernote
222 if (!VAPMailFactory::letPluginsManipulateMail('usernote', 'subject', $subject, $item))
223 {
224 // e-mail sending prevented
225 return false;
226 }
227
228 // trigger hook to allow content manipulation: onBeforeSendMailContentUsernote
229 if (!VAPMailFactory::letPluginsManipulateMail('usernote', 'content', $content, $item))
230 {
231 // e-mail sending prevented
232 return false;
233 }
234
235 // send e-mail
236 return VAPApplication::getInstance()->sendMail(
237 VikAppointments::getSenderMail(),
238 $sendername,
239 $customer->billing_mail,
240 $reply = null,
241 $subject,
242 $content,
243 $item->attachments,
244 $is_html = true
245 );
246 }
247
248 // invalid user note
249 return false;
250 }
251
252 /**
253 * Extend delete implementation to delete any related records
254 * stored within a separated table.
255 *
256 * @param mixed $ids Either the record ID or a list of records.
257 *
258 * @return boolean True on success, false otherwise.
259 */
260 public function delete($ids)
261 {
262 if (!$ids)
263 {
264 return false;
265 }
266
267 $files = array();
268
269 // only int values are accepted
270 $ids = array_map('intval', (array) $ids);
271
272 $dbo = JFactory::getDbo();
273
274 // load all attached files
275 $q = $dbo->getQuery(true)
276 ->select($dbo->qn('attachments'))
277 ->from($dbo->qn('#__vikappointments_user_notes'))
278 ->where($dbo->qn('id') . ' IN (' . implode(',', $ids) . ')');
279
280 $dbo->setQuery($q);
281
282 foreach ($dbo->loadColumn() as $json)
283 {
284 $files = array_merge($files, $json ? (array) json_decode($json, true) : array());
285 }
286
287 // invoke parent first
288 if (!parent::delete($ids))
289 {
290 // nothing to delete
291 return false;
292 }
293
294 $media = JModelVAP::getInstance('media');
295
296 // delete all files
297 foreach ($files as $file)
298 {
299 $media->delete(VAPCUSTOMERS_DOCUMENTS . DIRECTORY_SEPARATOR . $file);
300 }
301
302 // resursively scan the parent folders of the deleted files
303 // and remove all the directories without children
304 $this->deleteEmptyFolders($files);
305
306 return true;
307 }
308
309 /**
310 * Helper method used to clean those directories that do not
311 * own any children.
312 *
313 * @param array $paths A list of deleted files.
314 *
315 * @return void
316 */
317 protected function deleteEmptyFolders($paths)
318 {
319 // get all parent directories
320 $paths = array_unique(array_map(function($elem)
321 {
322 return dirname($elem);
323 }, $paths));
324
325 jimport('joomla.filesystem.folder');
326
327 $deleted = false;
328
329 // iterate all paths and checks whether they have any files
330 foreach ($paths as $path)
331 {
332 if ($path == '.' || $path == DIRECTORY_SEPARATOR || !$path)
333 {
334 // we reached the root, go ahead
335 continue;
336 }
337
338 // prepend base path
339 $path = VAPCUSTOMERS_DOCUMENTS . DIRECTORY_SEPARATOR . ltrim($path, DIRECTORY_SEPARATOR);
340
341 // make sure the directory exists
342 if (is_dir($path))
343 {
344 /**
345 * Check whether the directory is empty.
346 * As pointed by PHP doc, the valid() method seeks at the
347 * beginning of the list and, in case there are no files,
348 * it'll immediately return false.
349 *
350 * @link https://www.php.net/manual/en/directoryiterator.valid.php
351 */
352 $iterator = new FilesystemIterator($path);
353 if (!$iterator->valid())
354 {
355 // directory is empty, delete folder
356 JFolder::delete($path);
357
358 $deleted = true;
359 }
360 }
361 }
362
363 if ($deleted)
364 {
365 // recursively call this method only in case a folder has been deleted
366 $this->deleteEmptyFolders($paths);
367 }
368 }
369
370 /**
371 * Returns the attachment path assigned to the user note.
372 * In case the folder doesn't exist, a new one will be created.
373 *
374 * @param mixed $note Either a user note ID or an object.
375 * @param array $options An array of options.
376 *
377 * @return string The resulting upload path.
378 *
379 * @throws Exception
380 */
381 public function getUploadsPath($note = null, array $options = array())
382 {
383 if (!is_object($note))
384 {
385 // get note details
386 $note = $this->getItem($note, $blank = true);
387 }
388
389 if (!empty($options['id_user']))
390 {
391 // use the specified user ID
392 $note->id_user = $options['id_user'];
393 }
394
395 if (!empty($options['id_parent']))
396 {
397 // use the specified parent ID
398 $note->id_parent = $options['id_parent'];
399 }
400
401 if (!empty($options['group']))
402 {
403 // use the specified parent group
404 $note->group = $options['group'];
405 }
406
407 $parts = array();
408
409 if ($note->group)
410 {
411 // append group to path
412 $parts[] = $note->group;
413 }
414
415 if ($note->group == 'appointments')
416 {
417 // load appointment details
418 $appointment = JModelVAP::getInstance('reservation')->getItem($note->id_parent);
419
420 if (!$appointment)
421 {
422 throw new Exception('Appointment not found.', 404);
423 }
424
425 // build path as [ORDNUM]-[ORDKEY]
426 $parts[] = substr($appointment->id . '-' . $appointment->sid, 0, 12);
427 }
428 else
429 {
430 // load user note details
431 $customer = JModelVAP::getInstance('customer')->getItem($note->id_user);
432
433 if (!$customer)
434 {
435 throw new Exception('Customer not found.', 404);
436 }
437
438 VAPLoader::import('libraries.sef.helper');
439 // create safe string of the customer name
440 $name = VAPSefHelper::stringToAlias($customer->billing_name);
441
442 if (!$name)
443 {
444 // invalid name, use the e-mail
445 $name = $customer->billing_mail;
446 }
447 else
448 {
449 // append user ID to avoid conflicts between homonyms
450 $name .= '-' . $customer->id;
451 }
452
453 if (!$name)
454 {
455 throw new Exception('Invalid customer details.', 404);
456 }
457
458 if (!$note->secret)
459 {
460 // generate secret key for the user note
461 $note->secret = VikAppointments::generateSerialCode(12, 'usernote-secret');
462 }
463
464 // build path as [NOMINATIVE]/[TOKEN]
465 $parts = array_merge($parts, array($name, $note->secret));
466 }
467
468 if (!$parts)
469 {
470 // invalid path
471 throw new Exception('Invalid path', 500);
472 }
473
474 // create folder path
475 $path = VAPCUSTOMERS_DOCUMENTS . DIRECTORY_SEPARATOR . strtolower(implode(DIRECTORY_SEPARATOR, $parts));
476
477 // in case the path is not a folder, create it
478 if (!is_dir($path))
479 {
480 jimport('joomla.filesystem.folder');
481
482 if (!JFolder::create($path))
483 {
484 // unable to create the folder
485 throw new Exception(sprintf('Unable to create [%s] folder', $path), 500);
486 }
487 }
488
489 return $path;
490 }
491
492 /**
493 * Helper method used to check whether the provided note details
494 * result to be a duplicate record.
495 *
496 * @param array $data
497 *
498 * @return bool
499 *
500 * @since 1.7.5
501 */
502 public function isDuplicate(array $data)
503 {
504 if (!empty($data['id']))
505 {
506 // always ignore in case of update
507 return false;
508 }
509
510 // construct the search query
511 $search = array_filter([
512 'group' => $data['group'] ?? '',
513 'id_parent' => $data['id_parent'] ?? '',
514 'title' => $data['title'] ?? '',
515 'content' => $data['content'] ?? '',
516 ]);
517
518 if (!$search)
519 {
520 // not enough search terms
521 return false;
522 }
523
524 // check if we already have a similar note
525 return (bool) $this->getItem($search);
526 }
527 }
528