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 / order / export / drivers / ics.php
vikappointments / site / helpers / libraries / order / export / drivers Last commit date
csv.php 1 month ago excel.php 1 month ago ics.php 1 month ago index.html 1 month ago
ics.php
913 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 /**
15 * Driver class used to export the orders/appointments in ICS format.
16 *
17 * @since 1.7
18 */
19 class VAPOrderExportDriverIcs extends VAPOrderExportDriver
20 {
21 /**
22 * ICS declarations buffer.
23 *
24 * @var string
25 */
26 private $ics;
27
28 /**
29 * A list of custom fields.
30 *
31 * @var array
32 */
33 private $customFields;
34
35 /**
36 * Checks whether the specified group is supported by the
37 * export driver. Children classes can override this method
38 * to drop the support for a specific group.
39 *
40 * @param string $group The group to check.
41 *
42 * @return boolean True if supported, false otherwise.
43 */
44 public function isSupported($group)
45 {
46 // only appointments are supported here
47 return $this->isGroup('appointment');
48 }
49
50 /**
51 * @override
52 * Builds the form parameters required to the ICS driver.
53 *
54 * @return array
55 */
56 protected function buildForm()
57 {
58 return array(
59 /**
60 * An optional subject to be used instead of the
61 * default one.
62 *
63 * @var text
64 */
65 'subject' => array(
66 'type' => 'text',
67 'label' => JText::translate('VAP_EXPORT_DRIVER_ICS_SUBJECT_FIELD'),
68 'help' => JText::translate('VAP_EXPORT_DRIVER_ICS_SUBJECT_FIELD_HELP'),
69 ),
70
71 /**
72 * Include past events.
73 * If disabled, reservations older than the current
74 * month won't have to be included.
75 *
76 * @var checkbox
77 */
78 'pastevents' => array(
79 'type' => 'checkbox',
80 'label' => JText::translate('VAP_EXPORT_DRIVER_ICS_PAST_DATES_FIELD'),
81 'help' => JText::translate('VAP_EXPORT_DRIVER_ICS_PAST_DATES_FIELD_HELP'),
82 'default' => true,
83 ),
84
85 /**
86 * Events default reminder.
87 * The minutes in advance since the event date time
88 * for which the alert will be triggered.
89 *
90 * @var select
91 */
92 'reminder' => array(
93 'type' => 'select',
94 'label' => JText::translate('VAP_EXPORT_DRIVER_ICS_REMINDER_FIELD'),
95 'help' => JText::translate('VAP_EXPORT_DRIVER_ICS_REMINDER_FIELD_HELP'),
96 'default' => -1,
97 'options' => array(
98 -1 => JText::translate('VAP_EXPORT_DRIVER_ICS_REMINDER_OPT_NONE'),
99 0 => JText::translate('VAP_EXPORT_DRIVER_ICS_REMINDER_OPT_EVENT_TIME'),
100 5 => JText::sprintf('VAP_EXPORT_DRIVER_ICS_REMINDER_OPT_N_MIN', 5),
101 10 => JText::sprintf('VAP_EXPORT_DRIVER_ICS_REMINDER_OPT_N_MIN', 10),
102 15 => JText::sprintf('VAP_EXPORT_DRIVER_ICS_REMINDER_OPT_N_MIN', 15),
103 30 => JText::sprintf('VAP_EXPORT_DRIVER_ICS_REMINDER_OPT_N_MIN', 30),
104 60 => JText::plural('VAP_EXPORT_DRIVER_ICS_REMINDER_OPT_N_HOURS', 1),
105 120 => JText::plural('VAP_EXPORT_DRIVER_ICS_REMINDER_OPT_N_HOURS', 2),
106 ),
107 ),
108 );
109 }
110
111 /**
112 * @override
113 * Exports the orders in the given format.
114 *
115 * @return string The resulting export string.
116 */
117 public function export()
118 {
119 $dispatcher = VAPFactory::getEventDispatcher();
120
121 // init buffer
122 $this->ics = '';
123
124 // load custom fields
125 VAPLoader::import('libraries.customfields.loader');
126 $this->customFields = VAPCustomFieldsLoader::getInstance()
127 ->noRequiredCheckbox()
128 ->noInputFile()
129 ->noSeparator()
130 ->translate()
131 ->fetch();
132
133 /**
134 * Starts the calendar declaration.
135 *
136 * @link https://icalendar.org/iCalendar-RFC-5545/3-4-icalendar-object.html
137 */
138 $this->addLine('BEGIN', 'VCALENDAR');
139
140 // create ICS header
141 $this->createHeader();
142
143 /**
144 * Trigger event to allow the plugins to include custom options before
145 * the body of the ICS file.
146 *
147 * @param mixed $handler The current handler instance.
148 *
149 * @return string The rules to include.
150 *
151 * @since 1.6.6
152 */
153 $res = $dispatcher->trigger('onBuildBodyExportICS', array($this));
154
155 // include the custom rules before the body
156 $this->ics .= implode('', array_filter($res));
157
158 // iterate records to export
159 foreach ($this->getRecords() as $event)
160 {
161 // use registry for ease of use
162 $event = new JRegistry($event);
163
164 // add event properties
165 $this->addEvent($event);
166 }
167
168 /**
169 * Closes the calendar
170 *
171 * @see BEGIN:VCALENDAR
172 */
173 $this->addLine('END', 'VCALENDAR');
174
175 // return generated buffer
176 return $this->ics;
177 }
178
179 /**
180 * @override
181 * Downloads the orders in a file compatible with the given format.
182 *
183 * @param string $filename The name of the file that will be downloaded.
184 *
185 * @return void
186 *
187 * @uses export()
188 */
189 public function download($filename = null)
190 {
191 // obtain export string
192 $buffer = $this->export();
193
194 if ($filename)
195 {
196 // strip file extension
197 $filename = preg_replace("/\.ics$/i", '', $filename);
198 }
199 else
200 {
201 // use current date time as name
202 $filename = JHtml::fetch('date', 'now', 'Y-m-d H_i_s');
203 }
204
205 $app = JFactory::getApplication();
206
207 // declare headers
208 $app->setHeader('Content-Type', 'text/calendar; charset=utf-8');
209 $app->setHeader('Content-Disposition', 'attachment; filename=' . $filename . '.ics');
210 $app->setHeader('Content-Length', strlen($buffer));
211 $app->setHeader('Cache-Control', 'no-store, no-cache');
212
213 // send headers
214 $app->sendHeaders();
215
216 // output buffer for download
217 echo $buffer;
218 }
219
220 /**
221 * Returns the list of records to export.
222 *
223 * @return array A list of records.
224 */
225 protected function getRecords()
226 {
227 $dispatcher = VAPFactory::getEventDispatcher();
228
229 $dbo = JFactory::getDbo();
230
231 $q = $dbo->getQuery(true);
232
233 // select all reservation columns
234 $q->select('r.*');
235 $q->from($dbo->qn('#__vikappointments_reservation', 'r'));
236
237 // select service name
238 $q->select($dbo->qn('s.name', 'service_name'));
239 $q->leftjoin($dbo->qn('#__vikappointments_service', 's') . ' ON ' . $dbo->qn('r.id_service') . ' = ' . $dbo->qn('s.id'));
240
241 // select employee name and timezone
242 $q->select($dbo->qn('e.nickname', 'employee_name'));
243 $q->leftjoin($dbo->qn('#__vikappointments_employee', 'e') . ' ON ' . $dbo->qn('r.id_employee') . ' = ' . $dbo->qn('e.id'));
244
245 // exclude all parent orders
246 $q->where($dbo->qn('r.id_parent') . ' > 0');
247 // exclude all closures
248 $q->where($dbo->qn('r.closure') . ' = 0');
249
250 /**
251 * Obtain also the cancelled statuses to improve the synchronization with Google Calendar.
252 *
253 * @since 1.7.8
254 */
255 $statuses = array_merge(
256 // get approved statuses
257 JHtml::fetch('vaphtml.status.find', 'code', ['appointments' => 1, 'approved' => 1]),
258 // get cancelled statuses
259 JHtml::fetch('vaphtml.status.find', 'code', ['appointments' => 1, 'cancelled' => 1]),
260 );
261
262 if ($statuses)
263 {
264 // filter by status
265 $q->where($dbo->qn('r.status') . ' IN (' . implode(',', array_map(array($dbo, 'q'), $statuses)) . ')');
266 }
267
268 // include records with check-in equals or higher than
269 // the specified starting date
270 $from = $this->getOption('fromdate');
271
272 if (!VAPDateHelper::isNull($from))
273 {
274 $q->where($dbo->qn('r.checkin_ts') . ' >= ' . $dbo->q($from));
275 }
276
277 // include records with check-in equals or lower than
278 // the specified ending date
279 $to = $this->getOption('todate');
280
281 if (!VAPDateHelper::isNull($to))
282 {
283 $q->where($dbo->qn('r.checkin_ts') . ' <= ' . $dbo->q($to));
284 }
285
286 // retrieve only the selected records, if any
287 $ids = $this->getOption('cid');
288
289 if ($ids)
290 {
291 /**
292 * The export system is now able to fetch also the appointments assigned to a parent order.
293 *
294 * @since 1.7.4
295 */
296 $q->andWhere([
297 $dbo->qn('r.id') . ' IN (' . implode(',', array_map('intval', $ids)) . ')',
298 $dbo->qn('r.id_parent') . ' IN (' . implode(',', array_map('intval', $ids)) . ')',
299 ], 'OR');
300 }
301
302 // retrieve employee filter, if any
303 $id_emp = (int) $this->getOption('id_employee');
304
305 if ($id_emp > 0)
306 {
307 $q->where($dbo->qn('r.id_employee') . ' = ' . $id_emp);
308 }
309
310 // check whether the past events should be excluded
311 if (!$this->getOption('pastevents'))
312 {
313 // get current date at midnight
314 $date = JFactory::getDate('today 00:00:00', JFactory::getUser()->getTimezone());
315 // Back to the first day of the month.
316 // Do not use "first day of" modifier because PHP 7.3
317 // seems to experience some strange behaviors.
318 $date->modify($date->format('Y-m-01'));
319
320 $q->where($dbo->qn('r.checkin_ts') . ' >= ' . $dbo->q($date->toSql()));
321 }
322
323 /**
324 * Check whether the imported events should be excluded or not.
325 *
326 * @since 1.7.3
327 */
328 if ($this->getOption('imported', true) === false)
329 {
330 $q->andWhere([
331 $dbo->qn('r.icaluid') . ' IS NULL',
332 $dbo->qn('r.icaluid') . ' = ' . $dbo->q(''),
333 ], 'OR');
334 }
335
336 // order by ascending checkin
337 $q->order($dbo->qn('r.checkin_ts') . ' ASC');
338
339 /**
340 * Trigger event to allow the plugins to manipulate the query used to retrieve
341 * a standard list of records.
342 *
343 * @param mixed &$query The query string or a query builder object.
344 * @param mixed $options A configuration registry.
345 *
346 * @return void
347 *
348 * @since 1.6.6
349 */
350 $dispatcher->trigger('onBeforeListQueryExportICS', array(&$q, $this->options));
351
352 $dbo->setQuery($q);
353 return $dbo->loadObjectList();
354 }
355
356 /**
357 * Creates the header of the calendar.
358 *
359 * @return void
360 */
361 protected function createHeader()
362 {
363 $dispatcher = VAPFactory::getEventDispatcher();
364
365 // set up default head information
366 $head = array(
367 'version' => '2.0',
368 'prodid' => '-//e4j//VikAppointments ' . VIKAPPOINTMENTS_SOFTWARE_VERSION . '//EN',
369 'calscale' => 'GREGORIAN',
370 'calname' => VAPFactory::getConfig()->get('agencyname'),
371 );
372
373 /**
374 * Trigger event to allow the plugins to include custom options within the
375 * head of the ICS file.
376 *
377 * @param array &$head The default head data.
378 * @param mixed $handler The current handler instance.
379 *
380 * @return string The rules to include.
381 *
382 * @since 1.6.6
383 * @since 1.7 $head is now an array and it is passed by reference.
384 */
385 $res = $dispatcher->trigger('onBuildHeadExportICS', array(&$head, $this));
386
387 /**
388 * This property specifies the identifier corresponding to the highest version number
389 * or the minimum and maximum range of the iCalendar specification that is required
390 * in order to interpret the iCalendar object.
391 *
392 * @link https://icalendar.org/iCalendar-RFC-5545/3-7-4-version.html
393 */
394 $this->addLine('VERSION', $head['version']);
395
396 /**
397 * This property specifies the identifier for the product that created the iCalendar object.
398 *
399 * @link https://icalendar.org/iCalendar-RFC-5545/3-7-3-product-identifier.html
400 */
401 $this->addLine('PRODID', $head['prodid']);
402
403 /**
404 * This property defines the calendar scale used for the calendar information
405 * specified in the iCalendar object.
406 *
407 * @link https://icalendar.org/iCalendar-RFC-5545/3-7-1-calendar-scale.html
408 */
409 $this->addLine('CALSCALE', $head['calscale']);
410
411 /**
412 * This non standard property defines the default name that will be used
413 * when creating a new subscription.
414 *
415 * @since 1.6.5
416 */
417 $this->addLine('X-WR-CALNAME', $head['calname']);
418
419 // $this->addLine('X-WR-TIMEZONE', JFactory::getApplication()->get('offset', 'UTC'));
420
421 // append also the values that have been returned by the plugins
422 $this->ics .= implode('', array_filter($res));
423 }
424
425 /**
426 * Adds an appointment as event within the calendar.
427 *
428 * @param JRegistry $event The event to include.
429 *
430 * @return void
431 */
432 protected function addEvent($event)
433 {
434 $dispatcher = VAPFactory::getEventDispatcher();
435
436 $config = VAPFactory::getConfig();
437 $vik = VAPApplication::getInstance();
438
439 // fetch URI
440 $uri = 'index.php?option=com_vikappointments&view=order&ordnum=' . $event->get('id') . '&ordkey=' . $event->get('sid');
441 $uri = $vik->routeForExternalUse($uri);
442
443 // fetch summary
444 $summary = $this->getOption('subject');
445
446 // retrieve customer name
447 $customer = $event->get('purchaser_nominative');
448
449 if (!$summary)
450 {
451 // use default summary built as "service" for customer or
452 // "service - customer" for administrator
453 $summary = '{service}';
454
455 // sets a different title for the administrator
456 if ($this->getOption('admin') && $customer)
457 {
458 $summary .= ' - {customer}';
459 }
460 }
461
462 if (!$customer)
463 {
464 // use e-mail in case the name is missing
465 $customer = $event->get('purchaser_mail');
466 }
467
468 if (!$customer)
469 {
470 // fallback to "Guest"
471 $customer = 'Guest';
472 }
473
474 // retrieve service name
475 $service = $event->get('service_name');
476
477 // retrieve people
478 $people = (int) $event->get('people', 0);
479
480 // replace tags with reservation values
481 $summary = preg_replace("/{customer}/", $customer, $summary);
482 $summary = preg_replace("/{service}/", $service, $summary);
483 $summary = preg_replace("/{people}/", $people, $summary);
484
485 // fetch modified date
486 $modified = max(array($event->get('createdon'), $event->get('modifiedon')));
487
488 $description = '';
489
490 // fetch description
491 if ($this->getOption('admin'))
492 {
493 // decode custom fields and translate values
494 $cf = (array) json_decode($event->get('custom_f', '{}'), true);
495 $cf = VAPCustomFieldsLoader::translateObject($cf, $this->customFields);
496
497 // create description containing the user custom fields
498 foreach ($this->customFields as $field)
499 {
500 $k = $field['name'];
501
502 if (!array_key_exists($k, $cf))
503 {
504 // field not found inside the given object, go to next one
505 continue;
506 }
507
508 $v = $cf[$k];
509
510 // take only if the value is not empty
511 if ((is_scalar($v) && strlen($v)) || !empty($v))
512 {
513 // add colon as separator only in case the label doesn't
514 // end with a punctuation
515 if (preg_match("/[.,:;?!_\-]$/", $field['langname']))
516 {
517 // ends with a punctuation, do not use separator
518 $sep = '';
519 }
520 else
521 {
522 $sep = ':';
523 }
524
525 // get a more readable label/text of the saved value
526 $description .= $field['langname'] . $sep . ' ' . preg_replace("/\R/", "\\n", $v) . "\\n";
527 }
528 }
529 }
530 else
531 {
532 // no description for customer
533 }
534
535 /**
536 * @todo Include location once the database will support a FK to quickly access the
537 * details of the appointment. Otherwise we risk to slow down the whole process.
538 */
539
540 // build EVENT
541 $data = array(
542 'dtend' => VikAppointments::getCheckout($event->get('checkin_ts'), $event->get('duration')),
543 'uid' => $event->get('icaluid'),
544 'dtstamp' => $event->get('createdon'),
545 'location' => $config->get('agencyname'),
546 'description' => $description,
547 'url' => $uri,
548 'summary' => $summary,
549 'dtstart' => $event->get('checkin_ts'),
550 'modified' => $modified,
551 'status' => JHtml::fetch('vaphtml.status.iscancelled', 'appointments', $event->get('status')) ? 'CANCELLED' : 'CONFIRMED',
552 'sequence' => (int) $event->get('sequence', 0),
553 'attendees' => array(),
554 );
555
556 if (!$data['uid'])
557 {
558 // iCal UID not found, generate a new one
559 $data['uid'] = md5($event->get('id') . '-' . $event->get('sid'));
560 }
561
562 // get attendees and decode them
563 $attendees = $event->get('attendees');
564 $attendees = $attendees ? (array) json_decode($attendees, true) : [];
565
566 // inject customer details within attendee data
567 array_unshift($attendees, array(
568 'purchaser_nominative' => $event->get('purchaser_nominative'),
569 'purchaser_mail' => $event->get('purchaser_mail'),
570 ));
571
572 // build attendees ICS data
573 foreach ($attendees as $attendee)
574 {
575 if (empty($attendee['purchaser_mail']))
576 {
577 // go ahead, missing attendee e-mail
578 continue;
579 }
580
581 // register attendee structure
582 $data['attendees'][] = array(
583 'key' => array(
584 'ATTENDEE',
585 'CN=' . $attendee['purchaser_nominative'],
586 'CUTYPE=INDIVIDUAL',
587 'EMAIL=' . $attendee['purchaser_mail'],
588 ),
589 'value' => 'mailto:' . $attendee['purchaser_mail'],
590 );
591 }
592
593 /**
594 * Provide a grouping of component properties that describe an event.
595 *
596 * @link https://icalendar.org/iCalendar-RFC-5545/3-6-1-event-component.html
597 */
598 $this->addLine('BEGIN', 'VEVENT');
599
600 /**
601 * Trigger event to allow the plugins to manipulate the event
602 * details before being included.
603 *
604 * @param array &$event The event data.
605 * @param mixed $record The database record.
606 * @param mixed $handler The current handler instance.
607 *
608 * @return void
609 *
610 * @since 1.6.6
611 * @since 1.7 $record is now a registry.
612 */
613 $dispatcher->trigger('onBeforeBuildEventICS', array(&$data, $event, $this));
614
615 /**
616 * This property specifies the persistent, globally unique identifier for the
617 * iCalendar object. This can be used, for example, to identify duplicate calendar
618 * streams that a client may have been given access to.
619 *
620 * Generate a md5 string of the order number because "UID" values MUST NOT include any
621 * data that might identify a user, host, domain, or any other private sensitive information.
622 *
623 * @link https://icalendar.org/New-Properties-for-iCalendar-RFC-7986/5-3-uid-property.html
624 */
625 $this->addLine('UID', $data['uid']);
626
627 /**
628 * This property specifies when the calendar component begins.
629 *
630 * @link https://icalendar.org/iCalendar-RFC-5545/3-8-2-4-date-time-start.html
631 */
632 $this->addLine(
633 array('DTSTART', 'VALUE=DATE-TIME'),
634 $this->tsToCal($data['dtstart'])
635 );
636
637 /**
638 * This property specifies the date and time that a calendar component ends.
639 *
640 * @link https://icalendar.org/iCalendar-RFC-5545/3-8-2-2-date-time-end.html
641 */
642 $this->addLine(
643 array('DTEND', 'VALUE=DATE-TIME'),
644 $this->tsToCal($data['dtend'])
645 );
646
647 /**
648 * In the case of an iCalendar object that specifies a "METHOD" property, this property
649 * specifies the date and time that the instance of the iCalendar object was created.
650 * In the case of an iCalendar object that doesn't specify a "METHOD" property, this
651 * property specifies the date and time that the information associated with the calendar
652 * component was last revised in the calendar store.
653 *
654 * @link https://icalendar.org/iCalendar-RFC-5545/3-8-7-2-date-time-stamp.html
655 */
656 $this->addLine('DTSTAMP', $this->tsToCal($data['dtstamp']));
657
658 /**
659 * In case an event is modified through a client, it updates the Last-Modified property to the
660 * current time. When the calendar is going to refresh an event, in case the Last-Modified is
661 * not specified or it is lower than the current one, the changes will be discarded.
662 * For this reason, it is needed to specify our internal modified date in order to refresh
663 * any existing events with the updated details.
664 *
665 * @link https://icalendar.org/iCalendar-RFC-5545/3-8-7-3-last-modified.html
666 */
667 $this->addLine('LAST-MODIFIED', $this->tsToCal($data['modified']));
668
669 /**
670 * This property may be used to convey a location where a more dynamic
671 * rendition of the calendar information can be found.
672 *
673 * @link https://icalendar.org/New-Properties-for-iCalendar-RFC-7986/5-5-url-property.html
674 */
675 $this->addLine(array('URL', 'VALUE=URI'), $data['url']);
676
677 /**
678 * This property defines a short summary or subject for the calendar component.
679 *
680 * @link https://icalendar.org/iCalendar-RFC-5545/3-8-1-12-summary.html
681 */
682 $this->addLine('SUMMARY', $this->escape($data['summary']));
683
684 /**
685 * This property provides a more complete description of the calendar component
686 * than that provided by the "SUMMARY" property.
687 *
688 * @link https://icalendar.org/iCalendar-RFC-5545/3-8-1-5-description.html
689 */
690 if ($data['description'])
691 {
692 $this->addLine('DESCRIPTION', $this->escape($data['description']));
693 }
694
695 /**
696 * This property defines the intended venue for the activity defined by a calendar component.
697 *
698 * @link https://icalendar.org/iCalendar-RFC-5545/3-8-1-7-location.html
699 */
700 $this->addLine('LOCATION', $this->escape($data['location']));
701
702 /**
703 * This property defines whether or not an event is transparent to busy time searches.
704 *
705 * @link https://icalendar.org/iCalendar-RFC-5545/3-8-2-7-time-transparency.html
706 */
707 $this->addLine('TRANSP', 'OPAQUE');
708
709 /**
710 * This property defines the overall status or confirmation for the calendar component.
711 *
712 * @link https://icalendar.org/iCalendar-RFC-5545/3-8-1-11-status.html
713 *
714 * @since 1.7.8
715 */
716 $this->addLine('STATUS', $data['status']);
717
718 /**
719 * This property defines the revision sequence number of the calendar component within a sequence of revisions.
720 *
721 * @link https://icalendar.org/iCalendar-RFC-5545/3-8-7-4-sequence-number.html
722 *
723 * @since 1.7.8
724 */
725 $this->addLine('SEQUENCE', max(0, (int) $data['sequence']));
726
727 // iterate all attendees
728 foreach ($data['attendees'] as $attendee)
729 {
730 /**
731 * This property defines an "Attendee" within a calendar component.
732 *
733 * @link https://icalendar.org/iCalendar-RFC-5545/3-8-4-1-attendee.html
734 */
735 $this->addLine($attendee['key'], $attendee['value']);
736 }
737
738 // check if a reminder should be included
739 $reminder = (int) $this->getOption('reminder');
740
741 if ($reminder >= 0)
742 {
743 // create event alarm
744 $this->createAlarm($event, $reminder);
745 }
746
747 /**
748 * Trigger event to allow the plugins to include custom options within the
749 * current calendar event.
750 *
751 * @param array $event The event data.
752 * @param mixed $record The database record.
753 * @param mixed $handler The current handler instance.
754 *
755 * @return string The rules to include.
756 *
757 * @since 1.6.6
758 * @since 1.7 $record is now a registry.
759 */
760 $res = $dispatcher->trigger('onAfterBuildEventICS', array($data, $event, $this));
761
762 // append also the values that have been returned by the plugins
763 $this->ics .= implode('', array_filter($res));
764
765 /**
766 * Closes the event properties.
767 *
768 * @see BEGIN:VEVENT
769 */
770 $this->addLine('END', 'VEVENT');
771 }
772
773 /**
774 * Creates an alarm for the specified event.
775 *
776 * @param JRegistry $event The event to bind.
777 * @param integer $reminder The reminder in minutes.
778 *
779 * @return void
780 */
781 protected function createAlarm($event, $reminder = 0)
782 {
783 /**
784 * Provide a grouping of component properties that define an alarm.
785 *
786 * @link https://icalendar.org/iCalendar-RFC-5545/3-6-6-alarm-component.html
787 */
788 $this->addLine('BEGIN', 'VALARM');
789
790 /**
791 * This property specifies a positive duration of time.
792 *
793 * @link https://icalendar.org/iCalendar-RFC-5545/3-8-2-5-duration.html
794 */
795 if ($reminder == 0)
796 {
797 // trigger alert at event time
798 $duration = '-PT0S';
799 }
800 else if ($reminder < 60)
801 {
802 // trigger alert X minutes in advance
803 $duration = '-PT' . $reminder . 'M';
804 }
805 else
806 {
807 // trigger alert X hours in advance
808 $duration = '-PT' . floor($reminder / 60) . 'H' . ($reminder % 60) . 'M';
809 }
810
811 /**
812 * This property specifies when an alarm will trigger.
813 *
814 * @link https://icalendar.org/iCalendar-RFC-5545/3-8-6-3-trigger.html
815 */
816 $this->addLine(array('TRIGGER', 'RELATED=START'), $duration);
817
818 /**
819 * This property defines the action to be invoked when an alarm is triggered.
820 *
821 * @link https://icalendar.org/iCalendar-RFC-5545/3-8-6-1-action.html
822 */
823 $this->addLine('ACTION', 'DISPLAY');
824
825 /**
826 * In a DISPLAY alarm, the intended alarm effect is for the text value of
827 * the "DESCRIPTION" property to be displayed to the user.
828 *
829 * @link https://icalendar.org/iCalendar-RFC-5545/3-8-1-5-description.html
830 */
831 $this->addLine('DESCRIPTION', JText::translate('VAP_EXPORT_DRIVER_ICS_REMINDER_FIELD'));
832
833 /**
834 * Closes the alarm properties.
835 *
836 * @see BEGIN:VALARM
837 */
838 $this->addLine('END', 'VALARM');
839 }
840
841 /**
842 * Adds a line within the ICS buffer by caring of
843 * the iCalendar standards.
844 *
845 * @param mixed $rule Either the rule command or an array of commands to be concatenated (;).
846 * @param mixed $content Either the rule content or an array of contents to be concatenated (,).
847 *
848 * @return self This object to support chaining.
849 */
850 protected function addLine($rule, $content = null)
851 {
852 // concat rules in case of array
853 if (is_array($rule))
854 {
855 // rule with multiple parts, use semi-colon
856 $rule = implode(';', $rule);
857 }
858
859 // concat contents in case of array
860 if (is_array($content))
861 {
862 // multi-contents list, use comma
863 $content = implode(',', $content);
864 }
865
866 // create line
867 if (is_null($content))
868 {
869 // we had the full line within the rule
870 $line = $rule;
871 }
872 else
873 {
874 // merge rule and content
875 $line = $rule . ':' . $content;
876 }
877
878 // split string every 73 characters (reserve 2 chars to include new line and space)
879 $chunks = str_split($line, 73);
880
881 // merge lines togheter by using indentation technique,
882 // then add the line to the buffer
883 $this->ics .= implode("\n ", $chunks) . "\n";
884
885 return $this;
886 }
887
888 /**
889 * Converts a UNIX timestamp to a valid ICS date string.
890 *
891 * @param integer $ts The timestamp to convert.
892 *
893 * @return string The formatted date.
894 */
895 protected function tsToCal($ts)
896 {
897 return JFactory::getDate($ts)->format('Ymd\THis\Z');
898 }
899
900 /**
901 * Escapes a line value.
902 *
903 * @param string $str The string to escape.
904 *
905 * @return string The escaped string.
906 */
907 protected function escape($str)
908 {
909 // escape reserved characters
910 return preg_replace('/([\,;])/','\\\$1', $str);
911 }
912 }
913