date_ical.module 15 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
 * Adds ical functionality to Views, and an iCal parser to Feeds.
Karen Stevenson's avatar
Karen Stevenson committed
6 7
 */

8 9
/**
 * The version number of the current release. This is inserted into the PRODID
Robert Rollins's avatar
Robert Rollins committed
10 11
 * value of the iCal feeds created by Date iCal. It's primarily used for
 * debugging.
12
 */
13
define('DATE_ICAL_VERSION', '3.1-dev');
14

15
/**
Robert Rollins's avatar
Robert Rollins committed
16
 * Exception class for generic exceptions thrown by this module.
17
 */
Robert Rollins's avatar
Robert Rollins committed
18
class DateIcalException extends Exception {}
19

20
/**
21
 * Exception used by iCal Fields for when a date field is blank.
22
 */
Robert Rollins's avatar
Robert Rollins committed
23
class BlankDateFieldException extends DateIcalException {}
24 25

/**
Robert Rollins's avatar
Robert Rollins committed
26
 * Exception thrown when the Feeds parser plugin fails.
27 28 29 30 31 32 33
 */
class DateIcalParseException extends DateIcalException {}

/**
 * Implements hook_hook_info().
 */
function date_ical_hook_info() {
Robert Rollins's avatar
Robert Rollins committed
34 35 36 37 38 39 40 41 42 43 44 45
  $hooks = array(
    'date_ical_export_html_alter',
    'date_ical_export_raw_event_alter',
    'date_ical_export_vevent_alter',
    'date_ical_export_vcalendar_alter',
    'date_ical_export_post_render_alter',
    'date_ical_import_vcalendar_alter',
    'date_ical_import_component_alter',
    'date_ical_import_timezone_alter',
  );

  return array_fill_keys($hooks, array('group' => 'date_ical'));
46 47
}

Karen Stevenson's avatar
Karen Stevenson committed
48
/**
49
 * Implements hook_views_api().
Karen Stevenson's avatar
Karen Stevenson committed
50 51 52 53
 */
function date_ical_views_api() {
  return array(
    'api' => 3,
Robert Rollins's avatar
Robert Rollins committed
54
    'path' => drupal_get_path('module', 'date_ical') . '/includes',
Karen Stevenson's avatar
Karen Stevenson committed
55 56 57
  );
}

Robert Rollins's avatar
Robert Rollins committed
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
/**
 * Implements hook_feeds_plugins().
 */
function date_ical_feeds_plugins() {
  $path = drupal_get_path('module', 'date_ical') . '/includes';
  $info = array();
  $info['DateiCalFeedsParser'] = array(
    'name' => 'iCal parser',
    'description' => t('Parse iCal feeds.'),
    'handler' => array(
      'parent' => 'FeedsParser',
      'class' => 'DateiCalFeedsParser',
      'file' => 'DateiCalFeedsParser.inc',
      'path' => $path,
    ),
  );
  return $info;
}

Karen Stevenson's avatar
Karen Stevenson committed
77 78 79
/**
 * Implements hook_theme().
 */
80
function date_ical_theme($existing, $type, $theme, $path) {
Karen Stevenson's avatar
Karen Stevenson committed
81 82
  return array(
    'date_ical_icon' => array(
Robert Rollins's avatar
Robert Rollins committed
83
      'variables' => array('url' => NULL, 'tooltip' => NULL),
84 85
    ),
  );
Karen Stevenson's avatar
Karen Stevenson committed
86 87 88
}

/**
89 90
 * Theme function for the ical icon used by attached iCal feed.
 *
91 92 93 94 95
 * 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
96
 */
97
function theme_date_ical_icon($variables) {
98 99 100
  if (empty($variables['tooltip'])) {
    $variables['tooltip'] = t('Add this event to my calendar');
  }
Robert Rollins's avatar
Robert Rollins committed
101 102
  $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
103
  if ($image = theme('image', $variables)) {
Robert Rollins's avatar
Robert Rollins committed
104 105 106 107
    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
108 109 110 111
  }
}

/**
112
 * Implements hook_preprocess_HOOK().
113
 *
114 115 116
 * Hides the page elements which don't belong in an iCal DESCRIPTION element.
 * The display of the body and other fields is controlled by the Manage
 * Display settings for the nodes' iCal view mode.
Karen Stevenson's avatar
Karen Stevenson committed
117
 */
118 119
function date_ical_preprocess_node(&$variables) {
  if (isset($variables['view_mode']) && $variables['view_mode'] == 'ical') {
120 121
    // Trick the default node template into not displaying the page title, by
    // telling it that this *is* a page.
122
    $variables['page'] = TRUE;
Karen Stevenson's avatar
Karen Stevenson committed
123 124
    $variables['title_prefix'] = '';
    $variables['title_suffix'] = '';
125
    
Robert Rollins's avatar
Robert Rollins committed
126
    // We don't want to see the author information in our feed.
Karen Stevenson's avatar
Karen Stevenson committed
127
    $variables['display_submitted'] = FALSE;
128
    
Robert Rollins's avatar
Robert Rollins committed
129
    // Comments and links don't belong in an iCal feed.
130 131 132 133 134 135
    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
136 137 138 139 140 141
  }
}

/**
 * Implements hook_entity_info_alter().
 *
142 143
 * Adds an 'iCal' view mode for entities, which is used by the iCal Entity
 * View plugin.
Karen Stevenson's avatar
Karen Stevenson committed
144
 */
145 146
function date_ical_entity_info_alter(&$entity_info) {
  foreach ($entity_info as $entity_type => $info) {
Robert Rollins's avatar
Robert Rollins committed
147 148 149
    if (!isset($entity_info[$entity_type]['view modes'])) {
      $entity_info[$entity_type]['view modes'] = array();
    }
150
    $entity_info[$entity_type]['view modes'] += array(
Karen Stevenson's avatar
Karen Stevenson committed
151 152
      'ical' => array(
        'label' => t('iCal'),
153 154
        // Set the iCal view mode to default to same settings as the "default"
        // view mode, so it won't pollute Features.
155
        'custom settings' => FALSE,
Karen Stevenson's avatar
Karen Stevenson committed
156 157 158 159 160
      ),
    );
  }
}

Robert Rollins's avatar
Robert Rollins committed
161 162 163 164 165 166
/**
 * Implements hook_libraries_info().
 */
function date_ical_libraries_info() {
  $libraries['iCalcreator'] = array(
    'name' => 'iCalcreator',
167 168
    'vendor url' => 'http://github.com/iCalcreator/iCalcreator',
    'download url' => 'http://github.com/iCalcreator/iCalcreator',
Robert Rollins's avatar
Robert Rollins committed
169 170 171 172 173 174 175 176 177
    'version arguments' => array(
      'file' => 'iCalcreator.class.php',
      'pattern' => "/define\( 'ICALCREATOR_VERSION', 'iCalcreator ([\d\.]+)' \);/",
      'lines' => 100,
    ),
    'files' => array(
      'php' => array('iCalcreator.class.php'),
    ),
  );
178
  
Robert Rollins's avatar
Robert Rollins committed
179 180 181 182
  return $libraries;
}

/**
183
 * Implements hook_ctools_plugin_api().
Robert Rollins's avatar
Robert Rollins committed
184 185 186 187 188 189 190
 */
function date_ical_ctools_plugin_api($owner, $api) {
  if ($owner == 'feeds' && $api == 'plugins') {
    return array('version' => 2);
  }
}

191 192 193
/**
 * Implements hook_feeds_processor_targets_alter().
 *
194
 * Adds the "Field Name: Repeat Rule" target to Date Repeat fields.
195
 *
196
 * @see FeedsNodeProcessor::getMappingTargets()
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
 */
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,
      );
    }
  }
}

212 213
/**
 * Reformats the provided text to be compliant with the iCal spec.
214
 *
215 216 217
 * 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.
218
 *
Robert Rollins's avatar
Robert Rollins committed
219
 * @param string $text
220
 *   The text to be sanitized.
221
 */
222
function date_ical_sanitize_text($text = '') {
223 224
  // Use Drupal's built-in HTML to Text converter, which does a mostly
  // adequate job of making the text iCal-compliant.
225
  $text = trim(drupal_html_to_text($text));
226
  // Replace instances of more than one space with exactly one space. This
227
  // cleans up the whitespace mess that drupal_html_to_text() leaves behind.
228
  $text = preg_replace("/  +/", " ", $text);
229
  // The call to drupal_html_to_text() above converted <p> to \n\n, and also
230 231 232 233
  // 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.
234 235 236
  // However, \n's which are followed immediately by a > character should
  // remain, because of how drupal_html_to_text() converts <blockquote>.
  $text = preg_replace("/(?<!\n)\n(?![\n\>])/", " ", $text);
237 238 239
  return $text;
}

240 241 242
/**
 * Callback specified in date_ical_feeds_processor_targets_alter() for RRULEs.
 *
243
 * @param object $source
244
 *   The FeedsSource object.
245
 * @param object $entity
246
 *   The node that's being built from the iCal element that's being parsed.
247
 * @param string $target
248 249
 *   The machine name of the field into which this RRULE shall be parsed,
 *   with ":rrule" appended to the end.
250 251
 * @param string $repeat_rule
 *   The repeat rule string, formatted like "$rrule|$rdate|$exrule|$exdate".
252
 */
253 254 255
function date_ical_feeds_set_rrule($source, $entity, $target, $repeat_rule) {
  if (empty($repeat_rule)) {
    // Don't alter the entity if there's no repeat rule.
256 257
    return;
  }
258 259 260 261 262 263 264 265 266 267 268 269 270
  // $target looks like <field_name>:rrule, but we only need <field_name>.
  $field_name = current(explode(':', $target, 2));
  // Parse the repeat rule into RRULE, RDATE, EXRULE, and EXDATE strings.
  $repeat_data = array_combine(
    array('RRULE', 'RDATE', 'EXRULE', 'EXDATE'),
    explode('|', $repeat_rule)
  );
  
  module_load_include('inc', 'date_ical', 'date_ical.utils');
  // This "loop" is really just to make sure we get the right array keys. It
  // souldn't ever execute more than once.
  foreach ($entity->{$field_name} as $lang => $date_values) {
    $values = _date_ical_get_repeat_dates($field_name, $repeat_data, $date_values[0], $source);
271 272 273 274 275 276
    foreach ($values as $key => $value) {
      $entity->{$field_name}[$lang][$key] = $value;
    }
  }
}

277
/**
278
 * Identify all potential fields which could be used as an iCal LOCATION.
279 280 281 282
 */
function date_ical_get_location_fields($base = 'node', $reset = FALSE) {
  static $fields = array();
  $empty = array('name' => array(), 'alias' => array());
283
  
284 285 286 287 288 289 290 291 292 293 294 295 296 297
  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;
}

/**
298 299 300 301 302
 * Internal helper function for date_ical_get_location_fields().
 *
 * This is a cut down version of _date_views_fields() from
 * date_views_fields.inc in date_views module.
 *
Robert Rollins's avatar
Robert Rollins committed
303
 * @return array
304
 *   array with fieldname, type, and table.
305
 *
306
 * @see date_views_date_views_fields()
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
 */
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);
329 330 331 332 333 334
    $supported_location_fields = array(
      'text',
      'text_long',
      'text_with_summary',
      'node_reference',
      'addressfield',
335
      'location'
336 337
    );
    if (!$info || !in_array($info['type'], $supported_location_fields)) {
338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
      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;
}
360 361 362


/**
363
 * Identify all potential fields which could be used as an iCal SUMMARY.
364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
 */
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;
}

/**
383 384 385 386 387
 * Internal helper function for date_ical_get_summary_fields().
 *
 * This is a cut down version of _date_views_fields() from
 * date_views_fields.inc in date_views module.
 *
Robert Rollins's avatar
Robert Rollins committed
388
 * @return array
389
 *   Array with fieldname, type, and table.
390
 *
Robert Rollins's avatar
Robert Rollins committed
391
 * @see date_views_date_views_fields()
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413
 */
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);
414 415 416 417 418 419 420 421
    $supported_summary_fields = array(
      'text',
      'text_long',
      'text_with_summary',
      'node_reference',
      'taxonomy_term_reference',
    );
    if (!$info || !in_array($info['type'], $supported_summary_fields)) {
422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443
      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;
}
444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461

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