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. It's primarily used for
|
11
|
* debugging.
|
12
|
*/
|
13
|
define('DATE_ICAL_VERSION', '3.1');
|
14
|
|
15
|
/**
|
16
|
* Exception class for generic exceptions thrown by this module.
|
17
|
*/
|
18
|
class DateIcalException extends Exception {}
|
19
|
|
20
|
/**
|
21
|
* Exception used by iCal Fields for when a date field is blank.
|
22
|
*/
|
23
|
class BlankDateFieldException extends DateIcalException {}
|
24
|
|
25
|
/**
|
26
|
* Exception thrown when the Feeds parser plugin fails.
|
27
|
*/
|
28
|
class DateIcalParseException extends DateIcalException {}
|
29
|
|
30
|
/**
|
31
|
* Implements hook_hook_info().
|
32
|
*/
|
33
|
function date_ical_hook_info() {
|
34
|
$hooks = array(
|
35
|
'date_ical_export_html_alter',
|
36
|
'date_ical_export_raw_event_alter',
|
37
|
'date_ical_export_vevent_alter',
|
38
|
'date_ical_export_vcalendar_alter',
|
39
|
'date_ical_export_post_render_alter',
|
40
|
'date_ical_import_vcalendar_alter',
|
41
|
'date_ical_import_component_alter',
|
42
|
'date_ical_import_timezone_alter',
|
43
|
);
|
44
|
|
45
|
return array_fill_keys($hooks, array('group' => 'date_ical'));
|
46
|
}
|
47
|
|
48
|
/**
|
49
|
* Implements hook_views_api().
|
50
|
*/
|
51
|
function date_ical_views_api() {
|
52
|
return array(
|
53
|
'api' => 3,
|
54
|
'path' => drupal_get_path('module', 'date_ical') . '/includes',
|
55
|
);
|
56
|
}
|
57
|
|
58
|
/**
|
59
|
* Implements hook_feeds_plugins().
|
60
|
*/
|
61
|
function date_ical_feeds_plugins() {
|
62
|
$path = drupal_get_path('module', 'date_ical') . '/includes';
|
63
|
$info = array();
|
64
|
$info['DateiCalFeedsParser'] = array(
|
65
|
'name' => 'iCal parser',
|
66
|
'description' => t('Parse iCal feeds.'),
|
67
|
'handler' => array(
|
68
|
'parent' => 'FeedsParser',
|
69
|
'class' => 'DateiCalFeedsParser',
|
70
|
'file' => 'DateiCalFeedsParser.inc',
|
71
|
'path' => $path,
|
72
|
),
|
73
|
);
|
74
|
return $info;
|
75
|
}
|
76
|
|
77
|
/**
|
78
|
* Implements hook_theme().
|
79
|
*/
|
80
|
function date_ical_theme($existing, $type, $theme, $path) {
|
81
|
return array(
|
82
|
'date_ical_icon' => array(
|
83
|
'variables' => array('url' => NULL, 'tooltip' => NULL),
|
84
|
),
|
85
|
);
|
86
|
}
|
87
|
|
88
|
/**
|
89
|
* Theme function for the ical icon used by attached iCal feed.
|
90
|
*
|
91
|
* Available variables are:
|
92
|
* $variables['tooltip'] - The tooltip to be used for the ican feed icon.
|
93
|
* $variables['url'] - The url to the actual iCal feed.
|
94
|
* $variables['view'] - The view object from which the iCal feed is being
|
95
|
* built (useful for contextual information).
|
96
|
*/
|
97
|
function theme_date_ical_icon($variables) {
|
98
|
if (empty($variables['tooltip'])) {
|
99
|
$variables['tooltip'] = t('Add this event to my calendar');
|
100
|
}
|
101
|
$variables['path'] = drupal_get_path('module', 'date_ical') . '/images/ical-feed-icon-34x14.png';
|
102
|
$variables['alt'] = $variables['title'] = $variables['tooltip'];
|
103
|
if ($image = theme('image', $variables)) {
|
104
|
return "<a href='{$variables['url']}' class='ical-icon'>$image</a>";
|
105
|
}
|
106
|
else {
|
107
|
return "<a href='{$variables['url']}' class='ical-icon'>{$variables['tooltip']}</a>";
|
108
|
}
|
109
|
}
|
110
|
|
111
|
/**
|
112
|
* Implements hook_preprocess_HOOK().
|
113
|
*
|
114
|
* Hides the page elements which don't belong in an iCal DESCRIPTION element.
|
115
|
* The display of the body and other fields is controlled by the Manage
|
116
|
* Display settings for the nodes' iCal view mode.
|
117
|
*/
|
118
|
function date_ical_preprocess_node(&$variables) {
|
119
|
if (isset($variables['view_mode']) && $variables['view_mode'] == 'ical') {
|
120
|
// Trick the default node template into not displaying the page title, by
|
121
|
// telling it that this *is* a page.
|
122
|
$variables['page'] = TRUE;
|
123
|
$variables['title_prefix'] = '';
|
124
|
$variables['title_suffix'] = '';
|
125
|
|
126
|
// We don't want to see the author information in our feed.
|
127
|
$variables['display_submitted'] = FALSE;
|
128
|
|
129
|
// Comments and links don't belong in an iCal feed.
|
130
|
if (isset($variables['content']['comments'])) {
|
131
|
unset($variables['content']['comments']);
|
132
|
}
|
133
|
if (isset($variables['content']['links'])) {
|
134
|
unset($variables['content']['links']);
|
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
|
143
|
* View plugin.
|
144
|
*/
|
145
|
function date_ical_entity_info_alter(&$entity_info) {
|
146
|
foreach ($entity_info as $entity_type => $info) {
|
147
|
if (!isset($entity_info[$entity_type]['view modes'])) {
|
148
|
$entity_info[$entity_type]['view modes'] = array();
|
149
|
}
|
150
|
$entity_info[$entity_type]['view modes'] += array(
|
151
|
'ical' => array(
|
152
|
'label' => t('iCal'),
|
153
|
// Set the iCal view mode to default to same settings as the "default"
|
154
|
// view mode, so it won't pollute Features.
|
155
|
'custom settings' => FALSE,
|
156
|
),
|
157
|
);
|
158
|
}
|
159
|
}
|
160
|
|
161
|
/**
|
162
|
* Implements hook_libraries_info().
|
163
|
*/
|
164
|
function date_ical_libraries_info() {
|
165
|
$libraries['iCalcreator'] = array(
|
166
|
'name' => 'iCalcreator',
|
167
|
'vendor url' => 'http://github.com/iCalcreator/iCalcreator',
|
168
|
'download url' => 'http://github.com/iCalcreator/iCalcreator',
|
169
|
'version arguments' => array(
|
170
|
'file' => 'iCalcreator.class.php',
|
171
|
'pattern' => "/define\( 'ICALCREATOR_VERSION', 'iCalcreator ([\d\.]+)' \);/",
|
172
|
'lines' => 100,
|
173
|
),
|
174
|
'files' => array(
|
175
|
'php' => array('iCalcreator.class.php'),
|
176
|
),
|
177
|
);
|
178
|
|
179
|
return $libraries;
|
180
|
}
|
181
|
|
182
|
/**
|
183
|
* Implements hook_help().
|
184
|
*/
|
185
|
function date_ical_help($path, $arg) {
|
186
|
switch ($path) {
|
187
|
case 'admin/help#date_ical':
|
188
|
return '<pre>' . file_get_contents(drupal_get_path('module', 'date_ical') . "/README.txt") . '</pre>';
|
189
|
}
|
190
|
}
|
191
|
|
192
|
/**
|
193
|
* Implements hook_ctools_plugin_api().
|
194
|
*/
|
195
|
function date_ical_ctools_plugin_api($owner, $api) {
|
196
|
if ($owner == 'feeds' && $api == 'plugins') {
|
197
|
return array('version' => 2);
|
198
|
}
|
199
|
}
|
200
|
|
201
|
/**
|
202
|
* Implements hook_feeds_processor_targets_alter().
|
203
|
*
|
204
|
* Adds the "Field Name: Repeat Rule" target to Date Repeat fields.
|
205
|
*
|
206
|
* @see FeedsNodeProcessor::getMappingTargets()
|
207
|
*/
|
208
|
function date_ical_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_name) {
|
209
|
foreach (field_info_instances($entity_type, $bundle_name) as $name => $instance) {
|
210
|
$info = field_info_field($name);
|
211
|
if (in_array($info['type'], array('date', 'datestamp', 'datetime')) && isset($info['settings']['repeat']) && $info['settings']['repeat']) {
|
212
|
$targets[$name . ':rrule'] = array(
|
213
|
'name' => t('@name: Repeat Rule', array('@name' => $instance['label'])),
|
214
|
'callback' => 'date_ical_feeds_set_rrule',
|
215
|
'description' => t('The repeat rule for the @name field.', array('@name' => $instance['label'])),
|
216
|
'real_target' => $name,
|
217
|
);
|
218
|
}
|
219
|
}
|
220
|
}
|
221
|
|
222
|
/**
|
223
|
* Reformats the provided text to be compliant with the iCal spec.
|
224
|
*
|
225
|
* If the text contains HTML tags, those tags will be stripped (with <p> tags
|
226
|
* converted to "\n\n" and link tags converted to footnotes), and uneeded
|
227
|
* whitespace will be cleaned up.
|
228
|
*
|
229
|
* @param string $text
|
230
|
* The text to be sanitized.
|
231
|
*/
|
232
|
function date_ical_sanitize_text($text = '') {
|
233
|
// Use Drupal's built-in HTML to Text converter, which does a mostly
|
234
|
// adequate job of making the text iCal-compliant.
|
235
|
$text = trim(drupal_html_to_text($text));
|
236
|
// Replace instances of more than one space with exactly one space. This
|
237
|
// cleans up the whitespace mess that drupal_html_to_text() leaves behind.
|
238
|
$text = preg_replace("/ +/", " ", $text);
|
239
|
// The call to drupal_html_to_text() above converted <p> to \n\n, and also
|
240
|
// shoved a \n into the string every 80 characters. We don't want those
|
241
|
// single \n's lying around, because iCalcreator will properly "fold" long
|
242
|
// text fields for us. So, we need to remove all instances of \n which
|
243
|
// are neither immediately preceeded, nor followed, by another \n.
|
244
|
// However, \n's which are followed immediately by a > character should
|
245
|
// remain, because of how drupal_html_to_text() converts <blockquote>.
|
246
|
$text = preg_replace("/(?<!\n)\n(?![\n\>])/", " ", $text);
|
247
|
return $text;
|
248
|
}
|
249
|
|
250
|
/**
|
251
|
* Callback specified in date_ical_feeds_processor_targets_alter() for RRULEs.
|
252
|
*
|
253
|
* @param object $source
|
254
|
* The FeedsSource object.
|
255
|
* @param object $entity
|
256
|
* The node that's being built from the iCal element that's being parsed.
|
257
|
* @param string $target
|
258
|
* The machine name of the field into which this RRULE shall be parsed,
|
259
|
* with ":rrule" appended to the end.
|
260
|
* @param string $repeat_rule
|
261
|
* The repeat rule string, formatted like "$rrule|$rdate|$exrule|$exdate".
|
262
|
*/
|
263
|
function date_ical_feeds_set_rrule($source, $entity, $target, $repeat_rule) {
|
264
|
if (empty($repeat_rule)) {
|
265
|
// Don't alter the entity if there's no repeat rule.
|
266
|
return;
|
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
|
// souldn'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
|
);
|
347
|
if (!$info || !in_array($info['type'], $supported_location_fields)) {
|
348
|
continue;
|
349
|
}
|
350
|
|
351
|
// Build an array of the field info that we'll need.
|
352
|
$alias = str_replace('.', '_', $alias);
|
353
|
$fields['name'][$name] = array(
|
354
|
'label' => "{$val['group']}: {$val['title']} ($field_name)",
|
355
|
'table_name' => $table_name,
|
356
|
'field_name' => $field_name,
|
357
|
'type' => $info['type'],
|
358
|
);
|
359
|
|
360
|
// These are here only to make this $field array conform to the same format
|
361
|
// as the one returned by _date_views_fields(). They're probably not needed,
|
362
|
// but I thought that consistency would be a good idea.
|
363
|
$fields['name'][$name]['real_field_name'] = $field_name;
|
364
|
$fields['alias'][$alias] = $fields['name'][$name];
|
365
|
}
|
366
|
|
367
|
cache_set($cid, $fields, 'cache_views');
|
368
|
return $fields;
|
369
|
}
|
370
|
|
371
|
|
372
|
/**
|
373
|
* Identify all potential fields which could be used as an iCal SUMMARY.
|
374
|
*/
|
375
|
function date_ical_get_summary_fields($base = 'node', $reset = FALSE) {
|
376
|
static $fields = array();
|
377
|
$empty = array('name' => array(), 'alias' => array());
|
378
|
|
379
|
if (empty($fields[$base]) || $reset) {
|
380
|
$cid = 'date_ical_summary_fields_' . $base;
|
381
|
if (!$reset && $cached = cache_get($cid, 'cache_views')) {
|
382
|
$fields[$base] = $cached->data;
|
383
|
}
|
384
|
else {
|
385
|
$fields[$base] = _date_ical_get_summary_fields($base);
|
386
|
}
|
387
|
}
|
388
|
// Make sure that empty values will be arrays in the expected format.
|
389
|
return !empty($fields) && !empty($fields[$base]) ? $fields[$base] : $empty;
|
390
|
}
|
391
|
|
392
|
/**
|
393
|
* Internal helper function for date_ical_get_summary_fields().
|
394
|
*
|
395
|
* This is a cut down version of _date_views_fields() from
|
396
|
* date_views_fields.inc in date_views module.
|
397
|
*
|
398
|
* @return array
|
399
|
* Array with fieldname, type, and table.
|
400
|
*
|
401
|
* @see date_views_date_views_fields()
|
402
|
*/
|
403
|
function _date_ical_get_summary_fields($base = 'node') {
|
404
|
// Make sure $base is never empty.
|
405
|
if (empty($base)) {
|
406
|
$base = 'node';
|
407
|
}
|
408
|
|
409
|
$cid = 'date_ical_summary_fields_' . $base;
|
410
|
cache_clear_all($cid, 'cache_views');
|
411
|
|
412
|
// Iterate over all the fields that Views knows about.
|
413
|
$all_fields = date_views_views_fetch_fields($base, 'field');
|
414
|
$fields = array();
|
415
|
foreach ($all_fields as $alias => $val) {
|
416
|
$name = $alias;
|
417
|
$tmp = explode('.', $name);
|
418
|
$field_name = $tmp[1];
|
419
|
$table_name = $tmp[0];
|
420
|
|
421
|
// Skip unsupported field types and fields that weren't defined through
|
422
|
// the Field module.
|
423
|
$info = field_info_field($field_name);
|
424
|
$supported_summary_fields = array(
|
425
|
'text',
|
426
|
'text_long',
|
427
|
'text_with_summary',
|
428
|
'node_reference',
|
429
|
'taxonomy_term_reference',
|
430
|
);
|
431
|
if (!$info || !in_array($info['type'], $supported_summary_fields)) {
|
432
|
continue;
|
433
|
}
|
434
|
|
435
|
// Build an array of the field info that we'll need.
|
436
|
$alias = str_replace('.', '_', $alias);
|
437
|
$fields['name'][$name] = array(
|
438
|
'label' => "{$val['group']}: {$val['title']} ($field_name)",
|
439
|
'table_name' => $table_name,
|
440
|
'field_name' => $field_name,
|
441
|
'type' => $info['type'],
|
442
|
);
|
443
|
|
444
|
// These are here only to make this $field array conform to the same format
|
445
|
// as the one returned by _date_views_fields(). They're probably not needed,
|
446
|
// but I thought that consistency would be a good idea.
|
447
|
$fields['name'][$name]['real_field_name'] = $field_name;
|
448
|
$fields['alias'][$alias] = $fields['name'][$name];
|
449
|
}
|
450
|
|
451
|
cache_set($cid, $fields, 'cache_views');
|
452
|
return $fields;
|
453
|
}
|
454
|
|
455
|
/**
|
456
|
* Convert an rrule array to the iCalcreator format.
|
457
|
*
|
458
|
* iCalcreator expects the BYDAY element to be an array like this:
|
459
|
* (array) ( [([plus] ordwk / minus ordwk)], "DAY" => weekday )
|
460
|
*
|
461
|
* But the way that the Date API gives it to us is like this:
|
462
|
* (array) ( [([plus] ordwk / minus ordwk)]weekday )
|
463
|
*/
|
464
|
function _date_ical_convert_rrule_for_icalcreator($rrule) {
|
465
|
$new_rrule = array();
|
466
|
foreach ($rrule as $key => $value) {
|
467
|
if (strtoupper($key) == 'DATA') {
|
468
|
// iCalcreator doesn't expect the 'DATA' key that the Date API gives us.
|
469
|
continue;
|
470
|
}
|
471
|
if (strtoupper($key) == 'UNTIL') {
|
472
|
// iCalcreator expects the 'timestamp' to be array key for UNTIL.
|
473
|
$value['timestamp'] = strtotime($value['datetime']);
|
474
|
}
|
475
|
if (strtoupper($key) == 'BYDAY') {
|
476
|
$new_byday = array();
|
477
|
foreach ($value as $day) {
|
478
|
// Fortunately, the weekday values are always 2 characters.
|
479
|
$weekday = substr($day, -2);
|
480
|
$ordwk = substr($day, 0, -2);
|
481
|
$new_byday[] = array($ordwk, 'DAY' => $weekday);
|
482
|
}
|
483
|
$value = $new_byday;
|
484
|
}
|
485
|
$new_rrule[$key] = $value;
|
486
|
}
|
487
|
return $new_rrule;
|
488
|
}
|