Project

General

Profile

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

root / drupal7 / sites / all / modules / date_ical / date_ical.module @ 62e0cc08

1
<?php
2

    
3
/**
4
 * @file
5
 * Adds ical functionality to Views, and an iCal parser to Feeds.
6
 */
7

    
8
/**
9
 * Exception class for generic exceptions thrown by this module.
10
 */
11
class DateIcalException extends Exception {}
12

    
13
/**
14
 * Exception used by iCal Fields for when a date field is blank.
15
 */
16
class BlankDateFieldException extends DateIcalException {}
17

    
18
/**
19
 * Exception thrown when the Feeds parser plugin fails.
20
 */
21
class DateIcalParseException extends DateIcalException {}
22

    
23
/**
24
 * Implements hook_hook_info().
25
 */
26
function date_ical_hook_info() {
27
  $hooks = array(
28
    'date_ical_export_html_alter',
29
    'date_ical_export_raw_event_alter',
30
    'date_ical_export_vevent_alter',
31
    'date_ical_export_vcalendar_alter',
32
    'date_ical_export_post_render_alter',
33
    'date_ical_import_vcalendar_alter',
34
    'date_ical_import_component_alter',
35
    'date_ical_import_timezone_alter',
36
  );
37

    
38
  return array_fill_keys($hooks, array('group' => 'date_ical'));
39
}
40

    
41
/**
42
 * Implements hook_views_api().
43
 */
44
function date_ical_views_api() {
45
  return array(
46
    'api' => 3,
47
    'path' => drupal_get_path('module', 'date_ical') . '/includes',
48
  );
49
}
50

    
51
/**
52
 * Implements hook_feeds_plugins().
53
 */
54
function date_ical_feeds_plugins() {
55
  $path = drupal_get_path('module', 'date_ical') . '/includes';
56
  $info = array();
57
  $info['DateiCalFeedsParser'] = array(
58
    'name' => 'iCal parser',
59
    'description' => t('Parse iCal feeds.'),
60
    'handler' => array(
61
      'parent' => 'FeedsParser',
62
      'class' => 'DateiCalFeedsParser',
63
      'file' => 'DateiCalFeedsParser.inc',
64
      'path' => $path,
65
    ),
66
  );
67
  return $info;
68
}
69

    
70
/**
71
 * Implements hook_theme().
72
 */
73
function date_ical_theme($existing, $type, $theme, $path) {
74
  return array(
75
    'date_ical_icon' => array(
76
      'variables' => array('url' => NULL, 'tooltip' => NULL),
77
    ),
78
  );
79
}
80

    
81
/**
82
 * Theme function for the ical icon used by attached iCal feed.
83
 *
84
 * Available variables are:
85
 * $variables['tooltip'] - The tooltip to be used for the ican feed icon.
86
 * $variables['url'] - The url to the actual iCal feed.
87
 * $variables['view'] - The view object from which the iCal feed is being
88
 *   built (useful for contextual information).
89
 */
90
function theme_date_ical_icon($variables) {
91
  if (empty($variables['tooltip'])) {
92
    $variables['tooltip'] = t('Add this event to my calendar');
93
  }
94
  $variables['path'] = drupal_get_path('module', 'date_ical') . '/images/ical-feed-icon-34x14.png';
95
  $variables['alt'] = $variables['tooltip'];
96
  if ($image = theme('image', $variables)) {
97
    return "<a href='{$variables['url']}' class='ical-icon'>$image</a>";
98
  }
99
  else {
100
    return "<a href='{$variables['url']}' class='ical-icon'>{$variables['tooltip']}</a>";
101
  }
102
}
103

    
104
/**
105
 * Implements hook_preprocess_HOOK().
106
 *
107
 * Hides the page elements which don't belong in an iCal DESCRIPTION element.
108
 * The display of the body and other fields is controlled by the Manage
109
 * Display settings for the nodes' iCal view mode.
110
 */
111
function date_ical_preprocess_node(&$variables) {
112
  if (isset($variables['view_mode']) && $variables['view_mode'] == 'ical') {
113
    // Trick the default node template into not displaying the page title, by
114
    // telling it that this *is* a page.
115
    $variables['page'] = TRUE;
116
    $variables['title_prefix'] = array();
117
    $variables['title_suffix'] = array();
118

    
119
    // We don't want to see the author information in our feed.
120
    $variables['display_submitted'] = FALSE;
121

    
122
    // Comments and links don't belong in an iCal feed.
123
    if (isset($variables['content']['comments'])) {
124
      unset($variables['content']['comments']);
125
    }
126
    if (isset($variables['content']['links'])) {
127
      unset($variables['content']['links']);
128
    }
129
  }
130
}
131

    
132
/**
133
 * Implements hook_entity_info_alter().
134
 *
135
 * Adds an 'iCal' view mode for entities, which is used by the iCal Entity
136
 * View plugin.
137
 */
138
function date_ical_entity_info_alter(&$entity_info) {
139
  foreach ($entity_info as $entity_type => $info) {
140
    if (!isset($entity_info[$entity_type]['view modes'])) {
141
      $entity_info[$entity_type]['view modes'] = array();
142
    }
143
    $entity_info[$entity_type]['view modes'] += array(
144
      'ical' => array(
145
        'label' => t('iCal'),
146
        // Set the iCal view mode to default to same settings as the "default"
147
        // view mode, so it won't pollute Features.
148
        'custom settings' => FALSE,
149
      ),
150
    );
151
  }
152
}
153

    
154
/**
155
 * Implements hook_libraries_info().
156
 */
157
function date_ical_libraries_info() {
158
  $libraries['iCalcreator'] = array(
159
    'name' => 'iCalcreator',
160
    'vendor url' => 'http://github.com/iCalcreator/iCalcreator',
161
    'download url' => 'http://github.com/iCalcreator/iCalcreator',
162
    'version arguments' => array(
163
      'file' => 'iCalcreator.class.php',
164
      'pattern' => "/define.*?ICALCREATOR_VERSION.*?([\d\.]+)/",
165
      'lines' => 100,
166
    ),
167
    'files' => array(
168
      'php' => array('iCalcreator.class.php'),
169
    ),
170
  );
171

    
172
  return $libraries;
173
}
174

    
175
/**
176
 * Implements hook_help().
177
 */
178
function date_ical_help($path, $arg) {
179
  switch ($path) {
180
    case 'admin/help#date_ical':
181
      return '<pre>' . file_get_contents(drupal_get_path('module', 'date_ical') . "/README.txt") . '</pre>';
182
  }
183
}
184

    
185
/**
186
 * Implements hook_ctools_plugin_api().
187
 */
188
function date_ical_ctools_plugin_api($owner, $api) {
189
  if ($owner == 'feeds' && $api == 'plugins') {
190
    return array('version' => 2);
191
  }
192
}
193

    
194
/**
195
 * Implements hook_feeds_processor_targets_alter().
196
 *
197
 * Adds the "Field Name: Repeat Rule" target to Date Repeat fields.
198
 *
199
 * @see FeedsNodeProcessor::getMappingTargets()
200
 */
201
function date_ical_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_name) {
202
  foreach (field_info_instances($entity_type, $bundle_name) as $name => $instance) {
203
    $info = field_info_field($name);
204
    if (in_array($info['type'], array('date', 'datestamp', 'datetime')) && isset($info['settings']['repeat']) && $info['settings']['repeat']) {
205
      $targets[$name . ':rrule'] = array(
206
        'name' => t('@name: Repeat Rule', array('@name' => $instance['label'])),
207
        'callback' => 'date_ical_feeds_set_rrule',
208
        'description' => t('The repeat rule for the @name field.', array('@name' => $instance['label'])),
209
        'real_target' => $name,
210
      );
211
    }
212
  }
213
}
214

    
215
/**
216
 * Reformats the provided text to be compliant with the iCal spec.
217
 *
218
 * If the text contains HTML tags, those tags will be stripped. Paragraph,
219
 * heading, and div tags will be replaced with newlines.
220
 *
221
 * @param string $text
222
 *   The text to be sanitized.
223
 */
224
function date_ical_sanitize_text($text = '') {
225
  // HTML tags may get converted to &lt; and such by the View code, so we need
226
  // to convert them back to HTML so we can remove them with strip_tags().
227
  $text = decode_entities($text);
228

    
229
  // Convert <p> tags to double newlines.
230
  $text = trim(preg_replace("/<p.*?>/i", "\n\n", $text));
231
  // Separate heading tags from the text around them in both directions.
232
  $text = trim(preg_replace("/<\\?h\d.*?>/i", "\n\n", $text));
233
  // Add a newline for each <div>.
234
  $text = trim(preg_replace("/<div.*?>/i", "\n", $text));
235

    
236
  // Strip the remaining HTML.
237
  $text = strip_tags($text);
238
  // Remove newlines added at the beginning.
239
  return trim($text);
240
}
241

    
242
/**
243
 * Callback specified in date_ical_feeds_processor_targets_alter() for RRULEs.
244
 *
245
 * @param object $source
246
 *   The FeedsSource object.
247
 * @param object $entity
248
 *   The node that's being built from the iCal element that's being parsed.
249
 * @param string $target
250
 *   The machine name of the field into which this RRULE shall be parsed,
251
 *   with ":rrule" appended to the end.
252
 * @param string|array $repeat_rule
253
 *   The repeat rule string, formatted like "$rrule|$rdate|$exrule|$exdate".
254
 *   Newer versions of Feeds send this value as an array containing the string.
255
 */
256
function date_ical_feeds_set_rrule($source, $entity, $target, $repeat_rule) {
257
  if (is_array($repeat_rule)) {
258
    // Newer versions of Feeds return $repeat_rule as an array containing the
259
    // string we returned from ParseVcalendar::parseRepeatProperty().
260
    $repeat_rule = reset($repeat_rule);
261
  }
262

    
263
  if (empty($repeat_rule)) {
264
    // Don't alter the entity if there's no repeat rule.
265
    return;
266
  }
267

    
268
  // $target looks like <field_name>:rrule, but we only need <field_name>.
269
  $field_name = current(explode(':', $target, 2));
270
  // Parse the repeat rule into RRULE, RDATE, EXRULE, and EXDATE strings.
271
  $repeat_data = array_combine(
272
    array('RRULE', 'RDATE', 'EXRULE', 'EXDATE'),
273
    explode('|', $repeat_rule)
274
  );
275

    
276
  module_load_include('inc', 'date_ical', 'date_ical.utils');
277
  // This "loop" is really just to make sure we get the right array keys. It
278
  // shouldn't ever execute more than once.
279
  foreach ($entity->{$field_name} as $lang => $date_values) {
280
    $values = _date_ical_get_repeat_dates($field_name, $repeat_data, $date_values[0], $source);
281
    foreach ($values as $key => $value) {
282
      $entity->{$field_name}[$lang][$key] = $value;
283
    }
284
  }
285
}
286

    
287
/**
288
 * Identify all potential fields which could be used as an iCal LOCATION.
289
 */
290
function date_ical_get_location_fields($base = 'node', $reset = FALSE) {
291
  static $fields = array();
292
  $empty = array('name' => array(), 'alias' => array());
293

    
294
  if (empty($fields[$base]) || $reset) {
295
    $cid = 'date_ical_location_fields_' . $base;
296
    if (!$reset && $cached = cache_get($cid, 'cache_views')) {
297
      $fields[$base] = $cached->data;
298
    }
299
    else {
300
      $fields[$base] = _date_ical_get_location_fields($base);
301
    }
302
  }
303
  // Make sure that empty values will be arrays in the expected format.
304
  return !empty($fields) && !empty($fields[$base]) ? $fields[$base] : $empty;
305
}
306

    
307
/**
308
 * Internal helper function for date_ical_get_location_fields().
309
 *
310
 * This is a cut down version of _date_views_fields() from
311
 * date_views_fields.inc in date_views module.
312
 *
313
 * @return array
314
 *   array with fieldname, type, and table.
315
 *
316
 * @see date_views_date_views_fields()
317
 */
318
function _date_ical_get_location_fields($base = 'node') {
319
  // Make sure $base is never empty.
320
  if (empty($base)) {
321
    $base = 'node';
322
  }
323

    
324
  $cid = 'date_ical_location_fields_' . $base;
325
  cache_clear_all($cid, 'cache_views');
326

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

    
336
    // Skip unsupported field types and fields that weren't defined through
337
    // the Field module.
338
    $info = field_info_field($field_name);
339
    $supported_location_fields = array(
340
      'text',
341
      'text_long',
342
      'text_with_summary',
343
      'node_reference',
344
      'addressfield',
345
      'location',
346
      'node_reference',
347
      'taxonomy_term_reference',
348
    );
349
    if (!$info || !in_array($info['type'], $supported_location_fields)) {
350
      continue;
351
    }
352

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

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

    
369
  cache_set($cid, $fields, 'cache_views');
370
  return $fields;
371
}
372

    
373

    
374
/**
375
 * Identify all potential fields which could be used as an iCal SUMMARY.
376
 */
377
function date_ical_get_summary_fields($base = 'node', $reset = FALSE) {
378
  static $fields = array();
379
  $empty = array('name' => array(), 'alias' => array());
380

    
381
  if (empty($fields[$base]) || $reset) {
382
    $cid = 'date_ical_summary_fields_' . $base;
383
    if (!$reset && $cached = cache_get($cid, 'cache_views')) {
384
      $fields[$base] = $cached->data;
385
    }
386
    else {
387
      $fields[$base] = _date_ical_get_summary_fields($base);
388
    }
389
  }
390
  // Make sure that empty values will be arrays in the expected format.
391
  return !empty($fields) && !empty($fields[$base]) ? $fields[$base] : $empty;
392
}
393

    
394
/**
395
 * Internal helper function for date_ical_get_summary_fields().
396
 *
397
 * This is a cut down version of _date_views_fields() from
398
 * date_views_fields.inc in date_views module.
399
 *
400
 * @return array
401
 *   Array with fieldname, type, and table.
402
 *
403
 * @see date_views_date_views_fields()
404
 */
405
function _date_ical_get_summary_fields($base = 'node') {
406
  // Make sure $base is never empty.
407
  if (empty($base)) {
408
    $base = 'node';
409
  }
410

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

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

    
423
    // Skip unsupported field types and fields that weren't defined through
424
    // the Field module.
425
    $info = field_info_field($field_name);
426
    $supported_summary_fields = array(
427
      'text',
428
      'text_long',
429
      'text_with_summary',
430
      'node_reference',
431
      'taxonomy_term_reference',
432
    );
433
    if (!$info || !in_array($info['type'], $supported_summary_fields)) {
434
      continue;
435
    }
436

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

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

    
453
  cache_set($cid, $fields, 'cache_views');
454
  return $fields;
455
}
456

    
457
/**
458
 * Convert an rrule array to the iCalcreator format.
459
 *
460
 * iCalcreator expects the BYDAY element to be an array like this:
461
 * (array) ( [([plus] ordwk / minus ordwk)], "DAY" => weekday )
462
 *
463
 * But the way that the Date API gives it to us is like this:
464
 * (array) ( [([plus] ordwk / minus ordwk)]weekday )
465
 */
466
function _date_ical_convert_rrule_for_icalcreator($rrule) {
467
  $new_rrule = array();
468
  foreach ($rrule as $key => $value) {
469
    if (strtoupper($key) == 'DATA') {
470
      // iCalcreator doesn't expect the 'DATA' key that the Date API gives us.
471
      continue;
472
    }
473
    if (strtoupper($key) == 'UNTIL') {
474
      // iCalcreator expects the 'timestamp' to be array key for UNTIL.
475
      $value['timestamp'] = strtotime($value['datetime']);
476
    }
477
    if (strtoupper($key) == 'BYDAY') {
478
      $new_byday = array();
479
      foreach ($value as $day) {
480
        // Fortunately, the weekday values are always 2 characters.
481
        $weekday = substr($day, -2);
482
        $ordwk = substr($day, 0, -2);
483
        $new_byday[] = array($ordwk, 'DAY' => $weekday);
484
      }
485
      $value = $new_byday;
486
    }
487
    $new_rrule[$key] = $value;
488
  }
489
  return $new_rrule;
490
}