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