All-in-One Event Calendar - Version 2.3.4

Version Description

Download this release

Release Info

Developer lpawlik
Plugin Icon 128x128 All-in-One Event Calendar
Version 2.3.4
Comparing to
See all releases

Code changes from version 2.3.3 to 2.3.4

all-in-one-event-calendar.php CHANGED
@@ -5,7 +5,7 @@
5
  * Description: A calendar system with month, week, day, agenda views, upcoming events widget, color-coded categories, recurrence, and import/export of .ics feeds.
6
  * Author: Time.ly Network Inc.
7
  * Author URI: http://time.ly/
8
- * Version: 2.3.3
9
  * Text Domain: all-in-one-event-calendar
10
  * Domain Path: /language
11
  */
5
  * Description: A calendar system with month, week, day, agenda views, upcoming events widget, color-coded categories, recurrence, and import/export of .ics feeds.
6
  * Author: Time.ly Network Inc.
7
  * Author URI: http://time.ly/
8
+ * Version: 2.3.4
9
  * Text Domain: all-in-one-event-calendar
10
  * Domain Path: /language
11
  */
app/config/constants.php CHANGED
@@ -50,7 +50,7 @@ function ai1ec_initiate_constants( $ai1ec_base_dir, $ai1ec_base_url ) {
50
  // = Plugin Version =
51
  // ==================
52
  if ( ! defined( 'AI1EC_VERSION' ) ) {
53
- define( 'AI1EC_VERSION', '2.3.3' );
54
  }
55
 
56
  // ================
50
  // = Plugin Version =
51
  // ==================
52
  if ( ! defined( 'AI1EC_VERSION' ) ) {
53
+ define( 'AI1EC_VERSION', '2.3.4' );
54
  }
55
 
56
  // ================
lib/import-export/ics.php CHANGED
@@ -1,1252 +1,1249 @@
1
- <?php
2
-
3
- /**
4
- * The ics import/export engine.
5
- *
6
- * @author Time.ly Network Inc.
7
- * @since 2.0
8
- *
9
- * @package AI1EC
10
- * @subpackage AI1EC.Import-export
11
- */
12
- class Ai1ec_Ics_Import_Export_Engine
13
- extends Ai1ec_Base
14
- implements Ai1ec_Import_Export_Engine {
15
-
16
- /**
17
- * @var Ai1ec_Taxonomy
18
- */
19
- protected $_taxonomy_model = null;
20
-
21
- /**
22
- * Recurrence rule class. Contains filter method.
23
- *
24
- * @var Ai1ec_Recurrence_Rule
25
- */
26
- protected $_rule_filter = null;
27
-
28
- /* (non-PHPdoc)
29
- * @see Ai1ec_Import_Export_Engine::import()
30
- */
31
- public function import( array $arguments ) {
32
- $cal = $this->_registry->get( 'vcalendar' );
33
- if ( $cal->parse( $arguments['source'] ) ) {
34
- $count = 0;
35
- try {
36
- $result = $this->add_vcalendar_events_to_db(
37
- $cal,
38
- $arguments
39
- );
40
- } catch ( Ai1ec_Parse_Exception $exception ) {
41
- throw new Ai1ec_Parse_Exception(
42
- 'Processing "' . $arguments['source'] .
43
- '" triggered error: ' . $exception->getMessage()
44
- );
45
- }
46
- return $result;
47
- }
48
- throw new Ai1ec_Parse_Exception( 'The passed string is not a valid ics feed' );
49
- }
50
-
51
- /* (non-PHPdoc)
52
- * @see Ai1ec_Import_Export_Engine::export()
53
- */
54
- public function export( array $arguments, array $params = array() ) {
55
- $c = new vcalendar();
56
- $c->setProperty( 'calscale', 'GREGORIAN' );
57
- $c->setProperty( 'method', 'PUBLISH' );
58
- // if no post id are specified do not export those properties
59
- // as they would create a new calendar in outlook.
60
- // a user reported this in AIOEC-982 and said this would fix it
61
- if( true === $arguments['do_not_export_as_calendar'] ) {
62
- $c->setProperty( 'X-WR-CALNAME', get_bloginfo( 'name' ) );
63
- $c->setProperty( 'X-WR-CALDESC', get_bloginfo( 'description' ) );
64
- }
65
- $c->setProperty( 'X-FROM-URL', home_url() );
66
- // Timezone setup
67
- $tz = $this->_registry->get( 'date.timezone' )->get_default_timezone();
68
- if ( $tz ) {
69
- $c->setProperty( 'X-WR-TIMEZONE', $tz );
70
- $tz_xprops = array( 'X-LIC-LOCATION' => $tz );
71
- iCalUtilityFunctions::createTimezone( $c, $tz, $tz_xprops );
72
- }
73
-
74
- $this->_taxonomy_model = $this->_registry->get( 'model.taxonomy' );
75
- $post_ids = array();
76
- foreach ( $arguments['events'] as $event ) {
77
- $post_ids[] = $event->get( 'post_id' );
78
- }
79
- $this->_taxonomy_model->prepare_meta_for_ics( $post_ids );
80
- $this->_registry->get( 'controller.content-filter' )
81
- ->clear_the_content_filters();
82
- foreach ( $arguments['events'] as $event ) {
83
- $c = $this->_insert_event_in_calendar(
84
- $event,
85
- $c,
86
- true,
87
- $params
88
- );
89
- }
90
- $this->_registry->get( 'controller.content-filter' )
91
- ->restore_the_content_filters();
92
- $str = ltrim( $c->createCalendar() );
93
- return $str;
94
- }
95
-
96
- /**
97
- * Check if date-time specification has no (empty) time component.
98
- *
99
- * @param array $datetime Datetime array returned by iCalcreator.
100
- *
101
- * @return bool Timelessness.
102
- */
103
- protected function _is_timeless( array $datetime ) {
104
- $timeless = true;
105
- foreach ( array( 'hour', 'min', 'sec' ) as $field ) {
106
- $timeless &= (
107
- isset( $datetime[$field] ) &&
108
- 0 != $datetime[$field]
109
- )
110
- ? false
111
- : true;
112
- }
113
- return $timeless;
114
- }
115
-
116
- /**
117
- * Process vcalendar instance - add events to database.
118
- *
119
- * @param vcalendar $v Calendar to retrieve data from.
120
- * @param array $args Arbitrary arguments map.
121
- *
122
- * @throws Ai1ec_Parse_Exception
123
- *
124
- * @internal param stdClass $feed Instance of feed (see Ai1ecIcs plugin).
125
- * @internal param string $comment_status WP comment status: 'open' or 'closed'.
126
- * @internal param int $do_show_map Map display status (DB boolean: 0 or 1).
127
- *
128
- * @return int Count of events added to database.
129
- */
130
- public function add_vcalendar_events_to_db(
131
- vcalendar $v,
132
- array $args
133
- ) {
134
- $forced_timezone = null;
135
- $feed = isset( $args['feed'] ) ? $args['feed'] : null;
136
- $comment_status = isset( $args['comment_status'] ) ? $args['comment_status'] : 'open';
137
- $do_show_map = isset( $args['do_show_map'] ) ? $args['do_show_map'] : 0;
138
- $count = 0;
139
- $events_in_db = isset( $args['events_in_db'] ) ? $args['events_in_db'] : 0;
140
- $v->sort();
141
- // Reverse the sort order, so that RECURRENCE-IDs are listed before the
142
- // defining recurrence events, and therefore take precedence during
143
- // caching.
144
- $v->components = array_reverse( $v->components );
145
-
146
- // TODO: select only VEVENT components that occur after, say, 1 month ago.
147
- // Maybe use $v->selectComponents(), which takes into account recurrence
148
-
149
- // Fetch default timezone in case individual properties don't define it
150
- $tz = $v->getComponent( 'vtimezone' );
151
- $timezone = 'sys.default';
152
- if ( ! empty( $tz ) ) {
153
- $timezone = $tz->getProperty( 'TZID' );
154
- }
155
-
156
- $feed_name = $v->getProperty( 'X-WR-CALNAME' );
157
- $x_wr_timezone = $v->getProperty( 'X-WR-TIMEZONE' );
158
- if (
159
- isset( $x_wr_timezone[1] ) &&
160
- is_array( $x_wr_timezone )
161
- ) {
162
- $forced_timezone = (string)$x_wr_timezone[1];
163
- $timezone = empty( $timezone )
164
- ? (string)$x_wr_timezone[1]
165
- : $timezone;
166
- }
167
-
168
- $messages = array();
169
- $local_timezone = $this->_registry->get( 'date.timezone' )
170
- ->get_default_timezone();
171
- if ( empty( $forced_timezone ) ) {
172
- $forced_timezone = $local_timezone;
173
- }
174
- $current_timestamp = $this->_registry->get( 'date.time' )->format_to_gmt();
175
- // initialize empty custom exclusions structure
176
- $exclusions = array();
177
- // go over each event
178
- while ( $e = $v->getComponent( 'vevent' ) ) {
179
- // Event data array.
180
- $data = array();
181
- // =====================
182
- // = Start & end times =
183
- // =====================
184
- $start = $e->getProperty( 'dtstart', 1, true );
185
- $end = $e->getProperty( 'dtend', 1, true );
186
- // For cases where a "VEVENT" calendar component
187
- // specifies a "DTSTART" property with a DATE value type but none
188
- // of "DTEND" nor "DURATION" property, the event duration is taken to
189
- // be one day. For cases where a "VEVENT" calendar component
190
- // specifies a "DTSTART" property with a DATE-TIME value type but no
191
- // "DTEND" property, the event ends on the same calendar date and
192
- // time of day specified by the "DTSTART" property.
193
- if ( empty( $end ) ) {
194
- // #1 if duration is present, assign it to end time
195
- $end = $e->getProperty( 'duration', 1, true, true );
196
- if ( empty( $end ) ) {
197
- // #2 if only DATE value is set for start, set duration to 1 day
198
- if ( ! isset( $start['value']['hour'] ) ) {
199
- $end = array(
200
- 'value' => array(
201
- 'year' => $start['value']['year'],
202
- 'month' => $start['value']['month'],
203
- 'day' => $start['value']['day'] + 1,
204
- 'hour' => 0,
205
- 'min' => 0,
206
- 'sec' => 0,
207
- ),
208
- );
209
- if ( isset( $start['value']['tz'] ) ) {
210
- $end['value']['tz'] = $start['value']['tz'];
211
- }
212
- } else {
213
- // #3 set end date to start time
214
- $end = $start;
215
- }
216
- }
217
- }
218
-
219
- $categories = $e->getProperty( "CATEGORIES", false, true );
220
- $imported_cat = array( Ai1ec_Event_Taxonomy::CATEGORIES => array() );
221
- // If the user chose to preserve taxonomies during import, add categories.
222
- if( $categories && $feed->keep_tags_categories ) {
223
- $imported_cat = $this->add_categories_and_tags(
224
- $categories['value'],
225
- $imported_cat,
226
- false,
227
- true
228
- );
229
- }
230
- $feed_categories = $feed->feed_category;
231
- if( ! empty( $feed_categories ) ) {
232
- $imported_cat = $this->add_categories_and_tags(
233
- $feed_categories,
234
- $imported_cat,
235
- false,
236
- false
237
- );
238
- }
239
- $tags = $e->getProperty( "X-TAGS", false, true );
240
-
241
- $imported_tags = array( Ai1ec_Event_Taxonomy::TAGS => array() );
242
- // If the user chose to preserve taxonomies during import, add tags.
243
- if( $tags && $feed->keep_tags_categories ) {
244
- $imported_tags = $this->add_categories_and_tags(
245
- $tags[1]['value'],
246
- $imported_tags,
247
- true,
248
- true
249
- );
250
- }
251
- $feed_tags = $feed->feed_tags;
252
- if( ! empty( $feed_tags ) ) {
253
- $imported_tags = $this->add_categories_and_tags(
254
- $feed_tags,
255
- $imported_tags,
256
- true,
257
- true
258
- );
259
- }
260
- // Event is all-day if no time components are defined
261
- $allday = $this->_is_timeless( $start['value'] ) &&
262
- $this->_is_timeless( $end['value'] );
263
- // Also check the proprietary MS all-day field.
264
- $ms_allday = $e->getProperty( 'X-MICROSOFT-CDO-ALLDAYEVENT' );
265
- if ( ! empty( $ms_allday ) && $ms_allday[1] == 'TRUE' ) {
266
- $allday = true;
267
- }
268
- $event_timezone = $timezone;
269
- if ( $allday ) {
270
- $event_timezone = $local_timezone;
271
- }
272
- $start = $this->_time_array_to_datetime(
273
- $start,
274
- $event_timezone,
275
- $feed->import_timezone ? $forced_timezone : null
276
- );
277
- $end = $this->_time_array_to_datetime(
278
- $end,
279
- $event_timezone,
280
- $feed->import_timezone ? $forced_timezone : null
281
- );
282
-
283
- if ( false === $start || false === $end ) {
284
- throw new Ai1ec_Parse_Exception(
285
- 'Failed to parse one or more dates given timezone "' .
286
- var_export( $event_timezone, true ) . '"'
287
- );
288
- continue;
289
- }
290
-
291
- // If all-day, and start and end times are equal, then this event has
292
- // invalid end time (happens sometimes with poorly implemented iCalendar
293
- // exports, such as in The Event Calendar), so set end time to 1 day
294
- // after start time.
295
- if ( $allday && $start->format() === $end->format() ) {
296
- $end->adjust_day( +1 );
297
- }
298
-
299
- $data += compact( 'start', 'end', 'allday' );
300
-
301
- // =======================================
302
- // = Recurrence rules & recurrence dates =
303
- // =======================================
304
- if ( $rrule = $e->createRrule() ) {
305
- $rrule = explode( ':', $rrule );
306
- $rrule = trim( end( $rrule ) );
307
- }
308
-
309
- if ( $exrule = $e->createExrule() ) {
310
- $exrule = explode( ':', $exrule );
311
- $exrule = trim( end( $exrule ) );
312
- }
313
-
314
- if ( $rdate = $e->createRdate() ) {
315
- $rdate = explode( ':', $rdate );
316
- $rdate = trim( end( $rdate ) );
317
- }
318
-
319
- // ===================
320
- // = Exception dates =
321
- // ===================
322
- $exdate = '';
323
- if ( $exdates = $e->createExdate() ){
324
- // We may have two formats:
325
- // one exdate with many dates ot more EXDATE rules
326
- $exdates = explode( 'EXDATE', $exdates );
327
- $def_timezone = $this->_get_import_timezone( $event_timezone );
328
- foreach ( $exdates as $exd ) {
329
- if ( empty( $exd ) ) {
330
- continue;
331
- }
332
- $exploded = explode( ':', $exd );
333
- $excpt_timezone = $def_timezone;
334
- $excpt_date = null;
335
- foreach ( $exploded as $particle ) {
336
- if ( ';TZID=' === substr( $particle, 0, 6 ) ) {
337
- $excpt_timezone = substr( $particle, 6 );
338
- } else {
339
- $excpt_date = trim( $particle );
340
- }
341
- }
342
- // Google sends YYYYMMDD for all-day excluded events
343
- if (
344
- $allday &&
345
- 8 === strlen( $excpt_date )
346
- ) {
347
- $excpt_date .= 'T000000Z';
348
- $excpt_timezone = 'UTC';
349
- }
350
- $ex_dt = $this->_registry->get(
351
- 'date.time',
352
- $excpt_date,
353
- $excpt_timezone
354
- );
355
- if ( $ex_dt ) {
356
- if ( isset( $exdate{0} ) ) {
357
- $exdate .= ',';
358
- }
359
- $exdate .= $ex_dt->format( 'Ymd\THis', $excpt_timezone );
360
- }
361
- }
362
- }
363
- // Add custom exclusions if there any
364
- $recurrence_id = $e->getProperty( 'recurrence-id' );
365
- if (
366
- false === $recurrence_id &&
367
- ! empty( $exclusions[$e->getProperty( 'uid' )] )
368
- ) {
369
- if ( isset( $exdate{0} ) ) {
370
- $exdate .= ',';
371
- }
372
- $exdate .= implode( ',', $exclusions[$e->getProperty( 'uid' )] );
373
- }
374
- // ========================
375
- // = Latitude & longitude =
376
- // ========================
377
- $latitude = $longitude = NULL;
378
- $geo_tag = $e->getProperty( 'geo' );
379
- if ( is_array( $geo_tag ) ) {
380
- if (
381
- isset( $geo_tag['latitude'] ) &&
382
- isset( $geo_tag['longitude'] )
383
- ) {
384
- $latitude = (float)$geo_tag['latitude'];
385
- $longitude = (float)$geo_tag['longitude'];
386
- }
387
- } else if ( ! empty( $geo_tag ) && false !== strpos( $geo_tag, ';' ) ) {
388
- list( $latitude, $longitude ) = explode( ';', $geo_tag, 2 );
389
- $latitude = (float)$latitude;
390
- $longitude = (float)$longitude;
391
- }
392
- unset( $geo_tag );
393
- if ( NULL !== $latitude ) {
394
- $data += compact( 'latitude', 'longitude' );
395
- // Check the input coordinates checkbox, otherwise lat/long data
396
- // is not present on the edit event page
397
- $data['show_coordinates'] = 1;
398
- }
399
-
400
- // ===================
401
- // = Venue & address =
402
- // ===================
403
- $address = $venue = '';
404
- $location = $e->getProperty( 'location' );
405
- $matches = array();
406
- // This regexp matches a venue / address in the format
407
- // "venue @ address" or "venue - address".
408
- preg_match( '/\s*(.*\S)\s+[\-@]\s+(.*)\s*/', $location, $matches );
409
- // if there is no match, it's not a combined venue + address
410
- if ( empty( $matches ) ) {
411
- // temporary fix for Mac ICS import. Se AIOEC-2187
412
- // and https://github.com/iCalcreator/iCalcreator/issues/13
413
- $location = str_replace( '\n', "\n", $location );
414
- // if there is a comma, probably it's an address
415
- if ( false === strpos( $location, ',' ) ) {
416
- $venue = $location;
417
- } else {
418
- $address = $location;
419
- }
420
- } else {
421
- $venue = isset( $matches[1] ) ? $matches[1] : '';
422
- $address = isset( $matches[2] ) ? $matches[2] : '';
423
- }
424
-
425
- // =====================================================
426
- // = Set show map status based on presence of location =
427
- // =====================================================
428
- $event_do_show_map = $do_show_map;
429
- if (
430
- 1 === $do_show_map &&
431
- NULL === $latitude &&
432
- empty( $address )
433
- ) {
434
- $event_do_show_map = 0;
435
- }
436
-
437
- // ==================
438
- // = Cost & tickets =
439
- // ==================
440
- $cost = $e->getProperty( 'X-COST' );
441
- $cost = $cost ? $cost[1] : '';
442
- $ticket_url = $e->getProperty( 'X-TICKETS-URL' );
443
- $ticket_url = $ticket_url ? $ticket_url[1] : '';
444
-
445
- // ===============================
446
- // = Contact name, phone, e-mail =
447
- // ===============================
448
- $organizer = $e->getProperty( 'organizer' );
449
- if (
450
- 'MAILTO:' === substr( $organizer, 0, 7 ) &&
451
- false === strpos( $organizer, '@' )
452
- ) {
453
- $organizer = substr( $organizer, 7 );
454
- }
455
- $contact = $e->getProperty( 'contact' );
456
- $elements = explode( ';', $contact, 4 );
457
- foreach ( $elements as $el ) {
458
- $el = trim( $el );
459
- // Detect e-mail address.
460
- if ( false !== strpos( $el, '@' ) ) {
461
- $data['contact_email'] = $el;
462
- }
463
- // Detect URL.
464
- elseif ( false !== strpos( $el, '://' ) ) {
465
- $data['contact_url'] = $el;
466
- }
467
- // Detect phone number.
468
- elseif ( preg_match( '/\d/', $el ) ) {
469
- $data['contact_phone'] = $el;
470
- }
471
- // Default to name.
472
- else {
473
- $data['contact_name'] = $el;
474
- }
475
- }
476
- if ( ! isset( $data['contact_name'] ) || ! $data['contact_name'] ) {
477
- // If no contact name, default to organizer property.
478
- $data['contact_name'] = $organizer;
479
- }
480
- // Store yet-unsaved values to the $data array.
481
- $data += array(
482
- 'recurrence_rules' => $rrule,
483
- 'exception_rules' => $exrule,
484
- 'recurrence_dates' => $rdate,
485
- 'exception_dates' => $exdate,
486
- 'venue' => $venue,
487
- 'address' => $address,
488
- 'cost' => $cost,
489
- 'ticket_url' => $ticket_url,
490
- 'show_map' => $event_do_show_map,
491
- 'ical_feed_url' => $feed->feed_url,
492
- 'ical_source_url' => $e->getProperty( 'url' ),
493
- 'ical_organizer' => $organizer,
494
- 'ical_contact' => $contact,
495
- 'ical_uid' => $this->_get_ical_uid( $e ),
496
- 'categories' => array_keys( $imported_cat[Ai1ec_Event_Taxonomy::CATEGORIES] ),
497
- 'tags' => array_keys( $imported_tags[Ai1ec_Event_Taxonomy::TAGS] ),
498
- 'feed' => $feed,
499
- 'post' => array(
500
- 'post_status' => 'publish',
501
- 'comment_status' => $comment_status,
502
- 'post_type' => AI1EC_POST_TYPE,
503
- 'post_author' => 1,
504
- 'post_title' => $e->getProperty( 'summary' ),
505
- 'post_content' => stripslashes(
506
- str_replace(
507
- '\n',
508
- "\n",
509
- $e->getProperty( 'description' )
510
- )
511
- ),
512
- ),
513
- );
514
- // register any custom exclusions for given event
515
- $exclusions = $this->_add_recurring_events_exclusions(
516
- $e,
517
- $exclusions,
518
- $start
519
- );
520
-
521
- // Create event object.
522
- $data = apply_filters(
523
- 'ai1ec_pre_init_event_from_feed',
524
- $data,
525
- $e,
526
- $feed
527
- );
528
-
529
- $event = $this->_registry->get( 'model.event', $data );
530
-
531
- // Instant Event
532
- $is_instant = $e->getProperty( 'X-INSTANT-EVENT' );
533
- if ( $is_instant ) {
534
- $event->set_no_end_time();
535
- }
536
-
537
- $recurrence = $event->get( 'recurrence_rules' );
538
- $search = $this->_registry->get( 'model.search' );
539
- // first let's check by UID
540
- $matching_event_id = $search
541
- ->get_matching_event_by_uid_and_url(
542
- $event->get( 'ical_uid' ),
543
- $event->get( 'ical_feed_url' )
544
- );
545
- // if no result, perform the legacy check.
546
- if ( null === $matching_event_id ) {
547
- $matching_event_id = $search
548
- ->get_matching_event_id(
549
- $event->get( 'ical_uid' ),
550
- $event->get( 'ical_feed_url' ),
551
- $event->get( 'start' ),
552
- ! empty( $recurrence )
553
- );
554
- }
555
- if ( null === $matching_event_id ) {
556
- // =================================================
557
- // = Event was not found, so store it and the post =
558
- // =================================================
559
- $event->save();
560
- $count++;
561
- } else {
562
- // ======================================================
563
- // = Event was found, let's store the new event details =
564
- // ======================================================
565
-
566
- // Update the post
567
- $post = get_post( $matching_event_id );
568
-
569
- if ( null !== $post ) {
570
- $post->post_title = $event->get( 'post' )->post_title;
571
- $post->post_content = $event->get( 'post' )->post_content;
572
- wp_update_post( $post );
573
-
574
- // Update the event
575
- $event->set( 'post_id', $matching_event_id );
576
- $event->set( 'post', $post );
577
- $event->save( true );
578
- $count++;
579
- }
580
- }
581
- do_action( 'ai1ec_ics_event_saved', $event, $feed );
582
-
583
- // import not standard taxonomies.
584
- unset( $imported_cat[Ai1ec_Event_Taxonomy::CATEGORIES] );
585
- foreach ( $imported_cat as $tax_name => $ids ) {
586
- wp_set_post_terms( $event->get( 'post_id' ), array_keys( $ids ), $tax_name );
587
- }
588
-
589
- unset( $imported_tags[Ai1ec_Event_Taxonomy::TAGS] );
590
- foreach ( $imported_tags as $tax_name => $ids ) {
591
- wp_set_post_terms( $event->get( 'post_id' ), array_keys( $ids ), $tax_name );
592
- }
593
-
594
- unset( $events_in_db[$event->get( 'post_id' )] );
595
- }
596
-
597
- return array(
598
- 'count' => $count,
599
- 'events_to_delete' => $events_in_db,
600
- 'messages' => $messages,
601
- 'name' => $feed_name,
602
- );
603
- }
604
-
605
- /**
606
- * Parse importable feed timezone to sensible value.
607
- *
608
- * @param string $def_timezone Timezone value from feed.
609
- *
610
- * @return string Valid timezone name to use.
611
- */
612
- protected function _get_import_timezone( $def_timezone ) {
613
- $parser = $this->_registry->get( 'date.timezone' );
614
- $timezone = $parser->get_name( $def_timezone );
615
- if ( false === $timezone ) {
616
- return 'sys.default';
617
- }
618
- return $timezone;
619
- }
620
-
621
- /**
622
- * time_array_to_timestamp function
623
- *
624
- * Converts time array to time string.
625
- * Passed array: Array( 'year', 'month', 'day', ['hour', 'min', 'sec', ['tz']] )
626
- * Return int: UNIX timestamp in GMT
627
- *
628
- * @param array $time iCalcreator time property array
629
- * (*full* format expected)
630
- * @param string $def_timezone Default time zone in case not defined
631
- * in $time
632
- * @param null|string $forced_timezone Timezone to use instead of UTC.
633
- *
634
- * @return int UNIX timestamp
635
- **/
636
- protected function _time_array_to_datetime(
637
- array $time,
638
- $def_timezone,
639
- $forced_timezone = null
640
- ) {
641
- $timezone = '';
642
- if ( isset( $time['params']['TZID'] ) ) {
643
- $timezone = $time['params']['TZID'];
644
- } elseif (
645
- isset( $time['value']['tz'] ) &&
646
- 'Z' === $time['value']['tz']
647
- ) {
648
- $timezone = 'UTC';
649
- }
650
- if ( empty( $timezone ) ) {
651
- $timezone = $def_timezone;
652
- }
653
-
654
- $date_time = $this->_registry->get( 'date.time' );
655
-
656
- if ( ! empty( $timezone ) ) {
657
- $parser = $this->_registry->get( 'date.timezone' );
658
- $timezone = $parser->get_name( $timezone );
659
- if ( false === $timezone ) {
660
- return false;
661
- }
662
- $date_time->set_timezone( $timezone );
663
- }
664
-
665
- if ( ! isset( $time['value']['hour'] ) ) {
666
- $time['value']['hour'] = $time['value']['min'] =
667
- $time['value']['sec'] = 0;
668
- }
669
-
670
- $date_time->set_date(
671
- $time['value']['year'],
672
- $time['value']['month'],
673
- $time['value']['day']
674
- )->set_time(
675
- $time['value']['hour'],
676
- $time['value']['min'],
677
- $time['value']['sec']
678
- );
679
- if (
680
- 'UTC' === $timezone &&
681
- null !== $forced_timezone
682
- ) {
683
- $date_time->set_timezone( $forced_timezone );
684
- }
685
- return $date_time;
686
- }
687
-
688
- /**
689
- * Convert an event from a feed into a new Ai1ec_Event object and add it to
690
- * the calendar.
691
- *
692
- * @param Ai1ec_Event $event Event object.
693
- * @param vcalendar $calendar Calendar object.
694
- * @param bool $export States whether events are created for export.
695
- * @param array $params Additional parameters for export.
696
- *
697
- * @return void
698
- */
699
- protected function _insert_event_in_calendar(
700
- Ai1ec_Event $event,
701
- vcalendar $calendar,
702
- $export = false,
703
- array $params = array()
704
- ) {
705
-
706
- $tz = $this->_registry->get( 'date.timezone' )
707
- ->get_default_timezone();
708
-
709
- $e = & $calendar->newComponent( 'vevent' );
710
- $uid = '';
711
- if ( $event->get( 'ical_uid' ) ) {
712
- $uid = addcslashes( $event->get( 'ical_uid' ), "\\;,\n" );
713
- } else {
714
- $uid = $event->get_uid();
715
- $event->set( 'ical_uid', $uid );
716
- $event->save( true );
717
- }
718
- $e->setProperty( 'uid', $this->_sanitize_value( $uid ) );
719
- $e->setProperty(
720
- 'url',
721
- get_permalink( $event->get( 'post_id' ) )
722
- );
723
-
724
- // =========================
725
- // = Summary & description =
726
- // =========================
727
- $e->setProperty(
728
- 'summary',
729
- $this->_sanitize_value(
730
- html_entity_decode(
731
- apply_filters( 'the_title', $event->get( 'post' )->post_title ),
732
- ENT_QUOTES,
733
- 'UTF-8'
734
- )
735
- )
736
- );
737
-
738
- $content = apply_filters(
739
- 'ai1ec_the_content',
740
- apply_filters(
741
- 'the_content',
742
- $event->get( 'post' )->post_content
743
- )
744
- );
745
- $content = str_replace(']]>', ']]&gt;', $content);
746
- $content = html_entity_decode( $content, ENT_QUOTES, 'UTF-8' );
747
-
748
- // Prepend featured image if available.
749
- $size = null;
750
- $avatar = $this->_registry->get( 'view.event.avatar' );
751
- $matches = $avatar->get_image_from_content( $content );
752
- // if no img is already present - add thumbnail
753
- if ( empty( $matches ) ) {
754
- if ( $img_url = $avatar->get_post_thumbnail_url( $event, $size ) ) {
755
- $content = '<div class="ai1ec-event-avatar alignleft timely"><img src="' .
756
- esc_attr( $img_url ) . '" width="' . $size[0] . '" height="' .
757
- $size[1] . '" /></div>' . $content;
758
- }
759
- }
760
-
761
- if ( isset( $params['no_html'] ) && $params['no_html'] ) {
762
- $e->setProperty(
763
- 'description',
764
- $this->_sanitize_value(
765
- strip_tags( strip_shortcodes( $content ) )
766
- )
767
- );
768
- if ( ! empty( $content ) ) {
769
- $html_content = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">\n' .
770
- '<HTML>\n<HEAD>\n<TITLE></TITLE>\n</HEAD>\n<BODY>' . $content .
771
- '</BODY></HTML>';
772
- $e->setProperty(
773
- 'X-ALT-DESC',
774
- $this->_sanitize_value( $html_content ),
775
- array(
776
- 'FMTTYPE' => 'text/html',
777
- )
778
- );
779
- unset( $html_content );
780
- }
781
- } else {
782
- $e->setProperty( 'description', $this->_sanitize_value( $content ) );
783
- }
784
- $revision = (int)current(
785
- array_keys(
786
- wp_get_post_revisions( $event->get( 'post_id' ) )
787
- )
788
- );
789
- $e->setProperty( 'sequence', $revision );
790
-
791
- // =====================
792
- // = Start & end times =
793
- // =====================
794
- $dtstartstring = '';
795
- $dtstart = $dtend = array();
796
- if ( $event->is_allday() ) {
797
- $dtstart['VALUE'] = $dtend['VALUE'] = 'DATE';
798
- // For exporting all day events, don't set a timezone
799
- if ( $tz && ! $export ) {
800
- $dtstart['TZID'] = $dtend['TZID'] = $tz;
801
- }
802
-
803
- // For exportin' all day events, only set the date not the time
804
- if ( $export ) {
805
- $e->setProperty(
806
- 'dtstart',
807
- $this->_sanitize_value(
808
- $event->get( 'start' )->format( 'Ymd' )
809
- ),
810
- $dtstart
811
- );
812
- $e->setProperty(
813
- 'dtend',
814
- $this->_sanitize_value(
815
- $event->get( 'end' )->format( 'Ymd' )
816
- ),
817
- $dtend
818
- );
819
- } else {
820
- $e->setProperty(
821
- 'dtstart',
822
- $this->_sanitize_value(
823
- $event->get( 'start' )->format( "Ymd\T" )
824
- ),
825
- $dtstart
826
- );
827
- $e->setProperty(
828
- 'dtend',
829
- $this->_sanitize_value(
830
- $event->get( 'end' )->format( "Ymd\T" )
831
- ),
832
- $dtend
833
- );
834
- }
835
- } else {
836
- if ( $tz ) {
837
- $dtstart['TZID'] = $dtend['TZID'] = $tz;
838
- }
839
- // This is used later.
840
- $dtstartstring = $event->get( 'start' )->format( "Ymd\THis" );
841
- $e->setProperty(
842
- 'dtstart',
843
- $this->_sanitize_value( $dtstartstring ),
844
- $dtstart
845
- );
846
-
847
- if ( false === (bool)$event->get( 'instant_event' ) ) {
848
- $e->setProperty(
849
- 'dtend',
850
- $this->_sanitize_value(
851
- $event->get( 'end' )->format( "Ymd\THis" )
852
- ),
853
- $dtend
854
- );
855
- }
856
- }
857
-
858
- // ========================
859
- // = Latitude & longitude =
860
- // ========================
861
- if (
862
- floatval( $event->get( 'latitude' ) ) ||
863
- floatval( $event->get( 'longitude' ) )
864
- ) {
865
- $e->setProperty(
866
- 'geo',
867
- $event->get( 'latitude' ),
868
- $event->get( 'longitude' )
869
- );
870
- }
871
-
872
- // ===================
873
- // = Venue & address =
874
- // ===================
875
- if ( $event->get( 'venue' ) || $event->get( 'address' ) ) {
876
- $location = array(
877
- $event->get( 'venue' ),
878
- $event->get( 'address' )
879
- );
880
- $location = array_filter( $location );
881
- $location = implode( ' @ ', $location );
882
- $e->setProperty( 'location', $this->_sanitize_value( $location ) );
883
- }
884
-
885
- $categories = array();
886
- $language = get_bloginfo( 'language' );
887
-
888
- foreach (
889
- $this->_taxonomy_model->get_post_categories(
890
- $event->get( 'post_id' )
891
- )
892
- as $cat
893
- ) {
894
- $categories[] = $cat->name;
895
- }
896
- $e->setProperty(
897
- 'categories',
898
- implode( ',', $categories ),
899
- array( "LANGUAGE" => $language )
900
- );
901
- $tags = array();
902
- foreach (
903
- $this->_taxonomy_model->get_post_tags( $event->get( 'post_id' ) )
904
- as $tag
905
- ) {
906
- $tags[] = $tag->name;
907
- }
908
- if( ! empty( $tags) ) {
909
- $e->setProperty(
910
- 'X-TAGS',
911
- implode( ',', $tags ),
912
- array( "LANGUAGE" => $language )
913
- );
914
- }
915
- // ==================
916
- // = Cost & tickets =
917
- // ==================
918
- if ( $event->get( 'cost' ) ) {
919
- $e->setProperty(
920
- 'X-COST',
921
- $this->_sanitize_value( $event->get( 'cost' ) )
922
- );
923
- }
924
- if ( $event->get( 'ticket_url' ) ) {
925
- $e->setProperty(
926
- 'X-TICKETS-URL',
927
- $this->_sanitize_value(
928
- $event->get( 'ticket_url' )
929
- )
930
- );
931
- }
932
- // =================
933
- // = Instant Event =
934
- // =================
935
- if ( $event->is_instant() ) {
936
- $e->setProperty(
937
- 'X-INSTANT-EVENT',
938
- $this->_sanitize_value( $event->is_instant() )
939
- );
940
- }
941
-
942
- // ====================================
943
- // = Contact name, phone, e-mail, URL =
944
- // ====================================
945
- $contact = array(
946
- $event->get( 'contact_name' ),
947
- $event->get( 'contact_phone' ),
948
- $event->get( 'contact_email' ),
949
- $event->get( 'contact_url' ),
950
- );
951
- $contact = array_filter( $contact );
952
- $contact = implode( '; ', $contact );
953
- $e->setProperty( 'contact', $this->_sanitize_value( $contact ) );
954
-
955
- // ====================
956
- // = Recurrence rules =
957
- // ====================
958
- $rrule = array();
959
- $recurrence = $event->get( 'recurrence_rules' );
960
- $recurrence = $this->_filter_rule( $recurrence );
961
- if ( ! empty( $recurrence ) ) {
962
- $rules = array();
963
- foreach ( explode( ';', $recurrence ) as $v) {
964
- if ( strpos( $v, '=' ) === false ) {
965
- continue;
966
- }
967
-
968
- list( $k, $v ) = explode( '=', $v );
969
- $k = strtoupper( $k );
970
- // If $v is a comma-separated list, turn it into array for iCalcreator
971
- switch ( $k ) {
972
- case 'BYSECOND':
973
- case 'BYMINUTE':
974
- case 'BYHOUR':
975
- case 'BYDAY':
976
- case 'BYMONTHDAY':
977
- case 'BYYEARDAY':
978
- case 'BYWEEKNO':
979
- case 'BYMONTH':
980
- case 'BYSETPOS':
981
- $exploded = explode( ',', $v );
982
- break;
983
- default:
984
- $exploded = $v;
985
- break;
986
- }
987
- // iCalcreator requires a more complex array structure for BYDAY...
988
- if ( $k == 'BYDAY' ) {
989
- $v = array();
990
- foreach ( $exploded as $day ) {
991
- $v[] = array( 'DAY' => $day );
992
- }
993
- } else {
994
- $v = $exploded;
995
- }
996
- $rrule[ $k ] = $v;
997
- }
998
- }
999
-
1000
- // ===================
1001
- // = Exception rules =
1002
- // ===================
1003
- $exceptions = $event->get( 'exception_rules' );
1004
- $exceptions = $this->_filter_rule( $exceptions );
1005
- $exrule = array();
1006
- if ( ! empty( $exceptions ) ) {
1007
- $rules = array();
1008
-
1009
- foreach ( explode( ';', $exceptions ) as $v) {
1010
- if ( strpos( $v, '=' ) === false ) {
1011
- continue;
1012
- }
1013
-
1014
- list($k, $v) = explode( '=', $v );
1015
- $k = strtoupper( $k );
1016
- // If $v is a comma-separated list, turn it into array for iCalcreator
1017
- switch ( $k ) {
1018
- case 'BYSECOND':
1019
- case 'BYMINUTE':
1020
- case 'BYHOUR':
1021
- case 'BYDAY':
1022
- case 'BYMONTHDAY':
1023
- case 'BYYEARDAY':
1024
- case 'BYWEEKNO':
1025
- case 'BYMONTH':
1026
- case 'BYSETPOS':
1027
- $exploded = explode( ',', $v );
1028
- break;
1029
- default:
1030
- $exploded = $v;
1031
- break;
1032
- }
1033
- // iCalcreator requires a more complex array structure for BYDAY...
1034
- if ( $k == 'BYDAY' ) {
1035
- $v = array();
1036
- foreach ( $exploded as $day ) {
1037
- $v[] = array( 'DAY' => $day );
1038
- }
1039
- } else {
1040
- $v = $exploded;
1041
- }
1042
- $exrule[ $k ] = $v;
1043
- }
1044
- }
1045
-
1046
- // add rrule to exported calendar
1047
- if ( ! empty( $rrule ) && ! isset( $rrule['RDATE'] ) ) {
1048
- $e->setProperty( 'rrule', $this->_sanitize_value( $rrule ) );
1049
- }
1050
- // add exrule to exported calendar
1051
- if ( ! empty( $exrule ) && ! isset( $exrule['EXDATE'] ) ) {
1052
- $e->setProperty( 'exrule', $this->_sanitize_value( $exrule ) );
1053
- }
1054
-
1055
- // ===================
1056
- // = Exception dates =
1057
- // ===================
1058
- // For all day events that use a date as DTSTART, date must be supplied
1059
- // For other other events which use DATETIME, we must use that as well
1060
- // We must also match the exact starting time
1061
- $recurrence_dates = $event->get( 'recurrence_dates' );
1062
- $recurrence_dates = $this->_filter_rule( $recurrence_dates );
1063
- if ( ! empty( $recurrence_dates ) ) {
1064
- $params = array(
1065
- 'VALUE' => 'DATE-TIME',
1066
- 'TZID' => $tz,
1067
- );
1068
- $dt_suffix = $event->get( 'start' )->format( '\THis' );
1069
- foreach (
1070
- explode( ',', $recurrence_dates )
1071
- as $exdate
1072
- ) {
1073
- // date-time string in EXDATES is formatted as 'Ymd\THis\Z', that
1074
- // means - in UTC timezone, thus we use `format_to_gmt` here.
1075
- $exdate = $this->_registry->get( 'date.time', $exdate )
1076
- ->format_to_gmt( 'Ymd' );
1077
- $e->setProperty(
1078
- 'rdate',
1079
- array( $exdate . $dt_suffix ),
1080
- $params
1081
- );
1082
- }
1083
- }
1084
- $exception_dates = $event->get( 'exception_dates' );
1085
- $exception_dates = $this->_filter_rule( $exception_dates );
1086
- if ( ! empty( $exception_dates ) ) {
1087
- $params = array(
1088
- 'VALUE' => 'DATE-TIME',
1089
- 'TZID' => $tz,
1090
- );
1091
- $dt_suffix = $event->get( 'start' )->format( '\THis' );
1092
- foreach (
1093
- explode( ',', $exception_dates )
1094
- as $exdate
1095
- ) {
1096
- // date-time string in EXDATES is formatted as 'Ymd\THis\Z', that
1097
- // means - in UTC timezone, thus we use `format_to_gmt` here.
1098
- $exdate = $this->_registry->get( 'date.time', $exdate )
1099
- ->format_to_gmt( 'Ymd' );
1100
- $e->setProperty(
1101
- 'exdate',
1102
- array( $exdate . $dt_suffix ),
1103
- $params
1104
- );
1105
- }
1106
- }
1107
- return $calendar;
1108
- }
1109
-
1110
- /**
1111
- * _sanitize_value method
1112
- *
1113
- * Convert value, so it be safe to use on ICS feed. Used before passing to
1114
- * iCalcreator methods, for rendering.
1115
- *
1116
- * @param string $value Text to be sanitized
1117
- *
1118
- * @return string Safe value, for use in HTML
1119
- */
1120
- protected function _sanitize_value( $value ) {
1121
- if ( ! is_scalar( $value ) ) {
1122
- return $value;
1123
- }
1124
- $safe_eol = "\n";
1125
- $value = strtr(
1126
- trim( $value ),
1127
- array(
1128
- "\r\n" => $safe_eol,
1129
- "\r" => $safe_eol,
1130
- "\n" => $safe_eol,
1131
- )
1132
- );
1133
- $value = addcslashes( $value, '\\' );
1134
- return $value;
1135
- }
1136
-
1137
- /**
1138
- * Takes a comma-separated list of tags or categories.
1139
- * If they exist, reuses
1140
- * the existing ones. If not, creates them.
1141
- *
1142
- * The $imported_terms array uses keys to store values rather than values to
1143
- * speed up lookups (using isset() insted of in_array()).
1144
- *
1145
- * @param string $terms
1146
- * @param array $imported_terms
1147
- * @param boolean $is_tag
1148
- * @param boolean $use_name
1149
- *
1150
- * @return array
1151
- */
1152
- public function add_categories_and_tags(
1153
- $terms,
1154
- array $imported_terms,
1155
- $is_tag,
1156
- $use_name
1157
- ) {
1158
- $taxonomy = $is_tag ? 'events_tags' : 'events_categories';
1159
- $categories = explode( ',', $terms );
1160
- $event_taxonomy = $this->_registry->get( 'model.event.taxonomy' );
1161
-
1162
- foreach ( $categories as $cat_name ) {
1163
- $cat_name = trim( $cat_name );
1164
- if ( empty( $cat_name ) ) {
1165
- continue;
1166
- }
1167
- $term = $event_taxonomy->initiate_term( $cat_name, $taxonomy, ! $use_name );
1168
- if ( false !== $term ) {
1169
- if ( ! isset( $imported_terms[$term['taxonomy']] ) ) {
1170
- $imported_terms[$term['taxonomy']] = array();
1171
- }
1172
- $imported_terms[$term['taxonomy']][$term['term_id']] = true;
1173
- }
1174
- }
1175
- return $imported_terms;
1176
- }
1177
-
1178
- /**
1179
- * Returns modified ical uid for google recurring edited events.
1180
- *
1181
- * @param vevent $e Vevent object.
1182
- *
1183
- * @return string ICAL uid.
1184
- */
1185
- protected function _get_ical_uid( $e ) {
1186
- $ical_uid = $e->getProperty( 'uid' );
1187
- $recurrence_id = $e->getProperty( 'recurrence-id' );
1188
- if ( false !== $recurrence_id ) {
1189
- $ical_uid = implode( '', array_values( $recurrence_id ) ) . '-' .
1190
- $ical_uid;
1191
- }
1192
-
1193
- return $ical_uid;
1194
- }
1195
-
1196
- /**
1197
- * Returns modified exclusions structure for given event.
1198
- *
1199
- * @param vcalendar $e Vcalendar event object.
1200
- * @param array $exclusions Exclusions.
1201
- * @param Ai1ec_Date_Time $start Date time object.
1202
- *
1203
- * @return array Modified exclusions structure.
1204
- */
1205
- protected function _add_recurring_events_exclusions( $e, $exclusions, $start ) {
1206
- $recurrence_id = $e->getProperty( 'recurrence-id' );
1207
- if (
1208
- false === $recurrence_id ||
1209
- ! isset( $recurrence_id['year'] ) ||
1210
- ! isset( $recurrence_id['month'] ) ||
1211
- ! isset( $recurrence_id['day'] )
1212
- ) {
1213
- return $exclusions;
1214
- }
1215
- $year = $month = $day = $hour = $min = $sec = null;
1216
- extract( $recurrence_id, EXTR_IF_EXISTS );
1217
- $timezone = '';
1218
- $exdate = sprintf( '%04d%02d%02d', $year, $month, $day );
1219
- if (
1220
- null === $hour ||
1221
- null === $min ||
1222
- null === $sec
1223
- ) {
1224
- $hour = $min = $sec = '00';
1225
- $timezone = 'Z';
1226
- }
1227
- $exdate .= sprintf(
1228
- 'T%02d%02d%02d%s',
1229
- $hour,
1230
- $min,
1231
- $sec,
1232
- $timezone
1233
- );
1234
- $exclusions[$e->getProperty( 'uid' )][] = $exdate;
1235
- return $exclusions;
1236
- }
1237
-
1238
- /**
1239
- * Filter recurrence / exclusion rule or dates. Avoid throwing exception for old, malformed values.
1240
- *
1241
- * @param string $rule Rule or dates value.
1242
- *
1243
- * @return string Fixed rule or dates value.
1244
- */
1245
- protected function _filter_rule( $rule ) {
1246
- if ( null === $this->_rule_filter ) {
1247
- $this->_rule_filter = $this->_registry->get( 'recurrence.rule' );
1248
- }
1249
- return $this->_rule_filter->filter_rule( $rule );
1250
- }
1251
-
1252
- }
1
+ <?php
2
+
3
+ /**
4
+ * The ics import/export engine.
5
+ *
6
+ * @author Time.ly Network Inc.
7
+ * @since 2.0
8
+ *
9
+ * @package AI1EC
10
+ * @subpackage AI1EC.Import-export
11
+ */
12
+ class Ai1ec_Ics_Import_Export_Engine
13
+ extends Ai1ec_Base
14
+ implements Ai1ec_Import_Export_Engine {
15
+
16
+ /**
17
+ * @var Ai1ec_Taxonomy
18
+ */
19
+ protected $_taxonomy_model = null;
20
+
21
+ /**
22
+ * Recurrence rule class. Contains filter method.
23
+ *
24
+ * @var Ai1ec_Recurrence_Rule
25
+ */
26
+ protected $_rule_filter = null;
27
+
28
+ /* (non-PHPdoc)
29
+ * @see Ai1ec_Import_Export_Engine::import()
30
+ */
31
+ public function import( array $arguments ) {
32
+ $cal = $this->_registry->get( 'vcalendar' );
33
+ if ( $cal->parse( $arguments['source'] ) ) {
34
+ $count = 0;
35
+ try {
36
+ $result = $this->add_vcalendar_events_to_db(
37
+ $cal,
38
+ $arguments
39
+ );
40
+ } catch ( Ai1ec_Parse_Exception $exception ) {
41
+ throw new Ai1ec_Parse_Exception(
42
+ 'Processing "' . $arguments['source'] .
43
+ '" triggered error: ' . $exception->getMessage()
44
+ );
45
+ }
46
+ return $result;
47
+ }
48
+ throw new Ai1ec_Parse_Exception( 'The passed string is not a valid ics feed' );
49
+ }
50
+
51
+ /* (non-PHPdoc)
52
+ * @see Ai1ec_Import_Export_Engine::export()
53
+ */
54
+ public function export( array $arguments, array $params = array() ) {
55
+ $c = new vcalendar();
56
+ $c->setProperty( 'calscale', 'GREGORIAN' );
57
+ $c->setProperty( 'method', 'PUBLISH' );
58
+ // if no post id are specified do not export those properties
59
+ // as they would create a new calendar in outlook.
60
+ // a user reported this in AIOEC-982 and said this would fix it
61
+ if( true === $arguments['do_not_export_as_calendar'] ) {
62
+ $c->setProperty( 'X-WR-CALNAME', get_bloginfo( 'name' ) );
63
+ $c->setProperty( 'X-WR-CALDESC', get_bloginfo( 'description' ) );
64
+ }
65
+ $c->setProperty( 'X-FROM-URL', home_url() );
66
+ // Timezone setup
67
+ $tz = $this->_registry->get( 'date.timezone' )->get_default_timezone();
68
+ if ( $tz ) {
69
+ $c->setProperty( 'X-WR-TIMEZONE', $tz );
70
+ $tz_xprops = array( 'X-LIC-LOCATION' => $tz );
71
+ iCalUtilityFunctions::createTimezone( $c, $tz, $tz_xprops );
72
+ }
73
+
74
+ $this->_taxonomy_model = $this->_registry->get( 'model.taxonomy' );
75
+ $post_ids = array();
76
+ foreach ( $arguments['events'] as $event ) {
77
+ $post_ids[] = $event->get( 'post_id' );
78
+ }
79
+ $this->_taxonomy_model->prepare_meta_for_ics( $post_ids );
80
+ $this->_registry->get( 'controller.content-filter' )
81
+ ->clear_the_content_filters();
82
+ foreach ( $arguments['events'] as $event ) {
83
+ $c = $this->_insert_event_in_calendar(
84
+ $event,
85
+ $c,
86
+ true,
87
+ $params
88
+ );
89
+ }
90
+ $this->_registry->get( 'controller.content-filter' )
91
+ ->restore_the_content_filters();
92
+ $str = ltrim( $c->createCalendar() );
93
+ return $str;
94
+ }
95
+
96
+ /**
97
+ * Check if date-time specification has no (empty) time component.
98
+ *
99
+ * @param array $datetime Datetime array returned by iCalcreator.
100
+ *
101
+ * @return bool Timelessness.
102
+ */
103
+ protected function _is_timeless( array $datetime ) {
104
+ $timeless = true;
105
+ foreach ( array( 'hour', 'min', 'sec' ) as $field ) {
106
+ $timeless &= (
107
+ isset( $datetime[$field] ) &&
108
+ 0 != $datetime[$field]
109
+ )
110
+ ? false
111
+ : true;
112
+ }
113
+ return $timeless;
114
+ }
115
+
116
+ /**
117
+ * Process vcalendar instance - add events to database.
118
+ *
119
+ * @param vcalendar $v Calendar to retrieve data from.
120
+ * @param array $args Arbitrary arguments map.
121
+ *
122
+ * @throws Ai1ec_Parse_Exception
123
+ *
124
+ * @internal param stdClass $feed Instance of feed (see Ai1ecIcs plugin).
125
+ * @internal param string $comment_status WP comment status: 'open' or 'closed'.
126
+ * @internal param int $do_show_map Map display status (DB boolean: 0 or 1).
127
+ *
128
+ * @return int Count of events added to database.
129
+ */
130
+ public function add_vcalendar_events_to_db(
131
+ vcalendar $v,
132
+ array $args
133
+ ) {
134
+ $forced_timezone = null;
135
+ $feed = isset( $args['feed'] ) ? $args['feed'] : null;
136
+ $comment_status = isset( $args['comment_status'] ) ? $args['comment_status'] : 'open';
137
+ $do_show_map = isset( $args['do_show_map'] ) ? $args['do_show_map'] : 0;
138
+ $count = 0;
139
+ $events_in_db = isset( $args['events_in_db'] ) ? $args['events_in_db'] : 0;
140
+ $v->sort();
141
+ // Reverse the sort order, so that RECURRENCE-IDs are listed before the
142
+ // defining recurrence events, and therefore take precedence during
143
+ // caching.
144
+ $v->components = array_reverse( $v->components );
145
+
146
+ // TODO: select only VEVENT components that occur after, say, 1 month ago.
147
+ // Maybe use $v->selectComponents(), which takes into account recurrence
148
+
149
+ // Fetch default timezone in case individual properties don't define it
150
+ $tz = $v->getComponent( 'vtimezone' );
151
+ $local_timezone = $this->_registry->get( 'date.timezone' )->get_default_timezone();
152
+ $timezone = $local_timezone;
153
+ if ( ! empty( $tz ) ) {
154
+ $timezone = $tz->getProperty( 'TZID' );
155
+ }
156
+
157
+ $feed_name = $v->getProperty( 'X-WR-CALNAME' );
158
+ $x_wr_timezone = $v->getProperty( 'X-WR-TIMEZONE' );
159
+ if (
160
+ isset( $x_wr_timezone[1] ) &&
161
+ is_array( $x_wr_timezone )
162
+ ) {
163
+ $forced_timezone = (string)$x_wr_timezone[1];
164
+ $timezone = $forced_timezone;
165
+ }
166
+
167
+ $messages = array();
168
+ if ( empty( $forced_timezone ) ) {
169
+ $forced_timezone = $local_timezone;
170
+ }
171
+ $current_timestamp = $this->_registry->get( 'date.time' )->format_to_gmt();
172
+ // initialize empty custom exclusions structure
173
+ $exclusions = array();
174
+ // go over each event
175
+ while ( $e = $v->getComponent( 'vevent' ) ) {
176
+ // Event data array.
177
+ $data = array();
178
+ // =====================
179
+ // = Start & end times =
180
+ // =====================
181
+ $start = $e->getProperty( 'dtstart', 1, true );
182
+ $end = $e->getProperty( 'dtend', 1, true );
183
+ // For cases where a "VEVENT" calendar component
184
+ // specifies a "DTSTART" property with a DATE value type but none
185
+ // of "DTEND" nor "DURATION" property, the event duration is taken to
186
+ // be one day. For cases where a "VEVENT" calendar component
187
+ // specifies a "DTSTART" property with a DATE-TIME value type but no
188
+ // "DTEND" property, the event ends on the same calendar date and
189
+ // time of day specified by the "DTSTART" property.
190
+ if ( empty( $end ) ) {
191
+ // #1 if duration is present, assign it to end time
192
+ $end = $e->getProperty( 'duration', 1, true, true );
193
+ if ( empty( $end ) ) {
194
+ // #2 if only DATE value is set for start, set duration to 1 day
195
+ if ( ! isset( $start['value']['hour'] ) ) {
196
+ $end = array(
197
+ 'value' => array(
198
+ 'year' => $start['value']['year'],
199
+ 'month' => $start['value']['month'],
200
+ 'day' => $start['value']['day'] + 1,
201
+ 'hour' => 0,
202
+ 'min' => 0,
203
+ 'sec' => 0,
204
+ ),
205
+ );
206
+ if ( isset( $start['value']['tz'] ) ) {
207
+ $end['value']['tz'] = $start['value']['tz'];
208
+ }
209
+ } else {
210
+ // #3 set end date to start time
211
+ $end = $start;
212
+ }
213
+ }
214
+ }
215
+
216
+ $categories = $e->getProperty( "CATEGORIES", false, true );
217
+ $imported_cat = array( Ai1ec_Event_Taxonomy::CATEGORIES => array() );
218
+ // If the user chose to preserve taxonomies during import, add categories.
219
+ if( $categories && $feed->keep_tags_categories ) {
220
+ $imported_cat = $this->add_categories_and_tags(
221
+ $categories['value'],
222
+ $imported_cat,
223
+ false,
224
+ true
225
+ );
226
+ }
227
+ $feed_categories = $feed->feed_category;
228
+ if( ! empty( $feed_categories ) ) {
229
+ $imported_cat = $this->add_categories_and_tags(
230
+ $feed_categories,
231
+ $imported_cat,
232
+ false,
233
+ false
234
+ );
235
+ }
236
+ $tags = $e->getProperty( "X-TAGS", false, true );
237
+
238
+ $imported_tags = array( Ai1ec_Event_Taxonomy::TAGS => array() );
239
+ // If the user chose to preserve taxonomies during import, add tags.
240
+ if( $tags && $feed->keep_tags_categories ) {
241
+ $imported_tags = $this->add_categories_and_tags(
242
+ $tags[1]['value'],
243
+ $imported_tags,
244
+ true,
245
+ true
246
+ );
247
+ }
248
+ $feed_tags = $feed->feed_tags;
249
+ if( ! empty( $feed_tags ) ) {
250
+ $imported_tags = $this->add_categories_and_tags(
251
+ $feed_tags,
252
+ $imported_tags,
253
+ true,
254
+ true
255
+ );
256
+ }
257
+ // Event is all-day if no time components are defined
258
+ $allday = $this->_is_timeless( $start['value'] ) &&
259
+ $this->_is_timeless( $end['value'] );
260
+ // Also check the proprietary MS all-day field.
261
+ $ms_allday = $e->getProperty( 'X-MICROSOFT-CDO-ALLDAYEVENT' );
262
+ if ( ! empty( $ms_allday ) && $ms_allday[1] == 'TRUE' ) {
263
+ $allday = true;
264
+ }
265
+ $event_timezone = $timezone;
266
+ if ( $allday ) {
267
+ $event_timezone = $local_timezone;
268
+ }
269
+ $start = $this->_time_array_to_datetime(
270
+ $start,
271
+ $event_timezone,
272
+ $feed->import_timezone ? $forced_timezone : null
273
+ );
274
+ $end = $this->_time_array_to_datetime(
275
+ $end,
276
+ $event_timezone,
277
+ $feed->import_timezone ? $forced_timezone : null
278
+ );
279
+
280
+ if ( false === $start || false === $end ) {
281
+ throw new Ai1ec_Parse_Exception(
282
+ 'Failed to parse one or more dates given timezone "' .
283
+ var_export( $event_timezone, true ) . '"'
284
+ );
285
+ continue;
286
+ }
287
+
288
+ // If all-day, and start and end times are equal, then this event has
289
+ // invalid end time (happens sometimes with poorly implemented iCalendar
290
+ // exports, such as in The Event Calendar), so set end time to 1 day
291
+ // after start time.
292
+ if ( $allday && $start->format() === $end->format() ) {
293
+ $end->adjust_day( +1 );
294
+ }
295
+
296
+ $data += compact( 'start', 'end', 'allday' );
297
+
298
+ // =======================================
299
+ // = Recurrence rules & recurrence dates =
300
+ // =======================================
301
+ if ( $rrule = $e->createRrule() ) {
302
+ $rrule = explode( ':', $rrule );
303
+ $rrule = trim( end( $rrule ) );
304
+ }
305
+
306
+ if ( $exrule = $e->createExrule() ) {
307
+ $exrule = explode( ':', $exrule );
308
+ $exrule = trim( end( $exrule ) );
309
+ }
310
+
311
+ if ( $rdate = $e->createRdate() ) {
312
+ $rdate = explode( ':', $rdate );
313
+ $rdate = trim( end( $rdate ) );
314
+ }
315
+
316
+ // ===================
317
+ // = Exception dates =
318
+ // ===================
319
+ $exdate = '';
320
+ if ( $exdates = $e->createExdate() ){
321
+ // We may have two formats:
322
+ // one exdate with many dates ot more EXDATE rules
323
+ $exdates = explode( 'EXDATE', $exdates );
324
+ $def_timezone = $this->_get_import_timezone( $event_timezone );
325
+ foreach ( $exdates as $exd ) {
326
+ if ( empty( $exd ) ) {
327
+ continue;
328
+ }
329
+ $exploded = explode( ':', $exd );
330
+ $excpt_timezone = $def_timezone;
331
+ $excpt_date = null;
332
+ foreach ( $exploded as $particle ) {
333
+ if ( ';TZID=' === substr( $particle, 0, 6 ) ) {
334
+ $excpt_timezone = substr( $particle, 6 );
335
+ } else {
336
+ $excpt_date = trim( $particle );
337
+ }
338
+ }
339
+ // Google sends YYYYMMDD for all-day excluded events
340
+ if (
341
+ $allday &&
342
+ 8 === strlen( $excpt_date )
343
+ ) {
344
+ $excpt_date .= 'T000000Z';
345
+ $excpt_timezone = 'UTC';
346
+ }
347
+ $ex_dt = $this->_registry->get(
348
+ 'date.time',
349
+ $excpt_date,
350
+ $excpt_timezone
351
+ );
352
+ if ( $ex_dt ) {
353
+ if ( isset( $exdate{0} ) ) {
354
+ $exdate .= ',';
355
+ }
356
+ $exdate .= $ex_dt->format( 'Ymd\THis', $excpt_timezone );
357
+ }
358
+ }
359
+ }
360
+ // Add custom exclusions if there any
361
+ $recurrence_id = $e->getProperty( 'recurrence-id' );
362
+ if (
363
+ false === $recurrence_id &&
364
+ ! empty( $exclusions[$e->getProperty( 'uid' )] )
365
+ ) {
366
+ if ( isset( $exdate{0} ) ) {
367
+ $exdate .= ',';
368
+ }
369
+ $exdate .= implode( ',', $exclusions[$e->getProperty( 'uid' )] );
370
+ }
371
+ // ========================
372
+ // = Latitude & longitude =
373
+ // ========================
374
+ $latitude = $longitude = NULL;
375
+ $geo_tag = $e->getProperty( 'geo' );
376
+ if ( is_array( $geo_tag ) ) {
377
+ if (
378
+ isset( $geo_tag['latitude'] ) &&
379
+ isset( $geo_tag['longitude'] )
380
+ ) {
381
+ $latitude = (float)$geo_tag['latitude'];
382
+ $longitude = (float)$geo_tag['longitude'];
383
+ }
384
+ } else if ( ! empty( $geo_tag ) && false !== strpos( $geo_tag, ';' ) ) {
385
+ list( $latitude, $longitude ) = explode( ';', $geo_tag, 2 );
386
+ $latitude = (float)$latitude;
387
+ $longitude = (float)$longitude;
388
+ }
389
+ unset( $geo_tag );
390
+ if ( NULL !== $latitude ) {
391
+ $data += compact( 'latitude', 'longitude' );
392
+ // Check the input coordinates checkbox, otherwise lat/long data
393
+ // is not present on the edit event page
394
+ $data['show_coordinates'] = 1;
395
+ }
396
+
397
+ // ===================
398
+ // = Venue & address =
399
+ // ===================
400
+ $address = $venue = '';
401
+ $location = $e->getProperty( 'location' );
402
+ $matches = array();
403
+ // This regexp matches a venue / address in the format
404
+ // "venue @ address" or "venue - address".
405
+ preg_match( '/\s*(.*\S)\s+[\-@]\s+(.*)\s*/', $location, $matches );
406
+ // if there is no match, it's not a combined venue + address
407
+ if ( empty( $matches ) ) {
408
+ // temporary fix for Mac ICS import. Se AIOEC-2187
409
+ // and https://github.com/iCalcreator/iCalcreator/issues/13
410
+ $location = str_replace( '\n', "\n", $location );
411
+ // if there is a comma, probably it's an address
412
+ if ( false === strpos( $location, ',' ) ) {
413
+ $venue = $location;
414
+ } else {
415
+ $address = $location;
416
+ }
417
+ } else {
418
+ $venue = isset( $matches[1] ) ? $matches[1] : '';
419
+ $address = isset( $matches[2] ) ? $matches[2] : '';
420
+ }
421
+
422
+ // =====================================================
423
+ // = Set show map status based on presence of location =
424
+ // =====================================================
425
+ $event_do_show_map = $do_show_map;
426
+ if (
427
+ 1 === $do_show_map &&
428
+ NULL === $latitude &&
429
+ empty( $address )
430
+ ) {
431
+ $event_do_show_map = 0;
432
+ }
433
+
434
+ // ==================
435
+ // = Cost & tickets =
436
+ // ==================
437
+ $cost = $e->getProperty( 'X-COST' );
438
+ $cost = $cost ? $cost[1] : '';
439
+ $ticket_url = $e->getProperty( 'X-TICKETS-URL' );
440
+ $ticket_url = $ticket_url ? $ticket_url[1] : '';
441
+
442
+ // ===============================
443
+ // = Contact name, phone, e-mail =
444
+ // ===============================
445
+ $organizer = $e->getProperty( 'organizer' );
446
+ if (
447
+ 'MAILTO:' === substr( $organizer, 0, 7 ) &&
448
+ false === strpos( $organizer, '@' )
449
+ ) {
450
+ $organizer = substr( $organizer, 7 );
451
+ }
452
+ $contact = $e->getProperty( 'contact' );
453
+ $elements = explode( ';', $contact, 4 );
454
+ foreach ( $elements as $el ) {
455
+ $el = trim( $el );
456
+ // Detect e-mail address.
457
+ if ( false !== strpos( $el, '@' ) ) {
458
+ $data['contact_email'] = $el;
459
+ }
460
+ // Detect URL.
461
+ elseif ( false !== strpos( $el, '://' ) ) {
462
+ $data['contact_url'] = $el;
463
+ }
464
+ // Detect phone number.
465
+ elseif ( preg_match( '/\d/', $el ) ) {
466
+ $data['contact_phone'] = $el;
467
+ }
468
+ // Default to name.
469
+ else {
470
+ $data['contact_name'] = $el;
471
+ }
472
+ }
473
+ if ( ! isset( $data['contact_name'] ) || ! $data['contact_name'] ) {
474
+ // If no contact name, default to organizer property.
475
+ $data['contact_name'] = $organizer;
476
+ }
477
+ // Store yet-unsaved values to the $data array.
478
+ $data += array(
479
+ 'recurrence_rules' => $rrule,
480
+ 'exception_rules' => $exrule,
481
+ 'recurrence_dates' => $rdate,
482
+ 'exception_dates' => $exdate,
483
+ 'venue' => $venue,
484
+ 'address' => $address,
485
+ 'cost' => $cost,
486
+ 'ticket_url' => $ticket_url,
487
+ 'show_map' => $event_do_show_map,
488
+ 'ical_feed_url' => $feed->feed_url,
489
+ 'ical_source_url' => $e->getProperty( 'url' ),
490
+ 'ical_organizer' => $organizer,
491
+ 'ical_contact' => $contact,
492
+ 'ical_uid' => $this->_get_ical_uid( $e ),
493
+ 'categories' => array_keys( $imported_cat[Ai1ec_Event_Taxonomy::CATEGORIES] ),
494
+ 'tags' => array_keys( $imported_tags[Ai1ec_Event_Taxonomy::TAGS] ),
495
+ 'feed' => $feed,
496
+ 'post' => array(
497
+ 'post_status' => 'publish',
498
+ 'comment_status' => $comment_status,
499
+ 'post_type' => AI1EC_POST_TYPE,
500
+ 'post_author' => 1,
501
+ 'post_title' => $e->getProperty( 'summary' ),
502
+ 'post_content' => stripslashes(
503
+ str_replace(
504
+ '\n',
505
+ "\n",
506
+ $e->getProperty( 'description' )
507
+ )
508
+ ),
509
+ ),
510
+ );
511
+ // register any custom exclusions for given event
512
+ $exclusions = $this->_add_recurring_events_exclusions(
513
+ $e,
514
+ $exclusions,
515
+ $start
516
+ );
517
+
518
+ // Create event object.
519
+ $data = apply_filters(
520
+ 'ai1ec_pre_init_event_from_feed',
521
+ $data,
522
+ $e,
523
+ $feed
524
+ );
525
+
526
+ $event = $this->_registry->get( 'model.event', $data );
527
+
528
+ // Instant Event
529
+ $is_instant = $e->getProperty( 'X-INSTANT-EVENT' );
530
+ if ( $is_instant ) {
531
+ $event->set_no_end_time();
532
+ }
533
+
534
+ $recurrence = $event->get( 'recurrence_rules' );
535
+ $search = $this->_registry->get( 'model.search' );
536
+ // first let's check by UID
537
+ $matching_event_id = $search
538
+ ->get_matching_event_by_uid_and_url(
539
+ $event->get( 'ical_uid' ),
540
+ $event->get( 'ical_feed_url' )
541
+ );
542
+ // if no result, perform the legacy check.
543
+ if ( null === $matching_event_id ) {
544
+ $matching_event_id = $search
545
+ ->get_matching_event_id(
546
+ $event->get( 'ical_uid' ),
547
+ $event->get( 'ical_feed_url' ),
548
+ $event->get( 'start' ),
549
+ ! empty( $recurrence )
550
+ );
551
+ }
552
+ if ( null === $matching_event_id ) {
553
+ // =================================================
554
+ // = Event was not found, so store it and the post =
555
+ // =================================================
556
+ $event->save();
557
+ $count++;
558
+ } else {
559
+ // ======================================================
560
+ // = Event was found, let's store the new event details =
561
+ // ======================================================
562
+
563
+ // Update the post
564
+ $post = get_post( $matching_event_id );
565
+
566
+ if ( null !== $post ) {
567
+ $post->post_title = $event->get( 'post' )->post_title;
568
+ $post->post_content = $event->get( 'post' )->post_content;
569
+ wp_update_post( $post );
570
+
571
+ // Update the event
572
+ $event->set( 'post_id', $matching_event_id );
573
+ $event->set( 'post', $post );
574
+ $event->save( true );
575
+ $count++;
576
+ }
577
+ }
578
+ do_action( 'ai1ec_ics_event_saved', $event, $feed );
579
+
580
+ // import not standard taxonomies.
581
+ unset( $imported_cat[Ai1ec_Event_Taxonomy::CATEGORIES] );
582
+ foreach ( $imported_cat as $tax_name => $ids ) {
583
+ wp_set_post_terms( $event->get( 'post_id' ), array_keys( $ids ), $tax_name );
584
+ }
585
+
586
+ unset( $imported_tags[Ai1ec_Event_Taxonomy::TAGS] );
587
+ foreach ( $imported_tags as $tax_name => $ids ) {
588
+ wp_set_post_terms( $event->get( 'post_id' ), array_keys( $ids ), $tax_name );
589
+ }
590
+
591
+ unset( $events_in_db[$event->get( 'post_id' )] );
592
+ }
593
+
594
+ return array(
595
+ 'count' => $count,
596
+ 'events_to_delete' => $events_in_db,
597
+ 'messages' => $messages,
598
+ 'name' => $feed_name,
599
+ );
600
+ }
601
+
602
+ /**
603
+ * Parse importable feed timezone to sensible value.
604
+ *
605
+ * @param string $def_timezone Timezone value from feed.
606
+ *
607
+ * @return string Valid timezone name to use.
608
+ */
609
+ protected function _get_import_timezone( $def_timezone ) {
610
+ $parser = $this->_registry->get( 'date.timezone' );
611
+ $timezone = $parser->get_name( $def_timezone );
612
+ if ( false === $timezone ) {
613
+ return 'sys.default';
614
+ }
615
+ return $timezone;
616
+ }
617
+
618
+ /**
619
+ * time_array_to_timestamp function
620
+ *
621
+ * Converts time array to time string.
622
+ * Passed array: Array( 'year', 'month', 'day', ['hour', 'min', 'sec', ['tz']] )
623
+ * Return int: UNIX timestamp in GMT
624
+ *
625
+ * @param array $time iCalcreator time property array
626
+ * (*full* format expected)
627
+ * @param string $def_timezone Default time zone in case not defined
628
+ * in $time
629
+ * @param null|string $forced_timezone Timezone to use instead of UTC.
630
+ *
631
+ * @return int UNIX timestamp
632
+ **/
633
+ protected function _time_array_to_datetime(
634
+ array $time,
635
+ $def_timezone,
636
+ $forced_timezone = null
637
+ ) {
638
+ $timezone = '';
639
+ if ( isset( $time['params']['TZID'] ) ) {
640
+ $timezone = $time['params']['TZID'];
641
+ } elseif (
642
+ isset( $time['value']['tz'] ) &&
643
+ 'Z' === $time['value']['tz']
644
+ ) {
645
+ $timezone = 'UTC';
646
+ }
647
+ if ( empty( $timezone ) ) {
648
+ $timezone = $def_timezone;
649
+ }
650
+
651
+ $date_time = $this->_registry->get( 'date.time' );
652
+
653
+ if ( ! empty( $timezone ) ) {
654
+ $parser = $this->_registry->get( 'date.timezone' );
655
+ $timezone = $parser->get_name( $timezone );
656
+ if ( false === $timezone ) {
657
+ return false;
658
+ }
659
+ $date_time->set_timezone( $timezone );
660
+ }
661
+
662
+ if ( ! isset( $time['value']['hour'] ) ) {
663
+ $time['value']['hour'] = $time['value']['min'] =
664
+ $time['value']['sec'] = 0;
665
+ }
666
+
667
+ $date_time->set_date(
668
+ $time['value']['year'],
669
+ $time['value']['month'],
670
+ $time['value']['day']
671
+ )->set_time(
672
+ $time['value']['hour'],
673
+ $time['value']['min'],
674
+ $time['value']['sec']
675
+ );
676
+ if (
677
+ 'UTC' === $timezone &&
678
+ null !== $forced_timezone
679
+ ) {
680
+ $date_time->set_timezone( $forced_timezone );
681
+ }
682
+ return $date_time;
683
+ }
684
+
685
+ /**
686
+ * Convert an event from a feed into a new Ai1ec_Event object and add it to
687
+ * the calendar.
688
+ *
689
+ * @param Ai1ec_Event $event Event object.
690
+ * @param vcalendar $calendar Calendar object.
691
+ * @param bool $export States whether events are created for export.
692
+ * @param array $params Additional parameters for export.
693
+ *
694
+ * @return void
695
+ */
696
+ protected function _insert_event_in_calendar(
697
+ Ai1ec_Event $event,
698
+ vcalendar $calendar,
699
+ $export = false,
700
+ array $params = array()
701
+ ) {
702
+
703
+ $tz = $this->_registry->get( 'date.timezone' )
704
+ ->get_default_timezone();
705
+
706
+ $e = & $calendar->newComponent( 'vevent' );
707
+ $uid = '';
708
+ if ( $event->get( 'ical_uid' ) ) {
709
+ $uid = addcslashes( $event->get( 'ical_uid' ), "\\;,\n" );
710
+ } else {
711
+ $uid = $event->get_uid();
712
+ $event->set( 'ical_uid', $uid );
713
+ $event->save( true );
714
+ }
715
+ $e->setProperty( 'uid', $this->_sanitize_value( $uid ) );
716
+ $e->setProperty(
717
+ 'url',
718
+ get_permalink( $event->get( 'post_id' ) )
719
+ );
720
+
721
+ // =========================
722
+ // = Summary & description =
723
+ // =========================
724
+ $e->setProperty(
725
+ 'summary',
726
+ $this->_sanitize_value(
727
+ html_entity_decode(
728
+ apply_filters( 'the_title', $event->get( 'post' )->post_title ),
729
+ ENT_QUOTES,
730
+ 'UTF-8'
731
+ )
732
+ )
733
+ );
734
+
735
+ $content = apply_filters(
736
+ 'ai1ec_the_content',
737
+ apply_filters(
738
+ 'the_content',
739
+ $event->get( 'post' )->post_content
740
+ )
741
+ );
742
+ $content = str_replace(']]>', ']]&gt;', $content);
743
+ $content = html_entity_decode( $content, ENT_QUOTES, 'UTF-8' );
744
+
745
+ // Prepend featured image if available.
746
+ $size = null;
747
+ $avatar = $this->_registry->get( 'view.event.avatar' );
748
+ $matches = $avatar->get_image_from_content( $content );
749
+ // if no img is already present - add thumbnail
750
+ if ( empty( $matches ) ) {
751
+ if ( $img_url = $avatar->get_post_thumbnail_url( $event, $size ) ) {
752
+ $content = '<div class="ai1ec-event-avatar alignleft timely"><img src="' .
753
+ esc_attr( $img_url ) . '" width="' . $size[0] . '" height="' .
754
+ $size[1] . '" /></div>' . $content;
755
+ }
756
+ }
757
+
758
+ if ( isset( $params['no_html'] ) && $params['no_html'] ) {
759
+ $e->setProperty(
760
+ 'description',
761
+ $this->_sanitize_value(
762
+ strip_tags( strip_shortcodes( $content ) )
763
+ )
764
+ );
765
+ if ( ! empty( $content ) ) {
766
+ $html_content = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">\n' .
767
+ '<HTML>\n<HEAD>\n<TITLE></TITLE>\n</HEAD>\n<BODY>' . $content .
768
+ '</BODY></HTML>';
769
+ $e->setProperty(
770
+ 'X-ALT-DESC',
771
+ $this->_sanitize_value( $html_content ),
772
+ array(
773
+ 'FMTTYPE' => 'text/html',
774
+ )
775
+ );
776
+ unset( $html_content );
777
+ }
778
+ } else {
779
+ $e->setProperty( 'description', $this->_sanitize_value( $content ) );
780
+ }
781
+ $revision = (int)current(
782
+ array_keys(
783
+ wp_get_post_revisions( $event->get( 'post_id' ) )
784
+ )
785
+ );
786
+ $e->setProperty( 'sequence', $revision );
787
+
788
+ // =====================
789
+ // = Start & end times =
790
+ // =====================
791
+ $dtstartstring = '';
792
+ $dtstart = $dtend = array();
793
+ if ( $event->is_allday() ) {
794
+ $dtstart['VALUE'] = $dtend['VALUE'] = 'DATE';
795
+ // For exporting all day events, don't set a timezone
796
+ if ( $tz && ! $export ) {
797
+ $dtstart['TZID'] = $dtend['TZID'] = $tz;
798
+ }
799
+
800
+ // For exportin' all day events, only set the date not the time
801
+ if ( $export ) {
802
+ $e->setProperty(
803
+ 'dtstart',
804
+ $this->_sanitize_value(
805
+ $event->get( 'start' )->format( 'Ymd' )
806
+ ),
807
+ $dtstart
808
+ );
809
+ $e->setProperty(
810
+ 'dtend',
811
+ $this->_sanitize_value(
812
+ $event->get( 'end' )->format( 'Ymd' )
813
+ ),
814
+ $dtend
815
+ );
816
+ } else {
817
+ $e->setProperty(
818
+ 'dtstart',
819
+ $this->_sanitize_value(
820
+ $event->get( 'start' )->format( "Ymd\T" )
821
+ ),
822
+ $dtstart
823
+ );
824
+ $e->setProperty(
825
+ 'dtend',
826
+ $this->_sanitize_value(
827
+ $event->get( 'end' )->format( "Ymd\T" )
828
+ ),
829
+ $dtend
830
+ );
831
+ }
832
+ } else {
833
+ if ( $tz ) {
834
+ $dtstart['TZID'] = $dtend['TZID'] = $tz;
835
+ }
836
+ // This is used later.
837
+ $dtstartstring = $event->get( 'start' )->format( "Ymd\THis" );
838
+ $e->setProperty(
839
+ 'dtstart',
840
+ $this->_sanitize_value( $dtstartstring ),
841
+ $dtstart
842
+ );
843
+
844
+ if ( false === (bool)$event->get( 'instant_event' ) ) {
845
+ $e->setProperty(
846
+ 'dtend',
847
+ $this->_sanitize_value(
848
+ $event->get( 'end' )->format( "Ymd\THis" )
849
+ ),
850
+ $dtend
851
+ );
852
+ }
853
+ }
854
+
855
+ // ========================
856
+ // = Latitude & longitude =
857
+ // ========================
858
+ if (
859
+ floatval( $event->get( 'latitude' ) ) ||
860
+ floatval( $event->get( 'longitude' ) )
861
+ ) {
862
+ $e->setProperty(
863
+ 'geo',
864
+ $event->get( 'latitude' ),
865
+ $event->get( 'longitude' )
866
+ );
867
+ }
868
+
869
+ // ===================
870
+ // = Venue & address =
871
+ // ===================
872
+ if ( $event->get( 'venue' ) || $event->get( 'address' ) ) {
873
+ $location = array(
874
+ $event->get( 'venue' ),
875
+ $event->get( 'address' )
876
+ );
877
+ $location = array_filter( $location );
878
+ $location = implode( ' @ ', $location );
879
+ $e->setProperty( 'location', $this->_sanitize_value( $location ) );
880
+ }
881
+
882
+ $categories = array();
883
+ $language = get_bloginfo( 'language' );
884
+
885
+ foreach (
886
+ $this->_taxonomy_model->get_post_categories(
887
+ $event->get( 'post_id' )
888
+ )
889
+ as $cat
890
+ ) {
891
+ $categories[] = $cat->name;
892
+ }
893
+ $e->setProperty(
894
+ 'categories',
895
+ implode( ',', $categories ),
896
+ array( "LANGUAGE" => $language )
897
+ );
898
+ $tags = array();
899
+ foreach (
900
+ $this->_taxonomy_model->get_post_tags( $event->get( 'post_id' ) )
901
+ as $tag
902
+ ) {
903
+ $tags[] = $tag->name;
904
+ }
905
+ if( ! empty( $tags) ) {
906
+ $e->setProperty(
907
+ 'X-TAGS',
908
+ implode( ',', $tags ),
909
+ array( "LANGUAGE" => $language )
910
+ );
911
+ }
912
+ // ==================
913
+ // = Cost & tickets =
914
+ // ==================
915
+ if ( $event->get( 'cost' ) ) {
916
+ $e->setProperty(
917
+ 'X-COST',
918
+ $this->_sanitize_value( $event->get( 'cost' ) )
919
+ );
920
+ }
921
+ if ( $event->get( 'ticket_url' ) ) {
922
+ $e->setProperty(
923
+ 'X-TICKETS-URL',
924
+ $this->_sanitize_value(
925
+ $event->get( 'ticket_url' )
926
+ )
927
+ );
928
+ }
929
+ // =================
930
+ // = Instant Event =
931
+ // =================
932
+ if ( $event->is_instant() ) {
933
+ $e->setProperty(
934
+ 'X-INSTANT-EVENT',
935
+ $this->_sanitize_value( $event->is_instant() )
936
+ );
937
+ }
938
+
939
+ // ====================================
940
+ // = Contact name, phone, e-mail, URL =
941
+ // ====================================
942
+ $contact = array(
943
+ $event->get( 'contact_name' ),
944
+ $event->get( 'contact_phone' ),
945
+ $event->get( 'contact_email' ),
946
+ $event->get( 'contact_url' ),
947
+ );
948
+ $contact = array_filter( $contact );
949
+ $contact = implode( '; ', $contact );
950
+ $e->setProperty( 'contact', $this->_sanitize_value( $contact ) );
951
+
952
+ // ====================
953
+ // = Recurrence rules =
954
+ // ====================
955
+ $rrule = array();
956
+ $recurrence = $event->get( 'recurrence_rules' );
957
+ $recurrence = $this->_filter_rule( $recurrence );
958
+ if ( ! empty( $recurrence ) ) {
959
+ $rules = array();
960
+ foreach ( explode( ';', $recurrence ) as $v) {
961
+ if ( strpos( $v, '=' ) === false ) {
962
+ continue;
963
+ }
964
+
965
+ list( $k, $v ) = explode( '=', $v );
966
+ $k = strtoupper( $k );
967
+ // If $v is a comma-separated list, turn it into array for iCalcreator
968
+ switch ( $k ) {
969
+ case 'BYSECOND':
970
+ case 'BYMINUTE':
971
+ case 'BYHOUR':
972
+ case 'BYDAY':
973
+ case 'BYMONTHDAY':
974
+ case 'BYYEARDAY':
975
+ case 'BYWEEKNO':
976
+ case 'BYMONTH':
977
+ case 'BYSETPOS':
978
+ $exploded = explode( ',', $v );
979
+ break;
980
+ default:
981
+ $exploded = $v;
982
+ break;
983
+ }
984
+ // iCalcreator requires a more complex array structure for BYDAY...
985
+ if ( $k == 'BYDAY' ) {
986
+ $v = array();
987
+ foreach ( $exploded as $day ) {
988
+ $v[] = array( 'DAY' => $day );
989
+ }
990
+ } else {
991
+ $v = $exploded;
992
+ }
993
+ $rrule[ $k ] = $v;
994
+ }
995
+ }
996
+
997
+ // ===================
998
+ // = Exception rules =
999
+ // ===================
1000
+ $exceptions = $event->get( 'exception_rules' );
1001
+ $exceptions = $this->_filter_rule( $exceptions );
1002
+ $exrule = array();
1003
+ if ( ! empty( $exceptions ) ) {
1004
+ $rules = array();
1005
+
1006
+ foreach ( explode( ';', $exceptions ) as $v) {
1007
+ if ( strpos( $v, '=' ) === false ) {
1008
+ continue;
1009
+ }
1010
+
1011
+ list($k, $v) = explode( '=', $v );
1012
+ $k = strtoupper( $k );
1013
+ // If $v is a comma-separated list, turn it into array for iCalcreator
1014
+ switch ( $k ) {
1015
+ case 'BYSECOND':
1016
+ case 'BYMINUTE':
1017
+ case 'BYHOUR':
1018
+ case 'BYDAY':
1019
+ case 'BYMONTHDAY':
1020
+ case 'BYYEARDAY':
1021
+ case 'BYWEEKNO':
1022
+ case 'BYMONTH':
1023
+ case 'BYSETPOS':
1024
+ $exploded = explode( ',', $v );
1025
+ break;
1026
+ default:
1027
+ $exploded = $v;
1028
+ break;
1029
+ }
1030
+ // iCalcreator requires a more complex array structure for BYDAY...
1031
+ if ( $k == 'BYDAY' ) {
1032
+ $v = array();
1033
+ foreach ( $exploded as $day ) {
1034
+ $v[] = array( 'DAY' => $day );
1035
+ }
1036
+ } else {
1037
+ $v = $exploded;
1038
+ }
1039
+ $exrule[ $k ] = $v;
1040
+ }
1041
+ }
1042
+
1043
+ // add rrule to exported calendar
1044
+ if ( ! empty( $rrule ) && ! isset( $rrule['RDATE'] ) ) {
1045
+ $e->setProperty( 'rrule', $this->_sanitize_value( $rrule ) );
1046
+ }
1047
+ // add exrule to exported calendar
1048
+ if ( ! empty( $exrule ) && ! isset( $exrule['EXDATE'] ) ) {
1049
+ $e->setProperty( 'exrule', $this->_sanitize_value( $exrule ) );
1050
+ }
1051
+
1052
+ // ===================
1053
+ // = Exception dates =
1054
+ // ===================
1055
+ // For all day events that use a date as DTSTART, date must be supplied
1056
+ // For other other events which use DATETIME, we must use that as well
1057
+ // We must also match the exact starting time
1058
+ $recurrence_dates = $event->get( 'recurrence_dates' );
1059
+ $recurrence_dates = $this->_filter_rule( $recurrence_dates );
1060
+ if ( ! empty( $recurrence_dates ) ) {
1061
+ $params = array(
1062
+ 'VALUE' => 'DATE-TIME',
1063
+ 'TZID' => $tz,
1064
+ );
1065
+ $dt_suffix = $event->get( 'start' )->format( '\THis' );
1066
+ foreach (
1067
+ explode( ',', $recurrence_dates )
1068
+ as $exdate
1069
+ ) {
1070
+ // date-time string in EXDATES is formatted as 'Ymd\THis\Z', that
1071
+ // means - in UTC timezone, thus we use `format_to_gmt` here.
1072
+ $exdate = $this->_registry->get( 'date.time', $exdate )
1073
+ ->format_to_gmt( 'Ymd' );
1074
+ $e->setProperty(
1075
+ 'rdate',
1076
+ array( $exdate . $dt_suffix ),
1077
+ $params
1078
+ );
1079
+ }
1080
+ }
1081
+ $exception_dates = $event->get( 'exception_dates' );
1082
+ $exception_dates = $this->_filter_rule( $exception_dates );
1083
+ if ( ! empty( $exception_dates ) ) {
1084
+ $params = array(
1085
+ 'VALUE' => 'DATE-TIME',
1086
+ 'TZID' => $tz,
1087
+ );
1088
+ $dt_suffix = $event->get( 'start' )->format( '\THis' );
1089
+ foreach (
1090
+ explode( ',', $exception_dates )
1091
+ as $exdate
1092
+ ) {
1093
+ // date-time string in EXDATES is formatted as 'Ymd\THis\Z', that
1094
+ // means - in UTC timezone, thus we use `format_to_gmt` here.
1095
+ $exdate = $this->_registry->get( 'date.time', $exdate )
1096
+ ->format_to_gmt( 'Ymd' );
1097
+ $e->setProperty(
1098
+ 'exdate',
1099
+ array( $exdate . $dt_suffix ),
1100
+ $params
1101
+ );
1102
+ }
1103
+ }
1104
+ return $calendar;
1105
+ }
1106
+
1107
+ /**
1108
+ * _sanitize_value method
1109
+ *
1110
+ * Convert value, so it be safe to use on ICS feed. Used before passing to
1111
+ * iCalcreator methods, for rendering.
1112
+ *
1113
+ * @param string $value Text to be sanitized
1114
+ *
1115
+ * @return string Safe value, for use in HTML
1116
+ */
1117
+ protected function _sanitize_value( $value ) {
1118
+ if ( ! is_scalar( $value ) ) {
1119
+ return $value;
1120
+ }
1121
+ $safe_eol = "\n";
1122
+ $value = strtr(
1123
+ trim( $value ),
1124
+ array(
1125
+ "\r\n" => $safe_eol,
1126
+ "\r" => $safe_eol,
1127
+ "\n" => $safe_eol,
1128
+ )
1129
+ );
1130
+ $value = addcslashes( $value, '\\' );
1131
+ return $value;
1132
+ }
1133
+
1134
+ /**
1135
+ * Takes a comma-separated list of tags or categories.
1136
+ * If they exist, reuses
1137
+ * the existing ones. If not, creates them.
1138
+ *
1139
+ * The $imported_terms array uses keys to store values rather than values to
1140
+ * speed up lookups (using isset() insted of in_array()).
1141
+ *
1142
+ * @param string $terms
1143
+ * @param array $imported_terms
1144
+ * @param boolean $is_tag
1145
+ * @param boolean $use_name
1146
+ *
1147
+ * @return array
1148
+ */
1149
+ public function add_categories_and_tags(
1150
+ $terms,
1151
+ array $imported_terms,
1152
+ $is_tag,
1153
+ $use_name
1154
+ ) {
1155
+ $taxonomy = $is_tag ? 'events_tags' : 'events_categories';
1156
+ $categories = explode( ',', $terms );
1157
+ $event_taxonomy = $this->_registry->get( 'model.event.taxonomy' );
1158
+
1159
+ foreach ( $categories as $cat_name ) {
1160
+ $cat_name = trim( $cat_name );
1161
+ if ( empty( $cat_name ) ) {
1162
+ continue;
1163
+ }
1164
+ $term = $event_taxonomy->initiate_term( $cat_name, $taxonomy, ! $use_name );
1165
+ if ( false !== $term ) {
1166
+ if ( ! isset( $imported_terms[$term['taxonomy']] ) ) {
1167
+ $imported_terms[$term['taxonomy']] = array();
1168
+ }
1169
+ $imported_terms[$term['taxonomy']][$term['term_id']] = true;
1170
+ }
1171
+ }
1172
+ return $imported_terms;
1173
+ }
1174
+
1175
+ /**
1176
+ * Returns modified ical uid for google recurring edited events.
1177
+ *
1178
+ * @param vevent $e Vevent object.
1179
+ *
1180
+ * @return string ICAL uid.
1181
+ */
1182
+ protected function _get_ical_uid( $e ) {
1183
+ $ical_uid = $e->getProperty( 'uid' );
1184
+ $recurrence_id = $e->getProperty( 'recurrence-id' );
1185
+ if ( false !== $recurrence_id ) {
1186
+ $ical_uid = implode( '', array_values( $recurrence_id ) ) . '-' .
1187
+ $ical_uid;
1188
+ }
1189
+
1190
+ return $ical_uid;
1191
+ }
1192
+
1193
+ /**
1194
+ * Returns modified exclusions structure for given event.
1195
+ *
1196
+ * @param vcalendar $e Vcalendar event object.
1197
+ * @param array $exclusions Exclusions.
1198
+ * @param Ai1ec_Date_Time $start Date time object.
1199
+ *
1200
+ * @return array Modified exclusions structure.
1201
+ */
1202
+ protected function _add_recurring_events_exclusions( $e, $exclusions, $start ) {
1203
+ $recurrence_id = $e->getProperty( 'recurrence-id' );
1204
+ if (
1205
+ false === $recurrence_id ||
1206
+ ! isset( $recurrence_id['year'] ) ||
1207
+ ! isset( $recurrence_id['month'] ) ||
1208
+ ! isset( $recurrence_id['day'] )
1209
+ ) {
1210
+ return $exclusions;
1211
+ }
1212
+ $year = $month = $day = $hour = $min = $sec = null;
1213
+ extract( $recurrence_id, EXTR_IF_EXISTS );
1214
+ $timezone = '';
1215
+ $exdate = sprintf( '%04d%02d%02d', $year, $month, $day );
1216
+ if (
1217
+ null === $hour ||
1218
+ null === $min ||
1219
+ null === $sec
1220
+ ) {
1221
+ $hour = $min = $sec = '00';
1222
+ $timezone = 'Z';
1223
+ }
1224
+ $exdate .= sprintf(
1225
+ 'T%02d%02d%02d%s',
1226
+ $hour,
1227
+ $min,
1228
+ $sec,
1229
+ $timezone
1230
+ );
1231
+ $exclusions[$e->getProperty( 'uid' )][] = $exdate;
1232
+ return $exclusions;
1233
+ }
1234
+
1235
+ /**
1236
+ * Filter recurrence / exclusion rule or dates. Avoid throwing exception for old, malformed values.
1237
+ *
1238
+ * @param string $rule Rule or dates value.
1239
+ *
1240
+ * @return string Fixed rule or dates value.
1241
+ */
1242
+ protected function _filter_rule( $rule ) {
1243
+ if ( null === $this->_rule_filter ) {
1244
+ $this->_rule_filter = $this->_registry->get( 'recurrence.rule' );
1245
+ }
1246
+ return $this->_rule_filter->filter_rule( $rule );
1247
+ }
1248
+
1249
+ }
 
 
 
readme.txt CHANGED
@@ -4,8 +4,8 @@ Tags: calendar, events, ics, ics feed, wordpress ical importer, google
4
  calendar, ical, iCalendar, all-in-one, events sync, events widget,
5
  calendar widget
6
  Requires WordPress at least: 3.5
7
- Tested up to: 4.3
8
- Stable tag: 2.3.3
9
  License: GNU General Public License, version 3 (GPL-3.0)
10
 
11
  A calendar system with many views, upcoming events widget, color-coded
@@ -137,6 +137,9 @@ https://vimeo.com/135004810
137
 
138
  == Changelog ==
139
 
 
 
 
140
  = Version 2.3.3 =
141
  * Fixed bug with month view navigation
142
 
4
  calendar, ical, iCalendar, all-in-one, events sync, events widget,
5
  calendar widget
6
  Requires WordPress at least: 3.5
7
+ Tested up to: 4.3.1
8
+ Stable tag: 2.3.4
9
  License: GNU General Public License, version 3 (GPL-3.0)
10
 
11
  A calendar system with many views, upcoming events widget, color-coded
137
 
138
  == Changelog ==
139
 
140
+ = Version 2.3.4 =
141
+ * Fixed ICS import issue
142
+
143
  = Version 2.3.3 =
144
  * Fixed bug with month view navigation
145