Project

General

Profile

Paste
Download (18.8 KB) Statistics
| Branch: | Revision:

root / drupal7 / sites / all / modules / date_ical / includes / date_ical_plugin_style_ical_feed.inc @ 62e0cc08

1
<?php
2
/**
3
 * @file
4
 * Views style plugin for the Date iCal module.
5
 */
6

    
7
/**
8
 * Defines a Views style plugin that renders iCal feeds.
9
 */
10
class date_ical_plugin_style_ical_feed extends views_plugin_style {
11

    
12
  /**
13
   * Internal helper function.
14
   */
15
  protected function _get_option($option_name) {
16
    return isset($this->options[$option_name]) ? $this->options[$option_name] : '';
17
  }
18

    
19
  /**
20
   * Sets up the iCal feed icon on calendar pages.
21
   */
22
  public function attach_to($display_id, $path, $title) {
23
    $url_options = array();
24
    $input = $this->view->get_exposed_input();
25
    if ($input) {
26
      $url_options['query'] = $input;
27
    }
28
    $url_options['absolute'] = TRUE;
29

    
30
    // Only add arguments to ical path for the contextual filters in the feed display.
31
    // Clone view to prevent affecting the page view where this is attached.
32
    $clone = $this->view->clone_view();
33
    $clone->set_display($this->display->id);
34
    $contextual_filters = $clone->get_items('argument');
35
    $arg_number = count($contextual_filters);
36

    
37
    // Only include as many arguments as the feed display supports.
38
    foreach($clone->args as $key => $arg) {
39
      if ($key >= $arg_number) {
40
        unset($clone->args[$key]);
41
      }
42
    }
43

    
44
    $url = url($clone->get_url(NULL, $path), $url_options);
45

    
46
    // If the user didn't disable the option, change the scheme to webcal://
47
    // so calendar clients can automatically subscribe via the iCal link.
48
    if (!$this->_get_option('disable_webcal')) {
49
      $url = str_replace(array('http://', 'https://'), 'webcal://', $url);
50
    }
51

    
52
    // Render the feed icon and header tag (except during a View Preview and
53
    // if the display is disabled).
54
    if (empty($this->view->live_preview) && (!isset($this->display->display_options['enabled']) || $this->display->display_options['enabled'])) {
55
      $tooltip = t('Add to My Calendar');
56
      if (!isset($this->view->feed_icon)) {
57
        // In PHP 5.5, you're not allowed to concatinate onto an unset
58
        // property. But we need to do a concat, because there may be
59
        // other attachments.
60
        $this->view->feed_icon = '';
61
      }
62
      $variables = array(
63
        'url' => check_url($url),
64
        'tooltip' => $tooltip,
65
        'view' => $this->view,
66
      );
67
      $this->view->feed_icon .= theme('date_ical_icon', $variables);
68
      drupal_add_html_head_link(array(
69
        'rel' => 'alternate',
70
        'type' => 'text/calendar',
71
        'title' => $tooltip,
72
        'href' => $url,
73
      ));
74
    }
75
  }
76

    
77
  /**
78
   * Set up the options for the style plugin.
79
   */
80
  public function option_definition() {
81
    $false_bool = array('default' => FALSE, 'bool' => TRUE);
82

    
83
    $options = parent::option_definition();
84
    $options['cal_name'] = array('default' => array());
85
    $options['no_calname'] = $false_bool;
86
    $options['disable_webcal'] = $false_bool;
87
    $options['exclude_dtstamp'] = $false_bool;
88
    $options['unescape_punctuation'] = $false_bool;
89
    return $options;
90
  }
91

    
92
  /**
93
   * Build the form for setting the style plugin's options.
94
   */
95
  public function options_form(&$form, &$form_state) {
96
    parent::options_form($form, $form_state);
97
    // Allow users to override the default Calendar name (X-WR-CALNAME).
98
    $form['cal_name'] = array(
99
      '#type' => 'textfield',
100
      '#title' => t('iCal Calendar Name'),
101
      '#default_value' => $this->_get_option('cal_name'),
102
      '#description' => t('This will appear as the title of the iCal feed. If left blank, the View Title will be used.
103
        If that is also blank, the site name will be inserted as the iCal feed title.'),
104
    );
105
    $form['no_calname'] = array(
106
      '#type' => 'checkbox',
107
      '#title' => t('Exclude Calendar Name'),
108
      '#default_value' => $this->_get_option('no_calname'),
109
      '#description' => t("Excluding the X-WR-CALNAME value from the iCal Feed causes
110
        some calendar clients to add the events in the feed to an existing calendar, rather
111
        than creating a whole new calendar for them."),
112
    );
113
    $form['disable_webcal'] = array(
114
      '#type' => 'checkbox',
115
      '#title' => t('Disable webcal://'),
116
      '#default_value' => $this->_get_option('disable_webcal'),
117
      '#description' => t("By default, the feed URL will use the webcal:// scheme, which allows calendar
118
        clients to easily subscribe to the feed. If you want your users to instead download this iCal
119
        feed as a file, activate this option."),
120
    );
121
    $form['exclude_dtstamp'] = array(
122
      '#type' => 'checkbox',
123
      '#title' => t('Exclude DTSTAMP'),
124
      '#default_value' => $this->_get_option('exclude_dtstamp'),
125
      '#description' => t("By default, the feed will set each event's DTSTAMP property to the time at which the feed got downloaded.
126
        Some feed readers will (incorrectly) look at the DTSTAMP value when they compare different downloads of the same feed, and
127
        conclcude that the event has been updated (even though it hasn't actually changed). Enable this option to exclude the DTSTAMP
128
        field from your feeds, so that these buggy feed readers won't mark every event as updated every time they check."),
129
    );
130
    $form['unescape_punctuation'] = array(
131
      '#type' => 'checkbox',
132
      '#title' => t('Unescape Commas and Semicolons'),
133
      '#default_value' => $this->_get_option('unescape_punctuation'),
134
      '#description' => t('In order to comply with the iCal spec, Date iCal will "escape" commas and semicolons (prepend them with backslashes).
135
        However, many calendar clients are bugged to not unescape these characters, leaving the backslashes littered throughout your events.
136
        Enable this option to have Date iCal unescape these characters before it exports the iCal feed.'),
137
    );
138
  }
139

    
140
  /**
141
   * Render the event arrays returned by the row plugin into a VCALENDAR.
142
   */
143
  public function render() {
144
    if (empty($this->row_plugin) || !in_array($this->row_plugin->plugin_name, array('date_ical', 'date_ical_fields'))) {
145
      debug('date_ical_plugin_style_ical_feed: This style plugin supports only the "iCal Entity" and "iCal Fields" row plugins.', NULL, TRUE);
146
      return t('To enable iCal output, the view Format must be configured to Show: iCal Entity or iCal Fields.');
147
    }
148
    if ($this->row_plugin->plugin_name == 'date_ical_fields' && empty($this->row_plugin->options['date_field'])) {
149
      // Because the Date field is required by the form, this error state will
150
      // rarely occur. But I ran across it during testing, and the error that
151
      // resulted was totally non-sensical, so I'm adding this just in case.
152
      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'.");
153
    }
154
    $events = array();
155
    foreach ($this->view->result as $row_index => $row) {
156
      $this->view->row_index = $row_index;
157
      $row->index = $row_index;
158
      try {
159
        $events[] = $this->row_plugin->render($row);
160
      }
161
      catch (Exception $e) {
162
        debug($e->getMessage(), NULL, TRUE);
163
        return $e->getMessage();
164
      }
165
    }
166
    unset($this->view->row_index);
167

    
168
    // Try to load the iCalcreator library.
169
    $library = libraries_load('iCalcreator');
170
    if (!$library['loaded']) {
171
      // The iCalcreator library isn't available, so we can't output anything.
172
      $output = t('Please install the iCalcreator library to enable iCal output.');
173
    }
174
    else {
175
      // Create a vcalendar object using the iCalcreator library.
176
      $config = array('unique_id' => 'Date iCal');
177
      $vcalendar = new vcalendar($config);
178
      $vcalendar->setMethod('PUBLISH');
179

    
180
      // Only include the X-WR-CALNAME property if the user didn't enable
181
      // the "Exclude Calendar Name" option.
182
      if (!$this->_get_option('no_calname')) {
183
        $cal_name = $this->_get_option('cal_name');
184
        if (empty($cal_name)) {
185
          $cal_name = $this->view->get_title();
186
          if (empty($cal_name)) {
187
            $cal_name = variable_get('site_name', 'Drupal');
188
          }
189
        }
190
        if (!empty($cal_name)) {
191
          $vcalendar->setProperty('X-WR-CALNAME', $cal_name, array('VALUE' => 'TEXT'));
192
        }
193
      }
194

    
195
      // Now add the VEVENTs.
196
      $timezones = array();
197
      foreach ($events as $event) {
198
        if (empty($event)) {
199
          // The row plugin returned NULL for this row, which can happen due to
200
          // either various error conditions, or because an RRULE is involved.
201
          // When this happens, just skip it.
202
          continue;
203
        }
204

    
205
        $vevent = $vcalendar->newComponent('vevent');
206
        $vevent->setUid($event['uid']);
207
        $vevent->setSummary($event['summary']);
208

    
209
        // Get the start date as an array.
210
        $start = $event['start']->toArray();
211
        $start_timezone = $event['start']->getTimezone()->getName();
212
        $timezones[$start_timezone] = $start_timezone;
213

    
214
        if ($event['all_day']) {
215
          // All Day events need to be DATEs, rather than DATE-TIMEs.
216
          $vevent->setDtstart($start['year'], $start['month'], $start['day'],
217
            FALSE, FALSE, FALSE, FALSE, array('VALUE' => 'DATE'));
218
        }
219
        else {
220
          $vevent->setDtstart(
221
            $start['year'],
222
            $start['month'],
223
            $start['day'],
224
            $start['hour'],
225
            $start['minute'],
226
            $start['second'],
227
            $start_timezone
228
          );
229
        }
230

    
231
        // Add the Timezone info to the start date, for use later.
232
        $start['tz'] = $event['start']->getTimezone();
233

    
234
        // Only add the end date if there is one.
235
        if (!empty($event['end'])) {
236
          $end = $event['end']->toArray();
237
          $end_timezone = $event['end']->getTimezone()->getName();
238
          $timezones[$end_timezone] = $end_timezone;
239

    
240
          if ($event['all_day']) {
241
            $vevent->setDtend($end['year'], $end['month'], $end['day'],
242
              FALSE, FALSE, FALSE, FALSE, array('VALUE' => 'DATE'));
243
          }
244
          else {
245
            $vevent->setDtend(
246
              $end['year'],
247
              $end['month'],
248
              $end['day'],
249
              $end['hour'],
250
              $end['minute'],
251
              $end['second'],
252
              $end_timezone
253
            );
254
          }
255
          $end['tz'] = $event['end']->getTimezone();
256
        }
257

    
258
        // Handle repeating dates from the date_repeat module.
259
        if (!empty($event['rrule']) && module_exists('date_repeat')) {
260
          // Split the rrule into an RRULE and any additions and exceptions.
261
          module_load_include('inc', 'date_api', 'date_api_ical');
262
          module_load_include('inc', 'date_repeat', 'date_repeat_calc');
263
          list($rrule, $exceptions, $additions) = date_repeat_split_rrule($event['rrule']);
264

    
265
          // Add the RRULE itself. We need to massage the data a bit, since
266
          // iCalcreator expects RRULEs to be in a different format than how
267
          // Date API gives them to us.
268
          $vevent->setRrule(_date_ical_convert_rrule_for_icalcreator($rrule));
269

    
270
          // Convert any exceptions to EXDATE properties.
271
          if (!empty($exceptions)) {
272
            $exdates = array();
273
            foreach ($exceptions as $exception) {
274
              $except = date_ical_date($exception, 'UTC');
275
              $except->setTimezone($start['tz']);
276
              $exception_array = $except->toArray();
277
              $exdates[] = array(
278
                'year' =>  $exception_array['year'],
279
                'month' => $exception_array['month'],
280
                'day' =>   $exception_array['day'],
281
                // Use the time information from the start date, since Date
282
                // doesn't store time info for EXDATEs.
283
                'hour' =>   $start['hour'],
284
                'min' =>    $start['minute'],
285
                'second' => $start['second'],
286
                'tz' =>     $start['tz']->getName(),
287
              );
288
            }
289
            // Add each exclusion as a separate EXDATE property.
290
            // The spec supports putting multiple date values into one EXDATE,
291
            // but several popular calendar clients (*cough* Apple *cough*)
292
            // are bugged, and do not recognize multi-value EXDATEs.
293
            $value = $event['all_day'] == 1 ? "DATE" : "DATE-TIME";
294
            foreach ($exdates as $exdate) {
295
              $vevent->setExdate(array($exdate), array( "VALUE" => $value ));
296
            }
297
          }
298

    
299
          // Convert any additions to RDATE properties.
300
          if (!empty($additions)) {
301
            $rdates = array();
302
            foreach ($additions as $addition) {
303
              $add = date_ical_date($addition, 'UTC');
304
              $add->setTimezone($start['tz']);
305
              $addition_array = $add->toArray();
306

    
307
              $rdate = array(
308
                'year' =>  $addition_array['year'],
309
                'month' => $addition_array['month'],
310
                'day' =>   $addition_array['day'],
311
                // If the user's copy of Date has support for time in RDATEs,
312
                // use that. Otherwise use the time from the start date.
313
                'hour' =>   !empty($addition_array['hour']) ? $addition_array['hour'] : $start['hour'],
314
                'min' =>    !empty($addition_array['minute']) ? $addition_array['minute'] : $start['minute'],
315
                'second' => !empty($addition_array['second']) ? $addition_array['second'] : $start['second'],
316
                'tz' =>     $start['tz']->getName(),
317
              );
318

    
319
              // If an end date was was calculated above, use that too.
320
              // iCalcreator expects RDATEs that have end dates to be
321
              // specified as array($start_rdate, $end_rdate).
322
              if (isset($end)) {
323
                $rdate_with_end = array($rdate);
324
                $rdate_with_end[] = array(
325
                  'year' =>  $addition_array['year'],
326
                  'month' => $addition_array['month'],
327
                  'day' =>   $addition_array['day'],
328
                  // If the user's copy of Date has support for time in RDATEs,
329
                  // use that. Otherwise use the time from the end date.
330
                  'hour' =>   !empty($addition_array['hour']) ? $addition_array['hour'] : $end['hour'],
331
                  'min' =>    !empty($addition_array['minute']) ? $addition_array['minute'] : $end['minute'],
332
                  'second' => !empty($addition_array['second']) ? $addition_array['second'] : $end['second'],
333
                  'tz' =>     $end['tz']->getName(),
334
                );
335
                $rdate = $rdate_with_end;
336
              }
337

    
338
              $rdates[] = $rdate;
339
            }
340
            // Add each addition as a separate RDATE property.
341
            // The spec supports putting multiple date values into one RDATE,
342
            // but several popular calendar clients (*cough* Apple *cough*)
343
            // are bugged, and do not recognize multi-value RDATEs.
344
            foreach ($rdates as $rdate) {
345
              $vevent->setRdate(array($rdate));
346
            }
347
          }
348
        }
349
        if (!empty($event['url'])) {
350
          $vevent->setUrl($event['url'], array('type' => 'URI'));
351
        }
352
        if (!empty($event['location'])) {
353
          $vevent->setLocation($event['location']);
354
        }
355
        if (!empty($event['description'])) {
356
          $vevent->setDescription($event['description']);
357
        }
358
        if (!empty($event['categories'])) {
359
          $vevent->setCategories($event['categories']);
360
        }
361
        if (!empty($event['last-modified'])) {
362
          $lm = $event['last-modified']->toArray();
363
          $vevent->setLastModified(
364
            $lm['year'],
365
            $lm['month'],
366
            $lm['day'],
367
            $lm['hour'],
368
            $lm['minute'],
369
            $lm['second'],
370
            $lm['timezone']
371
          );
372
        }
373
        if (!empty($event['created'])) {
374
          $created = $event['created']->toArray();
375
          $vevent->setCreated(
376
            $created['year'],
377
            $created['month'],
378
            $created['day'],
379
            $created['hour'],
380
            $created['minute'],
381
            $created['second'],
382
            $created['timezone']
383
          );
384
        }
385

    
386
        // Allow other modules to alter the vevent before it's exported.
387
        drupal_alter('date_ical_export_vevent', $vevent, $this->view, $event);
388
      }
389

    
390
      // Now add to the calendar all the timezones used by the events.
391
      foreach ($timezones as $timezone) {
392
        if (strtoupper($timezone) != 'UTC') {
393
          iCalUtilityFunctions::createTimezone($vcalendar, $timezone);
394
        }
395
      }
396

    
397
      // Allow other modules to alter the vcalendar before it's exported.
398
      drupal_alter('date_ical_export_vcalendar', $vcalendar, $this->view);
399

    
400
      $output = $vcalendar->createCalendar();
401
      // iCalcreator escapes all commas and semicolons in string values, as the
402
      // spec demands. However, some calendar clients are buggy and fail to
403
      // unescape these characters. Users may choose to unescape them here to
404
      // sidestep those clients' bugs.
405
      // NOTE: This results in a non-compliant iCal feed, but it seems like a
406
      // LOT of major clients are bugged this way.
407
      if ($this->_get_option('unescape_punctuation')) {
408
        $output = str_replace('\,', ',', $output);
409
        $output = str_replace('\;', ';', $output);
410
      }
411

    
412
      // In order to respect the Exclude DTSTAMP option, we unfortunately have
413
      // to parse out the DTSTAMP properties after they get rendered. Simply
414
      // using deleteProperty('DTSTAMP') doesn't work, because iCalcreator
415
      // considers the DTSTAMP to be essential, and will re-create it when
416
      // createCalendar() is called.
417
      if ($this->_get_option('exclude_dtstamp')) {
418
        $filtered_lines = array();
419
        foreach (explode("\r\n", $output) as $line) {
420
          if (strpos($line, 'DTSTAMP') === 0) {
421
            continue;
422
          }
423
          $filtered_lines[] = $line;
424
        }
425
        $output = implode("\r\n", $filtered_lines);
426
      }
427
    }
428

    
429
    // These steps shouldn't be run during Preview on the View page.
430
    if (empty($this->view->live_preview)) {
431
      // Prevent devel module from appending queries to ical export.
432
      $GLOBALS['devel_shutdown'] = FALSE;
433

    
434
      drupal_add_http_header('Content-Type', 'text/calendar; charset=UTF-8');
435
      drupal_add_http_header('Cache-Control', 'no-cache, must-revalidate');
436
      drupal_add_http_header('Expires', 'Sat, 26 Jul 1997 05:00:00 GMT');
437

    
438
      // For sites with Clean URLs disabled, the Display's "path" value ends
439
      // up only in the query args, meaning the filename won't be set properly
440
      // when users download the feed. So we need to manually instruct browsers
441
      // to download a .ics file.
442
      if (!variable_get('clean_url', FALSE)) {
443
        $path_array = explode('/', $this->display->display_options['path']);
444
        $filename = end($path_array);
445
        drupal_add_http_header('Content-Disposition', "attachment; filename=\"$filename\"");
446
      }
447
    }
448

    
449
    // Allow other modules to alter the rendered calendar.
450
    drupal_alter('date_ical_export_post_render', $output, $this->view);
451

    
452
    return $output;
453
  }
454
}