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
|
$url = url($this->view->get_url(NULL, $path), $url_options);
|
31
|
// If the user didn't disable the option, change the scheme to webcal://
|
32
|
// so calendar clients can automatically subscribe via the iCal link.
|
33
|
if (!$this->_get_option('disable_webcal')) {
|
34
|
$url = str_replace(array('http://', 'https://'), 'webcal://', $url);
|
35
|
}
|
36
|
|
37
|
// Render the feed icon and header tag (except during a View Preview).
|
38
|
if (empty($this->view->live_preview)) {
|
39
|
$tooltip = t('Add to My Calendar');
|
40
|
if (!isset($this->view->feed_icon)) {
|
41
|
// In PHP 5.5, you're not allowed to concatinate onto an unset
|
42
|
// property. But we need to do a concat, because there may be
|
43
|
// other attachments.
|
44
|
$this->view->feed_icon = '';
|
45
|
}
|
46
|
$variables = array(
|
47
|
'url' => check_url($url),
|
48
|
'tooltip' => $tooltip,
|
49
|
'view' => $this->view,
|
50
|
);
|
51
|
$this->view->feed_icon .= theme('date_ical_icon', $variables);
|
52
|
drupal_add_html_head_link(array(
|
53
|
'rel' => 'alternate',
|
54
|
'type' => 'text/calendar',
|
55
|
'title' => $tooltip,
|
56
|
'href' => $url,
|
57
|
));
|
58
|
}
|
59
|
}
|
60
|
|
61
|
/**
|
62
|
* Set up the options for the style plugin.
|
63
|
*/
|
64
|
public function option_definition() {
|
65
|
$false_bool = array('default' => FALSE, 'bool' => TRUE);
|
66
|
|
67
|
$options = parent::option_definition();
|
68
|
$options['cal_name'] = array('default' => array());
|
69
|
$options['no_calname'] = $false_bool;
|
70
|
$options['disable_webcal'] = $false_bool;
|
71
|
$options['exclude_dtstamp'] = $false_bool;
|
72
|
$options['unescape_punctuation'] = $false_bool;
|
73
|
return $options;
|
74
|
}
|
75
|
|
76
|
/**
|
77
|
* Build the form for setting the style plugin's options.
|
78
|
*/
|
79
|
public function options_form(&$form, &$form_state) {
|
80
|
parent::options_form($form, $form_state);
|
81
|
// Allow users to override the default Calendar name (X-WR-CALNAME).
|
82
|
$form['cal_name'] = array(
|
83
|
'#type' => 'textfield',
|
84
|
'#title' => t('iCal Calendar Name'),
|
85
|
'#default_value' => $this->_get_option('cal_name'),
|
86
|
'#description' => t('This will appear as the title of the iCal feed. If left blank, the View Title will be used.
|
87
|
If that is also blank, the site name will be inserted as the iCal feed title.'),
|
88
|
);
|
89
|
$form['no_calname'] = array(
|
90
|
'#type' => 'checkbox',
|
91
|
'#title' => t('Exclude Calendar Name'),
|
92
|
'#default_value' => $this->_get_option('no_calname'),
|
93
|
'#description' => t("Excluding the X-WR-CALNAME value from the iCal Feed causes
|
94
|
some calendar clients to add the events in the feed to an existing calendar, rather
|
95
|
than creating a whole new calendar for them."),
|
96
|
);
|
97
|
$form['disable_webcal'] = array(
|
98
|
'#type' => 'checkbox',
|
99
|
'#title' => t('Disable webcal://'),
|
100
|
'#default_value' => $this->_get_option('disable_webcal'),
|
101
|
'#description' => t("By default, the feed URL will use the webcal:// scheme, which allows calendar
|
102
|
clients to easily subscribe to the feed. If you want your users to instead download this iCal
|
103
|
feed as a file, activate this option."),
|
104
|
);
|
105
|
$form['exclude_dtstamp'] = array(
|
106
|
'#type' => 'checkbox',
|
107
|
'#title' => t('Exclude DTSTAMP'),
|
108
|
'#default_value' => $this->_get_option('exclude_dtstamp'),
|
109
|
'#description' => t("By default, the feed will set each event's DTSTAMP property to the time at which the feed got downloaded.
|
110
|
Some feed readers will (incorrectly) look at the DTSTAMP value when they compare different downloads of the same feed, and
|
111
|
conclcude that the event has been updated (even though it hasn't actually changed). Enable this option to exclude the DTSTAMP
|
112
|
field from your feeds, so that these buggy feed readers won't mark every event as updated every time they check."),
|
113
|
);
|
114
|
$form['unescape_punctuation'] = array(
|
115
|
'#type' => 'checkbox',
|
116
|
'#title' => t('Unescape Commas and Semicolons'),
|
117
|
'#default_value' => $this->_get_option('unescape_punctuation'),
|
118
|
'#description' => t('In order to comply with the iCal spec, Date iCal will "escape" commas and semicolons (prepend them with backslashes).
|
119
|
However, many calendar clients are bugged to not unescape these characters, leaving the backslashes littered throughout your events.
|
120
|
Enable this option to have Date iCal unescape these characters before it exports the iCal feed.'),
|
121
|
);
|
122
|
}
|
123
|
|
124
|
/**
|
125
|
* Render the event arrays returned by the row plugin into a VCALENDAR.
|
126
|
*/
|
127
|
public function render() {
|
128
|
if (empty($this->row_plugin) || !in_array($this->row_plugin->plugin_name, array('date_ical', 'date_ical_fields'))) {
|
129
|
debug('date_ical_plugin_style_ical_feed: This style plugin supports only the "iCal Entity" and "iCal Fields" row plugins.', NULL, TRUE);
|
130
|
return t('To enable iCal output, the view Format must be configured to Show: iCal Entity or iCal Fields.');
|
131
|
}
|
132
|
if ($this->row_plugin->plugin_name == 'date_ical_fields' && empty($this->row_plugin->options['date_field'])) {
|
133
|
// Because the Date field is required by the form, this error state will
|
134
|
// rarely occur. But I ran across it during testing, and the error that
|
135
|
// resulted was totally non-sensical, so I'm adding this just in case.
|
136
|
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'.");
|
137
|
}
|
138
|
$events = array();
|
139
|
foreach ($this->view->result as $row_index => $row) {
|
140
|
$this->view->row_index = $row_index;
|
141
|
$row->index = $row_index;
|
142
|
try {
|
143
|
$events[] = $this->row_plugin->render($row);
|
144
|
}
|
145
|
catch (Exception $e) {
|
146
|
debug($e->getMessage(), NULL, TRUE);
|
147
|
return $e->getMessage();
|
148
|
}
|
149
|
}
|
150
|
unset($this->view->row_index);
|
151
|
|
152
|
// Try to load the iCalcreator library.
|
153
|
$library = libraries_load('iCalcreator');
|
154
|
if (!$library['loaded']) {
|
155
|
// The iCalcreator library isn't available, so we can't output anything.
|
156
|
$output = t('Please install the iCalcreator library to enable iCal output.');
|
157
|
}
|
158
|
else {
|
159
|
// Create a vcalendar object using the iCalcreator library.
|
160
|
$config = array('unique_id' => 'Date iCal v' . DATE_ICAL_VERSION);
|
161
|
$vcalendar = new vcalendar($config);
|
162
|
$vcalendar->setMethod('PUBLISH');
|
163
|
|
164
|
// Only include the X-WR-CALNAME property if the user didn't enable
|
165
|
// the "Exclude Calendar Name" option.
|
166
|
if (!$this->_get_option('no_calname')) {
|
167
|
$cal_name = $this->_get_option('cal_name');
|
168
|
if (empty($cal_name)) {
|
169
|
$cal_name = $this->view->get_title();
|
170
|
if (empty($cal_name)) {
|
171
|
$cal_name = variable_get('site_name', 'Drupal');
|
172
|
}
|
173
|
}
|
174
|
if (!empty($cal_name)) {
|
175
|
$vcalendar->setProperty('X-WR-CALNAME', $cal_name, array('VALUE' => 'TEXT'));
|
176
|
}
|
177
|
}
|
178
|
|
179
|
// Now add the VEVENTs.
|
180
|
$timezones = array();
|
181
|
foreach ($events as $event) {
|
182
|
if (empty($event)) {
|
183
|
// The row plugin returned NULL for this row, which can happen due to
|
184
|
// either various error conditions, or because an RRULE is involved.
|
185
|
// When this happens, just skip it.
|
186
|
continue;
|
187
|
}
|
188
|
|
189
|
$vevent = $vcalendar->newComponent('vevent');
|
190
|
$vevent->setUid($event['uid']);
|
191
|
$vevent->setSummary($event['summary']);
|
192
|
|
193
|
// Get the start date as an array.
|
194
|
$start = $event['start']->toArray();
|
195
|
$start_timezone = $event['start']->getTimezone()->getName();
|
196
|
$timezones[$start_timezone] = $start_timezone;
|
197
|
|
198
|
if ($event['all_day']) {
|
199
|
// All Day events need to be DATEs, rather than DATE-TIMEs.
|
200
|
$vevent->setDtstart($start['year'], $start['month'], $start['day'],
|
201
|
FALSE, FALSE, FALSE, FALSE, array('VALUE' => 'DATE'));
|
202
|
}
|
203
|
else {
|
204
|
$vevent->setDtstart(
|
205
|
$start['year'],
|
206
|
$start['month'],
|
207
|
$start['day'],
|
208
|
$start['hour'],
|
209
|
$start['minute'],
|
210
|
$start['second'],
|
211
|
$start_timezone
|
212
|
);
|
213
|
}
|
214
|
|
215
|
// Add the Timezone info to the start date, for use later.
|
216
|
$start['tz'] = $event['start']->getTimezone();
|
217
|
|
218
|
// Only add the end date if there is one.
|
219
|
if (!empty($event['end'])) {
|
220
|
$end = $event['end']->toArray();
|
221
|
$end_timezone = $event['end']->getTimezone()->getName();
|
222
|
$timezones[$end_timezone] = $end_timezone;
|
223
|
|
224
|
if ($event['all_day']) {
|
225
|
$vevent->setDtend($end['year'], $end['month'], $end['day'],
|
226
|
FALSE, FALSE, FALSE, FALSE, array('VALUE' => 'DATE'));
|
227
|
}
|
228
|
else {
|
229
|
$vevent->setDtend(
|
230
|
$end['year'],
|
231
|
$end['month'],
|
232
|
$end['day'],
|
233
|
$end['hour'],
|
234
|
$end['minute'],
|
235
|
$end['second'],
|
236
|
$end_timezone
|
237
|
);
|
238
|
}
|
239
|
$end['tz'] = $event['end']->getTimezone();
|
240
|
}
|
241
|
|
242
|
// Handle repeating dates from the date_repeat module.
|
243
|
if (!empty($event['rrule']) && module_exists('date_repeat')) {
|
244
|
// Split the rrule into an RRULE and any additions and exceptions.
|
245
|
module_load_include('inc', 'date_api', 'date_api_ical');
|
246
|
module_load_include('inc', 'date_repeat', 'date_repeat_calc');
|
247
|
list($rrule, $exceptions, $additions) = date_repeat_split_rrule($event['rrule']);
|
248
|
|
249
|
// Add the RRULE itself. We need to massage the data a bit, since
|
250
|
// iCalcreator expects RRULEs to be in a different format than how
|
251
|
// Date API gives them to us.
|
252
|
$vevent->setRrule(_date_ical_convert_rrule_for_icalcreator($rrule));
|
253
|
|
254
|
// Convert any exceptions to EXDATE properties.
|
255
|
if (!empty($exceptions)) {
|
256
|
$exdates = array();
|
257
|
foreach ($exceptions as $exception) {
|
258
|
$except = date_ical_date($exception, 'UTC');
|
259
|
$except->setTimezone($start['tz']);
|
260
|
$exception_array = $except->toArray();
|
261
|
$exdates[] = array(
|
262
|
'year' => $exception_array['year'],
|
263
|
'month' => $exception_array['month'],
|
264
|
'day' => $exception_array['day'],
|
265
|
// Use the time information from the start date, since Date
|
266
|
// doesn't store time info for EXDATEs.
|
267
|
'hour' => $start['hour'],
|
268
|
'min' => $start['minute'],
|
269
|
'second' => $start['second'],
|
270
|
'tz' => $start['tz']->getName(),
|
271
|
);
|
272
|
}
|
273
|
// Add each exclusion as a separate EXDATE property.
|
274
|
// The spec supports putting multiple date values into one EXDATE,
|
275
|
// but several popular calendar clients (*cough* Apple *cough*)
|
276
|
// are bugged, and do not recognize multi-value EXDATEs.
|
277
|
foreach ($exdates as $exdate) {
|
278
|
$vevent->setExdate(array($exdate));
|
279
|
}
|
280
|
}
|
281
|
|
282
|
// Convert any additions to RDATE properties.
|
283
|
if (!empty($additions)) {
|
284
|
$rdates = array();
|
285
|
foreach ($additions as $addition) {
|
286
|
$add = date_ical_date($addition, 'UTC');
|
287
|
$add->setTimezone($start['tz']);
|
288
|
$addition_array = $add->toArray();
|
289
|
|
290
|
$rdate = array(
|
291
|
'year' => $addition_array['year'],
|
292
|
'month' => $addition_array['month'],
|
293
|
'day' => $addition_array['day'],
|
294
|
// If the user's copy of Date has support for time in RDATEs,
|
295
|
// use that. Otherwise use the time from the start date.
|
296
|
'hour' => !empty($addition_array['hour']) ? $addition_array['hour'] : $start['hour'],
|
297
|
'min' => !empty($addition_array['minute']) ? $addition_array['minute'] : $start['minute'],
|
298
|
'second' => !empty($addition_array['second']) ? $addition_array['second'] : $start['second'],
|
299
|
'tz' => $start['tz']->getName(),
|
300
|
);
|
301
|
|
302
|
// If an end date was was calculated above, use that too.
|
303
|
// iCalcreator expects RDATEs that have end dates to be
|
304
|
// specified as array($start_rdate, $end_rdate).
|
305
|
if (isset($end)) {
|
306
|
$rdate_with_end = array($rdate);
|
307
|
$rdate_with_end[] = 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 end date.
|
313
|
'hour' => !empty($addition_array['hour']) ? $addition_array['hour'] : $end['hour'],
|
314
|
'min' => !empty($addition_array['minute']) ? $addition_array['minute'] : $end['minute'],
|
315
|
'second' => !empty($addition_array['second']) ? $addition_array['second'] : $end['second'],
|
316
|
'tz' => $end['tz']->getName(),
|
317
|
);
|
318
|
$rdate = $rdate_with_end;
|
319
|
}
|
320
|
|
321
|
$rdates[] = $rdate;
|
322
|
}
|
323
|
// Add each addition as a separate RDATE property.
|
324
|
// The spec supports putting multiple date values into one RDATE,
|
325
|
// but several popular calendar clients (*cough* Apple *cough*)
|
326
|
// are bugged, and do not recognize multi-value RDATEs.
|
327
|
foreach ($rdates as $rdate) {
|
328
|
$vevent->setRdate(array($rdate));
|
329
|
}
|
330
|
}
|
331
|
}
|
332
|
if (!empty($event['url'])) {
|
333
|
$vevent->setUrl($event['url'], array('type' => 'URI'));
|
334
|
}
|
335
|
if (!empty($event['location'])) {
|
336
|
$vevent->setLocation($event['location']);
|
337
|
}
|
338
|
if (!empty($event['description'])) {
|
339
|
$vevent->setDescription($event['description']);
|
340
|
}
|
341
|
if (!empty($event['last-modified'])) {
|
342
|
$lm = $event['last-modified']->toArray();
|
343
|
$vevent->setLastModified(
|
344
|
$lm['year'],
|
345
|
$lm['month'],
|
346
|
$lm['day'],
|
347
|
$lm['hour'],
|
348
|
$lm['minute'],
|
349
|
$lm['second'],
|
350
|
$lm['timezone']
|
351
|
);
|
352
|
}
|
353
|
if (!empty($event['created'])) {
|
354
|
$created = $event['created']->toArray();
|
355
|
$vevent->setCreated(
|
356
|
$created['year'],
|
357
|
$created['month'],
|
358
|
$created['day'],
|
359
|
$created['hour'],
|
360
|
$created['minute'],
|
361
|
$created['second'],
|
362
|
$created['timezone']
|
363
|
);
|
364
|
}
|
365
|
|
366
|
// Allow other modules to alter the vevent before it's exported.
|
367
|
drupal_alter('date_ical_export_vevent', $vevent, $this->view, $event);
|
368
|
}
|
369
|
|
370
|
// Now add to the calendar all the timezones used by the events.
|
371
|
foreach ($timezones as $timezone) {
|
372
|
if (strtoupper($timezone) != 'UTC') {
|
373
|
iCalUtilityFunctions::createTimezone($vcalendar, $timezone);
|
374
|
}
|
375
|
}
|
376
|
|
377
|
// Allow other modules to alter the vcalendar before it's exported.
|
378
|
drupal_alter('date_ical_export_vcalendar', $vcalendar, $this->view);
|
379
|
|
380
|
$output = $vcalendar->createCalendar();
|
381
|
// iCalcreator escapes all commas and semicolons in string values, as the
|
382
|
// spec demands. However, some calendar clients are buggy and fail to
|
383
|
// unescape these characters. Users may choose to unescape them here to
|
384
|
// sidestep those clients' bugs.
|
385
|
// NOTE: This results in a non-compliant iCal feed, but it seems like a
|
386
|
// LOT of major clients are bugged this way.
|
387
|
if ($this->_get_option('unescape_punctuation')) {
|
388
|
$output = str_replace('\,', ',', $output);
|
389
|
$output = str_replace('\;', ';', $output);
|
390
|
}
|
391
|
|
392
|
// In order to respect the Exclude DTSTAMP option, we unfortunately have
|
393
|
// to parse out the DTSTAMP properties after they get rendered. Simply
|
394
|
// using deleteProperty('DTSTAMP') doesn't work, because iCalcreator
|
395
|
// considers the DTSTAMP to be essential, and will re-create it when
|
396
|
// createCalendar() is called.
|
397
|
if ($this->_get_option('exclude_dtstamp')) {
|
398
|
$filtered_lines = array();
|
399
|
foreach (explode("\r\n", $output) as $line) {
|
400
|
if (strpos($line, 'DTSTAMP') === 0) {
|
401
|
continue;
|
402
|
}
|
403
|
$filtered_lines[] = $line;
|
404
|
}
|
405
|
$output = implode("\r\n", $filtered_lines);
|
406
|
}
|
407
|
}
|
408
|
|
409
|
// These steps shouldn't be run during Preview on the View page.
|
410
|
if (empty($this->view->live_preview)) {
|
411
|
// Prevent devel module from appending queries to ical export.
|
412
|
$GLOBALS['devel_shutdown'] = FALSE;
|
413
|
|
414
|
drupal_add_http_header('Content-Type', 'text/calendar; charset=UTF-8');
|
415
|
drupal_add_http_header('Cache-Control', 'no-cache, must-revalidate');
|
416
|
drupal_add_http_header('Expires', 'Sat, 26 Jul 1997 05:00:00 GMT');
|
417
|
|
418
|
// For sites with Clean URLs disabled, the Display's "path" value ends
|
419
|
// up only in the query args, meaning the filename won't be set properly
|
420
|
// when users download the feed. So we need to manually instruct browsers
|
421
|
// to download a .ics file.
|
422
|
if (!variable_get('clean_url', FALSE)) {
|
423
|
$path_array = explode('/', $this->display->display_options['path']);
|
424
|
$filename = end($path_array);
|
425
|
drupal_add_http_header('Content-Disposition', "attachment; filename=\"$filename\"");
|
426
|
}
|
427
|
}
|
428
|
|
429
|
// Allow other modules to alter the rendered calendar.
|
430
|
drupal_alter('date_ical_export_post_render', $output, $this->view);
|
431
|
|
432
|
return $output;
|
433
|
}
|
434
|
}
|