date_ical_plugin_style_ical_feed.inc 14.9 KB
Newer Older
Robert Rollins's avatar
Robert Rollins committed
1
2
3
4
5
6
7
8
9
10
11
12
<?php

/**
 * @file
 * Views style plugin for the Date iCal module.
 */

/**
 * Default style plugin to render an iCal feed.
 */
class date_ical_plugin_style_ical_feed extends views_plugin_style {
  
Robert Rollins's avatar
Robert Rollins committed
13
14
  function _get_option($option_name) {
    return isset($this->options[$option_name]) ? $this->options[$option_name] : '';
Robert Rollins's avatar
Robert Rollins committed
15
16
17
18
19
20
21
22
23
24
25
  }
  
  // Sets up the iCal feed icon on calendar pages.
  function attach_to($display_id, $path, $title) {
    $display = $this->view->display[$display_id]->handler;
    $url_options = array();
    $input = $this->view->get_exposed_input();
    if ($input) {
      $url_options['query'] = $input;
    }
    $url_options['absolute'] = TRUE;
Robert Rollins's avatar
Robert Rollins committed
26
    
Robert Rollins's avatar
Robert Rollins committed
27
    $url = url($this->view->get_url(NULL, $path), $url_options);
Robert Rollins's avatar
Robert Rollins committed
28
29
30
    // If the user didn't disable the option, change the scheme to webcal:// so
    // that calendar clients can automatically subscribe via the iCal link.
    if (!$this->_get_option('disable_webcal')) {
31
      $url = str_replace(array('http://', 'https://'), 'webcal://', $url);
Robert Rollins's avatar
Robert Rollins committed
32
33
    }
    
34
    // Render the feed icon and header tag (except during a View Preview).
Robert Rollins's avatar
Robert Rollins committed
35
36
    if (empty($this->view->live_preview)) {
      $tooltip = t('Add to My Calendar');
37
      $this->view->feed_icon .= theme('date_ical_icon', array('url' => check_url($url), 'tooltip' => $tooltip, 'view' => $this->view));
Robert Rollins's avatar
Robert Rollins committed
38
39
      drupal_add_html_head_link(array(
        'rel' => 'alternate',
Robert Rollins's avatar
Robert Rollins committed
40
41
        'type' => 'text/calendar',
        'title' => $tooltip,
Robert Rollins's avatar
Robert Rollins committed
42
43
44
45
46
        'href' => $url
      ));
    }
  }
  
47
48
49
50
51
52
53
54
  function option_definition() {
    $options = parent::option_definition();
    $options['cal_name'] = array('default' => array());
    $options['no_calname'] = array('default' => array());
    $options['disable_webcal'] = array('default' => array());
    return $options;
  }
  
Robert Rollins's avatar
Robert Rollins committed
55
56
57
58
59
60
  function options_form(&$form, &$form_state) {
    parent::options_form($form, $form_state);
    // Allow users to override the default Calendar name (X-WR-CALNAME).
    $form['cal_name'] = array(
      '#type' => 'textfield',
      '#title' => t('iCal Calendar Name'),
Robert Rollins's avatar
Robert Rollins committed
61
      '#default_value' => $this->_get_option('cal_name'),
62
63
      '#description' => t('This will appear as the title of the iCal feed. If left blank, the View Title will be used.
        If that is also blank, the site name will be inserted as the iCal feed title.'),
Robert Rollins's avatar
Robert Rollins committed
64
    );
Robert Rollins's avatar
Robert Rollins committed
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
    $form['no_calname'] = array(
      '#type' => 'checkbox',
      '#title' => t('Exclude Calendar Name'),
      '#default_value' => $this->_get_option('no_calname'),
      '#description' => t("Excluding the X-WR-CALNAME value from the iCal Feed causes
        some calendar clients to add the events in the feed to an existing calendar, rather
        than creating a whole new calendar for them."),
    );
    $form['disable_webcal'] = array(
      '#type' => 'checkbox',
      '#title' => t('Disable webcal://'),
      '#default_value' => $this->_get_option('disable_webcal'),
      '#description' => t("By default, the feed URL will use the webcal:// scheme, which allows calendar
        clients to easily subscribe to the feed. If you want your users to instead download this iCal
        feed as a file, activate this option."),
    );
Robert Rollins's avatar
Robert Rollins committed
81
82
83
  }
  
  function render() {
84
85
86
    if (empty($this->row_plugin) || !in_array($this->row_plugin->plugin_name, array('date_ical','date_ical_fields'))) {
      debug('views_plugin_style_ical_feed: This style plugin supports only iCal Entity row and iCal Fields plugins.', NULL, TRUE);
      return t('To enable iCal output, this view\'s Format must be configured to Show: iCal Entity or iCal Fields.');
Robert Rollins's avatar
Robert Rollins committed
87
    }
88
89
    if ($this->row_plugin->plugin_name == 'date_ical_fields' && empty($this->row_plugin->options['date_field'])) {
      // Because the Date field is required by the form, this error state will rarely occur. But I ran across it during
90
      // testing, and the error that resulted was totally non-sensical, so I'm adding this in case it does ever happen.
91
      return t("When using the iCal Fields row plugin, the Date field is required. Please set it up using the Settings link under 'Format -> Show: iCal Fields'.");
92
93
    }
    $events = array();
Robert Rollins's avatar
Robert Rollins committed
94
95
    foreach ($this->view->result as $row_index => $row) {
      $this->view->row_index = $row_index;
96
97
98
99
100
101
102
      try {
        $events[] = $this->row_plugin->render($row, $row_index);
      }
      catch (Exception $e) {
        debug($e->getMessage(), NULL, TRUE);
        return $e->getMessage();
      }
Robert Rollins's avatar
Robert Rollins committed
103
104
    }
    unset($this->view->row_index);
105
    
Robert Rollins's avatar
Robert Rollins committed
106
107
108
109
    // Try to load the iCalcreator library, and check if that worked.
    $library = libraries_load('iCalcreator');
    if (!empty($library['loaded'])) {
      // Create a vcalendar object using the iCalcreator library.
110
      $config = array('unique_id' => 'Drupal: Date iCal v' . DATE_ICAL_VERSION);
Robert Rollins's avatar
Robert Rollins committed
111
112
113
      $vcalendar = new vcalendar($config);
      $vcalendar->setMethod('PUBLISH');
      
114
      // If the iCal Calendar Name has been set in the Feed Style options, it's used as the
Robert Rollins's avatar
Robert Rollins committed
115
116
117
      // title in the iCal feed. If not, the View Title is used. If that is also blank, then
      // the Site Name is used.
      $cal_name = $this->_get_option('cal_name');
118
119
120
121
122
123
124
125
      if (empty($cal_name)) {
        $view_title = $this->view->get_title();
        if (!empty($view_title)) {
          $cal_name = $view_title;
        }
        else {
          $cal_name = variable_get('site_name', 'Drupal');
        }
Robert Rollins's avatar
Robert Rollins committed
126
      }
Robert Rollins's avatar
Robert Rollins committed
127
128
129
130
      // Only include the X-WR-CALNAME property if the user didn't check "Exclude Calendar Name".
      if (!$this->_get_option('no_calname')) {
        $vcalendar->setProperty('x-wr-calname', $cal_name, array('VALUE' => 'TEXT'));
      }
131
      
Robert Rollins's avatar
Robert Rollins committed
132
      // Now add the VEVENTs.
Robert Rollins's avatar
Robert Rollins committed
133
      $timezones = array();
134
135
      foreach ($events as $event) {
        if (empty($event)) {
136
137
138
139
140
          // The row plugin returned NULL for this row, which can happen due to
          // various error conditions. The only thing we can do is skip it.
          continue;
        }
        
Robert Rollins's avatar
Robert Rollins committed
141
142
        $vevent = $vcalendar->newComponent('vevent');
        // Get the start date as an array.
143
144
        $start = $event['start']->toArray();
        $timezone = $event['start']->getTimezone()->getName();
Robert Rollins's avatar
Robert Rollins committed
145
        $timezones[$timezone] = $timezone;
146
        
147
        if ($event['all_day']) {
148
149
150
151
152
153
154
155
156
157
158
159
160
161
          // All day events need to be specified as DATE, rather than DATE-TIME, or they get interpretted wrong.
          $vevent->setDtstart($start['year'], $start['month'], $start['day'],
            FALSE, FALSE, FALSE, FALSE, array('VALUE' => 'DATE'));
        }
        else {
          $vevent->setDtstart(
            $start['year'],
            $start['month'],
            $start['day'],
            $start['hour'],
            $start['minute'],
            $start['second'],
            $timezone);
        }
162
        
163
        // Add the Timezone info to the start date, for use later.
164
        $start['tz'] = $event['start']->getTimezone();
165
        
Robert Rollins's avatar
Robert Rollins committed
166
        // Only add the end date if there is one.
167
168
169
        if (!empty($event['end'])) {
          $end = $event['end']->toArray();
          $timezone = $event['start']->getTimezone()->getName();
Robert Rollins's avatar
Robert Rollins committed
170
          $timezones[$timezone] = $timezone;
171
          
172
          if ($event['all_day']) {
173
174
175
176
177
178
179
180
181
182
183
184
185
186
            $vevent->setDtend($end['year'], $end['month'], $end['day'],
              FALSE, FALSE, FALSE, FALSE, array('VALUE' => 'DATE'));
          }
          else {
            $vevent->setDtend(
              $end['year'],
              $end['month'],
              $end['day'],
              $end['hour'],
              $end['minute'],
              $end['second'],
              $timezone);
          }
          // Keep a copy of the end date, as it's useful later.
187
          $end['tz'] = $event['end']->getTimezone();
Robert Rollins's avatar
Robert Rollins committed
188
        }
189
190
        $vevent->setProperty('uid', $event['uid']);
        $vevent->setProperty('summary', $event['summary']);
191
        
Robert Rollins's avatar
Robert Rollins committed
192
        // Handle repeating dates from the date_repeat module.
193
        if (!empty($event['rrule']) && module_exists('date_repeat')) {
194
195
196
          // Split the rrule into the actual rule, exceptions, and additions.
          module_load_include('inc', 'date_api', 'date_api_ical');
          module_load_include('inc', 'date_repeat', 'date_repeat_calc');
197
          list($rrule, $exceptions, $additions) = date_repeat_split_rrule($event['rrule']);
198
199
200
          // Add the rrule itself. We need to massage the data a bit, since
          // iCalcreator expects RRULEs to be in a different format than how
          // Date API gives them to us.
201
          $rrule = self::convert_rrule_for_icalcreator($rrule);
Robert Rollins's avatar
Robert Rollins committed
202
203
204
205
206
207
          $vevent->setRrule($rrule);
          // Process exceptions if there are any.
          if (!empty($exceptions)) {
            $exdates = array();
            foreach ($exceptions as $exception) {
              $except = date_ical_date($exception, 'UTC');
208
209
              date_timezone_set($except, $start['tz']);
              $exception_array = $except->toArray();
Robert Rollins's avatar
Robert Rollins committed
210
              $exdates[] = array(
211
212
213
                'year' =>  $exception_array['year'],
                'month' => $exception_array['month'],
                'day' =>   $exception_array['day'],
Robert Rollins's avatar
Robert Rollins committed
214
                // Use the time information from the start date.
215
216
217
218
                'hour' =>   $start['hour'],
                'min' =>    $start['minute'],
                'second' => $start['second'],
                'tz' =>     $start['tz']->getName(),
Robert Rollins's avatar
Robert Rollins committed
219
220
221
222
223
224
225
226
227
228
              );
            }
            // Add those exclusions as 'EXDATE's.
            $vevent->setExdate($exdates);
          }
          // Process additions if there are any.
          if (!empty($additions)) {
            $add_dates = array();
            foreach ($additions as $addition) {
              $add = date_ical_date($addition, 'UTC');
229
230
              date_timezone_set($add, $start['tz']);
              $addition_array = $add->toArray();
231
              
Robert Rollins's avatar
Robert Rollins committed
232
              $add_date = array(
233
234
235
                'year' =>  $addition_array['year'],
                'month' => $addition_array['month'],
                'day' =>   $addition_array['day'],
Robert Rollins's avatar
Robert Rollins committed
236
                // Use the time information from the start date.
237
238
239
240
                'hour' =>   $start['hour'],
                'min' =>    $start['minute'],
                'second' => $start['second'],
                'tz' =>     $start['tz']->getName(),
Robert Rollins's avatar
Robert Rollins committed
241
242
              );
              // If there was an end date specified, use that too.
243
              if (!empty($event['end'])) {
Robert Rollins's avatar
Robert Rollins committed
244
245
                $add_date = array($add_date);
                $add_date[] = array(
246
247
248
                  'year' =>  $addition_array['year'],
                  'month' => $addition_array['month'],
                  'day' =>   $addition_array['day'],
Robert Rollins's avatar
Robert Rollins committed
249
                  // Use the time information from the end date.
250
251
252
253
                  'hour' =>   $end['hour'],
                  'min' =>    $end['minute'],
                  'second' => $end['second'],
                  'tz' =>     $end['tz']->getName(),
Robert Rollins's avatar
Robert Rollins committed
254
255
                );
              }
256
              
Robert Rollins's avatar
Robert Rollins committed
257
258
259
260
261
262
              $add_dates[] = $add_date;
            }
            // Add the additions as 'RDATE's.
            $vevent->setRdate($add_dates);
          }
        }
263
264
        if (!empty($event['url'])) {
          $vevent->setUrl($event['url'], array('type' => 'URI'));
Robert Rollins's avatar
Robert Rollins committed
265
        }
266
267
        if (!empty($event['location'])) {
          $vevent->setProperty('location', $event['location']);
Robert Rollins's avatar
Robert Rollins committed
268
        }
269
270
        if (!empty($event['description'])) {
          $vevent->setProperty('description', $event['description']);
Robert Rollins's avatar
Robert Rollins committed
271
        }
272
273
        if (!empty($event['last-modified'])) {
          $lm = $event['last-modified']->toArray();
Robert Rollins's avatar
Robert Rollins committed
274
275
276
277
278
279
280
281
282
283
284
285
          $vevent->setLastModified(
            $lm['year'],
            $lm['month'],
            $lm['day'],
            $lm['hour'],
            $lm['minute'],
            $lm['second'],
            $lm['timezone']
          );
        }
        
        // Allow other modules to alter the VEVENT object.
286
        drupal_alter('date_ical_feed_ical_vevent_render', $vevent, $this->view, $event);
Robert Rollins's avatar
Robert Rollins committed
287
      }
288
      
Robert Rollins's avatar
Robert Rollins committed
289
      // Now add all the timezones we just used to the calendar.
Robert Rollins's avatar
Robert Rollins committed
290
291
292
      foreach ($timezones as $timezone) {
        iCalUtilityFunctions::createTimezone($vcalendar, $timezone);
      }
293
      
Robert Rollins's avatar
Robert Rollins committed
294
295
      // Allow other modules to alter the calendar as a whole.
      drupal_alter('date_ical_feed_ical_vcalendar_render', $vcalendar, $this->view);
296
      
Robert Rollins's avatar
Robert Rollins committed
297
      $output = $vcalendar->createCalendar();
298
299
300
301
302
303
      // For some unknown reason (overzealous spec comnpliance?), iCalcreator
      // escapes all commas and semicolons in the strings that it outputs.
      // This unescapes them, though it does it with a pretty big hammer. This
      // might be going too far.
      $output = str_replace('\,', ',', $output);
      $output = str_replace('\;', ';', $output);
Robert Rollins's avatar
Robert Rollins committed
304
305
306
    }
    else {
      // The iCalcreator library isn't available, so we can't output anything.
307
      $output = t('Please install the iCalcreator library to enable iCal output.');
Robert Rollins's avatar
Robert Rollins committed
308
    }
Robert Rollins's avatar
Robert Rollins committed
309
310
311
    
    // These steps shouldn't be run when doing a Preview on the View config page.
    if (empty($this->view->live_preview)) {
Robert Rollins's avatar
Robert Rollins committed
312
313
      // Prevent devel module from appending queries to ical export.
      $GLOBALS['devel_shutdown'] = FALSE;
Robert Rollins's avatar
Robert Rollins committed
314
315
316
317
318
319
320
321
322
323
324
      
      drupal_add_http_header('Content-Type', 'text/calendar; charset=utf-8');
      
      // For sites with Clean URLs disabled, the "path" value in the view Display
      // doesn't actually get applied to the URL of the calendar feed. So, we
      // need to manually instruct browsers to download a .ics file.
      if (!variable_get('clean_url', FALSE)) {
        $path_array = explode('/', $this->display->display_options['path']);
        $filename = array_pop($path_array);
        drupal_add_http_header('Content-Disposition', "attachment; filename=\"$filename\"");
      }
325
326
    }
    
327
328
329
    // Allow other modules to alter the rendered calendar, just before it gets sent out.
    drupal_alter('date_ical_post_render', $output, $this->view);
    
Robert Rollins's avatar
Robert Rollins committed
330
331
    return $output;
  }
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
  
  /**
   * This function converts an rrule array to the iCalcreator format.
   *
   * iCalcreator expects the BYDAY element to be an array like this:
   * (array) ( [([plus] ordwk / minus ordwk)], "DAY" => weekday )
   *
   * But the way that the Date API gives it to us is like this:
   * (array) ( [([plus] ordwk / minus ordwk)]weekday )
   */
  public static function convert_rrule_for_icalcreator($rrule) {
    $new_rrule = array();
    foreach ($rrule as $key => $value) {
      if (strtoupper($key) == 'DATA') {
        // iCalcreator doesn't expect the 'DATA' key that the Date API gives us.
        continue;
      }
349
350
351
352
      if (strtoupper($key) == 'UNTIL') {
        // iCalcreator expects the 'timestamp' to be array key for UNTIL
        $value['timestamp'] = strtotime($value['datetime']);
      }
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
      if (strtoupper($key) == 'BYDAY') {
        $new_byday = array();
        foreach ($value as $day) {
          // Fortunately, the weekday values are always 2 characters, so it's easy to
          // split off the ordwk part, even though it could be 1 or 2 characters.
          $weekday = substr($day, -2);
          $ordwk = substr($day, 0, -2);
          $new_byday[] = array($ordwk, 'DAY' => $weekday);
        }
        $value = $new_byday;
      }
      $new_rrule[$key] = $value;
    }
    return $new_rrule;
  }
Robert Rollins's avatar
Robert Rollins committed
368
}