1
|
<?php
|
2
|
/**
|
3
|
* @file
|
4
|
* Defines the iCal Fields row style plugin, which lets users map view fields
|
5
|
* to the components of the VEVENTs in the iCal feed.
|
6
|
*/
|
7
|
|
8
|
/**
|
9
|
* A Views plugin which builds an iCal VEVENT from a views row with Fields.
|
10
|
*/
|
11
|
class date_ical_plugin_row_ical_fields extends views_plugin_row {
|
12
|
|
13
|
/**
|
14
|
* Set up the options for the row plugin.
|
15
|
*/
|
16
|
public function option_definition() {
|
17
|
$options = parent::option_definition();
|
18
|
$options['date_field'] = array('default' => '');
|
19
|
$options['title_field'] = array('default' => '');
|
20
|
$options['description_field'] = array('default' => '');
|
21
|
$options['location_field'] = array('default' => '');
|
22
|
$options['categories_field'] = array('default' => '');
|
23
|
$options['additional_settings']['skip_blank_dates'] = array('default' => FALSE);
|
24
|
return $options;
|
25
|
}
|
26
|
|
27
|
/**
|
28
|
* Build the form for setting the row plugin's options.
|
29
|
*/
|
30
|
public function options_form(&$form, &$form_state) {
|
31
|
parent::options_form($form, $form_state);
|
32
|
$all_field_labels = $this->display->handler->get_field_labels();
|
33
|
$date_field_labels = $this->get_date_field_candidates($all_field_labels);
|
34
|
$date_field_label_options = array_merge(array('first_available' => t('First populated Date field')), $date_field_labels);
|
35
|
$text_field_label_options = array_merge(array('' => t('- None -')), $all_field_labels);
|
36
|
|
37
|
$form['instructions'] = array(
|
38
|
// The surrounding <div> ensures that the settings dialog expands.
|
39
|
'#prefix' => '<div style="font-size: 90%">',
|
40
|
'#suffix' => '</div>',
|
41
|
'#markup' => t("Once you've finished setting up the fields for this View, you may want to return to this dialog to set the Date field."),
|
42
|
);
|
43
|
$form['date_field'] = array(
|
44
|
'#type' => 'select',
|
45
|
'#title' => t('Date field'),
|
46
|
'#description' => t('The views field to use as the start (and possibly end) time for each event (DTSTART/DTEND).
|
47
|
If you retain the default ("First populated Date field"), Date iCal will use the first non-empty Date field in the row.'),
|
48
|
'#options' => $date_field_label_options,
|
49
|
'#default_value' => $this->options['date_field'],
|
50
|
'#required' => TRUE,
|
51
|
);
|
52
|
$form['title_field'] = array(
|
53
|
'#type' => 'select',
|
54
|
'#title' => t('Title field'),
|
55
|
'#description' => t('The views field to use as the title for each event (SUMMARY).'),
|
56
|
'#options' => $text_field_label_options,
|
57
|
'#default_value' => $this->options['title_field'],
|
58
|
'#required' => FALSE,
|
59
|
);
|
60
|
$form['description_field'] = array(
|
61
|
'#type' => 'select',
|
62
|
'#title' => t('Description field'),
|
63
|
'#description' => t("The views field to use as the body text for each event (DESCRIPTION).<br>
|
64
|
If you wish to include more than one entity field in the event body, you may want to use the 'Content: Rendered Node' views field,
|
65
|
and set it to the 'iCal' view mode. Then configure the iCal view mode on your event nodes to include the text you want."),
|
66
|
'#options' => $text_field_label_options,
|
67
|
'#default_value' => $this->options['description_field'],
|
68
|
'#required' => FALSE,
|
69
|
);
|
70
|
$form['location_field'] = array(
|
71
|
'#type' => 'select',
|
72
|
'#title' => t('Location field'),
|
73
|
'#description' => t('(optional) The views field to use as the location for each event (LOCATION).'),
|
74
|
'#options' => $text_field_label_options,
|
75
|
'#default_value' => $this->options['location_field'],
|
76
|
'#required' => FALSE,
|
77
|
);
|
78
|
$form['categories_field'] = array(
|
79
|
'#type' => 'select',
|
80
|
'#title' => t('Categories field'),
|
81
|
'#description' => t('(optional) The views field to use as the categories for each event (CATEGORIES).'),
|
82
|
'#options' => $text_field_label_options,
|
83
|
'#default_value' => $this->options['categories_field'],
|
84
|
'#required' => FALSE,
|
85
|
);
|
86
|
$form['additional_settings'] = array(
|
87
|
'#type' => 'fieldset',
|
88
|
'#title' => t('Additional settings'),
|
89
|
'#collapsible' => FALSE,
|
90
|
'#collapsed' => FALSE,
|
91
|
);
|
92
|
$form['additional_settings']['skip_blank_dates'] = array(
|
93
|
'#type' => 'checkbox',
|
94
|
'#title' => t('Skip blank dates'),
|
95
|
'#description' => t('Normally, if a view result has a blank date field, the feed will display an error,
|
96
|
because it is impossible to create an iCal event with no date. This option makes Views silently skip those results, instead.'),
|
97
|
'#default_value' => $this->options['additional_settings']['skip_blank_dates'],
|
98
|
);
|
99
|
}
|
100
|
|
101
|
/**
|
102
|
* Set up the environment for the render() function.
|
103
|
*/
|
104
|
public function pre_render($result) {
|
105
|
// Get the language for this view.
|
106
|
$this->language = $this->display->handler->get_option('field_language');
|
107
|
$substitutions = views_views_query_substitutions($this->view);
|
108
|
if (array_key_exists($this->language, $substitutions)) {
|
109
|
$this->language = $substitutions[$this->language];
|
110
|
}
|
111
|
$this->repeated_dates = array();
|
112
|
}
|
113
|
|
114
|
/**
|
115
|
* Returns an Event array row in the query with index: $row->index.
|
116
|
*/
|
117
|
public function render($row) {
|
118
|
$date_field_name = $this->options['date_field'];
|
119
|
|
120
|
// If this view is set to use the first populated date field, check each
|
121
|
// field in the row to find the first non-NULL Date field.
|
122
|
if ($date_field_name == 'first_available') {
|
123
|
foreach (get_object_vars($row) as $name => $value) {
|
124
|
if (strpos($name, 'field_field') === 0) {
|
125
|
// This property's name starts with "field_field", which means it's
|
126
|
// the actual field data for a field in this view.
|
127
|
if (!empty($value[0]['raw']['date_type'])) {
|
128
|
// Cut off the first "field_" from $name to get the field name.
|
129
|
$date_field_name = substr($name, 6);
|
130
|
break;
|
131
|
}
|
132
|
}
|
133
|
}
|
134
|
}
|
135
|
|
136
|
// Fetch the event's date information.
|
137
|
try {
|
138
|
if ($date_field_name == 'first_available') {
|
139
|
// If $date_field_name is still 'first_available' at this point, we
|
140
|
// couldn't find an available Date value. Processing cannot proceed.
|
141
|
$title = strip_tags($this->view->style_plugin->get_field($row->index, $this->options['title_field']));
|
142
|
if (empty($title)) {
|
143
|
$title = "Undetermined title";
|
144
|
}
|
145
|
throw new BlankDateFieldException(
|
146
|
t('The row titled "@title" has no available Date value. An iCal entry cannot be created for it.',
|
147
|
array('@title' => $title)
|
148
|
)
|
149
|
);
|
150
|
}
|
151
|
$date = $this->get_row_date($row, $date_field_name);
|
152
|
}
|
153
|
catch (BlankDateFieldException $e) {
|
154
|
// Unless the user has specifically said that they want to skip rows
|
155
|
// with blank dates, let this exception percolate.
|
156
|
if ($this->options['additional_settings']['skip_blank_dates']) {
|
157
|
return NULL;
|
158
|
}
|
159
|
else {
|
160
|
throw $e;
|
161
|
}
|
162
|
}
|
163
|
|
164
|
// Create the event by starting with the date array from this row.
|
165
|
$event = $date;
|
166
|
|
167
|
$entity = $row->_field_data[$this->view->base_field]['entity'];
|
168
|
$entity_type = $row->_field_data[$this->view->base_field]['entity_type'];
|
169
|
// Add the CREATED, LAST-MODIFIED, and URL components based on the entity.
|
170
|
// According to the iCal standard, CREATED and LAST-MODIFIED must be UTC.
|
171
|
// Fortunately, Drupal stores timestamps in the DB as UTC, so we just need
|
172
|
// to specify that UTC be used rather than the server's local timezone.
|
173
|
if (isset($entity->created)) {
|
174
|
$event['created'] = new DateObject($entity->created, 'UTC');
|
175
|
}
|
176
|
if (isset($entity->changed)) {
|
177
|
$event['last-modified'] = new DateObject($entity->changed, 'UTC');
|
178
|
}
|
179
|
elseif (isset($entity->created)) {
|
180
|
// If changed is unset, but created is, use that for last-modified.
|
181
|
$event['last-modified'] = new DateObject($entity->created, 'UTC');
|
182
|
}
|
183
|
$uri = entity_uri($entity_type, $entity);
|
184
|
$uri['options']['absolute'] = TRUE;
|
185
|
$event['url'] = url($uri['path'], $uri['options']);
|
186
|
|
187
|
// Generate a unique ID for this event by emulating the way the Date module
|
188
|
// creates a Date ID.
|
189
|
$is_relationaship = FALSE;
|
190
|
$date_field_delta = 0;
|
191
|
if (isset($row->{"field_data_{$date_field_name}_delta"})) {
|
192
|
$date_field_delta = $row->{"field_data_{$date_field_name}_delta"};
|
193
|
}
|
194
|
else if (!empty($entity->{$date_field_name})) {
|
195
|
// I'm not sure why the "field_data_{field_name}_delta" field is part of
|
196
|
// the $row, so it's possible that it will sometimes be missing. If it
|
197
|
// is, make an educated guess about the delta by comparing this row's
|
198
|
// start date to each of the entity's dates.
|
199
|
foreach ($entity->{$date_field_name}['und'] as $ndx => $date_array) {
|
200
|
if ($date['start']->originalTime == $date_array['value']) {
|
201
|
$date_field_delta = $ndx;
|
202
|
break;
|
203
|
}
|
204
|
}
|
205
|
}
|
206
|
else if (!empty($this->display->display_options['fields'][$date_field_name]['relationship'])) {
|
207
|
// This block covers another potential situation where
|
208
|
// "field_data_{field_name}_delta" is missing: dates gathered through a
|
209
|
// relationship. We retrieve the name of the relationship through which
|
210
|
// the date field is being gathered, and the delta of that relationship,
|
211
|
// in case it's multi-delta.
|
212
|
$rel_name = $this->display->display_options['fields'][$date_field_name]['relationship'];
|
213
|
$rel_delta = $row->index;
|
214
|
$is_relationaship = TRUE;
|
215
|
}
|
216
|
else {
|
217
|
// We couldn't obtain enough info to determine the real $date_field_delta.
|
218
|
// So we just leave it as 0. Hopefully this won't cause problems.
|
219
|
}
|
220
|
$entity_id = $row->{$this->view->base_field};
|
221
|
global $base_url;
|
222
|
$domain = preg_replace('#^https?://#', '', $base_url);
|
223
|
if (!$is_relationaship) {
|
224
|
$event['uid'] = "calendar.$entity_id.$date_field_name.$date_field_delta@$domain";
|
225
|
}
|
226
|
else {
|
227
|
$event['uid'] = "calendar.$entity_id.$rel_name.$rel_delta.$date_field_name.$date_field_delta@$domain";
|
228
|
}
|
229
|
|
230
|
// Because of the way that Date implements repeating dates, we're going to
|
231
|
// be given a separate view result for each repeat. We only want to
|
232
|
// render a VEVENT (with an RRULE) for the first instance of that date, so
|
233
|
// we need to record the entity ID and field name for each result that has
|
234
|
// an RRULE, then skip any that we've already seen.
|
235
|
if (!empty($date['rrule'])) {
|
236
|
$repeat_id = "$entity_id.$date_field_name";
|
237
|
if (!isset($this->repeated_dates[$repeat_id])) {
|
238
|
$this->repeated_dates[$repeat_id] = $repeat_id;
|
239
|
}
|
240
|
else {
|
241
|
return FALSE;
|
242
|
}
|
243
|
}
|
244
|
|
245
|
// Retrieve the rendered text fields.
|
246
|
$text_fields['summary'] = $this->get_field($row->index, $this->options['title_field']);
|
247
|
$text_fields['description'] = $this->get_field($row->index, $this->options['description_field']);
|
248
|
$text_fields['location'] = $this->get_field($row->index, $this->options['location_field']);
|
249
|
$text_fields['categories'] = $this->get_field($row->index, $this->options['categories_field']);
|
250
|
|
251
|
// Allow other modules to alter the rendered text fields before they get
|
252
|
// sanitized for iCal-compliance. This is most useful for fields of type
|
253
|
// "Content: Rendered Node", which are likely to have complex HTML.
|
254
|
$context = array(
|
255
|
'row' => $row,
|
256
|
'row_index' => $row->index,
|
257
|
'language' => $this->language,
|
258
|
'options' => $this->options,
|
259
|
);
|
260
|
drupal_alter('date_ical_export_html', $text_fields, $this->view, $context);
|
261
|
|
262
|
// Sanitize the text fields for iCal compliance, and add them to the event.
|
263
|
$event['summary'] = date_ical_sanitize_text($text_fields['summary']);
|
264
|
$event['location'] = date_ical_sanitize_text($text_fields['location']);
|
265
|
$event['description'] = date_ical_sanitize_text($text_fields['description']);
|
266
|
$event['categories'] = date_ical_sanitize_text($text_fields['categories']);
|
267
|
|
268
|
// Allow other modules to alter the event object before it gets passed to
|
269
|
// the style plugin to be converted into an iCal VEVENT.
|
270
|
drupal_alter('date_ical_export_raw_event', $event, $this->view, $context);
|
271
|
|
272
|
return $event;
|
273
|
}
|
274
|
|
275
|
/**
|
276
|
* Returns an normalized array for the current row's datefield/timestamp.
|
277
|
*
|
278
|
* @param object $row
|
279
|
* The current row object.
|
280
|
* @param string $date_field_name
|
281
|
* The name of the date field.
|
282
|
*
|
283
|
* @return array
|
284
|
* The normalized array.
|
285
|
*/
|
286
|
protected function get_row_date($row, $date_field_name) {
|
287
|
$start = NULL;
|
288
|
$end = NULL;
|
289
|
$rrule = NULL;
|
290
|
$delta = 0;
|
291
|
$is_date_field = FALSE;
|
292
|
|
293
|
// Fetch the date field value.
|
294
|
$date_field_value = $this->view->style_plugin->get_field_value($row->index, $date_field_name);
|
295
|
|
296
|
// Handle date fields.
|
297
|
if (isset($date_field_value[$delta]) && is_array($date_field_value[$delta])) {
|
298
|
$is_date_field = TRUE;
|
299
|
$date_field = $date_field_value[$delta];
|
300
|
|
301
|
$start = new DateObject($date_field['value'], $date_field['timezone_db']);
|
302
|
if (!empty($date_field['value2'])) {
|
303
|
$end = new DateObject($date_field['value2'], $date_field['timezone_db']);
|
304
|
}
|
305
|
else {
|
306
|
$end = clone $start;
|
307
|
}
|
308
|
|
309
|
if (isset($date_field['rrule'])) {
|
310
|
$rrule = $date_field['rrule'];
|
311
|
}
|
312
|
}
|
313
|
elseif (is_numeric($date_field_value)) {
|
314
|
// Handle timestamps, which are always in UTC.
|
315
|
$start = new DateObject($date_field_value, 'UTC');
|
316
|
$end = new DateObject($date_field_value, 'UTC');
|
317
|
}
|
318
|
else {
|
319
|
// Processing cannot proceed with a blank date value.
|
320
|
$title = strip_tags($this->view->style_plugin->get_field($row->index, $this->options['title_field']));
|
321
|
throw new BlankDateFieldException(t("The row %title has a blank date. An iCal entry cannot be created for it.", array('%title' => $title)));
|
322
|
}
|
323
|
|
324
|
// Set the display timezone to whichever tz is stored for this field.
|
325
|
// If there isn't a stored TZ, use the site default.
|
326
|
$timezone = isset($date_field['timezone']) ? $date_field['timezone'] : date_default_timezone(FALSE);
|
327
|
$dtz = new DateTimeZone($timezone);
|
328
|
$start->setTimezone($dtz);
|
329
|
$end->setTimezone($dtz);
|
330
|
|
331
|
$granularity = 'second';
|
332
|
if ($is_date_field) {
|
333
|
$granularity_settings = $this->view->field[$date_field_name]->field_info['settings']['granularity'];
|
334
|
$granularity = date_granularity_precision($granularity_settings);
|
335
|
}
|
336
|
|
337
|
// Check if the start and end dates indicate that this is an All Day event.
|
338
|
$all_day = date_is_all_day(
|
339
|
date_format($start, DATE_FORMAT_DATETIME),
|
340
|
date_format($end, DATE_FORMAT_DATETIME),
|
341
|
$granularity
|
342
|
);
|
343
|
|
344
|
if ($all_day) {
|
345
|
// According to RFC 2445 (clarified in RFC 5545) the DTEND value is
|
346
|
// non-inclusive. When dealing with All Day values, they are DATEs rather
|
347
|
// than DATETIMEs, so we need to add a day to conform to RFC.
|
348
|
$end->modify("+1 day");
|
349
|
}
|
350
|
|
351
|
$date = array(
|
352
|
'start' => $start,
|
353
|
'end' => $end,
|
354
|
'all_day' => $all_day,
|
355
|
'rrule' => $rrule,
|
356
|
);
|
357
|
|
358
|
return $date;
|
359
|
}
|
360
|
|
361
|
/**
|
362
|
* Filter the list of views fields down to only supported date-type fields.
|
363
|
*
|
364
|
* The supported date-type fields are timestamps and the three Date fields.
|
365
|
*
|
366
|
* @param array $view_fields
|
367
|
* An associative array like views_plugin_display::get_field_labels().
|
368
|
*
|
369
|
* @return array
|
370
|
* An associative array (alias => label) of date fields.
|
371
|
*/
|
372
|
protected function get_date_field_candidates($view_fields) {
|
373
|
$handlers = $this->display->handler->get_handlers('field');
|
374
|
$field_candidates = array();
|
375
|
// These are Date, Date (ISO format), and Date (Unix timestamp).
|
376
|
$date_fields = array('datetime', 'date', 'datestamp');
|
377
|
|
378
|
foreach ($view_fields as $alias => $label) {
|
379
|
$handler_class = get_class($handlers[$alias]);
|
380
|
if ($handler_class == 'views_handler_field_date'
|
381
|
|| ($handler_class == 'views_handler_field_field'
|
382
|
&& in_array($handlers[$alias]->field_info['type'], $date_fields))) {
|
383
|
$field_candidates[$alias] = $label;
|
384
|
}
|
385
|
}
|
386
|
return $field_candidates;
|
387
|
}
|
388
|
|
389
|
/**
|
390
|
* Retrieves a field value from the style plugin.
|
391
|
*
|
392
|
* @param int $index
|
393
|
* The index count of the row
|
394
|
* @param string $field_id
|
395
|
* The ID assigned to the required field in the display.
|
396
|
*
|
397
|
* @see views_plugin_style::get_field()
|
398
|
*/
|
399
|
protected function get_field($index, $field_id) {
|
400
|
if (empty($this->view->style_plugin) || !is_object($this->view->style_plugin) || empty($field_id)) {
|
401
|
return '';
|
402
|
}
|
403
|
return $this->view->style_plugin->get_field($index, $field_id);
|
404
|
}
|
405
|
}
|