date_ical.module 17 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
12
13
/**
 * The version number of the current release. This is inserted into the PRODID
 * value of the iCal feeds created by Date iCal.
 */
Robert Rollins's avatar
Robert Rollins committed
14
define('DATE_ICAL_VERSION', '2.13-dev');
15

16
/**
17
 * Exception for when the date field for a row in the ical_fields row plugin is blank.
18
19
20
 */
class BlankDateFieldException extends Exception { }

Karen Stevenson's avatar
Karen Stevenson committed
21
/**
22
 * Implements hook_views_api().
Karen Stevenson's avatar
Karen Stevenson committed
23
24
25
26
 */
function date_ical_views_api() {
  return array(
    'api' => 3,
Robert Rollins's avatar
Robert Rollins committed
27
    'path' => drupal_get_path('module', 'date_ical') . '/includes',
Karen Stevenson's avatar
Karen Stevenson committed
28
29
30
31
32
33
  );
}

/**
 * Implements hook_theme().
 */
34
function date_ical_theme($existing, $type, $theme, $path) {
Karen Stevenson's avatar
Karen Stevenson committed
35
36
  return array(
    'date_ical_icon' => array(
Robert Rollins's avatar
Robert Rollins committed
37
      'variables' => array('url' => NULL, 'tooltip' => NULL),
38
39
    ),
  );
Karen Stevenson's avatar
Karen Stevenson committed
40
41
42
43
}

/**
 * The theme for the ical icon.
44
45
46
47
48
 * Available variables are:
 * $variables['tooltip'] - The tooltip to be used for the ican feed icon.
 * $variables['url'] - The url to the actual iCal feed.
 * $variables['view'] - The view object from which the iCal feed is being
 *   built (useful for contextual information).
Karen Stevenson's avatar
Karen Stevenson committed
49
 */
50
function theme_date_ical_icon($variables) {
51
52
53
  if (empty($variables['tooltip'])) {
    $variables['tooltip'] = t('Add this event to my calendar');
  }
Robert Rollins's avatar
Robert Rollins committed
54
55
  $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
56
  if ($image = theme('image', $variables)) {
Robert Rollins's avatar
Robert Rollins committed
57
58
59
60
    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
61
62
63
64
  }
}

/**
65
66
 * Implements hook_preprocess_HOOK() for nodes.
 *
Robert Rollins's avatar
Robert Rollins committed
67
 * Hide extraneous information when rendering the iCal view mode of a node.
Karen Stevenson's avatar
Karen Stevenson committed
68
 */
69
70
function date_ical_preprocess_node(&$variables) {
  if (isset($variables['view_mode']) && $variables['view_mode'] == 'ical') {
Karen Stevenson's avatar
Karen Stevenson committed
71
72
    // 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
73
74
    // by the Manage Display settings for the iCal view mode.
    
75
76
    // Trick the default node template into not displaying the page title by
    // telling it this is a page.
77
    $variables['page'] = TRUE;
Karen Stevenson's avatar
Karen Stevenson committed
78
79
    $variables['title_prefix'] = '';
    $variables['title_suffix'] = '';
80
    
Robert Rollins's avatar
Robert Rollins committed
81
    // We don't want to see the author information in our feed.
Karen Stevenson's avatar
Karen Stevenson committed
82
    $variables['display_submitted'] = FALSE;
83
    
Robert Rollins's avatar
Robert Rollins committed
84
    // Comments and links don't belong in an iCal feed.
85
86
87
88
89
90
    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
91
92
93
94
95
96
  }
}

/**
 * Implements hook_entity_info_alter().
 *
Robert Rollins's avatar
Robert Rollins committed
97
 * Add an 'iCal' view mode for entities, which will be used by the Views style plugin.
Karen Stevenson's avatar
Karen Stevenson committed
98
 */
99
100
function date_ical_entity_info_alter(&$entity_info) {
  foreach ($entity_info as $entity_type => $info) {
Robert Rollins's avatar
Robert Rollins committed
101
102
103
    if (!isset($entity_info[$entity_type]['view modes'])) {
      $entity_info[$entity_type]['view modes'] = array();
    }
104
    $entity_info[$entity_type]['view modes'] += array(
Karen Stevenson's avatar
Karen Stevenson committed
105
106
      'ical' => array(
        'label' => t('iCal'),
107
108
        // Set the iCal view mode to default to same settings as the "default"
        // view mode, so it won't pollute Features.
109
        'custom settings' => FALSE,
Karen Stevenson's avatar
Karen Stevenson committed
110
111
112
113
114
      ),
    );
  }
}

Robert Rollins's avatar
Robert Rollins committed
115
116
117
118
119
120
/**
 * Implements hook_libraries_info().
 */
function date_ical_libraries_info() {
  $libraries['iCalcreator'] = array(
    'name' => 'iCalcreator',
121
122
    'vendor url' => 'http://github.com/iCalcreator/iCalcreator',
    'download url' => 'http://github.com/iCalcreator/iCalcreator',
Robert Rollins's avatar
Robert Rollins committed
123
124
125
126
127
128
129
130
131
    'version arguments' => array(
      'file' => 'iCalcreator.class.php',
      'pattern' => "/define\( 'ICALCREATOR_VERSION', 'iCalcreator ([\d\.]+)' \);/",
      'lines' => 100,
    ),
    'files' => array(
      'php' => array('iCalcreator.class.php'),
    ),
  );
132
  
Robert Rollins's avatar
Robert Rollins committed
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
  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(
161
    'name' => 'iCal parser',
162
    'description' => t('Use the iCalcreator library to parse iCal feeds.'),
163
    'help' => 'Parse iCal feeds.',
Robert Rollins's avatar
Robert Rollins committed
164
165
166
167
168
169
170
171
172
173
    'handler' => array(
      'parent' => 'DateIcalFeedsParser',
      'class' => 'DateIcalIcalcreatorParser',
      'file' => 'DateIcalIcalcreatorParser.inc',
      'path' => $path,
    ),
  );
  
  return $info;
}
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195

/**
 * 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,
      );
    }
  }
}

196
197
/**
 * Reformats the provided text to be compliant with the iCal spec.
198
199
200
 * If the text contains HTML tags, those tags will be stripped (with <p> tags
 * converted to "\n\n" and link tags converted to footnotes), and uneeded
 * whitespace will be cleaned up.
201
202
 *
 * @param $text
203
 *   The text to be sanitized.
204
 */
205
function date_ical_sanitize_text($text = '') {
206
  // Use Drupal's built-in HTML to Text converter, which does a mostly adequate
207
  // job of making the text iCal-compliant.
208
  $text = trim(drupal_html_to_text($text));
209
210
  // Replace instances of more than one space with exactly one space. This
  // cleans up the whitespace mess that gets left behind by drupal_html_to_text().
211
  $text = preg_replace("/  +/", " ", $text);
212
  // The call to drupal_html_to_text() above converted <p> to \n\n, and also
213
214
215
216
217
  // shoved a \n into the string every 80 characters. We don't want those
  // single \n's lying around, because iCalcreator will properly "fold" long
  // text fields for us. So, we need to remove all instances of \n which
  // are neither immediately preceeded, nor followed, by another \n.
  $text = preg_replace("/(?<!\n)\n(?!\n)/", " ", $text);
218
219
220
  return $text;
}

221
222
223
224
225
226
227
228
229
230
231
232
233
234
/**
 * 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) {
235
236
237
238
  if (empty($feed_element)) {
    // Make sure that VEVENTs which have no RRULE aren't given repeating dates.
    return;
  }
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
  // 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'];
260
  
261
262
263
264
265
266
  if (empty($rrule)) {
    $rrule = date_api_ical_build_rrule($rrule_values);
  }
  elseif (empty($rrule_values)) {
    $rrule_values = date_ical_parse_rrule(NULL, $rrule);
  }
267
  
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
  // 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);
288
  
289
290
291
292
293
294
295
296
297
298
299
300
  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);
301
    $end_datetime = NULL;
302
  }
303
  
304
305
306
307
308
309
310
311
  // 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');
  }
312
  
313
314
315
316
317
318
  $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');
  }
319
  
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
  $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;
}
342
343
344
345
346
347
348

/**
 *  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());
349
  
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
  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
365
366
 *  This is a cut down version of _date_views_fields() from date_views_fields.inc
 *  in date_views module.
367
368
369
370
 *
 *  @return
 *    array with fieldname, type, and table.
 *  @see
Robert Rollins's avatar
Robert Rollins committed
371
372
 *    date_views_date_views_fields(), which implements hook_date_views_fields()
 *    for the core date fields.
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
 */
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);
395
    if (!$info || !in_array($info['type'], array('text', 'text_long', 'text_with_summary', 'node_reference', 'addressfield'))) {
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
      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;
}
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
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471


/**
 *  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);
472
    if (!$info || !in_array($info['type'], array('text', 'text_long', 'text_with_summary', 'node_reference', 'taxonomy_term_reference'))) {
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
      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;
}