From a2cadd7571eb76f1768b6cea4627356a69bb398f Mon Sep 17 00:00:00 2001 From: Francis Whittle Date: Thu, 30 Jun 2022 10:02:43 +1000 Subject: [PATCH] CIVICRM-1861 Embed timezone output for iCal CIVICRM-1988 Add HTML alternate description to iCAL CIVICRM-1987 Add to Google Calendar Link --- .../Smarty/plugins/modifier.crmICalText.php | 4 +- CRM/Event/BAO/Event.php | 33 +++--- CRM/Event/ICalendar.php | 76 ++++++++++++- CRM/Utils/ICalendar.php | 103 +++++++++++++++++- templates/CRM/Core/Calendar/ICal.tpl | 16 +++ templates/CRM/Event/Page/iCalLinks.tpl | 4 +- .../event_offline_receipt_html.tpl | 8 +- .../event_offline_receipt_text.tpl | 4 +- .../event_online_receipt_html.tpl | 8 +- .../event_online_receipt_text.tpl | 4 +- .../participant_confirm_html.tpl | 14 ++- .../participant_confirm_text.tpl | 4 +- 12 files changed, 240 insertions(+), 38 deletions(-) diff --git a/CRM/Core/Smarty/plugins/modifier.crmICalText.php b/CRM/Core/Smarty/plugins/modifier.crmICalText.php index 29cd892bd1..db3e3e906d 100644 --- a/CRM/Core/Smarty/plugins/modifier.crmICalText.php +++ b/CRM/Core/Smarty/plugins/modifier.crmICalText.php @@ -25,6 +25,6 @@ * @return string * formatted text */ -function smarty_modifier_crmICalText($str) { - return CRM_Utils_ICalendar::formatText($str); +function smarty_modifier_crmICalText($str, $keep_html = FALSE, $position = 0) { + return CRM_Utils_ICalendar::formatText($str, $keep_html, $position); } diff --git a/CRM/Event/BAO/Event.php b/CRM/Event/BAO/Event.php index b25413edba..5f0f604cd7 100644 --- a/CRM/Event/BAO/Event.php +++ b/CRM/Event/BAO/Event.php @@ -2416,18 +2416,7 @@ WHERE ce.loc_block_id = $locBlockId"; * All of the icons to show. */ public static function getICalLinks($eventId = NULL) { - $return = $eventId ? [] : [ - [ - 'url' => CRM_Utils_System::url('civicrm/event/ical', 'reset=1&list=1&html=1', TRUE, NULL, TRUE), - 'text' => ts('HTML listing of current and future public events.'), - 'icon' => 'fa-th-list', - ], - [ - 'url' => CRM_Utils_System::url('civicrm/event/ical', 'reset=1&list=1&rss=1', TRUE, NULL, TRUE), - 'text' => ts('Get RSS 2.0 feed for current and future public events.'), - 'icon' => 'fa-rss', - ], - ]; + $return = []; $query = [ 'reset' => 1, ]; @@ -2439,12 +2428,20 @@ WHERE ce.loc_block_id = $locBlockId"; 'text' => $eventId ? ts('Download iCalendar entry for this event.') : ts('Download iCalendar entry for current and future public events.'), 'icon' => 'fa-download', ]; - $query['list'] = 1; - $return[] = [ - 'url' => CRM_Utils_System::url('civicrm/event/ical', $query, TRUE, NULL, TRUE), - 'text' => $eventId ? ts('iCalendar feed for this event.') : ts('iCalendar feed for current and future public events.'), - 'icon' => 'fa-link', - ]; + if ($eventId) { + $return[] = [ + 'url' => CRM_Utils_System::url('civicrm/event/ical', ['gCalendar' => 1] + $query, TRUE, NULL, TRUE), + 'text' => ts('Add event to Google Calendar'), + 'icon' => 'fa-share', + ]; + } + else { + $return[] = [ + 'url' => CRM_Utils_System::url('civicrm/event/ical', $query, TRUE, NULL, TRUE), + 'text' => ts('iCalendar feed for current and future public events'), + 'icon' => 'fa-link', + ]; + } return $return; } diff --git a/CRM/Event/ICalendar.php b/CRM/Event/ICalendar.php index a4cc998fd7..faf2889b17 100644 --- a/CRM/Event/ICalendar.php +++ b/CRM/Event/ICalendar.php @@ -42,14 +42,22 @@ class CRM_Event_ICalendar { $iCalPage = CRM_Utils_Request::retrieveValue('list', 'Positive', 0); $gData = CRM_Utils_Request::retrieveValue('gData', 'Positive', 0); $rss = CRM_Utils_Request::retrieveValue('rss', 'Positive', 0); + $gCalendar = CRM_Utils_Request::retrieveValue('gCalendar', 'Positive', 0); + + $info = CRM_Event_BAO_Event::getCompleteInfo($start, $type, $id, $end); + + if ($gCalendar) { + return self::gCalRedirect($info); + } $template = CRM_Core_Smarty::singleton(); $config = CRM_Core_Config::singleton(); - $info = CRM_Event_BAO_Event::getCompleteInfo($start, $type, $id, $end); - $template->assign('events', $info); - $template->assign('timezone', @date_default_timezone_get()); + + $timezones = [@date_default_timezone_get()]; + + $template->assign('timezone', $timezones[0]); // Send data to the correct template for formatting (iCal vs. gData) if ($rss) { @@ -61,6 +69,17 @@ class CRM_Event_ICalendar { $calendar = $template->fetch('CRM/Core/Calendar/GData.tpl'); } else { + $date_min = min( + array_map(function ($event) { + return strtotime($event['start_date']); + }, $info) + ); + $date_max = max( + array_map(function ($event) { + return strtotime($event['end_date'] ?? $event['start_date']); + }, $info) + ); + $template->assign('timezones', CRM_Utils_ICalendar::generate_timezones($timezones, $date_min, $date_max)); $calendar = $template->fetch('CRM/Core/Calendar/ICal.tpl'); $calendar = preg_replace('/(? ts('Event'), 2 => count($events)])); + } + + $event = reset($events); + + // Fetch the required Date TimeStamps + $start_date = date_create($event['start_date']); + + // Google Requires that a Full Day event end day happens on the next Day + $end_date = ($event['end_date'] + ? date_create($event['end_date']) + : date_create($event['start_date']) + ->add(DateInterval::createFromDateString('1 day')) + ->setTime(0, 0, 0) + ); + + $dates = $start_date->format('Ymd\THis') . '/' . $end_date->format('Ymd\THis'); + + $event_details = $event['description']; + + // Add space after paragraph + $event_details = str_replace('

', '

', $event_details); + $event_details = strip_tags($event_details); + + // Truncate Event Description and add permalink if greater than 996 characters + if (strlen($event_details) > 996) { + if (preg_match('/^.{0,996}(?=\s|$_)/', $event_details, $m)) { + $event_details = $m[0] . '...'; + } + } + + $event_details .= "\n\n" . ts('View %1 Details', [1 => $event['event_type']]) . ''; + + $params = [ + 'action' => 'TEMPLATE', + 'text' => strip_tags($event['title']), + 'dates' => $dates, + 'details' => $event_details, + 'location' => str_replace("\n", "\t", $event['location']), + 'trp' => 'false', + 'sprop' => 'website:' . CRM_Utils_System::baseCMSURL(), + 'ctz' => @date_default_timezone_get(), + ]; + + $url = 'https://www.google.com/calendar/event?' . CRM_Utils_System::makeQueryString($params); + + CRM_Utils_System::redirect($url); + } + } diff --git a/CRM/Utils/ICalendar.php b/CRM/Utils/ICalendar.php index 170dbef162..40707d98c0 100644 --- a/CRM/Utils/ICalendar.php +++ b/CRM/Utils/ICalendar.php @@ -28,19 +28,58 @@ class CRM_Utils_ICalendar { * * @param string $text * Text to escape. + * @param bool $keep_html + * Flag to retain HTML formatting + * @param int $position + * Column number of the start of the string in the ICal output - used to + * determine allowable length of the first line * * @return string */ - public static function formatText($text) { - $text = strip_tags($text); - $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML401, 'UTF-8'); + public static function formatText($text, $keep_html = FALSE, int $position = 0) { + if (!$keep_html) { + $text = preg_replace( + '{ ]+ \\b href=(?: "( [^"]+ )" | \'( [^\']+ )\' ) [^>]* > ( [^<]* ) }xi', + '$3 ($1$2)', + $text + ); + $text = preg_replace( + '{ < / [^>]+ > \s* }', + "\$0 ", + $text + ); + $text = preg_replace( + '{ <(br|/tr|/div|/h[1-6]) (\s [^>]*)? > (\s* \n)? }xi', + "\$0\n", + $text + ); + $text = preg_replace( + '{

(\s* \n)? }xi', + "\$0\n\n", + $text + ); + $text = strip_tags($text); + $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML401, 'UTF-8'); + } + $text = str_replace("\\", "\\\\", $text); $text = str_replace(',', '\,', $text); $text = str_replace(';', '\;', $text); $text = str_replace(["\r\n", "\n", "\r"], "\\n ", $text); + // Remove this check after PHP 7.4 becomes a minimum requirement $str_split = function_exists('mb_str_split') ? 'mb_str_split' : 'str_split'; - $text = implode("\n ", $str_split($text, 50)); + + if ($keep_html) { + $text = '' . $text . ''; + } + $prefix = ''; + if ($position) { + $prefixlen = max(50 - $position, 0); + $prefix = mb_substr($text, 0, $prefixlen) . "\n "; + $text = mb_substr($text, $prefixlen); + } + $text = $prefix . implode("\n ", $str_split($text, 50)); return $text; } @@ -120,4 +159,60 @@ class CRM_Utils_ICalendar { echo $calendar; } + /** + * @param array $timezones - Timezone strings + * @param $date_min + * @param $date_max + * + * @return array + */ + public static function generate_timezones(array $timezones, $date_min, $date_max) { + if (empty($timezones)) { + return []; + } + + $tz_items = []; + + foreach ($timezones as $tzstr) { + $timezone = new DateTimeZone($tzstr); + + $transitions = $timezone->getTransitions($date_min, $date_max); + + if (count($transitions) === 1) { + $transitions[] = array_values($transitions)[0]; + } + + $item = [ + 'id' => $timezone->getName(), + 'transitions' => [], + ]; + + $last_transition = array_shift($transitions); + + foreach ($transitions as $transition) { + $item['transitions'][] = [ + 'type' => $transition['isdst'] ? 'DAYLIGHT' : 'STANDARD', + 'offset_from' => self::format_tz_offset($last_transition['offset']), + 'offset_to' => self::format_tz_offset($transition['offset']), + 'abbr' => $transition['abbr'], + 'dtstart' => date_create($transition['time'], $timezone)->format("Ymd\THis"), + ]; + + $last_transition = $transition; + } + + $tz_items[] = $item; + } + + return $tz_items; + } + + protected static function format_tz_offset($offset) { + $offset /= 60; + $hours = intval($offset / 60); + $minutes = abs(intval($offset % 60)); + + return sprintf('%+03d%02d', $hours, $minutes); + } + } diff --git a/templates/CRM/Core/Calendar/ICal.tpl b/templates/CRM/Core/Calendar/ICal.tpl index 3db616c76c..ba35c212f8 100644 --- a/templates/CRM/Core/Calendar/ICal.tpl +++ b/templates/CRM/Core/Calendar/ICal.tpl @@ -12,11 +12,27 @@ VERSION:2.0 PRODID:-//CiviCRM//NONSGML CiviEvent iCal//EN X-WR-TIMEZONE:{$timezone} METHOD:PUBLISH +{foreach from=$timezones item=tzItem} +BEGIN:VTIMEZONE +TZID:{$tzItem.id} +{foreach from=$tzItem.transitions item=tzTr} +BEGIN:{$tzTr.type} +TZOFFSETFROM:{$tzTr.offset_from} +TZOFFSETTO:{$tzTr.offset_to} +TZNAME:{$tzTr.abbr} +{if $tzTr.dtstart} +DTSTART:{$tzTr.dtstart|crmICalDate} +{/if} +END:{$tzTr.type} +{/foreach} +END:VTIMEZONE +{/foreach} {foreach from=$events key=uid item=event} BEGIN:VEVENT UID:{$event.uid} SUMMARY:{$event.title|crmICalText} {if $event.description} +X-ALT-DESC;FMTTYPE=text/html:{$event.description|crmICalText:true:29} DESCRIPTION:{$event.description|crmICalText} {/if} {if $event.event_type} diff --git a/templates/CRM/Event/Page/iCalLinks.tpl b/templates/CRM/Event/Page/iCalLinks.tpl index 05a676c694..2f6a315567 100644 --- a/templates/CRM/Event/Page/iCalLinks.tpl +++ b/templates/CRM/Event/Page/iCalLinks.tpl @@ -9,8 +9,8 @@ *} {* Display icons / links for ical download and feed for EventInfo.tpl, ThankYou.tpl, DashBoard.tpl, and ManageEvent.tpl *} {foreach from=$iCal item="iCalItem"} - + - {$iCalItem.text} + {$iCalItem.text} {/foreach} diff --git a/xml/templates/message_templates/event_offline_receipt_html.tpl b/xml/templates/message_templates/event_offline_receipt_html.tpl index b300c91402..99db3e71f7 100644 --- a/xml/templates/message_templates/event_offline_receipt_html.tpl +++ b/xml/templates/message_templates/event_offline_receipt_html.tpl @@ -116,7 +116,13 @@ {capture assign=icalFeed}{crmURL p='civicrm/event/ical' q="reset=1&id=`$event.id`" h=0 a=1 fe=1}{/capture} - {ts}Download iCalendar File{/ts} + {ts}Download iCalendar entry for this event.{/ts} + + + + + {capture assign=gCalendar}{crmURL p='civicrm/event/ical' q="gCalendar=1&reset=1&id=`$event.id`" h=0 a=1 fe=1}{/capture} + {ts}Add event to Google Calendar{/ts} {/if} diff --git a/xml/templates/message_templates/event_offline_receipt_text.tpl b/xml/templates/message_templates/event_offline_receipt_text.tpl index 6b116c9ad1..3fc2e74310 100644 --- a/xml/templates/message_templates/event_offline_receipt_text.tpl +++ b/xml/templates/message_templates/event_offline_receipt_text.tpl @@ -68,7 +68,9 @@ {if !empty($event.is_public)} {capture assign=icalFeed}{crmURL p='civicrm/event/ical' q="reset=1&id=`$event.id`" h=0 a=1 fe=1}{/capture} -{ts}Download iCalendar File:{/ts} {$icalFeed} +{ts}Download iCalendar entry for this event.{/ts} {$icalFeed} +{capture assign=gCalendar}{crmURL p='civicrm/event/ical' q="gCalendar=1&reset=1&id=`$event.id`" h=0 a=1 fe=1}{/capture} +{ts}Add event to Google Calendar{/ts} {$gCalendar} {/if} {if !empty($email)} diff --git a/xml/templates/message_templates/event_online_receipt_html.tpl b/xml/templates/message_templates/event_online_receipt_html.tpl index 7b771d41b1..13ea8b90e0 100644 --- a/xml/templates/message_templates/event_online_receipt_html.tpl +++ b/xml/templates/message_templates/event_online_receipt_html.tpl @@ -149,7 +149,13 @@ {capture assign=icalFeed}{crmURL p='civicrm/event/ical' q="reset=1&id=`$event.id`" h=0 a=1 fe=1}{/capture} - {ts}Download iCalendar File{/ts} + {ts}Download iCalendar entry for this event.{/ts} + + + + + {capture assign=gCalendar}{crmURL p='civicrm/event/ical' q="gCalendar=1&reset=1&id=`$event.id`" h=0 a=1 fe=1}{/capture} + {ts}Add event to Google Calendar{/ts} {/if} diff --git a/xml/templates/message_templates/event_online_receipt_text.tpl b/xml/templates/message_templates/event_online_receipt_text.tpl index 22473002a2..ee1ae65303 100644 --- a/xml/templates/message_templates/event_online_receipt_text.tpl +++ b/xml/templates/message_templates/event_online_receipt_text.tpl @@ -90,7 +90,9 @@ {if !empty($event.is_public)} {capture assign=icalFeed}{crmURL p='civicrm/event/ical' q="reset=1&id=`$event.id`" h=0 a=1 fe=1}{/capture} -{ts}Download iCalendar File:{/ts} {$icalFeed} +{ts}Download iCalendar entry for this event.{/ts} {$icalFeed} +{capture assign=gCalendar}{crmURL p='civicrm/event/ical' q="gCalendar=1&reset=1&id=`$event.id`" h=0 a=1 fe=1}{/capture} +{ts}Add event to Google Calendar{/ts} {$gCalendar} {/if} {if !empty($payer.name)} diff --git a/xml/templates/message_templates/participant_confirm_html.tpl b/xml/templates/message_templates/participant_confirm_html.tpl index bd8b599b16..577e0aa169 100644 --- a/xml/templates/message_templates/participant_confirm_html.tpl +++ b/xml/templates/message_templates/participant_confirm_html.tpl @@ -126,12 +126,18 @@ {/if} {if $event.is_public} - + - {capture assign=icalFeed}{crmURL p='civicrm/event/ical' q="reset=1&id=`$event.id`" h=0 a=1 fe=1}{/capture} - {ts}Download iCalendar File{/ts} + {capture assign=icalFeed}{crmURL p='civicrm/event/ical' q="reset=1&id=`$event.id`" h=0 a=1 fe=1}{/capture} + {ts}Download iCalendar entry for this event.{/ts} - + + + + {capture assign=gCalendar}{crmURL p='civicrm/event/ical' q="gCalendar=1&reset=1&id=`$event.id`" h=0 a=1 fe=1}{/capture} + {ts}Add event to Google Calendar{/ts} + + {/if} {if '{contact.email}'} diff --git a/xml/templates/message_templates/participant_confirm_text.tpl b/xml/templates/message_templates/participant_confirm_text.tpl index 0ccf6ad260..ea2abf605b 100644 --- a/xml/templates/message_templates/participant_confirm_text.tpl +++ b/xml/templates/message_templates/participant_confirm_text.tpl @@ -64,7 +64,9 @@ Click this link to go to a web page where you can confirm your registration onli {if $event.is_public} {capture assign=icalFeed}{crmURL p='civicrm/event/ical' q="reset=1&id=`$event.id`" h=0 a=1 fe=1}{/capture} -{ts}Download iCalendar File:{/ts} {$icalFeed} +{ts}Download iCalendar entry for this event.{/ts} {$icalFeed} +{capture assign=gCalendar}{crmURL p='civicrm/event/ical' q="gCalendar=1&reset=1&id=`$event.id`" h=0 a=1 fe=1}{/capture} +{ts}Add event to Google Calendar{/ts} {$gCalendar} {/if} {if '{contact.email}'} -- 2.25.1