1
|
<?php
|
2
|
|
3
|
/**
|
4
|
* @file
|
5
|
* Adds ical functionality to Views, and an iCal parser to Feeds.
|
6
|
*/
|
7
|
|
8
|
/**
|
9
|
* The version number of the current release. This is inserted into the PRODID
|
10
|
* value of the iCal feeds created by Date iCal, for debugging purposes.
|
11
|
*/
|
12
|
define('DATE_ICAL_VERSION', '3.6');
|
13
|
|
14
|
/**
|
15
|
* Exception class for generic exceptions thrown by this module.
|
16
|
*/
|
17
|
class DateIcalException extends Exception {}
|
18
|
|
19
|
/**
|
20
|
* Exception used by iCal Fields for when a date field is blank.
|
21
|
*/
|
22
|
class BlankDateFieldException extends DateIcalException {}
|
23
|
|
24
|
/**
|
25
|
* Exception thrown when the Feeds parser plugin fails.
|
26
|
*/
|
27
|
class DateIcalParseException extends DateIcalException {}
|
28
|
|
29
|
/**
|
30
|
* Implements hook_hook_info().
|
31
|
*/
|
32
|
function date_ical_hook_info() {
|
33
|
$hooks = array(
|
34
|
'date_ical_export_html_alter',
|
35
|
'date_ical_export_raw_event_alter',
|
36
|
'date_ical_export_vevent_alter',
|
37
|
'date_ical_export_vcalendar_alter',
|
38
|
'date_ical_export_post_render_alter',
|
39
|
'date_ical_import_vcalendar_alter',
|
40
|
'date_ical_import_component_alter',
|
41
|
'date_ical_import_timezone_alter',
|
42
|
);
|
43
|
|
44
|
return array_fill_keys($hooks, array('group' => 'date_ical'));
|
45
|
}
|
46
|
|
47
|
/**
|
48
|
* Implements hook_views_api().
|
49
|
*/
|
50
|
function date_ical_views_api() {
|
51
|
return array(
|
52
|
'api' => 3,
|
53
|
'path' => drupal_get_path('module', 'date_ical') . '/includes',
|
54
|
);
|
55
|
}
|
56
|
|
57
|
/**
|
58
|
* Implements hook_feeds_plugins().
|
59
|
*/
|
60
|
function date_ical_feeds_plugins() {
|
61
|
$path = drupal_get_path('module', 'date_ical') . '/includes';
|
62
|
$info = array();
|
63
|
$info['DateiCalFeedsParser'] = array(
|
64
|
'name' => 'iCal parser',
|
65
|
'description' => t('Parse iCal feeds.'),
|
66
|
'handler' => array(
|
67
|
'parent' => 'FeedsParser',
|
68
|
'class' => 'DateiCalFeedsParser',
|
69
|
'file' => 'DateiCalFeedsParser.inc',
|
70
|
'path' => $path,
|
71
|
),
|
72
|
);
|
73
|
return $info;
|
74
|
}
|
75
|
|
76
|
/**
|
77
|
* Implements hook_theme().
|
78
|
*/
|
79
|
function date_ical_theme($existing, $type, $theme, $path) {
|
80
|
return array(
|
81
|
'date_ical_icon' => array(
|
82
|
'variables' => array('url' => NULL, 'tooltip' => NULL),
|
83
|
),
|
84
|
);
|
85
|
}
|
86
|
|
87
|
/**
|
88
|
* Theme function for the ical icon used by attached iCal feed.
|
89
|
*
|
90
|
* Available variables are:
|
91
|
* $variables['tooltip'] - The tooltip to be used for the ican feed icon.
|
92
|
* $variables['url'] - The url to the actual iCal feed.
|
93
|
* $variables['view'] - The view object from which the iCal feed is being
|
94
|
* built (useful for contextual information).
|
95
|
*/
|
96
|
function theme_date_ical_icon($variables) {
|
97
|
if (empty($variables['tooltip'])) {
|
98
|
$variables['tooltip'] = t('Add this event to my calendar');
|
99
|
}
|
100
|
$variables['path'] = drupal_get_path('module', 'date_ical') . '/images/ical-feed-icon-34x14.png';
|
101
|
$variables['alt'] = $variables['title'] = $variables['tooltip'];
|
102
|
if ($image = theme('image', $variables)) {
|
103
|
return "<a href='{$variables['url']}' class='ical-icon'>$image</a>";
|
104
|
}
|
105
|
else {
|
106
|
return "<a href='{$variables['url']}' class='ical-icon'>{$variables['tooltip']}</a>";
|
107
|
}
|
108
|
}
|
109
|
|
110
|
/**
|
111
|
* Implements hook_preprocess_HOOK().
|
112
|
*
|
113
|
* Hides the page elements which don't belong in an iCal DESCRIPTION element.
|
114
|
* The display of the body and other fields is controlled by the Manage
|
115
|
* Display settings for the nodes' iCal view mode.
|
116
|
*/
|
117
|
function date_ical_preprocess_node(&$variables) {
|
118
|
if (isset($variables['view_mode']) && $variables['view_mode'] == 'ical') {
|
119
|
// Trick the default node template into not displaying the page title, by
|
120
|
// telling it that this *is* a page.
|
121
|
$variables['page'] = TRUE;
|
122
|
$variables['title_prefix'] = '';
|
123
|
$variables['title_suffix'] = '';
|
124
|
|
125
|
// We don't want to see the author information in our feed.
|
126
|
$variables['display_submitted'] = FALSE;
|
127
|
|
128
|
// Comments and links don't belong in an iCal feed.
|
129
|
if (isset($variables['content']['comments'])) {
|
130
|
unset($variables['content']['comments']);
|
131
|
}
|
132
|
if (isset($variables['content']['links'])) {
|
133
|
unset($variables['content']['links']);
|
134
|
}
|
135
|
}
|
136
|
}
|
137
|
|
138
|
/**
|
139
|
* Implements hook_entity_info_alter().
|
140
|
*
|
141
|
* Adds an 'iCal' view mode for entities, which is used by the iCal Entity
|
142
|
* View plugin.
|
143
|
*/
|
144
|
function date_ical_entity_info_alter(&$entity_info) {
|
145
|
foreach ($entity_info as $entity_type => $info) {
|
146
|
if (!isset($entity_info[$entity_type]['view modes'])) {
|
147
|
$entity_info[$entity_type]['view modes'] = array();
|
148
|
}
|
149
|
$entity_info[$entity_type]['view modes'] += array(
|
150
|
'ical' => array(
|
151
|
'label' => t('iCal'),
|
152
|
// Set the iCal view mode to default to same settings as the "default"
|
153
|
// view mode, so it won't pollute Features.
|
154
|
'custom settings' => FALSE,
|
155
|
),
|
156
|
);
|
157
|
}
|
158
|
}
|
159
|
|
160
|
/**
|
161
|
* Implements hook_libraries_info().
|
162
|
*/
|
163
|
function date_ical_libraries_info() {
|
164
|
$libraries['iCalcreator'] = array(
|
165
|
'name' => 'iCalcreator',
|
166
|
'vendor url' => 'http://github.com/iCalcreator/iCalcreator',
|
167
|
'download url' => 'http://github.com/iCalcreator/iCalcreator',
|
168
|
'version callback' => '_date_ical_detect_icalcreator_version',
|
169
|
'versions' => array(
|
170
|
'2.20' => array(
|
171
|
'files' => array(
|
172
|
'php' => array('iCalcreator.class.php'),
|
173
|
),
|
174
|
),
|
175
|
'2.22+' => array(
|
176
|
'files' => array(
|
177
|
'php' => array('iCalcreator.php'),
|
178
|
),
|
179
|
),
|
180
|
),
|
181
|
);
|
182
|
|
183
|
return $libraries;
|
184
|
}
|
185
|
|
186
|
/**
|
187
|
* Implements hook_help().
|
188
|
*/
|
189
|
function date_ical_help($path, $arg) {
|
190
|
switch ($path) {
|
191
|
case 'admin/help#date_ical':
|
192
|
return '<pre>' . file_get_contents(drupal_get_path('module', 'date_ical') . "/README.txt") . '</pre>';
|
193
|
}
|
194
|
}
|
195
|
|
196
|
/**
|
197
|
* Implements hook_ctools_plugin_api().
|
198
|
*/
|
199
|
function date_ical_ctools_plugin_api($owner, $api) {
|
200
|
if ($owner == 'feeds' && $api == 'plugins') {
|
201
|
return array('version' => 2);
|
202
|
}
|
203
|
}
|
204
|
|
205
|
/**
|
206
|
* Implements hook_feeds_processor_targets_alter().
|
207
|
*
|
208
|
* Adds the "Field Name: Repeat Rule" target to Date Repeat fields.
|
209
|
*
|
210
|
* @see FeedsNodeProcessor::getMappingTargets()
|
211
|
*/
|
212
|
function date_ical_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_name) {
|
213
|
foreach (field_info_instances($entity_type, $bundle_name) as $name => $instance) {
|
214
|
$info = field_info_field($name);
|
215
|
if (in_array($info['type'], array('date', 'datestamp', 'datetime')) && isset($info['settings']['repeat']) && $info['settings']['repeat']) {
|
216
|
$targets[$name . ':rrule'] = array(
|
217
|
'name' => t('@name: Repeat Rule', array('@name' => $instance['label'])),
|
218
|
'callback' => 'date_ical_feeds_set_rrule',
|
219
|
'description' => t('The repeat rule for the @name field.', array('@name' => $instance['label'])),
|
220
|
'real_target' => $name,
|
221
|
);
|
222
|
}
|
223
|
}
|
224
|
}
|
225
|
|
226
|
/**
|
227
|
* Reformats the provided text to be compliant with the iCal spec.
|
228
|
*
|
229
|
* If the text contains HTML tags, those tags will be stripped. Paragraph,
|
230
|
* heading, and div tags will be replaced with newlines.
|
231
|
*
|
232
|
* @param string $text
|
233
|
* The text to be sanitized.
|
234
|
*/
|
235
|
function date_ical_sanitize_text($text = '') {
|
236
|
// HTML tags may get converted to < and such by the View code, so we need
|
237
|
// to convert them back to HTML so we can remove them with strip_tags().
|
238
|
$text = decode_entities($text);
|
239
|
|
240
|
// Convert <p> tags to double newlines.
|
241
|
$text = trim(preg_replace("/<p.*?>/i", "\n\n", $text));
|
242
|
// Separate heading tags from the text around them in both directions.
|
243
|
$text = trim(preg_replace("/<\\?h\d.*?>/i", "\n\n", $text));
|
244
|
// Add a newline for each <div>.
|
245
|
$text = trim(preg_replace("/<div.*?>/i", "\n", $text));
|
246
|
|
247
|
// Strip the remaining HTML.
|
248
|
$text = strip_tags($text);
|
249
|
// Remove newlines added at the beginning.
|
250
|
return trim($text);
|
251
|
}
|
252
|
|
253
|
/**
|
254
|
* Callback specified in date_ical_feeds_processor_targets_alter() for RRULEs.
|
255
|
*
|
256
|
* @param object $source
|
257
|
* The FeedsSource object.
|
258
|
* @param object $entity
|
259
|
* The node that's being built from the iCal element that's being parsed.
|
260
|
* @param string $target
|
261
|
* The machine name of the field into which this RRULE shall be parsed,
|
262
|
* with ":rrule" appended to the end.
|
263
|
* @param string|array $repeat_rule
|
264
|
* The repeat rule string, formatted like "$rrule|$rdate|$exrule|$exdate".
|
265
|
* Newer versions of Feeds send this value as an array containing the string.
|
266
|
*/
|
267
|
function date_ical_feeds_set_rrule($source, $entity, $target, $repeat_rule) {
|
268
|
if (empty($repeat_rule)) {
|
269
|
// Don't alter the entity if there's no repeat rule.
|
270
|
return;
|
271
|
}
|
272
|
|
273
|
if (is_array($repeat_rule)) {
|
274
|
// Newer versions of Feeds return $repeat_rule as an array containing the
|
275
|
// string we returned from ParseVcalendar::parseRepeatProperty().
|
276
|
$repeat_rule = reset($repeat_rule);
|
277
|
}
|
278
|
|
279
|
// $target looks like <field_name>:rrule, but we only need <field_name>.
|
280
|
$field_name = current(explode(':', $target, 2));
|
281
|
// Parse the repeat rule into RRULE, RDATE, EXRULE, and EXDATE strings.
|
282
|
$repeat_data = array_combine(
|
283
|
array('RRULE', 'RDATE', 'EXRULE', 'EXDATE'),
|
284
|
explode('|', $repeat_rule)
|
285
|
);
|
286
|
|
287
|
module_load_include('inc', 'date_ical', 'date_ical.utils');
|
288
|
// This "loop" is really just to make sure we get the right array keys. It
|
289
|
// souldn't ever execute more than once.
|
290
|
foreach ($entity->{$field_name} as $lang => $date_values) {
|
291
|
$values = _date_ical_get_repeat_dates($field_name, $repeat_data, $date_values[0], $source);
|
292
|
foreach ($values as $key => $value) {
|
293
|
$entity->{$field_name}[$lang][$key] = $value;
|
294
|
}
|
295
|
}
|
296
|
}
|
297
|
|
298
|
/**
|
299
|
* Identify all potential fields which could be used as an iCal LOCATION.
|
300
|
*/
|
301
|
function date_ical_get_location_fields($base = 'node', $reset = FALSE) {
|
302
|
static $fields = array();
|
303
|
$empty = array('name' => array(), 'alias' => array());
|
304
|
|
305
|
if (empty($fields[$base]) || $reset) {
|
306
|
$cid = 'date_ical_location_fields_' . $base;
|
307
|
if (!$reset && $cached = cache_get($cid, 'cache_views')) {
|
308
|
$fields[$base] = $cached->data;
|
309
|
}
|
310
|
else {
|
311
|
$fields[$base] = _date_ical_get_location_fields($base);
|
312
|
}
|
313
|
}
|
314
|
// Make sure that empty values will be arrays in the expected format.
|
315
|
return !empty($fields) && !empty($fields[$base]) ? $fields[$base] : $empty;
|
316
|
}
|
317
|
|
318
|
/**
|
319
|
* Internal helper function for date_ical_get_location_fields().
|
320
|
*
|
321
|
* This is a cut down version of _date_views_fields() from
|
322
|
* date_views_fields.inc in date_views module.
|
323
|
*
|
324
|
* @return array
|
325
|
* array with fieldname, type, and table.
|
326
|
*
|
327
|
* @see date_views_date_views_fields()
|
328
|
*/
|
329
|
function _date_ical_get_location_fields($base = 'node') {
|
330
|
// Make sure $base is never empty.
|
331
|
if (empty($base)) {
|
332
|
$base = 'node';
|
333
|
}
|
334
|
|
335
|
$cid = 'date_ical_location_fields_' . $base;
|
336
|
cache_clear_all($cid, 'cache_views');
|
337
|
|
338
|
// Iterate over all the fields that Views knows about.
|
339
|
$all_fields = date_views_views_fetch_fields($base, 'field');
|
340
|
$fields = array();
|
341
|
foreach ($all_fields as $alias => $val) {
|
342
|
$name = $alias;
|
343
|
$tmp = explode('.', $name);
|
344
|
$field_name = $tmp[1];
|
345
|
$table_name = $tmp[0];
|
346
|
|
347
|
// Skip unsupported field types and fields that weren't defined through
|
348
|
// the Field module.
|
349
|
$info = field_info_field($field_name);
|
350
|
$supported_location_fields = array(
|
351
|
'text',
|
352
|
'text_long',
|
353
|
'text_with_summary',
|
354
|
'node_reference',
|
355
|
'addressfield',
|
356
|
'location'
|
357
|
);
|
358
|
if (!$info || !in_array($info['type'], $supported_location_fields)) {
|
359
|
continue;
|
360
|
}
|
361
|
|
362
|
// Build an array of the field info that we'll need.
|
363
|
$alias = str_replace('.', '_', $alias);
|
364
|
$fields['name'][$name] = array(
|
365
|
'label' => "{$val['group']}: {$val['title']} ($field_name)",
|
366
|
'table_name' => $table_name,
|
367
|
'field_name' => $field_name,
|
368
|
'type' => $info['type'],
|
369
|
);
|
370
|
|
371
|
// These are here only to make this $field array conform to the same format
|
372
|
// as the one returned by _date_views_fields(). They're probably not needed,
|
373
|
// but I thought that consistency would be a good idea.
|
374
|
$fields['name'][$name]['real_field_name'] = $field_name;
|
375
|
$fields['alias'][$alias] = $fields['name'][$name];
|
376
|
}
|
377
|
|
378
|
cache_set($cid, $fields, 'cache_views');
|
379
|
return $fields;
|
380
|
}
|
381
|
|
382
|
|
383
|
/**
|
384
|
* Identify all potential fields which could be used as an iCal SUMMARY.
|
385
|
*/
|
386
|
function date_ical_get_summary_fields($base = 'node', $reset = FALSE) {
|
387
|
static $fields = array();
|
388
|
$empty = array('name' => array(), 'alias' => array());
|
389
|
|
390
|
if (empty($fields[$base]) || $reset) {
|
391
|
$cid = 'date_ical_summary_fields_' . $base;
|
392
|
if (!$reset && $cached = cache_get($cid, 'cache_views')) {
|
393
|
$fields[$base] = $cached->data;
|
394
|
}
|
395
|
else {
|
396
|
$fields[$base] = _date_ical_get_summary_fields($base);
|
397
|
}
|
398
|
}
|
399
|
// Make sure that empty values will be arrays in the expected format.
|
400
|
return !empty($fields) && !empty($fields[$base]) ? $fields[$base] : $empty;
|
401
|
}
|
402
|
|
403
|
/**
|
404
|
* Internal helper function for date_ical_get_summary_fields().
|
405
|
*
|
406
|
* This is a cut down version of _date_views_fields() from
|
407
|
* date_views_fields.inc in date_views module.
|
408
|
*
|
409
|
* @return array
|
410
|
* Array with fieldname, type, and table.
|
411
|
*
|
412
|
* @see date_views_date_views_fields()
|
413
|
*/
|
414
|
function _date_ical_get_summary_fields($base = 'node') {
|
415
|
// Make sure $base is never empty.
|
416
|
if (empty($base)) {
|
417
|
$base = 'node';
|
418
|
}
|
419
|
|
420
|
$cid = 'date_ical_summary_fields_' . $base;
|
421
|
cache_clear_all($cid, 'cache_views');
|
422
|
|
423
|
// Iterate over all the fields that Views knows about.
|
424
|
$all_fields = date_views_views_fetch_fields($base, 'field');
|
425
|
$fields = array();
|
426
|
foreach ($all_fields as $alias => $val) {
|
427
|
$name = $alias;
|
428
|
$tmp = explode('.', $name);
|
429
|
$field_name = $tmp[1];
|
430
|
$table_name = $tmp[0];
|
431
|
|
432
|
// Skip unsupported field types and fields that weren't defined through
|
433
|
// the Field module.
|
434
|
$info = field_info_field($field_name);
|
435
|
$supported_summary_fields = array(
|
436
|
'text',
|
437
|
'text_long',
|
438
|
'text_with_summary',
|
439
|
'node_reference',
|
440
|
'taxonomy_term_reference',
|
441
|
);
|
442
|
if (!$info || !in_array($info['type'], $supported_summary_fields)) {
|
443
|
continue;
|
444
|
}
|
445
|
|
446
|
// Build an array of the field info that we'll need.
|
447
|
$alias = str_replace('.', '_', $alias);
|
448
|
$fields['name'][$name] = array(
|
449
|
'label' => "{$val['group']}: {$val['title']} ($field_name)",
|
450
|
'table_name' => $table_name,
|
451
|
'field_name' => $field_name,
|
452
|
'type' => $info['type'],
|
453
|
);
|
454
|
|
455
|
// These are here only to make this $field array conform to the same format
|
456
|
// as the one returned by _date_views_fields(). They're probably not needed,
|
457
|
// but I thought that consistency would be a good idea.
|
458
|
$fields['name'][$name]['real_field_name'] = $field_name;
|
459
|
$fields['alias'][$alias] = $fields['name'][$name];
|
460
|
}
|
461
|
|
462
|
cache_set($cid, $fields, 'cache_views');
|
463
|
return $fields;
|
464
|
}
|
465
|
|
466
|
/**
|
467
|
* Convert an rrule array to the iCalcreator format.
|
468
|
*
|
469
|
* iCalcreator expects the BYDAY element to be an array like this:
|
470
|
* (array) ( [([plus] ordwk / minus ordwk)], "DAY" => weekday )
|
471
|
*
|
472
|
* But the way that the Date API gives it to us is like this:
|
473
|
* (array) ( [([plus] ordwk / minus ordwk)]weekday )
|
474
|
*/
|
475
|
function _date_ical_convert_rrule_for_icalcreator($rrule) {
|
476
|
$new_rrule = array();
|
477
|
foreach ($rrule as $key => $value) {
|
478
|
if (strtoupper($key) == 'DATA') {
|
479
|
// iCalcreator doesn't expect the 'DATA' key that the Date API gives us.
|
480
|
continue;
|
481
|
}
|
482
|
if (strtoupper($key) == 'UNTIL') {
|
483
|
// iCalcreator expects the 'timestamp' to be array key for UNTIL.
|
484
|
$value['timestamp'] = strtotime($value['datetime']);
|
485
|
}
|
486
|
if (strtoupper($key) == 'BYDAY') {
|
487
|
$new_byday = array();
|
488
|
foreach ($value as $day) {
|
489
|
// Fortunately, the weekday values are always 2 characters.
|
490
|
$weekday = substr($day, -2);
|
491
|
$ordwk = substr($day, 0, -2);
|
492
|
$new_byday[] = array($ordwk, 'DAY' => $weekday);
|
493
|
}
|
494
|
$value = $new_byday;
|
495
|
}
|
496
|
$new_rrule[$key] = $value;
|
497
|
}
|
498
|
return $new_rrule;
|
499
|
}
|
500
|
|
501
|
function _date_ical_detect_icalcreator_version() {
|
502
|
$path = libraries_get_path('iCalcreator');
|
503
|
if (file_exists(DRUPAL_ROOT . "/$path/iCalcreator.class.php")) {
|
504
|
return '2.20';
|
505
|
}
|
506
|
else if (file_exists(DRUPAL_ROOT . "/$path//iCalcreator.php")) {
|
507
|
return '2.22+';
|
508
|
}
|
509
|
else {
|
510
|
return FALSE;
|
511
|
}
|
512
|
}
|