CIVICRM-1861 Embed timezone output for iCal
authorFrancis Whittle <francis@agileware.com.au>
Thu, 30 Jun 2022 00:02:43 +0000 (10:02 +1000)
committerFrancis Whittle <francis@agileware.com.au>
Thu, 30 Jun 2022 00:21:25 +0000 (10:21 +1000)
    CIVICRM-1988 Add HTML alternate description to iCAL
    CIVICRM-1987 Add to Google Calendar Link

12 files changed:
CRM/Core/Smarty/plugins/modifier.crmICalText.php
CRM/Event/BAO/Event.php
CRM/Event/ICalendar.php
CRM/Utils/ICalendar.php
templates/CRM/Core/Calendar/ICal.tpl
templates/CRM/Event/Page/iCalLinks.tpl
xml/templates/message_templates/event_offline_receipt_html.tpl
xml/templates/message_templates/event_offline_receipt_text.tpl
xml/templates/message_templates/event_online_receipt_html.tpl
xml/templates/message_templates/event_online_receipt_text.tpl
xml/templates/message_templates/participant_confirm_html.tpl
xml/templates/message_templates/participant_confirm_text.tpl

index 29cd892bd10c2a11a1d54cb42f8081e349d91c02..db3e3e906d273ea1c764a78e2567bebad0640b1c 100644 (file)
@@ -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);
 }
index b25413edbab8a7edfea82033c462c110f4c52b46..5f0f604cd7b77dc523f502ff51b9c4d016507094 100644 (file)
@@ -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;
   }
 
index a4cc998fd7b8f59890f48b4aeb6bc98323a8071a..faf2889b1797c0a78381697e7f6129c24da7f6bb 100644 (file)
@@ -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('/(?<!\r)\n/', "\r\n", $calendar);
     }
@@ -80,4 +99,55 @@ class CRM_Event_ICalendar {
     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);
+  }
+
 }
index 170dbef1622f82dfe63ae11544add127eac2e4d5..40707d98c0e3ed6e315193df87604138f2a2215a 100644 (file)
@@ -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(
+        '{ <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);
+
     // 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 = '<!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;
   }
 
@@ -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);
+  }
+
 }
index 3db616c76c5932b18fbc620777451533fa34abbb..ba35c212f8b2bee97d1ce211a520bf7ce0af05c2 100644 (file)
@@ -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}
index 05a676c69469e94a579873146c6f290fc36ce78f..2f6a31556762d912b51d2076a2159935bb772dbb 100644 (file)
@@ -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}
index b300c91402e5d8cd898f3bdb6fc23719fd957cab..99db3e71f7760e9f32c07338e1ce7cd63877ba91 100644 (file)
       <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}
index 6b116c9ad152f87fec3bd3629e4f30477d7b66c9..3fc2e743103d391d3859acf61ab52e8028a55610 100644 (file)
@@ -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)}
index 7b771d41b18000cf2d36a7bd3e3d635d83d0bdb4..13ea8b90e05a7cd93e7b003e9806dc973b8836e3 100644 (file)
       <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}
index 22473002a288de1a1049394235990d21c21250ad..ee1ae65303e9d7fa4f22a608f66b59edcb7ccdee 100644 (file)
@@ -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)}
index bd8b599b166a02a75c57643da60080a98d6ff63d..577e0aa169475ef1c81e69d5fcd4b898538a6344 100644 (file)
      {/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}'}
index 0ccf6ad260524621d887dee3c36af9c5acd0e443..ea2abf605be427c8a2b1426c1bcb78196bde1598 100644 (file)
@@ -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}'}