date_ical.module 15.1 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
 * value of the iCal feeds created by Date iCal, for debugging purposes.
11
 */
Robert Rollins's avatar
Robert Rollins committed
12
define('DATE_ICAL_VERSION', '3.5');
13

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

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

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

/**
 * Implements hook_hook_info().
 */
function date_ical_hook_info() {
Robert Rollins's avatar
Robert Rollins committed
33 34 35 36 37 38 39 40 41 42 43 44
  $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'));
45 46
}

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

Robert Rollins's avatar
Robert Rollins committed
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
/**
 * 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
76 77 78
/**
 * Implements hook_theme().
 */
79
function date_ical_theme($existing, $type, $theme, $path) {
Karen Stevenson's avatar
Karen Stevenson committed
80 81
  return array(
    'date_ical_icon' => array(
Robert Rollins's avatar
Robert Rollins committed
82
      'variables' => array('url' => NULL, 'tooltip' => NULL),
83 84
    ),
  );
Karen Stevenson's avatar
Karen Stevenson committed
85 86 87
}

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

/**
111
 * Implements hook_preprocess_HOOK().
112
 *
113 114 115
 * 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
116
 */
117 118
function date_ical_preprocess_node(&$variables) {
  if (isset($variables['view_mode']) && $variables['view_mode'] == 'ical') {
119 120
    // Trick the default node template into not displaying the page title, by
    // telling it that this *is* a page.
121
    $variables['page'] = TRUE;
Karen Stevenson's avatar
Karen Stevenson committed
122 123
    $variables['title_prefix'] = '';
    $variables['title_suffix'] = '';
124

Robert Rollins's avatar
Robert Rollins committed
125
    // We don't want to see the author information in our feed.
Karen Stevenson's avatar
Karen Stevenson committed
126
    $variables['display_submitted'] = FALSE;
127

Robert Rollins's avatar
Robert Rollins committed
128
    // Comments and links don't belong in an iCal feed.
129 130 131 132 133 134
    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
135 136 137 138 139 140
  }
}

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

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

Robert Rollins's avatar
Robert Rollins committed
178 179 180
  return $libraries;
}

181 182 183 184 185 186
/**
 * Implements hook_help().
 */
function date_ical_help($path, $arg) {
  switch ($path) {
    case 'admin/help#date_ical':
187
      return '<pre>' . file_get_contents(drupal_get_path('module', 'date_ical') . "/README.txt") . '</pre>';
188 189 190
  }
}

Robert Rollins's avatar
Robert Rollins committed
191
/**
192
 * Implements hook_ctools_plugin_api().
Robert Rollins's avatar
Robert Rollins committed
193 194 195 196 197 198 199
 */
function date_ical_ctools_plugin_api($owner, $api) {
  if ($owner == 'feeds' && $api == 'plugins') {
    return array('version' => 2);
  }
}

200 201 202
/**
 * Implements hook_feeds_processor_targets_alter().
 *
203
 * Adds the "Field Name: Repeat Rule" target to Date Repeat fields.
204
 *
205
 * @see FeedsNodeProcessor::getMappingTargets()
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
 */
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,
      );
    }
  }
}

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

235 236 237 238 239 240
  // Convert <p> tags to double newlines.
  $text = trim(preg_replace("/<p.*?>/i", "\n\n", $text));
  // Separate heading tags from the text around them in both directions.
  $text = trim(preg_replace("/<\\?h\d.*?>/i", "\n\n", $text));
  // Add a newline for each <div>.
  $text = trim(preg_replace("/<div.*?>/i", "\n", $text));
241

242 243 244 245
  // Strip the remaining HTML.
  $text = strip_tags($text);
  // Remove newlines added at the beginning.
  return trim($text);
246 247
}

248 249 250
/**
 * Callback specified in date_ical_feeds_processor_targets_alter() for RRULEs.
 *
251
 * @param object $source
252
 *   The FeedsSource object.
253
 * @param object $entity
254
 *   The node that's being built from the iCal element that's being parsed.
255
 * @param string $target
256 257
 *   The machine name of the field into which this RRULE shall be parsed,
 *   with ":rrule" appended to the end.
258
 * @param string|array $repeat_rule
259
 *   The repeat rule string, formatted like "$rrule|$rdate|$exrule|$exdate".
260
 *   Newer versions of Feeds send this value as an array containing the string.
261
 */
262 263 264
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.
265 266
    return;
  }
267 268 269 270 271 272 273

  if (is_array($repeat_rule)) {
    // Newer versions of Feeds return $repeat_rule as an array containing the
    // string we returned from ParseVcalendar::parseRepeatProperty().
    $repeat_rule = reset($repeat_rule);
  }

274 275 276 277 278 279 280
  // $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)
  );
281

282 283 284 285 286
  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);
287 288 289 290 291 292
    foreach ($values as $key => $value) {
      $entity->{$field_name}[$lang][$key] = $value;
    }
  }
}

293
/**
294
 * Identify all potential fields which could be used as an iCal LOCATION.
295 296 297 298
 */
function date_ical_get_location_fields($base = 'node', $reset = FALSE) {
  static $fields = array();
  $empty = array('name' => array(), 'alias' => array());
299

300 301 302 303 304 305 306 307 308 309 310 311 312 313
  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;
}

/**
314 315 316 317 318
 * 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
319
 * @return array
320
 *   array with fieldname, type, and table.
321
 *
322
 * @see date_views_date_views_fields()
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';
  }
329

330 331
  $cid = 'date_ical_location_fields_' . $base;
  cache_clear_all($cid, 'cache_views');
332

333 334 335 336 337 338 339 340
  // 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];
341

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

357 358 359 360 361 362 363 364
    // 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'],
    );
365

366 367 368 369 370 371
    // 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];
  }
372

373 374 375
  cache_set($cid, $fields, 'cache_views');
  return $fields;
}
376 377 378


/**
379
 * Identify all potential fields which could be used as an iCal SUMMARY.
380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398
 */
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;
}

/**
399 400 401 402 403
 * 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
404
 * @return array
405
 *   Array with fieldname, type, and table.
406
 *
Robert Rollins's avatar
Robert Rollins committed
407
 * @see date_views_date_views_fields()
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429
 */
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);
430 431 432 433 434 435 436 437
    $supported_summary_fields = array(
      'text',
      'text_long',
      'text_with_summary',
      'node_reference',
      'taxonomy_term_reference',
    );
    if (!$info || !in_array($info['type'], $supported_summary_fields)) {
438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459
      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;
}
460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477

/**
 * 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
478
      // iCalcreator expects the 'timestamp' to be array key for UNTIL.
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494
      $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;
}