From 84ecc1cbd295415c612a0c2fc8c51537441be8f8 Mon Sep 17 00:00:00 2001 From: Kuldip Pujara <kuldip@compuco.io> Date: Thu, 23 Feb 2023 18:35:42 +0530 Subject: [PATCH] CIVIIB-85: CiviEvent daylight saving time issue Included in CiviCRM 5.52.2 PR: https://github.com/civicrm/civicrm-core/pull/23808 --- .../Smarty/plugins/modifier.crmICalText.php | 4 +- CRM/Event/BAO/Event.php | 33 +++--- CRM/Event/ICalendar.php | 76 ++++++++++++- CRM/Utils/ICalendar.php | 104 +++++++++++++++++- 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, 242 insertions(+), 37 deletions(-) diff --git a/CRM/Core/Smarty/plugins/modifier.crmICalText.php b/CRM/Core/Smarty/plugins/modifier.crmICalText.php index 29cd892bd10c..db3e3e906d27 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 b25413edbab8..5f0f604cd7b7 100644 --- a/CRM/Event/BAO/Event.php +++ b/CRM/Event/BAO/Event.php @@ -2416,18 +2416,7 @@ public static function getEntityRefFilters() { * 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 @@ public static function getICalLinks($eventId = NULL) { '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 a4cc998fd7b8..faf2889b1797 100644 --- a/CRM/Event/ICalendar.php +++ b/CRM/Event/ICalendar.php @@ -42,14 +42,22 @@ public static function run() { $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 @@ public static function run() { $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('/(?<!\r)\n/', "\r\n", $calendar); } @@ -80,4 +99,55 @@ public static function run() { CRM_Utils_System::civiExit(); } + protected static function gCalRedirect(array $events) { + if (count($events) != 1) { + throw new CRM_Core_Exception(ts('Expected one %1, found %2', [1 => 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('</p>', '</p> ', $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<a href=\"{$event['url']}\">" . ts('View %1 Details', [1 => $event['event_type']]) . '</a>'; + + $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 7eee09c9ebc7..b8a69ac7689e 100644 --- a/CRM/Utils/ICalendar.php +++ b/CRM/Utils/ICalendar.php @@ -28,16 +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); + public static function formatText($text, $keep_html = FALSE, int $position = 0) { + if (!$keep_html) { + $text = preg_replace( + '{ <a [^>]+ \\b href=(?: "( [^"]+ )" | \'( [^\']+ )\' ) [^>]* > ( [^<]* ) </a> }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( + '{ </p> (\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); - $text = implode("\n ", str_split($text, 50)); + + // Remove this check after PHP 7.4 becomes a minimum requirement + $str_split = function_exists('mb_str_split') ? 'mb_str_split' : 'str_split'; + + if ($keep_html) { + $text = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN"><html><body>' . $text . '</body></html>'; + } + $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; } @@ -116,4 +158,60 @@ public static function send($calendar, $content_type = 'text/calendar', $charset 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 f38502d5adf5..009d1bbfc30e 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 05a676c69469..2f6a31556762 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"} - <a href="{$iCalItem.url}" title="{$iCalItem.text}"{if !empty($event)} class="crm-event-feed-link"{/if}> + <a href="{$iCalItem.url}" {if !empty($event)} class="crm-event-feed-link"{/if}> <span class="fa-stack" aria-hidden="true"><i class="crm-i fa-calendar-o fa-stack-2x"></i><i style="top: 15%;" class="crm-i {$iCalItem.icon} fa-stack-1x"></i></span> - <span class="sr-only">{$iCalItem.text}</span> + <span class="label">{$iCalItem.text}</span> </a> {/foreach} diff --git a/xml/templates/message_templates/event_offline_receipt_html.tpl b/xml/templates/message_templates/event_offline_receipt_html.tpl index b300c91402e5..99db3e71f776 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 @@ <tr> <td colspan="2" {$valueStyle}> {capture assign=icalFeed}{crmURL p='civicrm/event/ical' q="reset=1&id=`$event.id`" h=0 a=1 fe=1}{/capture} - <a href="{$icalFeed}">{ts}Download iCalendar File{/ts}</a> + <a href="{$icalFeed}">{ts}Download iCalendar entry for this event.{/ts}</a> + </td> + </tr> + <tr> + <td colspan="2" {$valueStyle}> + {capture assign=gCalendar}{crmURL p='civicrm/event/ical' q="gCalendar=1&reset=1&id=`$event.id`" h=0 a=1 fe=1}{/capture} + <a href="{$gCalendar}">{ts}Add event to Google Calendar{/ts}</a> </td> </tr> {/if} diff --git a/xml/templates/message_templates/event_offline_receipt_text.tpl b/xml/templates/message_templates/event_offline_receipt_text.tpl index 6b116c9ad152..3fc2e743103d 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 7b771d41b180..13ea8b90e05a 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 @@ <tr> <td colspan="2" {$valueStyle}> {capture assign=icalFeed}{crmURL p='civicrm/event/ical' q="reset=1&id=`$event.id`" h=0 a=1 fe=1}{/capture} - <a href="{$icalFeed}">{ts}Download iCalendar File{/ts}</a> + <a href="{$icalFeed}">{ts}Download iCalendar entry for this event.{/ts}</a> + </td> + </tr> + <tr> + <td colspan="2" {$valueStyle}> + {capture assign=gCalendar}{crmURL p='civicrm/event/ical' q="gCalendar=1&reset=1&id=`$event.id`" h=0 a=1 fe=1}{/capture} + <a href="{$gCalendar}">{ts}Add event to Google Calendar{/ts}</a> </td> </tr> {/if} diff --git a/xml/templates/message_templates/event_online_receipt_text.tpl b/xml/templates/message_templates/event_online_receipt_text.tpl index 22473002a288..ee1ae65303e9 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 bd8b599b166a..577e0aa16947 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} - <tr> + <tr> <td colspan="2" {$valueStyle}> - {capture assign=icalFeed}{crmURL p='civicrm/event/ical' q="reset=1&id=`$event.id`" h=0 a=1 fe=1}{/capture} - <a href="{$icalFeed}">{ts}Download iCalendar File{/ts}</a> + {capture assign=icalFeed}{crmURL p='civicrm/event/ical' q="reset=1&id=`$event.id`" h=0 a=1 fe=1}{/capture} + <a href="{$icalFeed}">{ts}Download iCalendar entry for this event.{/ts}</a> </td> - </tr> + </tr> + <tr> + <td colspan="2" {$valueStyle}> + {capture assign=gCalendar}{crmURL p='civicrm/event/ical' q="gCalendar=1&reset=1&id=`$event.id`" h=0 a=1 fe=1}{/capture} + <a href="{$gCalendar}">{ts}Add event to Google Calendar{/ts}</a> + </td> + </tr> {/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 0ccf6ad26052..ea2abf605be4 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}'}