date_ical.module 14.9 KB
Newer Older
Karen Stevenson's avatar
Karen Stevenson committed
1
<?php
2

Karen Stevenson's avatar
Karen Stevenson committed
3
4
/**
 * @file
Robert Rollins's avatar
Robert Rollins committed
5
6
7
 * Adds ical functionality to Views, and an iCal parser to Feeds.
 *
 * TODO Figure out how to incorporate VVENUE information into the parser.
Karen Stevenson's avatar
Karen Stevenson committed
8
9
10
 */

/**
11
 * Implements hook_views_api().
Karen Stevenson's avatar
Karen Stevenson committed
12
13
14
15
 */
function date_ical_views_api() {
  return array(
    'api' => 3,
Robert Rollins's avatar
Robert Rollins committed
16
    'path' => drupal_get_path('module', 'date_ical') . '/includes',
Karen Stevenson's avatar
Karen Stevenson committed
17
18
19
20
21
22
  );
}

/**
 * Implements hook_theme().
 */
23
function date_ical_theme($existing, $type, $theme, $path) {
Karen Stevenson's avatar
Karen Stevenson committed
24
25
  return array(
    'date_ical_icon' => array(
Robert Rollins's avatar
Robert Rollins committed
26
      'variables' => array('url' => NULL, 'tooltip' => NULL),
27
28
    ),
  );
Karen Stevenson's avatar
Karen Stevenson committed
29
30
31
32
33
}

/**
 * The theme for the ical icon.
 */
34
function theme_date_ical_icon($variables) {
Robert Rollins's avatar
Robert Rollins committed
35
36
  $variables['path'] = drupal_get_path('module', 'date_ical') . '/images/ical-feed-icon-34x14.png';
  $variables['alt'] = $variables['title'] = $variables['tooltip'];
Karen Stevenson's avatar
Karen Stevenson committed
37
  if ($image = theme('image', $variables)) {
Robert Rollins's avatar
Robert Rollins committed
38
39
40
41
    return "<a href='{$variables['url']}' class='ical-icon'>$image</a>";
  }
  else {
    return "<a href='{$variables['url']}' class='ical-icon'>{$variables['tooltip']}</a>";
Karen Stevenson's avatar
Karen Stevenson committed
42
43
44
45
  }
}

/**
46
47
 * Implements hook_preprocess_HOOK() for nodes.
 *
Robert Rollins's avatar
Robert Rollins committed
48
 * Hide extraneous information when rendering the iCal view mode of a node.
Karen Stevenson's avatar
Karen Stevenson committed
49
 */
50
51
function date_ical_preprocess_node(&$variables) {
  if (isset($variables['view_mode']) && $variables['view_mode'] == 'ical') {
Karen Stevenson's avatar
Karen Stevenson committed
52
53
    // We hide the page elements we won't want to see.
    // The display of the body and other fields will be controlled
Robert Rollins's avatar
Robert Rollins committed
54
55
    // by the Manage Display settings for the iCal view mode.
    
56
57
    // Trick the default node template into not displaying the page title by telling it this is page.
    $variables['page'] = TRUE;
Karen Stevenson's avatar
Karen Stevenson committed
58
59
    $variables['title_prefix'] = '';
    $variables['title_suffix'] = '';
60
    
Robert Rollins's avatar
Robert Rollins committed
61
    // We don't want to see the author information in our feed.
Karen Stevenson's avatar
Karen Stevenson committed
62
    $variables['display_submitted'] = FALSE;
63
    
Robert Rollins's avatar
Robert Rollins committed
64
    // Comments and links don't belong in an iCal feed.
65
66
67
68
69
70
    if (isset($variables['content']['comments'])) {
      unset($variables['content']['comments']);
    }
    if (isset($variables['content']['links'])) {
      unset($variables['content']['links']);
    }
Karen Stevenson's avatar
Karen Stevenson committed
71
72
73
74
75
76
  }
}

/**
 * Implements hook_entity_info_alter().
 *
Robert Rollins's avatar
Robert Rollins committed
77
 * Add an 'iCal' view mode for entities, which will be used by the Views style plugin.
Karen Stevenson's avatar
Karen Stevenson committed
78
 */
79
80
function date_ical_entity_info_alter(&$entity_info) {
  foreach ($entity_info as $entity_type => $info) {
Robert Rollins's avatar
Robert Rollins committed
81
82
83
    if (!isset($entity_info[$entity_type]['view modes'])) {
      $entity_info[$entity_type]['view modes'] = array();
    }
84
    $entity_info[$entity_type]['view modes'] += array(
Karen Stevenson's avatar
Karen Stevenson committed
85
86
      'ical' => array(
        'label' => t('iCal'),
87
        'custom settings' => TRUE,
Karen Stevenson's avatar
Karen Stevenson committed
88
89
90
91
92
      ),
    );
  }
}

Robert Rollins's avatar
Robert Rollins committed
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
/**
 * Implements hook_libraries_info().
 */
function date_ical_libraries_info() {
  $libraries['iCalcreator'] = array(
    'name' => 'iCalcreator',
    'vendor url' => 'http://kigkonsult.se/iCalcreator/index.php',
    'download url' => 'http://kigkonsult.se/downloads/index.php#icalcreator',
    'version arguments' => array(
      'file' => 'iCalcreator.class.php',
      'pattern' => "/define\( 'ICALCREATOR_VERSION', 'iCalcreator ([\d\.]+)' \);/",
      'lines' => 100,
    ),
    'files' => array(
      'php' => array('iCalcreator.class.php'),
    ),
  );
110
  
Robert Rollins's avatar
Robert Rollins committed
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
  return $libraries;
}

/**
 * Implementation of hook_ctools_plugin_api().
 */
function date_ical_ctools_plugin_api($owner, $api) {
  if ($owner == 'feeds' && $api == 'plugins') {
    return array('version' => 2);
  }
}

/**
 * Implementation of ctools plugin for feeds hook_feeds_plugins().
 */
function date_ical_feeds_plugins() {
  $path = drupal_get_path('module', 'date_ical') . '/includes';
  $info = array();
  $info['DateIcalFeedsParser'] = array(
    'hidden' => TRUE,
    'handler' => array(
      'parent' => 'FeedsParser',
      'class' => 'DateIcalFeedsParser',
      'file' => 'DateIcalFeedsParser.inc',
      'path' => $path,
    ),
  );
  $info['DateIcalIcalcreatorParser'] = array(
139
140
141
    'name' => 'iCal parser',
    'description' => 'Use the iCalcreator library to parse iCal feeds.',
    'help' => 'Parse iCal feeds.',
Robert Rollins's avatar
Robert Rollins committed
142
143
144
145
146
147
148
149
150
151
    'handler' => array(
      'parent' => 'DateIcalFeedsParser',
      'class' => 'DateIcalIcalcreatorParser',
      'file' => 'DateIcalIcalcreatorParser.inc',
      'path' => $path,
    ),
  );
  
  return $info;
}
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187

/**
 * Implements hook_feeds_processor_targets_alter().
 *
 * Adds the "Field Name: Repeat Rule" target to Date Repeat Fields.
 *
 * @see FeedsNodeProcessor::getMappingTargets().
 */
function date_ical_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_name) {
  foreach (field_info_instances($entity_type, $bundle_name) as $name => $instance) {
    $info = field_info_field($name);
    if (in_array($info['type'], array('date', 'datestamp', 'datetime')) && isset($info['settings']['repeat']) && $info['settings']['repeat']) {
      $targets[$name . ':rrule'] = array(
        'name' => t('@name: Repeat Rule', array('@name' => $instance['label'])),
        'callback' => 'date_ical_feeds_set_rrule',
        'description' => t('The repeat rule for the @name field.', array('@name' => $instance['label'])),
        'real_target' => $name,
      );
    }
  }
}

/**
 * Callback specified in date_ical_feeds_processor_targets_alter() for RRULEs.
 *
 * @param $source
 *   The FeedsSource object.
 * @param $entity
 *   The node that's being built from the iCal element that's being parsed.
 * @param $target
 *   The machine name of the field into which this RRULE shall be parsed,
 *   with ":rrule" appended to the end.
 * @param $feed_element
 *   The RRULE string (with optional EXDATEs and RDATEs separated by \n).
 */
function date_ical_feeds_set_rrule($source, $entity, $target, $feed_element) {
188
189
190
191
  if (empty($feed_element)) {
    // Make sure that VEVENTs which have no RRULE aren't given repeating dates.
    return;
  }
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
  // Add the RRULE value to the field in $entity.
  list($field_name, $trash) = explode(':', $target, 2);
  module_load_include('inc', 'date_api', 'date_api_ical');
  $info = field_info_field($field_name);
  foreach ($entity->{$field_name} as $lang => $field_array) {
    // Add the multiple date values that Date Repeat Field uses to represent recurring dates.
    $values = date_ical_build_repeating_dates($feed_element, NULL, $info, $field_array[0]);
    foreach ($values as $key => $value) {
      $entity->{$field_name}[$lang][$key] = $value;
    }
  }
}

/**
 * 99% copy-pasta from date_repeat_field.module's date_repeat_build_dates() function.
 * The only change is that we assume COUNT=52 on indefinitely repeating RRULEs, rather than
 * giving up completely.
 */
function date_ical_build_repeating_dates($rrule = NULL, $rrule_values = NULL, $field, $item) {
  module_load_include('inc', 'date_api', 'date_api_ical');
  $field_name = $field['field_name'];
213
  
214
215
216
217
218
219
  if (empty($rrule)) {
    $rrule = date_api_ical_build_rrule($rrule_values);
  }
  elseif (empty($rrule_values)) {
    $rrule_values = date_ical_parse_rrule(NULL, $rrule);
  }
220
  
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
  // By the time we get here, the start and end dates have been
  // adjusted back to UTC, but we want localtime dates to do
  // things like '+1 Tuesday', so adjust back to localtime.
  $timezone = date_get_timezone($field['settings']['tz_handling'], $item['timezone']);
  $timezone_db = date_get_timezone_db($field['settings']['tz_handling']);
  $start = new DateObject($item['value'], $timezone_db, date_type_format($field['type']));
  $start->limitGranularity($field['settings']['granularity']);
  if ($timezone != $timezone_db) {
    date_timezone_set($start, timezone_open($timezone));
  }
  if (!empty($item['value2']) && $item['value2'] != $item['value']) {
    $end = new DateObject($item['value2'], date_get_timezone_db($field['settings']['tz_handling']), date_type_format($field['type']));
    $end->limitGranularity($field['settings']['granularity']);
    date_timezone_set($end, timezone_open($timezone));
  }
  else {
    $end = $start;
  }
  $duration = $start->difference($end);
  $start_datetime = date_format($start, DATE_FORMAT_DATETIME);
241
  
242
243
244
245
246
247
248
249
250
251
252
253
  if (!empty($rrule_values['UNTIL']['datetime'])) {
    $end = date_ical_date($rrule_values['UNTIL'], $timezone);
    $end_datetime = date_format($end, DATE_FORMAT_DATETIME);
  }
  elseif (!empty($rrule_values['COUNT'])) {
    $end_datetime = NULL;
  }
  else {
    // No UNTIL and no COUNT means this is an indefinitely repeating RRULE, which Date Repeat Field doesn't support.
    // The best we can do is pretend it has a repeat count of 52 (52 weeks in a year, most repeats are weekly)
    // by inserting a COUNT=52 param into the string, right after 'RRULE:'.
    $rrule = substr_replace($rrule, 'COUNT=52;', 6, 0);
254
    $end_datetime = NULL;
255
  }
256
  
257
258
259
260
261
262
263
264
  // Split the RRULE into RRULE, EXDATE, and RDATE parts.
  $parts = date_repeat_split_rrule($rrule);
  $parsed_exceptions = (array) $parts[1];
  $exceptions = array();
  foreach ($parsed_exceptions as $exception) {
    $date = date_ical_date($exception, $timezone);
    $exceptions[] = date_format($date, 'Y-m-d');
  }
265
  
266
267
268
269
270
271
  $parsed_rdates = (array) $parts[2];
  $additions = array();
  foreach ($parsed_rdates as $rdate) {
    $date = date_ical_date($rdate, $timezone);
    $additions[] = date_format($date, 'Y-m-d');
  }
272
  
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
  $dates = date_repeat_calc($rrule, $start_datetime, $end_datetime, $exceptions, $timezone, $additions);
  $value = array();
  foreach ($dates as $delta => $date) {
    // date_repeat_calc always returns DATE_DATETIME dates, which is
    // not necessarily $field['type'] dates.
    // Convert returned dates back to db timezone before storing.
    $date_start = new DateObject($date, $timezone, DATE_FORMAT_DATETIME);
    $date_start->limitGranularity($field['settings']['granularity']);
    date_timezone_set($date_start, timezone_open($timezone_db));
    $date_end = clone($date_start);
    date_modify($date_end, '+' . $duration . ' seconds');
    $value[$delta] = array(
      'value' => date_format($date_start, date_type_format($field['type'])),
      'value2' => date_format($date_end, date_type_format($field['type'])),
      'offset' => date_offset_get($date_start),
      'offset2' => date_offset_get($date_end),
      'timezone' => $timezone,
      'rrule' => $rrule,
      );
  }
  return $value;
}
295
296
297
298
299
300
301

/**
 *  Identify all potential fields that could populate the optional LOCATION component of iCal output.
 */
function date_ical_get_location_fields($base = 'node', $reset = FALSE) {
  static $fields = array();
  $empty = array('name' => array(), 'alias' => array());
302
  
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
  if (empty($fields[$base]) || $reset) {
    $cid = 'date_ical_location_fields_' . $base;
    if (!$reset && $cached = cache_get($cid, 'cache_views')) {
      $fields[$base] = $cached->data;
    }
    else {
      $fields[$base] = _date_ical_get_location_fields($base);
    }
  }
  // Make sure that empty values will be arrays in the expected format.
  return !empty($fields) && !empty($fields[$base]) ? $fields[$base] : $empty;
}

/**
 *  Identify all potential LOCATION fields.
Robert Rollins's avatar
Robert Rollins committed
318
319
 *  This is a cut down version of _date_views_fields() from date_views_fields.inc
 *  in date_views module.
320
321
322
323
 *
 *  @return
 *    array with fieldname, type, and table.
 *  @see
Robert Rollins's avatar
Robert Rollins committed
324
325
 *    date_views_date_views_fields(), which implements hook_date_views_fields()
 *    for the core date fields.
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
 */
function _date_ical_get_location_fields($base = 'node') {
  // Make sure $base is never empty.
  if (empty($base)) {
    $base = 'node';
  }
  
  $cid = 'date_ical_location_fields_' . $base;
  cache_clear_all($cid, 'cache_views');
  
  // Iterate over all the fields that Views knows about.
  $all_fields = date_views_views_fetch_fields($base, 'field');
  $fields = array();
  foreach ($all_fields as $alias => $val) {
    $name = $alias;
    $tmp = explode('.', $name);
    $field_name = $tmp[1];
    $table_name = $tmp[0];
    
    // Skip unsupported field types and fields that weren't defined through
    // the Field module.
    $info = field_info_field($field_name);
348
    if (!$info || !in_array($info['type'], array('text', 'node_reference', 'addressfield'))) {
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
      continue;
    }
    
    // Build an array of the field info that we'll need.
    $alias = str_replace('.', '_', $alias);
    $fields['name'][$name] = array(
      'label' => "{$val['group']}: {$val['title']} ($field_name)",
      'table_name' => $table_name,
      'field_name' => $field_name,
      'type' => $info['type'],
    );
    
    // These are here only to make this $field array conform to the same format
    // as the one returned by _date_views_fields(). They're probably not needed,
    // but I thought that consistency would be a good idea.
    $fields['name'][$name]['real_field_name'] = $field_name;
    $fields['alias'][$alias] = $fields['name'][$name];
  }
  
  cache_set($cid, $fields, 'cache_views');
  return $fields;
}
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447


/**
 *  Identify all potential fields that could populate the custom SUMMARY field
 */
function date_ical_get_summary_fields($base = 'node', $reset = FALSE) {
  static $fields = array();
  $empty = array('name' => array(), 'alias' => array());

  if (empty($fields[$base]) || $reset) {
    $cid = 'date_ical_summary_fields_' . $base;
    if (!$reset && $cached = cache_get($cid, 'cache_views')) {
      $fields[$base] = $cached->data;
    }
    else {
      $fields[$base] = _date_ical_get_summary_fields($base);
    }
  }
  // Make sure that empty values will be arrays in the expected format.
  return !empty($fields) && !empty($fields[$base]) ? $fields[$base] : $empty;
}

/**
 *  Identify all potential SUMMARY fields.
 *  This is a cut down version of _date_views_fields() from date_views_fields.inc
 *  in date_views module.
 *
 *  @return
 *    array with fieldname, type, and table.
 *  @see
 *    date_views_date_views_fields(), which implements hook_date_views_fields()
 *    for the core date fields.
 */
function _date_ical_get_summary_fields($base = 'node') {
  // Make sure $base is never empty.
  if (empty($base)) {
    $base = 'node';
  }

  $cid = 'date_ical_summary_fields_' . $base;
  cache_clear_all($cid, 'cache_views');

  // Iterate over all the fields that Views knows about.
  $all_fields = date_views_views_fetch_fields($base, 'field');
  $fields = array();
  foreach ($all_fields as $alias => $val) {
    $name = $alias;
    $tmp = explode('.', $name);
    $field_name = $tmp[1];
    $table_name = $tmp[0];

    // Skip unsupported field types and fields that weren't defined through
    // the Field module.
    $info = field_info_field($field_name);
    if (!$info || !in_array($info['type'], array('text', 'node_reference', 'taxonomy_term_reference'))) {
      continue;
    }

    // Build an array of the field info that we'll need.
    $alias = str_replace('.', '_', $alias);
    $fields['name'][$name] = array(
      'label' => "{$val['group']}: {$val['title']} ($field_name)",
      'table_name' => $table_name,
      'field_name' => $field_name,
      'type' => $info['type'],
    );

    // These are here only to make this $field array conform to the same format
    // as the one returned by _date_views_fields(). They're probably not needed,
    // but I thought that consistency would be a good idea.
    $fields['name'][$name]['real_field_name'] = $field_name;
    $fields['alias'][$alias] = $fields['name'][$name];
  }

  cache_set($cid, $fields, 'cache_views');
  return $fields;
}