calendar_ical::exportVCal()   F
last analyzed

Complexity

Conditions 222
Paths 64

Size

Total Lines 878
Code Lines 479

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 222
eloc 479
c 2
b 1
f 0
nc 64
nop 7
dl 0
loc 878
rs 3.3333

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * EGroupware - Calendar iCal import and export via Horde iCalendar classes
4
 *
5
 * @link http://www.egroupware.org
6
 * @author Lars Kneschke <[email protected]>
7
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
8
 * @author Joerg Lehrke <[email protected]>
9
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
10
 * @package calendar
11
 * @subpackage export
12
 */
13
14
use EGroupware\Api;
15
use EGroupware\Api\Acl;
16
17
/**
18
 * iCal import and export via Horde iCalendar classes
19
 *
20
 * @ToDo: NOT changing default-timezone as it messes up timezone calculation of timestamps eg. in calendar_boupdate::send_update
21
 * 	(currently fixed by restoring server-timezone in calendar_boupdate::send_update)
22
 */
23
class calendar_ical extends calendar_boupdate
24
{
25
	/**
26
	 * @var array $supportedFields array containing the supported fields of the importing device
27
	 */
28
	var $supportedFields;
29
30
	/**
31
	 * @var array $status_egw2ical conversation of the participant status egw => ical
32
	 */
33
	var $status_egw2ical = array(
34
		'U' => 'NEEDS-ACTION',
35
		'A' => 'ACCEPTED',
36
		'R' => 'DECLINED',
37
		'T' => 'TENTATIVE',
38
		'D' => 'DELEGATED'
39
	);
40
	/**
41
	 * @var array conversation of the participant status ical => egw
42
	 */
43
	var $status_ical2egw = array(
44
		'NEEDS-ACTION' => 'U',
45
		'NEEDS ACTION' => 'U',
46
		'ACCEPTED'     => 'A',
47
		'DECLINED'     => 'R',
48
		'TENTATIVE'    => 'T',
49
		'DELEGATED'    => 'D',
50
		'X-UNINVITED'  => 'G', // removed
51
	);
52
53
	/**
54
	 * @var array $priority_egw2ical conversion of the priority egw => ical
55
	 */
56
	var $priority_egw2ical = array(
57
		0 => 0,		// undefined
58
		1 => 9,		// low
59
		2 => 5,		// normal
60
		3 => 1,		// high
61
	);
62
63
	/**
64
	 * @var array $priority_ical2egw conversion of the priority ical => egw
65
	 */
66
	var $priority_ical2egw = array(
67
		0 => 0,		// undefined
68
		9 => 1,	8 => 1, 7 => 1, 6 => 1,	// low
69
		5 => 2,		// normal
70
		4 => 3, 3 => 3, 2 => 3, 1 => 3,	// high
71
	);
72
73
	/**
74
	 * @var array $priority_egw2funambol conversion of the priority egw => funambol
75
	 */
76
	var $priority_egw2funambol = array(
77
		0 => 1,		// undefined (mapped to normal since undefined does not exist)
78
		1 => 0,		// low
79
		2 => 1,		// normal
80
		3 => 2,		// high
81
	);
82
83
	/**
84
	 * @var array $priority_funambol2egw conversion of the priority funambol => egw
85
	 */
86
	var $priority_funambol2egw = array(
87
		0 => 1,		// low
88
		1 => 2,		// normal
89
		2 => 3,		// high
90
	);
91
92
	/**
93
	 * manufacturer and name of the sync-client
94
	 *
95
	 * @var string
96
	 */
97
	var $productManufacturer = 'file';
98
	var $productName = '';
99
100
	/**
101
	 * user preference: import all-day events as non blocking
102
	 *
103
	 * @var boolean
104
	 */
105
	var $nonBlockingAllday = false;
106
107
	/**
108
	 * user preference: attach UID entries to the DESCRIPTION
109
	 *
110
	 * @var boolean
111
	 */
112
	var $uidExtension = false;
113
114
	/**
115
	 * user preference: calendar to synchronize with
116
	 *
117
	 * @var int
118
	 */
119
	var $calendarOwner = 0;
120
121
	/**
122
	 * user preference: Use this timezone for import from and export to device
123
	 *
124
	 * === false => use event's TZ
125
	 * === null  => export in UTC
126
	 * string    => device TZ
127
	 *
128
	 * @var string|boolean
129
	 */
130
	var $tzid = null;
131
132
	/**
133
	 * Device CTCap Properties
134
	 *
135
	 * @var array
136
	 */
137
	var $clientProperties;
138
139
	/**
140
	 * vCalendar Instance for parsing
141
	 *
142
	 * @var array
143
	 */
144
	var $vCalendar;
145
146
	/**
147
	 * Addressbook BO instance
148
	 *
149
	 * @var array
150
	 */
151
	var $addressbook;
152
153
	/**
154
	 * Set Logging
155
	 *
156
	 * @var boolean
157
	 */
158
	var $log = false;
159
	var $logfile="/tmp/log-vcal";
160
161
	/**
162
	 * Event callback
163
	 * If set, this will be called on each discovered event so it can be
164
	 * modified.  Event is passed by reference, return true to keep the event
165
	 * or false to skip it.
166
	 *
167
	 * @var callable
168
	 */
169
	var $event_callback = null;
170
171
	/**
172
	 * Conflict callback
173
	 * If set, conflict checking will be enabled, and the event as well as
174
	 * conflicts are passed as parameters to this callback
175
	 */
176
	var $conflict_callback = null;
177
178
	/**
179
	 * Constructor
180
	 *
181
	 * @param array $_clientProperties		client properties
182
	 */
183
	function __construct(&$_clientProperties = array())
184
	{
185
		parent::__construct();
186
		if ($this->log) $this->logfile = $GLOBALS['egw_info']['server']['temp_dir']."/log-vcal";
187
		$this->clientProperties = $_clientProperties;
188
		$this->vCalendar = new Horde_Icalendar;
0 ignored issues
show
Documentation Bug introduced by
It seems like new Horde_Icalendar() of type Horde_Icalendar is incompatible with the declared type array of property $vCalendar.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
189
		$this->addressbook = new Api\Contacts;
0 ignored issues
show
Documentation Bug introduced by
It seems like new EGroupware\Api\Contacts() of type EGroupware\Api\Contacts is incompatible with the declared type array of property $addressbook.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
190
	}
191
192
193
	/**
194
	 * Exports one calendar event to an iCalendar item
195
	 *
196
	 * @param int|array $events (array of) cal_id or array of the events with timestamps in server time
197
	 * @param string $version ='1.0' could be '2.0' too
198
	 * @param string $method ='PUBLISH'
199
	 * @param int $recur_date =0	if set export the next recurrence at or after the timestamp,
200
	 *                          default 0 => export whole series (or events, if not recurring)
201
	 * @param string $principalURL ='' Used for CalDAV exports
202
	 * @param string $charset ='UTF-8' encoding of the vcalendar, default UTF-8
203
	 * @param int|string $current_user =0 uid of current user to only export that one as participant for method=REPLY
204
	 * @return string|boolean string with iCal or false on error (e.g. no permission to read the event)
205
	 */
206
	function &exportVCal($events, $version='1.0', $method='PUBLISH', $recur_date=0, $principalURL='', $charset='UTF-8', $current_user=0)
207
	{
208
		if ($this->log)
209
		{
210
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
211
				"($version, $method, $recur_date, $principalURL, $charset)\n",
212
				3, $this->logfile);
213
		}
214
		$egwSupportedFields = array(
215
			'CLASS'			=> 'public',
216
			'SUMMARY'		=> 'title',
217
			'DESCRIPTION'	=> 'description',
218
			'LOCATION'		=> 'location',
219
			'DTSTART'		=> 'start',
220
			'DTEND'			=> 'end',
221
			'ATTENDEE'		=> 'participants',
222
			'ORGANIZER'		=> 'owner',
223
			'RRULE'			=> 'recur_type',
224
			'EXDATE'		=> 'recur_exception',
225
			'PRIORITY'		=> 'priority',
226
			'TRANSP'		=> 'non_blocking',
227
			'CATEGORIES'	=> 'category',
228
			'UID'			=> 'uid',
229
			'RECURRENCE-ID' => 'recurrence',
230
			'SEQUENCE'		=> 'etag',
231
			'STATUS'		=> 'status',
232
			'ATTACH'        => 'attachments',
233
		);
234
235
		if (!is_array($this->supportedFields)) $this->setSupportedFields();
0 ignored issues
show
introduced by
The condition is_array($this->supportedFields) is always true.
Loading history...
236
237
		if ($this->productManufacturer == '' )
238
		{	// syncevolution is broken
239
			$version = '2.0';
240
		}
241
242
		$vcal = new Horde_Icalendar;
243
		$vcal->setAttribute('PRODID','-//EGroupware//NONSGML EGroupware Calendar '.$GLOBALS['egw_info']['apps']['calendar']['version'].'//'.
244
			strtoupper($GLOBALS['egw_info']['user']['preferences']['common']['lang']));
245
		$vcal->setAttribute('VERSION', $version);
246
		if ($method) $vcal->setAttribute('METHOD', $method);
247
		$events_exported = false;
248
249
		if (!is_array($events)) $events = array($events);
250
251
		$vtimezones_added = array();
252
		foreach ($events as $event)
253
		{
254
			$organizerURL = '';
255
			$organizerCN = false;
256
			$recurrence = $this->date2usertime($recur_date);
257
			$tzid = null;
258
259
			if ((!is_array($event) || empty($event['tzid']) && ($event = $event['id'])) &&
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (! is_array($event) || e...rence, false, 'server'), Probably Intended Meaning: ! is_array($event) || (e...ence, false, 'server'))
Loading history...
260
				!($event = $this->read($event, $recurrence, false, 'server')))
261
			{
262
				if ($this->read($event, $recurrence, true, 'server'))
263
				{
264
					if ($this->check_perms(calendar_bo::ACL_FREEBUSY, $event, 0, 'server'))
265
					{
266
						$this->clear_private_infos($event, array($this->user, $event['owner']));
267
					}
268
					else
269
					{
270
						if ($this->log)
271
						{
272
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
273
								'() User does not have the permission to read event ' . $event['id']. "\n",
274
								3,$this->logfile);
275
						}
276
						return -1; // Permission denied
277
					}
278
				}
279
				else
280
				{
281
					$retval = false;  // Entry does not exist
282
					if ($this->log)
283
					{
284
						error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
285
							"() Event $event not found.\n",
286
							3, $this->logfile);
287
					}
288
				}
289
				continue;
290
			}
291
292
			if ($this->log)
293
			{
294
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
295
					'() export event UID: ' . $event['uid'] . ".\n",
296
					3, $this->logfile);
297
			}
298
299
			if ($this->tzid)
300
			{
301
				// explicit device timezone
302
				$tzid = $this->tzid;
303
			}
304
			elseif ($this->tzid === false)
305
			{
306
				// use event's timezone
307
				$tzid = $event['tzid'];
308
			}
309
310
			if (!isset(self::$tz_cache[$event['tzid']]))
311
			{
312
				try {
313
					self::$tz_cache[$event['tzid']] = calendar_timezones::DateTimeZone($event['tzid']);
314
				}
315
				catch (Exception $e) {
316
					// log unknown timezones
317
					if (!empty($event['tzid'])) _egw_log_exception($e);
318
					// default for no timezone and unkown to user timezone
319
					self::$tz_cache[$event['tzid']] = Api\DateTime::$user_timezone;
320
				}
321
			}
322
323
			if ($this->so->isWholeDay($event)) $event['whole_day'] = true;
324
325
			if ($this->log)
326
			{
327
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
328
					'(' . $event['id']. ',' . $recurrence . ")\n" .
329
					array2string($event)."\n",3,$this->logfile);
330
			}
331
332
			if ($recurrence)
333
			{
334
				if (!($master = $this->read($event['id'], 0, true, 'server'))) continue;
335
336
				if (!isset($this->supportedFields['participants']))
337
				{
338
					$days = $this->so->get_recurrence_exceptions($master, $tzid, 0, 0, 'tz_rrule');
339
					if (isset($days[$recurrence]))
340
					{
341
						$recurrence = $days[$recurrence]; // use remote representation
0 ignored issues
show
Unused Code introduced by
The assignment to $recurrence is dead and can be removed.
Loading history...
342
					}
343
					else
344
					{
345
						// We don't need status only exceptions
346
						if ($this->log)
347
						{
348
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
349
								"(, $recurrence) Gratuitous pseudo exception, skipped ...\n",
350
								3,$this->logfile);
351
						}
352
						continue; // unsupported status only exception
353
					}
354
				}
355
				else
356
				{
357
					$days = $this->so->get_recurrence_exceptions($master, $tzid, 0, 0, 'rrule');
358
					if ($this->log)
359
					{
360
						error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" .
361
							array2string($days)."\n",3,$this->logfile);
362
					}
363
					$recurrence = $days[$recurrence]; // use remote representation
364
				}
365
				// force single event
366
				foreach (array('recur_enddate','recur_interval','recur_exception','recur_data','recur_date','id','etag') as $name)
367
				{
368
					unset($event[$name]);
369
				}
370
				$event['recur_type'] = MCAL_RECUR_NONE;
371
			}
372
373
			// check if tzid of event (not only recuring ones) is already added to export
374
			if ($tzid && $tzid != 'UTC' && !in_array($tzid,$vtimezones_added))
375
			{
376
				// check if we have vtimezone component data for tzid of event, if not default to user timezone (default to server tz)
377
				if (calendar_timezones::add_vtimezone($vcal, $tzid) ||
378
					!in_array($tzid = Api\DateTime::$user_timezone->getName(), $vtimezones_added) &&
379
						calendar_timezones::add_vtimezone($vcal, $tzid))
380
				{
381
					$vtimezones_added[] = $tzid;
382
					if (!isset(self::$tz_cache[$tzid]))
383
					{
384
						self::$tz_cache[$tzid] = calendar_timezones::DateTimeZone($tzid);
385
					}
386
				}
387
			}
388
			if ($this->productManufacturer != 'file' && $this->uidExtension)
389
			{
390
				// Append UID to DESCRIPTION
391
				if (!preg_match('/\[UID:.+\]/m', $event['description'])) {
392
					$event['description'] .= "\n[UID:" . $event['uid'] . "]";
393
				}
394
			}
395
396
			$vevent = Horde_Icalendar::newComponent('VEVENT', $vcal);
397
			$parameters = $attributes = $values = array();
398
399
			if ($this->productManufacturer == 'sonyericsson')
400
			{
401
				$eventDST = date('I', $event['start']);
402
				if ($eventDST)
403
				{
404
					$attributes['X-SONYERICSSON-DST'] = 4;
405
				}
406
			}
407
408
			if ($event['recur_type'] != MCAL_RECUR_NONE)
409
			{
410
				$exceptions = array();
411
412
				// dont use "virtual" exceptions created by participant status for GroupDAV or file export
413
				if (!in_array($this->productManufacturer,array('file','groupdav')))
414
				{
415
					$filter = isset($this->supportedFields['participants']) ? 'rrule' : 'tz_rrule';
416
					$exceptions = $this->so->get_recurrence_exceptions($event, $tzid, 0, 0, $filter);
417
					if ($this->log)
418
					{
419
						error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."(EXCEPTIONS)\n" .
420
							array2string($exceptions)."\n",3,$this->logfile);
421
					}
422
				}
423
				elseif (is_array($event['recur_exception']))
424
				{
425
					$exceptions = array_unique($event['recur_exception']);
426
					sort($exceptions);
427
				}
428
				$event['recur_exception'] = $exceptions;
429
			}
430
			foreach ($egwSupportedFields as $icalFieldName => $egwFieldName)
431
			{
432
				if (!isset($this->supportedFields[$egwFieldName]))
433
				{
434
					if ($this->log)
435
					{
436
						error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
437
							'(' . $event['id'] . ") [$icalFieldName] not supported\n",
438
							3,$this->logfile);
439
					}
440
					continue;
441
				}
442
				$values[$icalFieldName] = array();
443
				switch ($icalFieldName)
444
				{
445
					case 'ATTENDEE':
446
						foreach ((array)$event['participants'] as $uid => $status)
447
						{
448
							$quantity = $role = null;
449
							calendar_so::split_status($status, $quantity, $role);
450
							// do not include event owner/ORGANIZER as participant in his own calendar, if he is only participant
451
							if (count($event['participants']) == 1 && $event['owner'] == $uid && $uid == $this->user) continue;
452
453
							if (!($info = $this->resource_info($uid))) continue;
454
455
							if (in_array($status, array('X','E'))) continue;	// dont include deleted participants
456
457
							if ($this->log)
458
							{
459
								error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ .
460
									'()attendee:' . array2string($info) ."\n",3,$this->logfile);
461
							}
462
							$participantCN = str_replace(array('\\', ',', ';', ':'),
463
												array('\\\\', '\\,', '\\;', '\\:'),
464
												trim(empty($info['cn']) ? $info['name'] : $info['cn']));
465
							if ($version == '1.0')
466
							{
467
								$participantURL = trim('"' . $participantCN . '"' . (empty($info['email']) ? '' : ' <' . $info['email'] .'>'));
468
							}
469
							else
470
							{
471
								$participantURL = empty($info['email']) ? '' : 'mailto:' . $info['email'];
472
							}
473
							// RSVP={TRUE|FALSE}	// resonse expected, not set in eGW => status=U
474
							$rsvp = $status == 'U' ? 'TRUE' : 'FALSE';
475
							if ($role == 'CHAIR')
476
							{
477
								$organizerURL = $participantURL;
478
								$rsvp = '';
479
								$organizerCN = $participantCN;
480
								$organizerUID = ($info['type'] != 'e' ? (string)$uid : '');
481
							}
482
							// iCal method=REPLY only exports replying / current user, except external organiser / chair above
483
							if ($method == 'REPLY' && $current_user && (string)$current_user !== (string)$uid)
484
							{
485
								continue;
486
							}
487
							// PARTSTAT={NEEDS-ACTION|ACCEPTED|DECLINED|TENTATIVE|DELEGATED|COMPLETED|IN-PROGRESS} everything from delegated is NOT used by eGW atm.
488
							$status = $this->status_egw2ical[$status];
489
							// CUTYPE={INDIVIDUAL|GROUP|RESOURCE|ROOM|UNKNOWN}
490
							switch ($info['type'])
491
							{
492
								case 'g':
493
									$cutype = 'GROUP';
494
									$participantURL = 'urn:uuid:'.Api\CalDAV::generate_uid('accounts', $uid);
495
									if (!isset($event['participants'][$this->user]) &&
496
										($members = $GLOBALS['egw']->accounts->members($uid, true)) && in_array($this->user, $members))
497
									{
498
										$user = $this->resource_info($this->user);
499
										$attributes['ATTENDEE'][] = 'mailto:' . $user['email'];
500
										$parameters['ATTENDEE'][] = array(
501
											'CN'		=>	$user['name'],
502
											'ROLE'		=> 'REQ-PARTICIPANT',
503
											'PARTSTAT'	=> 'NEEDS-ACTION',
504
											'CUTYPE'	=> 'INDIVIDUAL',
505
											'RSVP'		=> 'TRUE',
506
											'X-EGROUPWARE-UID'	=> (string)$this->user,
507
										);
508
										$event['participants'][$this->user] = true;
509
									}
510
									break;
511
								case 'r':
512
									$participantURL = 'urn:uuid:'.Api\CalDAV::generate_uid('resources', substr($uid, 1));
513
									$cutype = Api\CalDAV\Principals::resource_is_location(substr($uid, 1)) ? 'ROOM' : 'RESOURCE';
514
									// unset resource email (email of responsible user) as iCal at least has problems,
515
									// if resonpsible is also pariticipant or organizer
516
									unset($info['email']);
517
									break;
518
								case 'u':	// account
519
								case 'c':	// contact
520
								case 'e':	// email address
521
									$cutype = 'INDIVIDUAL';
522
									break;
523
								default:
524
									$cutype = 'UNKNOWN';
525
									break;
526
							}
527
							// generate urn:uuid, if we have no other participant URL
528
							if (empty($participantURL) && $info && $info['app'])
529
							{
530
								$participantURL = 'urn:uuid:'.Api\CalDAV::generate_uid($info['app'], substr($uid, 1));
531
							}
532
							// ROLE={CHAIR|REQ-PARTICIPANT|OPT-PARTICIPANT|NON-PARTICIPANT|X-*}
533
							$options = array();
534
							if (!empty($participantCN)) $options['CN'] = $participantCN;
535
							if (!empty($role)) $options['ROLE'] = $role;
536
							if (!empty($status)) $options['PARTSTAT'] = $status;
537
							if (!empty($cutype)) $options['CUTYPE'] = $cutype;
538
							if (!empty($rsvp)) $options['RSVP'] = $rsvp;
539
							if (!empty($info['email']) && $participantURL != 'mailto:'.$info['email'])
540
							{
541
								$options['EMAIL'] = $info['email'];	// only add EMAIL attribute, if not already URL, as eg. Akonadi is reported to have problems with it
542
							}
543
							if ($info['type'] != 'e') $options['X-EGROUPWARE-UID'] = (string)$uid;
544
							if ($quantity > 1)
545
							{
546
								$options['X-EGROUPWARE-QUANTITY'] = (string)$quantity;
547
								$options['CN'] .= ' ('.$quantity.')';
548
							}
549
							$attributes['ATTENDEE'][] = $participantURL;
550
							$parameters['ATTENDEE'][] = $options;
551
						}
552
						break;
553
554
					case 'CLASS':
555
						if ($event['public']) continue 2;	// public is default, no need to export, fails CalDAVTester if added as default
556
						$attributes['CLASS'] = $event['public'] ? 'PUBLIC' : 'PRIVATE';
557
						// Apple iCal on OS X uses X-CALENDARSERVER-ACCESS: CONFIDENTIAL on VCALANDAR (not VEVENT!)
558
						if (!$event['public'] && $this->productManufacturer == 'groupdav')
559
						{
560
							$vcal->setAttribute('X-CALENDARSERVER-ACCESS', 'CONFIDENTIAL');
561
						}
562
						break;
563
564
					case 'ORGANIZER':
565
						if (!$organizerURL)
566
						{
567
							$organizerCN = '"' . trim($GLOBALS['egw']->accounts->id2name($event['owner'],'account_firstname')
568
								. ' ' . $GLOBALS['egw']->accounts->id2name($event['owner'],'account_lastname')) . '"';
569
							$organizerEMail = $GLOBALS['egw']->accounts->id2name($event['owner'],'account_email');
570
							if ($version == '1.0')
571
							{
572
								$organizerURL = trim($organizerCN . (empty($organizerURL) ? '' : ' <' . $organizerURL .'>'));
573
							}
574
							else
575
							{
576
								$organizerURL = empty($organizerEMail) ? '' : 'mailto:' . $organizerEMail;
577
							}
578
							$organizerUID = $event['owner'];
579
						}
580
						// do NOT use ORGANIZER for events without further participants or a different organizer
581
						if (count($event['participants']) > 1 || !isset($event['participants'][$event['owner']]) || $event['owner'] != $this->user)
582
						{
583
							$attributes['ORGANIZER'] = $organizerURL;
584
							$parameters['ORGANIZER']['CN'] = $organizerCN;
585
							if (!empty($organizerUID))
586
							{
587
								$parameters['ORGANIZER']['X-EGROUPWARE-UID'] = $organizerUID;
588
							}
589
						}
590
						break;
591
592
					case 'DTSTART':
593
						if (empty($event['whole_day']))
594
						{
595
							$attributes['DTSTART'] = self::getDateTime($event['start'],$tzid,$parameters['DTSTART']);
596
						}
597
						break;
598
599
					case 'DTEND':
600
						if (empty($event['whole_day']))
601
						{
602
							// Hack for CalDAVTester to export duration instead of endtime
603
							if ($tzid == 'UTC' && $event['end'] - $event['start'] <= 86400)
604
								$attributes['duration'] = $event['end'] - $event['start'];
605
							else
606
								$attributes['DTEND'] = self::getDateTime($event['end'],$tzid,$parameters['DTEND']);
607
						}
608
						else
609
						{
610
							// write start + end of whole day events as dates
611
							$event['end-nextday'] = $event['end'] + 12*3600;	// we need the date of the next day, as DTEND is non-inclusive (= exclusive) in rfc2445
612
							foreach (array('start' => 'DTSTART','end-nextday' => 'DTEND') as $f => $t)
613
							{
614
								$time = new Api\DateTime($event[$f],Api\DateTime::$server_timezone);
615
								$arr = Api\DateTime::to($time,'array');
616
								$vevent->setAttribute($t, array('year' => $arr['year'],'month' => $arr['month'],'mday' => $arr['day']),
617
									array('VALUE' => 'DATE'));
618
							}
619
							unset($attributes['DTSTART']);
620
							// Outlook does NOT care about type of DTSTART/END, only setting X-MICROSOFT-CDO-ALLDAYEVENT is used to determine an event is a whole-day event
621
							$vevent->setAttribute('X-MICROSOFT-CDO-ALLDAYEVENT','TRUE');
622
						}
623
						break;
624
625
					case 'RRULE':
626
						if ($event['recur_type'] == MCAL_RECUR_NONE) break;		// no recuring event
627
						$rriter = calendar_rrule::event2rrule($event, false, $tzid);
628
						$rrule = $rriter->generate_rrule($version);
629
						if ($event['recur_enddate'])
630
						{
631
							if (!$tzid || $version != '1.0')
632
							{
633
								if (!isset(self::$tz_cache['UTC']))
634
								{
635
									self::$tz_cache['UTC'] = calendar_timezones::DateTimeZone('UTC');
636
								}
637
								if (empty($event['whole_day']))
638
								{
639
									$rrule['UNTIL']->setTimezone(self::$tz_cache['UTC']);
640
									$rrule['UNTIL'] = $rrule['UNTIL']->format('Ymd\THis\Z');
641
								}
642
								// for whole-day events UNTIL must be just the inclusive date
643
								else
644
								{
645
									$rrule['UNTIL'] = $rrule['UNTIL']->format('Ymd');
646
								}
647
							}
648
						}
649
						if ($version == '1.0')
650
						{
651
							if ($event['recur_enddate'] && $tzid)
652
							{
653
								$rrule['UNTIL'] = self::getDateTime($rrule['UNTIL'],$tzid);
654
							}
655
							$attributes['RRULE'] = $rrule['FREQ'].' '.$rrule['UNTIL'];
656
						}
657
						else // $version == '2.0'
658
						{
659
							$attributes['RRULE'] = '';
660
							foreach($rrule as $n => $v)
661
							{
662
								$attributes['RRULE'] .= ($attributes['RRULE']?';':'').$n.'='.$v;
663
							}
664
						}
665
						break;
666
667
					case 'EXDATE':
668
						if ($event['recur_type'] == MCAL_RECUR_NONE) break;
669
						if (!empty($event['recur_exception']))
670
						{
671
							if (empty($event['whole_day']))
672
							{
673
								foreach ($event['recur_exception'] as $key => $timestamp)
674
								{
675
									// current Horde_Icalendar 2.1.4 exports EXDATE always postfixed with a Z :(
676
									// so if we set a timezone here, we have to remove the Z, see the hack at the end of this method
677
									// Apple calendar on OS X 10.11.4 uses a timezone, so does Horde eg. for Recurrence-ID
678
									$event['recur_exception'][$key] = self::getDateTime($timestamp, $tzid, $parameters['EXDATE']);
679
								}
680
							}
681
							else
682
							{
683
								// use 'DATE' instead of 'DATE-TIME' on whole day events
684
								foreach ($event['recur_exception'] as $id => $timestamp)
685
								{
686
									$time = new Api\DateTime($timestamp,Api\DateTime::$server_timezone);
687
									$time->setTimezone(self::$tz_cache[$event['tzid']]);
688
									$arr = Api\DateTime::to($time,'array');
689
									$days[$id] = array(
690
										'year'  => $arr['year'],
691
										'month' => $arr['month'],
692
										'mday'  => $arr['day'],
693
									);
694
								}
695
								$event['recur_exception'] = $days;
696
								if ($version != '1.0') $parameters['EXDATE']['VALUE'] = 'DATE';
697
							}
698
							$vevent->setAttribute('EXDATE', $event['recur_exception'], $parameters['EXDATE']);
699
						}
700
						break;
701
702
					case 'PRIORITY':
703
						if (!$event['priority']) continue 2;	// 0=undefined is default, no need to export, fails CalDAVTester if our default is added
704
						if ($this->productManufacturer == 'funambol' &&
705
							(strpos($this->productName, 'outlook') !== false
706
								|| strpos($this->productName, 'pocket pc') !== false))
707
						{
708
							$attributes['PRIORITY'] = (int) $this->priority_egw2funambol[$event['priority']];
709
						}
710
						else
711
						{
712
							$attributes['PRIORITY'] = (int) $this->priority_egw2ical[$event['priority']];
713
						}
714
						break;
715
716
					case 'TRANSP':
717
						if (!$event['non_blocking']) continue 2;	// OPAQUE is default, no need to export, fails CalDAVTester if added as default
718
						if ($version == '1.0')
719
						{
720
							$attributes['TRANSP'] = ($event['non_blocking'] ? 1 : 0);
721
						}
722
						else
723
						{
724
							$attributes['TRANSP'] = ($event['non_blocking'] ? 'TRANSPARENT' : 'OPAQUE');
725
						}
726
						break;
727
728
					case 'STATUS':
729
						$attributes['STATUS'] = 'CONFIRMED';
730
						break;
731
732
					case 'CATEGORIES':
733
						if ($event['category'] && ($values['CATEGORIES'] = $this->get_categories($event['category'])))
734
						{
735
							if (count($values['CATEGORIES']) == 1)
736
							{
737
								$attributes['CATEGORIES'] = array_shift($values['CATEGORIES']);
738
							}
739
							else
740
							{
741
								$attributes['CATEGORIES'] = '';
742
							}
743
						}
744
						break;
745
746
					case 'RECURRENCE-ID':
747
						if ($version == '1.0')
748
						{
749
								$icalFieldName = 'X-RECURRENCE-ID';
750
						}
751
						if ($recur_date)
752
						{
753
							// We handle a pseudo exception
754
							if (empty($event['whole_day']))
755
							{
756
								$attributes[$icalFieldName] = self::getDateTime($recur_date,$tzid,$parameters[$icalFieldName]);
757
							}
758
							else
759
							{
760
								$time = new Api\DateTime($recur_date,Api\DateTime::$server_timezone);
761
								$time->setTimezone(self::$tz_cache[$event['tzid']]);
762
								$arr = Api\DateTime::to($time,'array');
763
								$vevent->setAttribute($icalFieldName, array(
764
									'year' => $arr['year'],
765
									'month' => $arr['month'],
766
									'mday' => $arr['day']),
767
									array('VALUE' => 'DATE')
768
								);
769
							}
770
						}
771
						elseif ($event['recurrence'] && $event['reference'])
772
						{
773
							// $event['reference'] is a calendar_id, not a timestamp
774
							if (!($revent = $this->read($event['reference']))) break;	// referenced event does not exist
775
776
							if (empty($revent['whole_day']))
777
							{
778
								$attributes[$icalFieldName] = self::getDateTime($event['recurrence'],$tzid,$parameters[$icalFieldName]);
779
							}
780
							else
781
							{
782
								$time = new Api\DateTime($event['recurrence'],Api\DateTime::$server_timezone);
783
								$time->setTimezone(self::$tz_cache[$event['tzid']]);
784
								$arr = Api\DateTime::to($time,'array');
785
								$vevent->setAttribute($icalFieldName, array(
786
									'year' => $arr['year'],
787
									'month' => $arr['month'],
788
									'mday' => $arr['day']),
789
									array('VALUE' => 'DATE')
790
								);
791
							}
792
793
							unset($revent);
794
						}
795
						break;
796
797
					case 'ATTACH':
798
						if (!empty($event['id']))
799
						{
800
							Api\CalDAV::add_attach('calendar', $event['id'], $attributes, $parameters);
801
						}
802
						break;
803
804
					default:
805
						if (isset($this->clientProperties[$icalFieldName]['Size']))
806
						{
807
							$size = $this->clientProperties[$icalFieldName]['Size'];
808
							$noTruncate = $this->clientProperties[$icalFieldName]['NoTruncate'];
809
							if ($this->log && $size > 0)
810
							{
811
								error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ .
812
									"() $icalFieldName Size: $size, NoTruncate: " .
813
									($noTruncate ? 'TRUE' : 'FALSE') . "\n",3,$this->logfile);
814
							}
815
							//Horde::logMessage("vCalendar $icalFieldName Size: $size, NoTruncate: " .
816
							//	($noTruncate ? 'TRUE' : 'FALSE'), __FILE__, __LINE__, PEAR_LOG_DEBUG);
817
						}
818
						else
819
						{
820
							$size = -1;
821
							$noTruncate = false;
822
						}
823
						$value = $event[$egwFieldName];
824
						$cursize = strlen($value);
825
						if ($size > 0 && $cursize > $size)
826
						{
827
							if ($noTruncate)
828
							{
829
								if ($this->log)
830
								{
831
									error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ .
832
										"() $icalFieldName omitted due to maximum size $size\n",3,$this->logfile);
833
								}
834
								//Horde::logMessage("vCalendar $icalFieldName omitted due to maximum size $size",
835
								//	__FILE__, __LINE__, PEAR_LOG_WARNING);
836
								continue 2; // skip field
837
							}
838
							// truncate the value to size
839
							$value = substr($value, 0, $size - 1);
840
							if ($this->log)
841
							{
842
								error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ .
843
									"() $icalFieldName truncated to maximum size $size\n",3,$this->logfile);
844
							}
845
							//Horde::logMessage("vCalendar $icalFieldName truncated to maximum size $size",
846
							//	__FILE__, __LINE__, PEAR_LOG_INFO);
847
						}
848
						if (!empty($value) || ($size >= 0 && !$noTruncate))
849
						{
850
							$attributes[$icalFieldName] = $value;
851
						}
852
				}
853
			}
854
855
			// for CalDAV add all X-Properties previously parsed
856
			if ($this->productManufacturer == 'groupdav' || $this->productManufacturer == 'file')
857
			{
858
				foreach($event as $name => $value)
859
				{
860
					if (substr($name, 0, 2) == '##')
861
					{
862
						if ($value[0] === '{' && ($attr = json_decode($value, true)) && is_array($attr))
863
						{
864
							// check if attribute was stored compressed --> uncompress it
865
							if (count($attr) === 1 && !empty($attr['gzcompress']))
866
							{
867
								$attr = json_decode(gzuncompress(base64_decode($attr['gzcompress'])), true);
868
								if (!is_array($attr)) continue;
869
							}
870
							$vevent->setAttribute(substr($name, 2), $attr['value'], $attr['params'], true, $attr['values']);
871
						}
872
						else
873
						{
874
							$vevent->setAttribute(substr($name, 2), $value);
875
						}
876
					}
877
				}
878
			}
879
880
			if ($this->productManufacturer == 'nokia')
881
			{
882
				if ($event['special'] == '1')
883
				{
884
					$attributes['X-EPOCAGENDAENTRYTYPE'] = 'ANNIVERSARY';
885
					$attributes['DTEND'] = $attributes['DTSTART'];
886
				}
887
				elseif ($event['special'] == '2' || !empty($event['whole_day']))
888
				{
889
					$attributes['X-EPOCAGENDAENTRYTYPE'] = 'EVENT';
890
				}
891
				else
892
				{
893
					$attributes['X-EPOCAGENDAENTRYTYPE'] = 'APPOINTMENT';
894
				}
895
			}
896
897
			if ($event['created'] || $event['modified'])
898
			{
899
				$attributes['CREATED'] = $event['created'] ? $event['created'] : $event['modified'];
900
			}
901
			if ($event['modified'])
902
			{
903
				$attributes['LAST-MODIFIED'] = $event['modified'];
904
			}
905
			$attributes['DTSTAMP'] = time();
906
			foreach ((array)$event['alarm'] as $alarmData)
907
			{
908
				// skip over alarms that don't have the minimum required info
909
				if (!isset($alarmData['offset']) && !isset($alarmData['time'])) continue;
910
911
				// skip alarms not being set for all users and alarms owned by other users
912
				if ($alarmData['all'] != true && $alarmData['owner'] != $this->user)
913
				{
914
					continue;
915
				}
916
917
				if ($alarmData['offset'])
918
				{
919
					$alarmData['time'] = $event['start'] - $alarmData['offset'];
920
				}
921
922
				$description = trim(preg_replace("/\r?\n?\\[[A-Z_]+:.*\\]/i", '', $event['description']));
923
924
				if ($version == '1.0')
925
				{
926
					if ($event['title']) $description = $event['title'];
927
					if ($description)
928
					{
929
						$values['DALARM']['snooze_time'] = '';
930
						$values['DALARM']['repeat count'] = '';
931
						$values['DALARM']['display text'] = $description;
932
						$values['AALARM']['snooze_time'] = '';
933
						$values['AALARM']['repeat count'] = '';
934
						$values['AALARM']['display text'] = $description;
935
					}
936
					$attributes['DALARM'] = self::getDateTime($alarmData['time'],$tzid,$parameters['DALARM']);
937
					$attributes['AALARM'] = self::getDateTime($alarmData['time'],$tzid,$parameters['AALARM']);
938
					// lets take only the first alarm
939
					break;
940
				}
941
				else
942
				{
943
					// VCalendar 2.0 / RFC 2445
944
945
					// RFC requires DESCRIPTION for DISPLAY
946
					if (!$event['title'] && !$description) $description = 'Alarm';
947
948
					/* Disabling for now
949
					// Lightning infinitly pops up alarms for recuring events, if the only use an offset
950
					if ($this->productName == 'lightning' && $event['recur_type'] != MCAL_RECUR_NONE)
951
					{
952
						// return only future alarms to lightning
953
						if (($nextOccurence = $this->read($event['id'], $this->now_su + $alarmData['offset'], false, 'server')))
954
						{
955
							$alarmData['time'] = $nextOccurence['start'] - $alarmData['offset'];
956
							$alarmData['offset'] = false;
957
						}
958
						else
959
						{
960
							continue;
961
						}
962
					}*/
963
964
					// for SyncML non-whole-day events always use absolute times
965
					// (probably because some devices have no clue about timezones)
966
					// GroupDAV uses offsets, as web UI assumes alarms are relative too
967
					// (with absolute times GroupDAV clients do NOT move alarms, if events move!)
968
					if ($this->productManufacturer != 'groupdav' &&
969
						!empty($event['whole_day']) && $alarmData['offset'])
970
					{
971
						$alarmData['offset'] = false;
972
					}
973
974
					$valarm = Horde_Icalendar::newComponent('VALARM',$vevent);
975
					if ($alarmData['offset'] !== false)
976
					{
977
						$valarm->setAttribute('TRIGGER', -$alarmData['offset'],
978
							array('VALUE' => 'DURATION', 'RELATED' => 'START'));
979
					}
980
					else
981
					{
982
						$params = array('VALUE' => 'DATE-TIME');
983
						$value = self::getDateTime($alarmData['time'],$tzid,$params);
984
						$valarm->setAttribute('TRIGGER', $value, $params);
985
					}
986
					if (!empty($alarmData['uid']))
987
					{
988
						$valarm->setAttribute('UID', $alarmData['uid']);
989
						$valarm->setAttribute('X-WR-ALARMUID', $alarmData['uid']);
990
					}
991
					// set evtl. existing attributes set by iCal clients not used by EGroupware
992
					if (isset($alarmData['attrs']))
993
					{
994
						foreach($alarmData['attrs'] as $attr => $data)
995
						{
996
							$valarm->setAttribute($attr, $data['value'], $data['params']);
997
						}
998
					}
999
					// set default ACTION and DESCRIPTION, if not set by a client
1000
					if (!isset($alarmData['attrs']) || !isset($alarmData['attrs']['ACTION']))
1001
					{
1002
						$valarm->setAttribute('ACTION','DISPLAY');
1003
					}
1004
					if (!isset($alarmData['attrs']) || !isset($alarmData['attrs']['DESCRIPTION']))
1005
					{
1006
						$valarm->setAttribute('DESCRIPTION',$event['title'] ? $event['title'] : $description);
1007
					}
1008
					$vevent->addComponent($valarm);
1009
				}
1010
			}
1011
1012
			foreach ($attributes as $key => $value)
1013
			{
1014
				foreach (is_array($value) && $parameters[$key]['VALUE']!='DATE' ? $value : array($value) as $valueID => $valueData)
1015
				{
1016
					$valueData = Api\Translation::convert($valueData,Api\Translation::charset(),$charset);
1017
	                $paramData = (array) Api\Translation::convert(is_array($value) ?
1018
	                		$parameters[$key][$valueID] : $parameters[$key],
1019
	                        Api\Translation::charset(),$charset);
1020
	                $valuesData = (array) Api\Translation::convert($values[$key],
1021
	                		Api\Translation::charset(),$charset);
1022
	                $content = $valueData . implode(';', $valuesData);
0 ignored issues
show
Bug introduced by
Are you sure $valueData of type array|null|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1022
	                $content = /** @scrutinizer ignore-type */ $valueData . implode(';', $valuesData);
Loading history...
1023
1024
					if ($version == '1.0' && (preg_match('/[^\x20-\x7F]/', $content) ||
1025
						($paramData['CN'] && preg_match('/[^\x20-\x7F]/', $paramData['CN']))))
1026
					{
1027
						$paramData['CHARSET'] = $charset;
1028
						switch ($this->productManufacturer)
1029
						{
1030
							case 'groupdav':
1031
								if ($this->productName == 'kde')
1032
								{
1033
									$paramData['ENCODING'] = 'QUOTED-PRINTABLE';
1034
								}
1035
								else
1036
								{
1037
									$paramData['CHARSET'] = '';
1038
									if (preg_match(Api\CalDAV\Handler::REQUIRE_QUOTED_PRINTABLE_ENCODING, $valueData))
1039
									{
1040
										$paramData['ENCODING'] = 'QUOTED-PRINTABLE';
1041
									}
1042
									else
1043
									{
1044
										$paramData['ENCODING'] = '';
1045
									}
1046
								}
1047
								break;
1048
							case 'funambol':
1049
								$paramData['ENCODING'] = 'FUNAMBOL-QP';
1050
						}
1051
					}
1052
					/*
1053
					if (preg_match('/([\000-\012])/', $valueData))
1054
					{
1055
						if ($this->log)
1056
						{
1057
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ .
1058
								"() Has invalid XML data: $valueData",3,$this->logfile);
1059
						}
1060
					}
1061
					*/
1062
					$vevent->setAttribute($key, $valueData, $paramData, true, $valuesData);
1063
				}
1064
			}
1065
			$vcal->addComponent($vevent);
1066
			$events_exported = true;
1067
		}
1068
1069
		$retval = $events_exported ? $vcal->exportvCalendar() : false;
1070
 		if ($this->log)
1071
 		{
1072
 			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ .
1073
				"() '$this->productManufacturer','$this->productName'\n",3,$this->logfile);
1074
 			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ .
1075
				"()\n".array2string($retval)."\n",3,$this->logfile);
1076
 		}
1077
1078
		// hack to fix iCalendar exporting EXDATE always postfixed with a Z
1079
		// EXDATE can have multiple values and therefore be folded into multiple lines
1080
		return preg_replace_callback("/\nEXDATE;TZID=[^:]+:[0-9TZ \n,]+/", function($matches)
1081
			{
1082
				return preg_replace('/([0-9 ])Z/', '$1', $matches[0]);
1083
			}, $retval);
1084
	}
1085
1086
	/**
1087
	 * Get DateTime value for a given time and timezone
1088
	 *
1089
	 * @param int|string|DateTime $time in server-time as returned by calendar_bo for $data_format='server'
1090
	 * @param string $tzid TZID of event or 'UTC' or NULL for palmos timestamps in usertime
1091
	 * @param array &$params=null parameter array to set TZID
1092
	 * @return mixed attribute value to set: integer timestamp if $tzid == 'UTC' otherwise Ymd\THis string IN $tzid
1093
	 */
1094
	static function getDateTime($time,$tzid,array &$params=null)
1095
	{
1096
		if (empty($tzid) || $tzid == 'UTC')
1097
		{
1098
			return Api\DateTime::to($time,'ts');
1099
		}
1100
		if (!is_a($time,'DateTime'))
1101
		{
1102
			$time = new Api\DateTime($time,Api\DateTime::$server_timezone);
1103
		}
1104
		if (!isset(self::$tz_cache[$tzid]))
1105
		{
1106
			self::$tz_cache[$tzid] = calendar_timezones::DateTimeZone($tzid);
1107
		}
1108
		$time->setTimezone(self::$tz_cache[$tzid]);
1109
		$params['TZID'] = $tzid;
1110
1111
		return $time->format('Ymd\THis');
1112
	}
1113
1114
	/**
1115
	 * Number of events imported in last call to importVCal
1116
	 *
1117
	 * @var int
1118
	 */
1119
	var $events_imported;
1120
1121
	/**
1122
	 * Import an iCal
1123
	 *
1124
	 * @param string|resource $_vcalData
1125
	 * @param int $cal_id=-1 must be -1 for new entries!
1126
	 * @param string $etag=null if an etag is given, it has to match the current etag or the import will fail
1127
	 * @param boolean $merge=false	merge data with existing entry
1128
	 * @param int $recur_date=0 if set, import the recurrence at this timestamp,
1129
	 *                          default 0 => import whole series (or events, if not recurring)
1130
	 * @param string $principalURL='' Used for CalDAV imports
1131
	 * @param int $user=null account_id of owner, default null
1132
	 * @param string $charset  The encoding charset for $text. Defaults to
1133
	 *                         utf-8 for new format, iso-8859-1 for old format.
1134
	 * @param string $caldav_name=null name from CalDAV client or null (to use default)
1135
	 * @return int|boolean|null cal_id > 0 on success, false on failure or 0 for a failed etag|permission denied or null for "403 Forbidden"
1136
	 */
1137
	function importVCal($_vcalData, $cal_id=-1, $etag=null, $merge=false, $recur_date=0, $principalURL='', $user=null, $charset=null, $caldav_name=null,$skip_notification=false)
1138
	{
1139
		//error_log(__METHOD__."(, $cal_id, $etag, $merge, $recur_date, $principalURL, $user, $charset, $caldav_name)");
1140
		$this->events_imported = 0;
1141
		$replace = $delete_exceptions= false;
1142
1143
		if (!is_array($this->supportedFields)) $this->setSupportedFields();
0 ignored issues
show
introduced by
The condition is_array($this->supportedFields) is always true.
Loading history...
1144
1145
		if (!($events = $this->icaltoegw($_vcalData, $principalURL, $charset)))
1146
		{
1147
			return false;
1148
		}
1149
		if (!is_array($events)) $cal_id = -1;	// just to be sure, as iterator does NOT allow array access (eg. $events[0])
0 ignored issues
show
introduced by
The condition is_array($events) is always false.
Loading history...
1150
1151
		if ($cal_id > 0)
1152
		{
1153
			if (count($events) == 1)
1154
			{
1155
				$replace = $recur_date == 0;
1156
				$events[0]['id'] = $cal_id;
1157
				if (!is_null($etag)) $events[0]['etag'] = (int) $etag;
1158
				if ($recur_date) $events[0]['recurrence'] = $recur_date;
1159
			}
1160
			elseif (($foundEvent = $this->find_event(array('id' => $cal_id), 'exact')) &&
1161
					($eventId = array_shift($foundEvent)) &&
1162
					($egwEvent = $this->read($eventId)))
1163
			{
1164
				foreach ($events as $k => $event)
1165
				{
1166
					if (!isset($event['uid'])) $events[$k]['uid'] = $egwEvent['uid'];
1167
				}
1168
			}
1169
		}
1170
1171
		// check if we are importing an event series with exceptions in CalDAV
1172
		// only first event / series master get's cal_id from URL
1173
		// other events are exceptions and need to be checked if they are new
1174
		// and for real (not status only) exceptions their recurrence-id need
1175
		// to be included as recur_exception to the master
1176
		if ($this->productManufacturer == 'groupdav' && $cal_id > 0 &&
1177
			$events[0]['recur_type'] != MCAL_RECUR_NONE)
1178
		{
1179
			calendar_groupdav::fix_series($events);
1180
		}
1181
1182
		if ($this->tzid)
1183
		{
1184
			$tzid = $this->tzid;
1185
		}
1186
		else
1187
		{
1188
			$tzid = Api\DateTime::$user_timezone->getName();
1189
		}
1190
1191
		date_default_timezone_set($tzid);
1192
1193
		$msg = null;
1194
		foreach ($events as $event)
1195
		{
1196
			if (!is_array($event)) continue; // the iterator may return false
1197
1198
			// Run event through callback
1199
			if($this->event_callback && is_callable($this->event_callback))
1200
			{
1201
				if(!call_user_func_array($this->event_callback, array(&$event)))
1202
				{
1203
					// Callback cancelled event
1204
					continue;
1205
				}
1206
			}
1207
			++$this->events_imported;
1208
1209
			if ($this->so->isWholeDay($event)) $event['whole_day'] = true;
1210
			if (is_array($event['category']))
1211
			{
1212
				$event['category'] = $this->find_or_add_categories($event['category'],
1213
					isset($event['id']) ? $event['id'] : -1);
1214
			}
1215
			if ($this->log)
1216
			{
1217
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
1218
					."($cal_id, $etag, $recur_date, $principalURL, $user, $charset)\n"
1219
					. array2string($event)."\n",3,$this->logfile);
1220
			}
1221
1222
			$updated_id = false;
1223
1224
			if ($replace)
1225
			{
1226
				$event_info['type'] = $event['recur_type'] == MCAL_RECUR_NONE ?
1227
					'SINGLE' : 'SERIES-MASTER';
1228
				$event_info['acl_edit'] = $this->check_perms(Acl::EDIT, $cal_id);
1229
				if (($event_info['stored_event'] = $this->read($cal_id, 0, false, 'server')) &&
1230
					$event_info['stored_event']['recur_type'] != MCAL_RECUR_NONE &&
1231
					($event_info['stored_event']['recur_type'] != $event['recur_type']
1232
					|| $event_info['stored_event']['recur_interval'] != $event['recur_interval']
1233
					|| $event_info['stored_event']['recur_data'] != $event['recur_data']
1234
					|| $event_info['stored_event']['start'] != $event['start']))
1235
				{
1236
					// handle the old exceptions
1237
					$recur_exceptions = $this->so->get_related($event_info['stored_event']['uid']);
1238
					foreach ($recur_exceptions as $id)
1239
					{
1240
						if ($delete_exceptions)
1241
						{
1242
							$this->delete($id,0,false,$skip_notification);
1243
						}
1244
						else
1245
						{
1246
							if (!($exception = $this->read($id))) continue;
1247
							$exception['uid'] = Api\CalDAV::generate_uid('calendar', $id);
1248
							$exception['reference'] = $exception['recurrence'] = 0;
1249
							$this->update($exception, true,true,false,true,$msg,$skip_notification);
1250
						}
1251
					}
1252
				}
1253
			}
1254
			else
1255
			{
1256
				$event_info = $this->get_event_info($event);
1257
			}
1258
1259
			// common adjustments for existing events
1260
			if (is_array($event_info['stored_event']))
1261
			{
1262
				if ($this->log)
1263
				{
1264
					error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
1265
						. "(UPDATE Event)\n"
1266
						. array2string($event_info['stored_event'])."\n",3,$this->logfile);
1267
				}
1268
				if (empty($event['uid']))
1269
				{
1270
					$event['uid'] = $event_info['stored_event']['uid']; // restore the UID if it was not delivered
1271
				}
1272
				elseif (empty($event['id']))
1273
				{
1274
					$event['id'] = $event_info['stored_event']['id']; // CalDAV does only provide UIDs
1275
				}
1276
				if (is_array($event['participants']))
1277
				{
1278
					// if the client does not return a status, we restore the original one
1279
					foreach ($event['participants'] as $uid => $status)
1280
					{
1281
						if ($status[0] == 'X')
1282
						{
1283
							if (isset($event_info['stored_event']['participants'][$uid]))
1284
							{
1285
								if ($this->log)
1286
								{
1287
									error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
1288
										"() Restore status for $uid\n",3,$this->logfile);
1289
								}
1290
								$event['participants'][$uid] = $event_info['stored_event']['participants'][$uid];
1291
							}
1292
							else
1293
							{
1294
								$event['participants'][$uid] = calendar_so::combine_status('U');
1295
							}
1296
						}
1297
						// restore resource-quantity from existing event as neither iOS nor Thunderbird returns our X-EGROUPWARE-QUANTITY
1298
						elseif ($uid[0] === 'r' && isset($event_info['stored_event']['participants'][$uid]))
1299
						{
1300
							$quantity = $role = $old_quantity = null;
1301
							calendar_so::split_status($status, $quantity, $role);
1302
							calendar_so::split_status($event_info['stored_event']['participants'][$uid], $old_quantity);
1303
							if ($old_quantity > 1)
1304
							{
1305
								$event['participants'][$uid] = calendar_so::combine_status('U', $old_quantity, $role);
1306
							}
1307
						}
1308
					}
1309
				}
1310
				// unset old X-* attributes stored in custom-fields
1311
				foreach ($event_info['stored_event'] as $key => $value)
1312
				{
1313
					if ($key[0] == '#' && $key[1] == '#' && !isset($event[$key]))
1314
					{
1315
						$event[$key] = '';
1316
					}
1317
				}
1318
				if ($merge)
1319
				{
1320
					if ($this->log)
1321
					{
1322
						error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
1323
							"()[MERGE]\n",3,$this->logfile);
1324
					}
1325
					// overwrite with server data for merge
1326
					foreach ($event_info['stored_event'] as $key => $value)
1327
					{
1328
						switch ($key)
1329
						{
1330
							case 'participants_types':
1331
								continue 2;	// +1 for switch
1332
1333
							case 'participants':
1334
								foreach ($event_info['stored_event']['participants'] as $uid => $status)
1335
								{
1336
									// Is a participant and no longer present in the event?
1337
									if (!isset($event['participants'][$uid]))
1338
									{
1339
										// Add it back in
1340
										$event['participants'][$uid] = $status;
1341
									}
1342
								}
1343
								break;
1344
1345
							default:
1346
								if (!empty($value)) $event[$key] = $value;
1347
						}
1348
					}
1349
				}
1350
				else
1351
				{
1352
					// no merge
1353
					if(!isset($this->supportedFields['category']))
1354
					{
1355
						$event['category'] = $event_info['stored_event']['category'];
1356
					}
1357
					if (!isset($this->supportedFields['participants'])
1358
						|| !$event['participants']
1359
						|| !is_array($event['participants'])
1360
						|| !count($event['participants']))
1361
					{
1362
						if ($this->log)
1363
						{
1364
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
1365
							"() No participants\n",3,$this->logfile);
1366
						}
1367
1368
						// If this is an updated meeting, and the client doesn't support
1369
						// participants OR the event no longer contains participants, add them back
1370
						unset($event['participants']);
1371
					}
1372
					// since we export now all participants in CalDAV as urn:uuid, if they have no email,
1373
					// we dont need and dont want that special treatment anymore, as it keeps client from changing resources
1374
					elseif ($this->productManufacturer != 'groupdav')
1375
					{
1376
						foreach ($event_info['stored_event']['participants'] as $uid => $status)
1377
						{
1378
							// Is it a resource and no longer present in the event?
1379
							if ($uid[0] == 'r' && !isset($event['participants'][$uid]))
1380
							{
1381
								if ($this->log)
1382
								{
1383
									error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
1384
										"() Restore resource $uid to status $status\n",3,$this->logfile);
1385
								}
1386
								// Add it back in
1387
								$event['participants'][$uid] = $status;
1388
							}
1389
						}
1390
					}
1391
1392
					/* Modifying an existing event with timezone different from default timezone of user
1393
					 * to a whole-day event (no timezone allowed according to iCal rfc)
1394
					 * --> code to modify start- and end-time here creates a one day longer event!
1395
					 * Skipping that code, creates the event correct in default timezone of user
1396
 					if (!empty($event['whole_day']) && $event['tzid'] != $event_info['stored_event']['tzid'])
1397
					{
1398
						// Adjust dates to original TZ
1399
						$time = new Api\DateTime($event['start'],Api\DateTime::$server_timezone);
1400
						$time =& $this->so->startOfDay($time, $event_info['stored_event']['tzid']);
1401
						$event['start'] = Api\DateTime::to($time,'server');
1402
						//$time = new Api\DateTime($event['end'],Api\DateTime::$server_timezone);
1403
						//$time =& $this->so->startOfDay($time, $event_info['stored_event']['tzid']);
1404
						//$time->setTime(23, 59, 59);
1405
						$time->modify('+'.round(($event['end']-$event['start'])/DAY_s).' day');
1406
						$event['end'] = Api\DateTime::to($time,'server');
1407
						if ($event['recur_type'] != MCAL_RECUR_NONE)
1408
						{
1409
							foreach ($event['recur_exception'] as $key => $day)
1410
							{
1411
								$time = new Api\DateTime($day,Api\DateTime::$server_timezone);
1412
								$time =& $this->so->startOfDay($time, $event_info['stored_event']['tzid']);
1413
								$event['recur_exception'][$key] = Api\DateTime::to($time,'server');
1414
							}
1415
						}
1416
						elseif ($event['recurrence'])
1417
						{
1418
							$time = new Api\DateTime($event['recurrence'],Api\DateTime::$server_timezone);
1419
							$time =& $this->so->startOfDay($time, $event_info['stored_event']['tzid']);
1420
							$event['recurrence'] = Api\DateTime::to($time,'server');
1421
						}
1422
						error_log(__METHOD__."() TZ adjusted {$event_info['stored_event']['tzid']} --> {$event['tzid']} event=".array2string($event));
1423
					}*/
1424
1425
					calendar_rrule::rrule2tz($event, $event_info['stored_event']['start'],
1426
						$event_info['stored_event']['tzid']);
1427
1428
					$event['tzid'] = $event_info['stored_event']['tzid'];
1429
					// avoid that iCal changes the organizer, which is not allowed
1430
					$event['owner'] = $event_info['stored_event']['owner'];
1431
				}
1432
				$event['caldav_name'] = $event_info['stored_event']['caldav_name'];
1433
1434
				// as we no longer export event owner/ORGANIZER as only participant, we have to re-add owner as participant
1435
				// to not loose him, as EGroupware knows events without owner/ORGANIZER as participant
1436
				if (isset($event_info['stored_event']['participants'][$event['owner']]) && !isset($event['participants'][$event['owner']]))
1437
				{
1438
					$event['participants'][$event['owner']] = $event_info['stored_event']['participants'][$event['owner']];
1439
				}
1440
			}
1441
			else // common adjustments for new events
1442
			{
1443
				unset($event['id']);
1444
				if ($caldav_name) $event['caldav_name'] = $caldav_name;
1445
				// set non blocking all day depending on the user setting
1446
				if (!empty($event['whole_day']) && $this->nonBlockingAllday)
1447
				{
1448
					$event['non_blocking'] = 1;
1449
				}
1450
1451
				if (!is_null($user))
1452
				{
1453
					if ($user > 0 && $this->check_perms(Acl::ADD, 0, $user))
1454
					{
1455
						$event['owner'] = $user;
1456
					}
1457
					elseif ($user > 0)
1458
					{
1459
						date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']);
1460
						return 0; // no permission
1461
					}
1462
					else
1463
					{
1464
						// add group or resource invitation
1465
						$event['owner'] = $this->user;
1466
						if (!isset($event['participants'][$this->user]))
1467
						{
1468
							$event['participants'][$this->user] = calendar_so::combine_status('A', 1, 'CHAIR');
1469
						}
1470
						// for resources check which new-status to give (eg. with direct booking permision 'A' instead 'U')
1471
						$event['participants'][$user] = calendar_so::combine_status(
1472
							$user < 0 || !isset($this->resources[$user[0]]['new_status']) ? 'U' :
1473
							ExecMethod($this->resources[$user[0]]['new_status'], substr($user, 1)));
0 ignored issues
show
Deprecated Code introduced by
The function ExecMethod() has been deprecated: use autoloadable class-names, instanciate and call method or use static methods ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1473
							/** @scrutinizer ignore-deprecated */ ExecMethod($this->resources[$user[0]]['new_status'], substr($user, 1)));

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
1474
					}
1475
				}
1476
				// check if an owner is set and the current user has add rights
1477
				// for that owners calendar; if not set the current user
1478
				elseif (!isset($event['owner'])
1479
					|| !$this->check_perms(Acl::ADD, 0, $event['owner']))
1480
				{
1481
					$event['owner'] = $this->user;
1482
				}
1483
1484
				if (!$event['participants']
1485
					|| !is_array($event['participants'])
1486
					|| !count($event['participants'])
1487
					// for new events, allways add owner as participant. Users expect to participate too, if they invite further participants.
1488
					// They can now only remove themselfs, if that is desired, after storing the event first.
1489
					|| !isset($event['participants'][$event['owner']]))
1490
				{
1491
					$status = calendar_so::combine_status($event['owner'] == $this->user ? 'A' : 'U', 1, 'CHAIR');
1492
					if (!is_array($event['participants'])) $event['participants'] = array();
1493
					$event['participants'][$event['owner']] = $status;
1494
				}
1495
				else
1496
				{
1497
					foreach ($event['participants'] as $uid => $status)
1498
					{
1499
						// if the client did not give us a proper status => set default
1500
						if ($status[0] == 'X')
1501
						{
1502
							if ($uid == $event['owner'])
1503
							{
1504
								$event['participants'][$uid] = calendar_so::combine_status('A', 1, 'CHAIR');
1505
							}
1506
							else
1507
							{
1508
								$event['participants'][$uid] = calendar_so::combine_status('U');
1509
							}
1510
						}
1511
					}
1512
				}
1513
			}
1514
1515
			// update alarms depending on the given event type
1516
			if (count($event['alarm']) > 0 || isset($this->supportedFields['alarm']))
1517
			{
1518
				switch ($event_info['type'])
1519
				{
1520
					case 'SINGLE':
1521
					case 'SERIES-MASTER':
1522
					case 'SERIES-EXCEPTION':
1523
					case 'SERIES-EXCEPTION-PROPAGATE':
1524
						if (isset($event['alarm']))
1525
						{
1526
							$this->sync_alarms($event, (array)$event_info['stored_event']['alarm'], $this->user);
1527
						}
1528
						break;
1529
1530
					case 'SERIES-PSEUDO-EXCEPTION':
1531
						// nothing to do here
1532
						break;
1533
				}
1534
			}
1535
1536
			if ($this->log)
1537
			{
1538
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . '('
1539
					. $event_info['type'] . ")\n"
1540
					. array2string($event)."\n",3,$this->logfile);
1541
			}
1542
1543
			// Android (any maybe others) delete recurrences by setting STATUS: CANCELLED
1544
			// as we ignore STATUS we have to delete the recurrence by calling delete
1545
			if (in_array($event_info['type'], array('SERIES-EXCEPTION', 'SERIES-EXCEPTION-PROPAGATE', 'SERIES-PSEUDO-EXCEPTION')) &&
1546
				$event['status'] == 'CANCELLED')
1547
			{
1548
				if (!$this->delete($event['id'] ? $event['id'] : $cal_id, $event['recurrence'],false,$skip_notification))
1549
				{
1550
					// delete fails (because no rights), reject recurrence
1551
					$this->set_status($event['id'] ? $event['id'] : $cal_id, $this->user, 'R', $event['recurrence'],false,true,$skip_notification);
1552
				}
1553
				continue;
1554
			}
1555
1556
			// save event depending on the given event type
1557
			switch ($event_info['type'])
1558
			{
1559
				case 'SINGLE':
1560
					if ($this->log)
1561
					{
1562
						error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
1563
							"(): event SINGLE\n",3,$this->logfile);
1564
					}
1565
1566
					// update the event
1567
					if ($event_info['acl_edit'])
1568
					{
1569
						// Force SINGLE
1570
						$event['reference'] = 0;
1571
						$event_to_store = $event; // prevent $event from being changed by the update method
1572
						$this->server2usertime($event_to_store);
1573
						$updated_id = $this->update($event_to_store, true,true,false,true,$msg,$skip_notification);
1574
						unset($event_to_store);
1575
					}
1576
					break;
1577
1578
				case 'SERIES-MASTER':
1579
					if ($this->log)
1580
					{
1581
						error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
1582
							"(): event SERIES-MASTER\n",3,$this->logfile);
1583
					}
1584
1585
					// remove all known pseudo exceptions and update the event
1586
					if ($event_info['acl_edit'])
1587
					{
1588
						$filter = isset($this->supportedFields['participants']) ? 'map' : 'tz_map';
1589
						$days = $this->so->get_recurrence_exceptions($event_info['stored_event'], $this->tzid, 0, 0, $filter);
1590
						if ($this->log)
1591
						{
1592
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."(EXCEPTIONS MAPPING):\n" .
1593
								array2string($days)."\n",3,$this->logfile);
1594
						}
1595
						if (is_array($days))
1596
						{
1597
							$recur_exceptions = array();
1598
1599
							foreach ($event['recur_exception'] as $recur_exception)
1600
							{
1601
								if (isset($days[$recur_exception]))
1602
								{
1603
									$recur_exceptions[] = $days[$recur_exception];
1604
								}
1605
							}
1606
							$event['recur_exception'] = $recur_exceptions;
1607
						}
1608
1609
						$event_to_store = $event; // prevent $event from being changed by the update method
1610
						$this->server2usertime($event_to_store);
1611
						$updated_id = $this->update($event_to_store, true,true,false,true,$msg,$skip_notification);
1612
						unset($event_to_store);
1613
					}
1614
					break;
1615
1616
				case 'SERIES-EXCEPTION':
1617
				case 'SERIES-EXCEPTION-PROPAGATE':
1618
					if ($this->log)
1619
					{
1620
						error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
1621
							"(): event SERIES-EXCEPTION\n",3,$this->logfile);
1622
					}
1623
1624
					// update event
1625
					if ($event_info['acl_edit'])
1626
					{
1627
						if (isset($event_info['stored_event']['id']))
1628
						{
1629
							// We update an existing exception
1630
							$event['id'] = $event_info['stored_event']['id'];
1631
							$event['category'] = $event_info['stored_event']['category'];
1632
						}
1633
						else
1634
						{
1635
							// We create a new exception
1636
							unset($event['id']);
1637
							unset($event_info['stored_event']);
1638
							$event['recur_type'] = MCAL_RECUR_NONE;
1639
							if (empty($event['recurrence']))
1640
							{
1641
								// find an existing exception slot
1642
								$occurence = $exception = false;
1643
								foreach ($event_info['master_event']['recur_exception'] as $exception)
1644
								{
1645
									if ($exception > $event['start']) break;
1646
									$occurence = $exception;
1647
								}
1648
								if (!$occurence)
1649
								{
1650
									if (!$exception)
1651
									{
1652
										// use start as dummy recurrence
1653
										$event['recurrence'] = $event['start'];
1654
									}
1655
									else
1656
									{
1657
										$event['recurrence'] = $exception;
1658
									}
1659
								}
1660
								else
1661
								{
1662
									$event['recurrence'] = $occurence;
1663
								}
1664
							}
1665
							else
1666
							{
1667
								$event_info['master_event']['recur_exception'] =
1668
									array_unique(array_merge($event_info['master_event']['recur_exception'],
1669
										array($event['recurrence'])));
1670
							}
1671
1672
							$event['reference'] = $event_info['master_event']['id'];
1673
							$event['category'] = $event_info['master_event']['category'];
1674
							$event['owner'] = $event_info['master_event']['owner'];
1675
							$event_to_store = $event_info['master_event']; // prevent the master_event from being changed by the update method
1676
							$this->server2usertime($event_to_store);
1677
							$this->update($event_to_store, true,true,false,true,$msg,$skip_notification);
1678
							unset($event_to_store);
1679
						}
1680
1681
						$event_to_store = $event; // prevent $event from being changed by update method
1682
						$this->server2usertime($event_to_store);
1683
						$updated_id = $this->update($event_to_store, true,true,false,true,$msg,$skip_notification);
1684
						unset($event_to_store);
1685
					}
1686
					break;
1687
1688
				case 'SERIES-PSEUDO-EXCEPTION':
1689
					if ($this->log)
1690
					{
1691
						error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
1692
							"(): event SERIES-PSEUDO-EXCEPTION\n",3,$this->logfile);
1693
					}
1694
					//Horde::logMessage('importVCAL event SERIES-PSEUDO-EXCEPTION',
1695
					//	__FILE__, __LINE__, PEAR_LOG_DEBUG);
1696
1697
					if ($event_info['acl_edit'])
1698
					{
1699
						// truncate the status only exception from the series master
1700
						$recur_exceptions = array();
1701
						foreach ($event_info['master_event']['recur_exception'] as $recur_exception)
1702
						{
1703
							if ($recur_exception != $event['recurrence'])
1704
							{
1705
								$recur_exceptions[] = $recur_exception;
1706
							}
1707
						}
1708
						$event_info['master_event']['recur_exception'] = $recur_exceptions;
1709
1710
						// save the series master with the adjusted exceptions
1711
						$event_to_store = $event_info['master_event']; // prevent the master_event from being changed by the update method
1712
						$this->server2usertime($event_to_store);
1713
						$updated_id = $this->update($event_to_store, true, true, false, false,$msg,$skip_notification);
1714
						unset($event_to_store);
1715
					}
1716
1717
					break;
1718
			}
1719
1720
			// read stored event into info array for fresh stored (new) events
1721
			if (!is_array($event_info['stored_event']) && $updated_id > 0)
1722
			{
1723
				$event_info['stored_event'] = $this->read($updated_id, 0, false, 'server');
1724
			}
1725
1726
			if (isset($event['participants']))
1727
			{
1728
				// update status depending on the given event type
1729
				switch ($event_info['type'])
1730
				{
1731
					case 'SINGLE':
1732
					case 'SERIES-MASTER':
1733
					case 'SERIES-EXCEPTION':
1734
					case 'SERIES-EXCEPTION-PROPAGATE':
1735
						if (is_array($event_info['stored_event'])) // status update requires a stored event
1736
						{
1737
							if ($event_info['acl_edit'])
1738
							{
1739
								// update all participants if we have the right to do that
1740
								$this->update_status($event, $event_info['stored_event'],0,$skip_notification);
1741
							}
1742
							elseif (isset($event['participants'][$this->user]) || isset($event_info['stored_event']['participants'][$this->user]))
1743
							{
1744
								// update the users status only
1745
								$this->set_status($event_info['stored_event']['id'], $this->user,
1746
									($event['participants'][$this->user] ? $event['participants'][$this->user] : 'R'), 0, true,true,$skip_notification);
1747
							}
1748
						}
1749
						break;
1750
1751
					case 'SERIES-PSEUDO-EXCEPTION':
1752
						if (is_array($event_info['master_event'])) // status update requires a stored master event
1753
						{
1754
							$recurrence = $this->date2usertime($event['recurrence']);
1755
							if ($event_info['acl_edit'])
1756
							{
1757
								// update all participants if we have the right to do that
1758
								$this->update_status($event, $event_info['stored_event'], $recurrence,$skip_notification);
1759
							}
1760
							elseif (isset($event['participants'][$this->user]) || isset($event_info['master_event']['participants'][$this->user]))
1761
							{
1762
								// update the users status only
1763
								$this->set_status($event_info['master_event']['id'], $this->user,
1764
									($event['participants'][$this->user] ? $event['participants'][$this->user] : 'R'), $recurrence, true,true,$skip_notification);
1765
							}
1766
						}
1767
						break;
1768
				}
1769
			}
1770
1771
			// choose which id to return to the client
1772
			switch ($event_info['type'])
1773
			{
1774
				case 'SINGLE':
1775
				case 'SERIES-MASTER':
1776
				case 'SERIES-EXCEPTION':
1777
					$return_id = is_array($event_info['stored_event']) ? $event_info['stored_event']['id'] : false;
1778
					break;
1779
1780
				case 'SERIES-PSEUDO-EXCEPTION':
1781
					$return_id = is_array($event_info['master_event']) ? $event_info['master_event']['id'] . ':' . $event['recurrence'] : false;
1782
					break;
1783
1784
				case 'SERIES-EXCEPTION-PROPAGATE':
1785
					if ($event_info['acl_edit'] && is_array($event_info['stored_event']))
1786
					{
1787
						// we had sufficient rights to propagate the status only exception to a real one
1788
						$return_id = $event_info['stored_event']['id'];
1789
					}
1790
					else
1791
					{
1792
						// we did not have sufficient rights to propagate the status only exception to a real one
1793
						// we have to keep the SERIES-PSEUDO-EXCEPTION id and keep the event untouched
1794
						$return_id = $event_info['master_event']['id'] . ':' . $event['recurrence'];
1795
					}
1796
					break;
1797
			}
1798
1799
			// handle ATTACH attribute for managed attachments
1800
			if ($updated_id && Api\CalDAV::handle_attach('calendar', $updated_id, $event['attach'], $event['attach-delete-by-put']) === false)
1801
			{
1802
				$return_id = null;
1803
			}
1804
1805
			if ($this->log)
1806
			{
1807
				$event_info['stored_event'] = $this->read($event_info['stored_event']['id']);
1808
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()[$updated_id]\n" .
1809
					array2string($event_info['stored_event'])."\n",3,$this->logfile);
1810
			}
1811
		}
1812
		date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']);
1813
1814
		return $updated_id === 0 ? 0 : $return_id;
1815
	}
1816
1817
	/**
1818
	 * Override parent update function to handle conflict checking callback, if set
1819
	 *
1820
	 * @param array &$event event-array, on return some values might be changed due to set defaults
1821
	 * @param boolean $ignore_conflicts =false just ignore conflicts or do a conflict check and return the conflicting events.
1822
	 *	Set to false if $this->conflict_callback is set
1823
	 * @param boolean $touch_modified =true NOT USED ANYMORE (was only used in old csv-import), modified&modifier is always updated!
1824
	 * @param boolean $ignore_acl =false should we ignore the acl
1825
	 * @param boolean $updateTS =true update the content history of the event
1826
	 * @param array &$messages=null messages about because of missing ACL removed participants or categories
1827
	 * @param boolean $skip_notification =false true: send NO notifications, default false = send them
1828
	 * @return mixed on success: int $cal_id > 0, on error or conflicts false.
1829
	 *	Conflicts are passed to $this->conflict_callback
1830
	 */
1831
	public function update(&$event,$ignore_conflicts=false,$touch_modified=true,$ignore_acl=false,$updateTS=true,&$messages=null, $skip_notification=false)
1832
	{
1833
		if($this->conflict_callback !== null)
1834
		{
1835
			// calendar_ical overrides search(), which breaks conflict checking
1836
			// so we make sure to use the original from parent
1837
			static $bo = null;
1838
			if(!$bo)
1839
			{
1840
				$bo = new calendar_boupdate();
1841
			}
1842
			$conflicts = $bo->conflicts($event);
1843
			if(is_array($conflicts) && count($conflicts) > 0)
1844
			{
1845
				call_user_func_array($this->conflict_callback, array(&$event, &$conflicts));
1846
				return false;
1847
			}
1848
		}
1849
		return parent::update($event, $ignore_conflicts, $touch_modified, $ignore_acl, $updateTS, $messages, $skip_notification);
1850
	}
1851
1852
	/**
1853
	 * Sync alarms of current user: add alarms added on client and remove the ones removed
1854
	 *
1855
	 * @param array& $event
1856
	 * @param array $old_alarms
1857
	 * @param int $user account_id of user to create alarm for
1858
	 * @return int number of modified alarms
1859
	 */
1860
	public function sync_alarms(array &$event, array $old_alarms, $user)
1861
	{
1862
		if ($this->debug) error_log(__METHOD__."(".array2string($event).', old_alarms='.array2string($old_alarms).", $user,)");
1863
		$modified = 0;
1864
		foreach($event['alarm'] as &$alarm)
1865
		{
1866
			// check if alarm is already stored or from other users
1867
			$found = false;
1868
			foreach($old_alarms as $id => $old_alarm)
1869
			{
1870
				// not current users alarm --> ignore
1871
				if (!$old_alarm['all'] && $old_alarm['owner'] != $user)
1872
				{
1873
					unset($old_alarm[$id]);
1874
					continue;
1875
				}
1876
				// alarm found --> stop
1877
				if (empty($alarm['uid']) && $alarm['offset'] == $old_alarm['offset'] || $alarm['uid'] && $alarm['uid'] == $old_alarm['uid'])
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (empty($alarm['uid']) &&...'] == $old_alarm['uid'], Probably Intended Meaning: empty($alarm['uid']) && ...] == $old_alarm['uid'])
Loading history...
1878
				{
1879
					$found = true;
1880
					unset($old_alarms[$id]);
1881
					break;
1882
				}
1883
			}
1884
			if ($this->debug) error_log(__METHOD__."($event[title] (#$event[id]), ..., $user) processing ".($found?'existing':'new')." alarm ".array2string($alarm));
1885
			if (!empty($alarm['attrs']['X-LIC-ERROR']))
1886
			{
1887
				if ($this->debug) error_log(__METHOD__."($event[title] (#$event[id]), ..., $user) ignored X-LIC-ERROR=".array2string($alarm['X-LIC-ERROR']));
1888
				unset($alarm['attrs']['X-LIC-ERROR']);
1889
			}
1890
			// alarm not found --> add it
1891
			if (!$found)
1892
			{
1893
				$alarm['owner'] = $user;
1894
				if (!isset($alarm['time'])) $alarm['time'] = $event['start'] - $alarm['offset'];
1895
				if ($alarm['time'] < time()) calendar_so::shift_alarm($event, $alarm);
1896
				if ($this->debug) error_log(__METHOD__."() adding new alarm from client ".array2string($alarm));
1897
				if ($event['id']) $alarm['id'] = $this->save_alarm($event['id'], $alarm);
1898
				++$modified;
1899
			}
1900
			// existing alarm --> update it
1901
			else
1902
			{
1903
				if (!isset($alarm['time'])) $alarm['time'] = $event['start'] - $alarm['offset'];
1904
				if ($alarm['time'] < time()) calendar_so::shift_alarm($event, $alarm);
1905
				$alarm = array_merge($old_alarm, $alarm);
1906
				if ($this->debug) error_log(__METHOD__."() updating existing alarm from client ".array2string($alarm));
1907
				$alarm['id'] = $this->save_alarm($event['id'], $alarm);
1908
				++$modified;
1909
			}
1910
		}
1911
		// remove all old alarms left from current user
1912
		foreach($old_alarms as $id => $old_alarm)
1913
		{
1914
			// not current users alarm --> ignore
1915
			if (!$old_alarm['all'] && $old_alarm['owner'] != $user)
1916
			{
1917
				unset($old_alarm[$id]);
1918
				continue;
1919
			}
1920
			if ($this->debug) error_log(__METHOD__."() deleting alarm '$id' deleted on client ".array2string($old_alarm));
1921
			$this->delete_alarm($id);
1922
			++$modified;
1923
		}
1924
		return $modified;
1925
	}
1926
1927
	/**
1928
	 * get the value of an attribute by its name
1929
	 *
1930
	 * @param array $components
1931
	 * @param string $name eg. 'DTSTART'
1932
	 * @param string $what ='value'
1933
	 * @return mixed
1934
	 */
1935
	static function _get_attribute($components,$name,$what='value')
1936
	{
1937
		foreach ($components as $attribute)
1938
		{
1939
			if ($attribute['name'] == $name)
1940
			{
1941
				return !$what ? $attribute : $attribute[$what];
1942
			}
1943
		}
1944
		return false;
1945
	}
1946
1947
	/**
1948
	 * Parsing a valarm component preserving all attributes unknown to EGw
1949
	 *
1950
	 * @param array &$alarms on return alarms parsed
1951
	 * @param Horde_Icalendar_Valarm $valarm valarm component
1952
	 * @param int $duration in seconds to be able to convert RELATED=END
1953
	 * @return int number of parsed alarms
1954
	 */
1955
	static function valarm2egw(&$alarms, Horde_Icalendar_Valarm $valarm, $duration)
1956
	{
1957
		$alarm = array();
1958
		foreach ($valarm->getAllAttributes() as $vattr)
1959
		{
1960
			switch ($vattr['name'])
1961
			{
1962
				case 'TRIGGER':
1963
					$vtype = (isset($vattr['params']['VALUE']))
1964
						? $vattr['params']['VALUE'] : 'DURATION'; //default type
1965
					switch ($vtype)
1966
					{
1967
						case 'DURATION':
1968
							if (isset($vattr['params']['RELATED']) && $vattr['params']['RELATED'] == 'END')
1969
							{
1970
								$alarm['offset'] = $duration -$vattr['value'];
1971
							}
1972
							elseif (isset($vattr['params']['RELATED']) && $vattr['params']['RELATED'] != 'START')
1973
							{
1974
								error_log("Unsupported VALARM offset anchor ".$vattr['params']['RELATED']);
1975
								return;
1976
							}
1977
							else
1978
							{
1979
								$alarm['offset'] = -$vattr['value'];
1980
							}
1981
							break;
1982
						case 'DATE-TIME':
1983
							$alarm['time'] = $vattr['value'];
1984
							break;
1985
						default:
1986
							error_log('VALARM/TRIGGER: unsupported value type:' . $vtype);
1987
					}
1988
					break;
1989
1990
				case 'UID':
1991
				case 'X-WR-ALARMUID':
1992
					$alarm['uid'] = $vattr['value'];
1993
					break;
1994
1995
				default:	// store all other attributes, so we dont loose them
1996
					$alarm['attrs'][$vattr['name']] = array(
1997
						'params' => $vattr['params'],
1998
						'value'  => $vattr['value'],
1999
					);
2000
			}
2001
		}
2002
		if (isset($alarm['offset']) || isset($alarm['time']))
2003
		{
2004
			//error_log(__METHOD__."(..., ".$valarm->exportvCalendar().", $duration) alarm=".array2string($alarm));
2005
			$alarms[] = $alarm;
2006
			return 1;
2007
		}
2008
		return 0;
2009
	}
2010
2011
	function setSupportedFields($_productManufacturer='', $_productName='')
2012
	{
2013
		$state =& $_SESSION['SyncML.state'];
2014
		if (isset($state))
2015
		{
2016
			$deviceInfo = $state->getClientDeviceInfo();
2017
		}
2018
2019
		// store product manufacturer and name for further usage
2020
		if ($_productManufacturer)
2021
		{
2022
				$this->productManufacturer = strtolower($_productManufacturer);
2023
				$this->productName = strtolower($_productName);
2024
		}
2025
2026
		if (isset($deviceInfo) && is_array($deviceInfo))
2027
		{
2028
			/*
2029
			if ($this->log)
2030
			{
2031
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2032
					'() ' . array2string($deviceInfo) . "\n",3,$this->logfile);
2033
			}
2034
			*/
2035
			if (isset($deviceInfo['uidExtension']) &&
2036
				$deviceInfo['uidExtension'])
2037
			{
2038
				$this->uidExtension = true;
2039
			}
2040
			if (isset($deviceInfo['nonBlockingAllday']) &&
2041
				$deviceInfo['nonBlockingAllday'])
2042
			{
2043
				$this->nonBlockingAllday = true;
2044
			}
2045
			if (isset($deviceInfo['tzid']) &&
2046
				$deviceInfo['tzid'])
2047
			{
2048
				switch ($deviceInfo['tzid'])
2049
				{
2050
					case -1:
2051
						$this->tzid = false; // use event's TZ
2052
						break;
2053
					case -2:
2054
						$this->tzid = null; // use UTC for export
2055
						break;
2056
					default:
2057
						$this->tzid = $deviceInfo['tzid'];
2058
				}
2059
			}
2060
			elseif (strpos($this->productName, 'palmos') !== false)
2061
			{
2062
				// for palmos we have to use user-time and NO timezone
2063
				$this->tzid = false;
2064
			}
2065
2066
			if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['calendar_owner']))
2067
			{
2068
				$owner = $GLOBALS['egw_info']['user']['preferences']['syncml']['calendar_owner'];
2069
				switch ($owner)
2070
				{
2071
					case 'G':
2072
					case 'P':
2073
					case 0:
2074
					case -1:
2075
						$owner = $this->user;
0 ignored issues
show
Unused Code introduced by
The assignment to $owner is dead and can be removed.
Loading history...
2076
						break;
2077
					default:
2078
						if ((int)$owner && $this->check_perms(Acl::EDIT, 0, $owner))
2079
						{
2080
							$this->calendarOwner = $owner;
2081
						}
2082
				}
2083
			}
2084
			if (!isset($this->productManufacturer) ||
2085
				 $this->productManufacturer == '' ||
2086
				 $this->productManufacturer == 'file')
2087
			{
2088
				$this->productManufacturer = strtolower($deviceInfo['manufacturer']);
2089
			}
2090
			if (!isset($this->productName) || $this->productName == '')
2091
			{
2092
				$this->productName = strtolower($deviceInfo['model']);
2093
			}
2094
		}
2095
2096
		$defaultFields['minimal'] = array(
0 ignored issues
show
Comprehensibility Best Practice introduced by
$defaultFields was never initialized. Although not strictly required by PHP, it is generally a good practice to add $defaultFields = array(); before regardless.
Loading history...
2097
			'public'			=> 'public',
2098
			'description'		=> 'description',
2099
			'end'				=> 'end',
2100
			'start'				=> 'start',
2101
			'location'			=> 'location',
2102
			'recur_type'		=> 'recur_type',
2103
			'recur_interval'	=> 'recur_interval',
2104
			'recur_data'		=> 'recur_data',
2105
			'recur_enddate'		=> 'recur_enddate',
2106
			'recur_exception'	=> 'recur_exception',
2107
			'title'				=> 'title',
2108
			'alarm'				=> 'alarm',
2109
			'whole_day'			=> 'whole_day',
2110
		);
2111
2112
		$defaultFields['basic'] = $defaultFields['minimal'] + array(
2113
			'priority'			=> 'priority',
2114
		);
2115
2116
		$defaultFields['nexthaus'] = $defaultFields['basic'] + array(
2117
			'participants'		=> 'participants',
2118
			'uid'				=> 'uid',
2119
		);
2120
2121
		$defaultFields['s60'] = $defaultFields['basic'] + array(
2122
			'category'			=> 'category',
2123
			'recurrence'			=> 'recurrence',
2124
			'uid'				=> 'uid',
2125
		);
2126
2127
		$defaultFields['synthesis'] = $defaultFields['basic'] + array(
2128
			'participants'		=> 'participants',
2129
			'owner'				=> 'owner',
2130
			'category'			=> 'category',
2131
			'non_blocking'		=> 'non_blocking',
2132
			'uid'				=> 'uid',
2133
			'recurrence'		=> 'recurrence',
2134
			'etag'				=> 'etag',
2135
		);
2136
2137
		$defaultFields['funambol'] = $defaultFields['basic'] + array(
2138
			'participants'		=> 'participants',
2139
			'owner'				=> 'owner',
2140
			'category'			=> 'category',
2141
			'non_blocking'		=> 'non_blocking',
2142
		);
2143
2144
		$defaultFields['evolution'] = $defaultFields['basic'] + array(
2145
			'participants'		=> 'participants',
2146
			'owner'				=> 'owner',
2147
			'category'			=> 'category',
2148
			'uid'				=> 'uid',
2149
		);
2150
2151
		$defaultFields['full'] = $defaultFields['basic'] + array(
2152
			'participants'		=> 'participants',
2153
			'owner'				=> 'owner',
2154
			'category'			=> 'category',
2155
			'non_blocking'		=> 'non_blocking',
2156
			'uid'				=> 'uid',
2157
			'recurrence'		=> 'recurrence',
2158
			'etag'				=> 'etag',
2159
			'status'			=> 'status',
2160
		);
2161
2162
2163
		switch ($this->productManufacturer)
2164
		{
2165
			case 'nexthaus corporation':
2166
			case 'nexthaus corp':
2167
				switch ($this->productName)
2168
				{
2169
					default:
2170
						$this->supportedFields = $defaultFields['nexthaus'];
2171
						break;
2172
				}
2173
				break;
2174
2175
			// multisync does not provide anymore information then the manufacturer
2176
			// we suppose multisync with evolution
2177
			case 'the multisync project':
2178
				switch ($this->productName)
2179
				{
2180
					default:
2181
						$this->supportedFields = $defaultFields['basic'];
2182
						break;
2183
				}
2184
				break;
2185
2186
			case 'siemens':
2187
				switch ($this->productName)
2188
				{
2189
					case 'sx1':
2190
						$this->supportedFields = $defaultFields['minimal'];
2191
						break;
2192
					default:
2193
						error_log("Unknown Siemens phone '$_productName', using minimal set");
2194
						$this->supportedFields = $defaultFields['minimal'];
2195
						break;
2196
				}
2197
				break;
2198
2199
			case 'nokia':
2200
				switch ($this->productName)
2201
				{
2202
					case 'e61':
2203
						$this->supportedFields = $defaultFields['minimal'];
2204
						break;
2205
					case 'e51':
2206
					case 'e90':
2207
					case 'e71':
2208
					case 'e72-1':
2209
					case 'e75-1':
2210
					case 'e66':
2211
					case '6120c':
2212
					case 'nokia 6131':
2213
					case 'n97':
2214
					case 'n97 mini':
2215
					case '5800 xpressmusic':
2216
						$this->supportedFields = $defaultFields['s60'];
2217
						break;
2218
					default:
2219
						if ($this->productName[0] == 'e')
2220
						{
2221
							$model = 'E90';
2222
							$this->supportedFields = $defaultFields['s60'];
2223
						}
2224
						else
2225
						{
2226
							$model = 'E61';
2227
							$this->supportedFields = $defaultFields['minimal'];
2228
						}
2229
						error_log("Unknown Nokia phone '$_productName', assuming same as '$model'");
2230
						break;
2231
				}
2232
				break;
2233
2234
			case 'sonyericsson':
2235
			case 'sony ericsson':
2236
				switch ($this->productName)
2237
				{
2238
					case 'd750i':
2239
					case 'p910i':
2240
					case 'g705i':
2241
					case 'w890i':
2242
						$this->supportedFields = $defaultFields['basic'];
2243
						break;
2244
					default:
2245
						error_log("Unknown Sony Ericsson phone '$this->productName' assuming d750i");
2246
						$this->supportedFields = $defaultFields['basic'];
2247
						break;
2248
				}
2249
				break;
2250
2251
			case 'synthesis ag':
2252
				switch ($this->productName)
2253
				{
2254
					case 'sysync client pocketpc std':
2255
					case 'sysync client pocketpc pro':
2256
					case 'sysync client iphone contacts':
2257
					case 'sysync client iphone contacts+todoz':
2258
					default:
2259
						$this->supportedFields = $defaultFields['synthesis'];
2260
						break;
2261
				}
2262
				break;
2263
2264
			//Syncevolution compatibility
2265
			case 'patrick ohly':
2266
				$this->supportedFields = $defaultFields['evolution'];
2267
				break;
2268
2269
			case '': // seems syncevolution 0.5 doesn't send a manufacturer
2270
				error_log("No vendor name, assuming syncevolution 0.5");
2271
				$this->supportedFields = $defaultFields['evolution'];
2272
				break;
2273
2274
			case 'file':	// used outside of SyncML, eg. by the calendar itself ==> all possible fields
2275
				if ($this->cal_prefs['export_timezone'])
2276
				{
2277
					$this->tzid = $this->cal_prefs['export_timezone'];
2278
				}
2279
				else	// not set or '0' = use event TZ
2280
				{
2281
					$this->tzid = false; // use event's TZ
2282
				}
2283
				$this->supportedFields = $defaultFields['full'];
2284
				break;
2285
2286
			case 'full':
2287
			case 'groupdav':		// all GroupDAV access goes through here
2288
				$this->tzid = false; // use event's TZ
2289
				switch ($this->productName)
2290
				{
2291
					default:
2292
						$this->supportedFields = $defaultFields['full'];
2293
						unset($this->supportedFields['whole_day']);
2294
				}
2295
				break;
2296
2297
			case 'funambol':
2298
				$this->supportedFields = $defaultFields['funambol'];
2299
				break;
2300
2301
			// the fallback for SyncML
2302
			default:
2303
				error_log("Unknown calendar SyncML client: manufacturer='$this->productManufacturer'  product='$this->productName'");
2304
				$this->supportedFields = $defaultFields['synthesis'];
2305
		}
2306
2307
		if ($this->log)
2308
		{
2309
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2310
				'(' . $this->productManufacturer .
2311
				', '. $this->productName .', ' .
2312
				($this->tzid ? $this->tzid : Api\DateTime::$user_timezone->getName()) .
0 ignored issues
show
Bug introduced by
Are you sure $this->tzid ? $this->tzi...ser_timezone->getName() of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2312
				(/** @scrutinizer ignore-type */ $this->tzid ? $this->tzid : Api\DateTime::$user_timezone->getName()) .
Loading history...
2313
				', ' . $this->calendarOwner . ")\n" , 3, $this->logfile);
2314
		}
2315
2316
		//Horde::logMessage('setSupportedFields(' . $this->productManufacturer . ', '
2317
		//	. $this->productName .', ' .
2318
		//	($this->tzid ? $this->tzid : Api\DateTime::$user_timezone->getName()) .')',
2319
		//	__FILE__, __LINE__, PEAR_LOG_DEBUG);
2320
	}
2321
2322
	/**
2323
	 * Convert vCalendar data in EGw events
2324
	 *
2325
	 * @param string|resource $_vcalData
2326
	 * @param string $principalURL ='' Used for CalDAV imports
2327
	 * @param string $charset  The encoding charset for $text. Defaults to
2328
	 *                         utf-8 for new format, iso-8859-1 for old format.
2329
	 * @return Iterator|array|boolean Iterator if resource given or array of events on success, false on failure
2330
	 */
2331
	function icaltoegw($_vcalData, $principalURL='', $charset=null)
2332
	{
2333
		if ($this->log)
2334
		{
2335
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."($principalURL, $charset)\n" .
2336
				array2string($_vcalData)."\n",3,$this->logfile);
2337
		}
2338
2339
		if (!is_array($this->supportedFields)) $this->setSupportedFields();
0 ignored issues
show
introduced by
The condition is_array($this->supportedFields) is always true.
Loading history...
2340
2341
		// we use Api\CalDAV\IcalIterator only on resources, as calling importVCal() accesses single events like an array (eg. $events[0])
2342
		if (is_resource($_vcalData))
2343
		{
2344
			return new Api\CalDAV\IcalIterator($_vcalData, 'VCALENDAR', $charset,
2345
				// true = add container as last parameter to callback parameters
2346
				array($this, '_ical2egw_callback'), array($this->tzid, $principalURL), true);
2347
		}
2348
2349
		if ($this->tzid)
2350
		{
2351
			$tzid = $this->tzid;
2352
		}
2353
		else
2354
		{
2355
			$tzid = Api\DateTime::$user_timezone->getName();
2356
		}
2357
2358
		date_default_timezone_set($tzid);
2359
2360
		$events = array();
2361
		$vcal = new Horde_Icalendar;
2362
		if ($charset && $charset != 'utf-8')
2363
		{
2364
			$_vcalData = Api\Translation::convert($_vcalData, $charset, 'utf-8');
2365
		}
2366
		if (!$vcal->parsevCalendar($_vcalData, 'VCALENDAR'))
2367
		{
2368
			if ($this->log)
2369
			{
2370
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2371
					"(): No vCalendar Container found!\n",3,$this->logfile);
2372
			}
2373
			date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']);
2374
			return false;
2375
		}
2376
		foreach ($vcal->getComponents() as $component)
2377
		{
2378
			if (($event = $this->_ical2egw_callback($component,$this->tzid,$principalURL,$vcal)))
2379
			{
2380
				$events[] = $event;
2381
			}
2382
		}
2383
		date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']);
2384
2385
		return $events;
2386
	}
2387
2388
	/**
2389
	 * Get email of organizer of first vevent in given iCalendar
2390
	 *
2391
	 * @param string $_ical
2392
	 * @return string|boolean
2393
	 */
2394
	public static function getIcalOrganizer($_ical)
2395
	{
2396
		$vcal = new Horde_Icalendar;
2397
		if (!$vcal->parsevCalendar($_ical, 'VCALENDAR'))
2398
		{
2399
			return false;
2400
		}
2401
		if (($vevent = $vcal->findComponentByAttribute('Vevent', 'ORGANIZER')))
2402
		{
2403
			$organizer = $vevent->getAttribute('ORGANIZER');
2404
			if (stripos($organizer, 'mailto:') === 0)
2405
			{
2406
				return substr($organizer, 7);
2407
			}
2408
			$params = $vevent->getAttribute('ORGANIZER', true);
2409
			return $params['email'];
2410
		}
2411
		return false;
2412
	}
2413
2414
	/**
2415
	 * Callback for Api\CalDAV\IcalIterator to convert Horde_iCalendar_Vevent to EGw event array
2416
	 *
2417
	 * @param Horde_iCalendar $component
2418
	 * @param string $tzid timezone
2419
	 * @param string $principalURL ='' Used for CalDAV imports
2420
	 * @param Horde_Icalendar $container =null container to access attributes on container
2421
	 * @return array|boolean event array or false if $component is no Horde_Icalendar_Vevent
2422
	 */
2423
	function _ical2egw_callback(Horde_Icalendar $component, $tzid, $principalURL='', Horde_Icalendar $container=null)
2424
	{
2425
		//unset($component->_container); _debug_array($component);
2426
2427
		if ($this->log)
2428
		{
2429
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.'() '.get_class($component)." found\n",3,$this->logfile);
2430
		}
2431
2432
		// eg. Mozilla holiday calendars contain only a X-WR-TIMEZONE on vCalendar component
2433
		if (!$tzid && $container && ($tz = $container->getAttributeDefault('X-WR-TIMEZONE')))
2434
		{
2435
			$tzid = $tz;
2436
		}
2437
2438
		if (!is_a($component, 'Horde_Icalendar_Vevent') ||
2439
			!($event = $this->vevent2egw($component, $container ? $container->getAttributeDefault('VERSION', '2.0') : '2.0',
2440
				$this->supportedFields, $principalURL, null, $container)))
2441
		{
2442
			return false;
2443
		}
2444
		//common adjustments
2445
		if ($this->productManufacturer == '' && $this->productName == '' && !empty($event['recur_enddate']))
2446
		{
2447
			// syncevolution needs an adjusted recur_enddate
2448
			$event['recur_enddate'] = (int)$event['recur_enddate'] + 86400;
2449
		}
2450
		if ($event['recur_type'] != MCAL_RECUR_NONE)
2451
		{
2452
			// No reference or RECURRENCE-ID for the series master
2453
			$event['reference'] = $event['recurrence'] = 0;
2454
		}
2455
		if (Api\DateTime::to($event['start'], 'H:i:s') == '00:00:00' && Api\DateTime::to($event['end'], 'H:i:s') == '00:00:00')
0 ignored issues
show
introduced by
The condition EGroupware\Api\DateTime:... 'H:i:s') == '00:00:00' is always false.
Loading history...
2456
		{
2457
			// 'All day' event that ends at midnight the next day, avoid that
2458
			$event['end']--;
2459
		}
2460
2461
		// handle the alarms
2462
		$alarms = $event['alarm'];
2463
		foreach ($component->getComponents() as $valarm)
2464
		{
2465
			if (is_a($valarm, 'Horde_Icalendar_Valarm'))
2466
			{
2467
				self::valarm2egw($alarms, $valarm, $event['end'] - $event['start']);
2468
			}
2469
		}
2470
		$event['alarm'] = $alarms;
2471
		if ($tzid || empty($event['tzid']))
2472
		{
2473
			$event['tzid'] = $tzid;
2474
		}
2475
		return $event;
2476
	}
2477
2478
	/**
2479
	 * Parse a VEVENT
2480
	 *
2481
	 * @param array $component			VEVENT
2482
	 * @param string $version			vCal version (1.0/2.0)
2483
	 * @param array $supportedFields	supported fields of the device
2484
	 * @param string $principalURL =''	Used for CalDAV imports, no longer used in favor of Api\CalDAV\Principals::url2uid()
2485
	 * @param string $check_component ='Horde_Icalendar_Vevent'
2486
	 * @param Horde_Icalendar $container =null container to access attributes on container
2487
	 * @return array|boolean			event on success, false on failure
2488
	 */
2489
	function vevent2egw($component, $version, $supportedFields, $principalURL='', $check_component='Horde_Icalendar_Vevent', Horde_Icalendar $container=null)
2490
	{
2491
		unset($principalURL);	// not longer used, but required in function signature
2492
2493
		if ($check_component && !is_a($component, $check_component))
2494
		{
2495
			if ($this->log)
2496
			{
2497
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.'()' .
2498
					get_class($component)." found\n",3,$this->logfile);
2499
			}
2500
			return false;
2501
		}
2502
2503
		if (!empty($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length']))
2504
		{
2505
			$minimum_uid_length = $GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'];
2506
		}
2507
		else
2508
		{
2509
			$minimum_uid_length = 8;
2510
		}
2511
2512
		$isDate = false;
2513
		$event		= array();
2514
		$alarms		= array();
2515
		$organizer_status = $organizer_uid = null;
2516
		$vcardData	= array(
2517
			'recur_type'		=> MCAL_RECUR_NONE,
2518
			'recur_exception'	=> array(),
2519
			'priority'          => 0,	// iCalendar default is 0=undefined, not EGroupware 5=normal
2520
			'public'            => 1,
2521
		);
2522
		// we need to parse DTSTART, DTEND or DURATION (in that order!) first
2523
		foreach (array_merge(
2524
			$component->getAllAttributes('DTSTART'),
2525
			$component->getAllAttributes('DTEND'),
2526
			$component->getAllAttributes('DURATION')) as $attributes)
2527
		{
2528
			//error_log(__METHOD__."() attribute=".array2string($attributes));
2529
			switch ($attributes['name'])
2530
			{
2531
				case 'DTSTART':
2532
					if (isset($attributes['params']['VALUE'])
2533
							&& $attributes['params']['VALUE'] == 'DATE')
2534
					{
2535
						$isDate = true;
2536
					}
2537
					$dtstart_ts = is_numeric($attributes['value']) ? $attributes['value'] : $this->date2ts($attributes['value']);
2538
					$vcardData['start']	= $dtstart_ts;
2539
2540
					// set event timezone from dtstart, if specified there
2541
					if (!empty($attributes['params']['TZID']))
2542
					{
2543
						// import TZID, if PHP understands it (we only care about TZID of starttime,
2544
						// as we store only a TZID for the whole event)
2545
						try
2546
						{
2547
							$tz = calendar_timezones::DateTimeZone($attributes['params']['TZID']);
2548
							// sometimes we do not get an Api\DateTime object but no exception is thrown
2549
							// may be php 5.2.x related. occurs when a NokiaE72 tries to open Outlook invitations
2550
							if ($tz instanceof DateTimeZone)
2551
							{
2552
								$event['tzid'] = $tz->getName();
2553
							}
2554
							else
2555
							{
2556
								error_log(__METHOD__ . '() unknown TZID='
2557
									. $attributes['params']['TZID'] . ', defaulting to timezone "'
2558
									. date_default_timezone_get() . '".'.array2string($tz));
2559
								$event['tzid'] = date_default_timezone_get();	// default to current timezone
2560
							}
2561
						}
2562
						catch(Exception $e)
2563
						{
2564
							error_log(__METHOD__ . '() unknown TZID='
2565
								. $attributes['params']['TZID'] . ', defaulting to timezone "'
2566
								. date_default_timezone_get() . '".'.$e->getMessage());
2567
							$event['tzid'] = date_default_timezone_get();	// default to current timezone
2568
						}
2569
					}
2570
					// if no timezone given and one is specified in class (never the case for CalDAV)
2571
					elseif ($this->tzid)
2572
					{
2573
						$event['tzid'] = $this->tzid;
2574
					}
2575
					// Horde seems not to distinguish between an explicit UTC time postfixed with Z and one without
2576
					// assuming for now UTC to pass CalDAVTester tests
2577
					// ToDo: fix Horde_Icalendar to return UTC for timestamp postfixed with Z
2578
					elseif (!$isDate)
2579
					{
2580
						$event['tzid'] = 'UTC';
2581
					}
2582
					// default to use timezone to better kope with floating time
2583
					else
2584
					{
2585
						$event['tzid'] = Api\DateTime::$user_timezone->getName();
2586
					}
2587
					break;
2588
2589
				case 'DTEND':
2590
					$dtend_ts = is_numeric($attributes['value']) ? $attributes['value'] : $this->date2ts($attributes['value']);
2591
					if (date('H:i:s',$dtend_ts) == '00:00:00')
2592
					{
2593
						$dtend_ts -= 1;
2594
					}
2595
					$vcardData['end']	= $dtend_ts;
2596
					break;
2597
2598
				case 'DURATION':	// clients can use DTSTART+DURATION, instead of DTSTART+DTEND
2599
					if (!isset($vcardData['end']))
2600
					{
2601
						$vcardData['end'] = $vcardData['start'] + $attributes['value'];
2602
					}
2603
					else
2604
					{
2605
						error_log(__METHOD__."() find DTEND AND DURATION --> ignoring DURATION");
2606
					}
2607
					break;
2608
			}
2609
		}
2610
		if (!isset($vcardData['start']))
2611
		{
2612
			if ($this->log)
2613
			{
2614
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2615
					. "() DTSTART missing!\n",3,$this->logfile);
2616
			}
2617
			return false; // not a valid entry
2618
		}
2619
		// if neither duration nor dtend specified, default for dtstart as date is 1 day
2620
		if (!isset($vcardData['end']) && !$isDate)
2621
		{
2622
			$end = new Api\DateTime($vcardData['start']);
2623
			$end->add('1 day');
2624
			$vcardData['end'] = $end->format('ts');
2625
		}
2626
		// lets see what we can get from the vcard
2627
		foreach ($component->getAllAttributes() as $attributes)
2628
		{
2629
			switch ($attributes['name'])
2630
			{
2631
				case 'X-MICROSOFT-CDO-ALLDAYEVENT':
2632
					if (isset($supportedFields['whole_day']))
2633
					{
2634
						$event['whole_day'] = (isset($attributes['value'])?strtoupper($attributes['value'])=='TRUE':true);
2635
					}
2636
					break;
2637
				case 'AALARM':
2638
				case 'DALARM':
2639
					$alarmTime = $attributes['value'];
2640
					$alarms[$alarmTime] = array(
2641
						'time' => $alarmTime
2642
					);
2643
					break;
2644
				case 'CLASS':
2645
					$vcardData['public'] = (int)(strtolower($attributes['value']) == 'public');
2646
					break;
2647
				case 'DESCRIPTION':
2648
					$vcardData['description'] = str_replace("\r\n", "\n", $attributes['value']);
2649
					$matches = null;
2650
					if (preg_match('/\s*\[UID:(.+)?\]/Usm', $attributes['value'], $matches))
2651
					{
2652
						if (!isset($vcardData['uid'])
2653
								&& strlen($matches[1]) >= $minimum_uid_length)
2654
						{
2655
							$vcardData['uid'] = $matches[1];
2656
						}
2657
					}
2658
					break;
2659
				case 'RECURRENCE-ID':
2660
				case 'X-RECURRENCE-ID':
2661
					$vcardData['recurrence'] = $attributes['value'];
2662
					break;
2663
				case 'LOCATION':
2664
					$vcardData['location']	= str_replace("\r\n", "\n", $attributes['value']);
2665
					break;
2666
				case 'RRULE':
2667
					unset($vcardData['recur_type']);	// it wont be set by +=
2668
					$vcardData += calendar_rrule::parseRrule($attributes['value']);
2669
					if (!empty($vcardData['recur_enddate'])) self::check_fix_endate ($vcardData);
2670
					break;
2671
				case 'EXDATE':	// current Horde_Icalendar returns dates, no timestamps
2672
					if ($attributes['values'])
2673
					{
2674
						$days = array();
2675
						$hour = date('H', $vcardData['start']);
2676
						$minutes = date('i', $vcardData['start']);
2677
						$seconds = date('s', $vcardData['start']);
2678
						foreach ($attributes['values'] as $day)
2679
						{
2680
							$days[] = mktime(
2681
								$hour,
2682
								$minutes,
2683
								$seconds,
2684
								$day['month'],
2685
								$day['mday'],
2686
								$day['year']);
2687
						}
2688
						$vcardData['recur_exception'] = array_merge($vcardData['recur_exception'], $days);
2689
					}
2690
					break;
2691
				case 'SUMMARY':
2692
					$vcardData['title'] = str_replace("\r\n", "\n", $attributes['value']);
2693
					break;
2694
				case 'UID':
2695
					if (strlen($attributes['value']) >= $minimum_uid_length)
2696
					{
2697
						$event['uid'] = $vcardData['uid'] = $attributes['value'];
2698
					}
2699
					break;
2700
				case 'TRANSP':
2701
					if ($version == '1.0')
2702
					{
2703
						$vcardData['non_blocking'] = ($attributes['value'] == 1);
2704
					}
2705
					else
2706
					{
2707
						$vcardData['non_blocking'] = ($attributes['value'] == 'TRANSPARENT');
2708
					}
2709
					break;
2710
				case 'PRIORITY':
2711
					if ($this->productManufacturer == 'funambol' &&
2712
						(strpos($this->productName, 'outlook') !== false
2713
							|| strpos($this->productName, 'pocket pc') !== false))
2714
					{
2715
						$vcardData['priority'] = (int) $this->priority_funambol2egw[$attributes['value']];
2716
					}
2717
					else
2718
					{
2719
						$vcardData['priority'] = (int) $this->priority_ical2egw[$attributes['value']];
2720
					}
2721
					break;
2722
				case 'CATEGORIES':
2723
					if ($attributes['value'])
2724
					{
2725
						$vcardData['category'] = explode(',', $attributes['value']);
2726
					}
2727
					else
2728
					{
2729
						$vcardData['category'] = array();
2730
					}
2731
					break;
2732
				case 'ORGANIZER':
2733
					$event['organizer'] = $attributes['value'];	// no egw field, but needed in AS
2734
					if (strtolower(substr($event['organizer'],0,7)) == 'mailto:')
2735
					{
2736
						$event['organizer'] = substr($event['organizer'],7);
2737
					}
2738
					if (!empty($attributes['params']['CN']))
2739
					{
2740
						$event['organizer'] = $attributes['params']['CN'].' <'.$event['organizer'].'>';
2741
					}
2742
					// fall throught
2743
				case 'ATTENDEE':
2744
					// work around Ligthning sending @ as %40
2745
					$attributes['value'] = str_replace('%40', '@', $attributes['value']);
2746
					if (isset($attributes['params']['PARTSTAT']))
2747
					{
2748
						$attributes['params']['STATUS'] = $attributes['params']['PARTSTAT'];
2749
					}
2750
					if (isset($attributes['params']['STATUS']))
2751
					{
2752
						$status = $this->status_ical2egw[strtoupper($attributes['params']['STATUS'])];
2753
						if (empty($status)) $status = 'X';
2754
					}
2755
					else
2756
					{
2757
						$status = 'X'; // client did not return the status
2758
					}
2759
					$uid = $email = $cn = '';
2760
					$quantity = 1;
2761
					$role = 'REQ-PARTICIPANT';
2762
					if (!empty($attributes['params']['ROLE']))
2763
					{
2764
						$role = $attributes['params']['ROLE'];
2765
					}
2766
					// CN explicit given --> use it
2767
					if (strtolower(substr($attributes['value'], 0, 7)) == 'mailto:' &&
2768
						!empty($attributes['params']['CN']))
2769
					{
2770
						$email = substr($attributes['value'], 7);
2771
						$cn = $attributes['params']['CN'];
2772
					}
2773
					// try parsing email and cn from attendee
2774
					elseif (preg_match('/mailto:([@.a-z0-9_-]+)|mailto:"?([.a-z0-9_ -]*)"?[ ]*<([@.a-z0-9_-]*)>/i',
2775
						$attributes['value'],$matches))
2776
					{
2777
						$email = $matches[1] ? $matches[1] : $matches[3];
2778
						$cn = isset($matches[2]) ? $matches[2]: '';
2779
					}
2780
					elseif (!empty($attributes['value']) &&
2781
						preg_match('/"?([.a-z0-9_ -]*)"?[ ]*<([@.a-z0-9_-]*)>/i',
2782
						$attributes['value'],$matches))
2783
					{
2784
						$cn = $matches[1];
2785
						$email = $matches[2];
2786
					}
2787
					elseif (strpos($attributes['value'],'@') !== false)
2788
					{
2789
						$email = $attributes['value'];
2790
					}
2791
					// try X-EGROUPWARE-UID, but only if it resolves to same email (otherwise we are in trouble if different EGw installs talk to each other)
2792
					if (!$uid && !empty($attributes['params']['X-EGROUPWARE-UID']) &&
2793
						($res_info = $this->resource_info($attributes['params']['X-EGROUPWARE-UID'])) &&
2794
						!strcasecmp($res_info['email'], $email))
2795
					{
2796
						$uid = $attributes['params']['X-EGROUPWARE-UID'];
2797
						if (!empty($attributes['params']['X-EGROUPWARE-QUANTITY']))
2798
						{
2799
							$quantity = $attributes['params']['X-EGROUPWARE-QUANTITY'];
2800
						}
2801
						if ($this->log)
2802
						{
2803
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2804
								. "(): Found X-EGROUPWARE-UID: '$uid'\n",3,$this->logfile);
2805
						}
2806
					}
2807
					elseif ($attributes['value'] == 'Unknown')
2808
					{
2809
							// we use the current user
2810
							$uid = $this->user;
2811
					}
2812
					// check principal url from CalDAV here after X-EGROUPWARE-UID and to get optional X-EGROUPWARE-QUANTITY
2813
					if (!$uid) $uid = Api\CalDAV\Principals::url2uid($attributes['value'], null, $cn);
2814
2815
					// try to find an email address
2816
					if (!$uid && $email && ($uid = $GLOBALS['egw']->accounts->name2id($email, 'account_email')))
2817
					{
2818
						// we use the account we found
2819
						if ($this->log)
2820
						{
2821
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2822
								. "() Found account: '$uid', '$cn', '$email'\n",3,$this->logfile);
2823
						}
2824
					}
2825
					if (!$uid)
2826
					{
2827
						$searcharray = array();
2828
						// search for provided email address ...
2829
						if ($email)
2830
						{
2831
							$searcharray = array('email' => $email, 'email_home' => $email);
2832
						}
2833
						// ... and for provided CN
2834
						if (!empty($attributes['params']['CN']))
2835
						{
2836
							$cn = str_replace(array('\\,', '\\;', '\\:', '\\\\'),
2837
										array(',', ';', ':', '\\'),
2838
										$attributes['params']['CN']);
2839
							if ($cn[0] == '"' && substr($cn,-1) == '"')
2840
							{
2841
								$cn = substr($cn,1,-1);
2842
							}
2843
							// not searching for $cn, as match can be not unique or without an email address
2844
							// --> notification will fail, better store just as email
2845
						}
2846
2847
						if ($this->log)
2848
						{
2849
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2850
								. "() Search participant: '$cn', '$email'\n",3,$this->logfile);
2851
						}
2852
2853
						//elseif (//$attributes['params']['CUTYPE'] == 'GROUP'
2854
						if (preg_match('/(.*) '. lang('Group') . '/', $cn, $matches))
2855
						{
2856
							// we found a group
2857
							if ($this->log)
2858
							{
2859
								error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2860
									. "() Found group: '$matches[1]', '$cn', '$email'\n",3,$this->logfile);
2861
							}
2862
							if (($uid =  $GLOBALS['egw']->accounts->name2id($matches[1], 'account_lid', 'g')))
2863
							{
2864
								//Horde::logMessage("vevent2egw: group participant $uid",
2865
								//			__FILE__, __LINE__, PEAR_LOG_DEBUG);
2866
								if (!isset($vcardData['participants'][$this->user]) &&
2867
									$status != 'X' && $status != 'U')
2868
								{
2869
									// User tries to reply to the group invitiation
2870
									if (($members = $GLOBALS['egw']->accounts->members($uid, true)) &&
2871
										in_array($this->user, $members))
2872
									{
2873
										//Horde::logMessage("vevent2egw: set status to " . $status,
2874
										//		__FILE__, __LINE__, PEAR_LOG_DEBUG);
2875
										$vcardData['participants'][$this->user] =
2876
											calendar_so::combine_status($status,$quantity,$role);
2877
									}
2878
								}
2879
								$status = 'U'; // keep the group
2880
							}
2881
							else continue 2; // can't find this group
2882
						}
2883
						elseif (empty($searcharray))
2884
						{
2885
							continue 2;	// participants without email AND CN --> ignore it
2886
						}
2887
						elseif ((list($data) = $this->addressbook->search($searcharray,
2888
							array('id','egw_addressbook.account_id as account_id','n_fn'),
2889
							'egw_addressbook.account_id IS NOT NULL DESC, n_fn IS NOT NULL DESC',
2890
							'','',false,'OR')))
2891
						{
2892
							// found an addressbook entry
2893
							$uid = $data['account_id'] ? (int)$data['account_id'] : 'c'.$data['id'];
2894
							if ($this->log)
2895
							{
2896
								error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2897
									. "() Found addressbook entry: '$uid', '$cn', '$email'\n",3,$this->logfile);
2898
							}
2899
						}
2900
						else
2901
						{
2902
							if (!$email)
2903
							{
2904
								$email = '[email protected]';	// set dummy email to store the CN
2905
							}
2906
							$uid = 'e'. ($cn ? $cn . ' <' . $email . '>' : $email);
2907
							if ($this->log)
2908
							{
2909
								error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2910
									. "() Not Found, create dummy: '$uid', '$cn', '$email'\n",3,$this->logfile);
2911
							}
2912
						}
2913
					}
2914
					switch($attributes['name'])
2915
					{
2916
						case 'ATTENDEE':
2917
							if (!isset($attributes['params']['ROLE']) &&
2918
								isset($event['owner']) && $event['owner'] == $uid)
2919
							{
2920
								$attributes['params']['ROLE'] = 'CHAIR';
2921
							}
2922
							if (!isset($vcardData['participants'][$uid]) ||
2923
									$vcardData['participants'][$uid][0] != 'A')
2924
							{
2925
								// keep role 'CHAIR' from an external organizer, even if he is a regular participant with a different role
2926
								// as this is currently the only way to store an external organizer and send him iMip responses
2927
								$q = $r = null;
2928
								if (isset($vcardData['participants'][$uid]) && ($s=$vcardData['participants'][$uid]) &&
2929
									calendar_so::split_status($s, $q, $r) && $r == 'CHAIR')
2930
								{
2931
									$role = 'CHAIR';
2932
								}
2933
								// for multiple entries the ACCEPT wins
2934
								// add quantity and role
2935
								$vcardData['participants'][$uid] =
2936
									calendar_so::combine_status(
2937
										// Thunderbird: if there is a PARTICIPANT for the ORGANIZER AND ORGANZIER has PARTSTAT
2938
										// --> use the one from ORGANIZER
2939
										$uid === $organizer_uid && !empty($organizer_status) && $organizer_status !== 'X' ?
2940
										$organizer_status : $status, $quantity, $role);
2941
2942
								try {
2943
									if (!$this->calendarOwner && is_numeric($uid) && $role == 'CHAIR')
2944
										$component->getAttribute('ORGANIZER');
2945
								}
2946
								catch(Horde_Icalendar_Exception $e)
2947
								{
2948
									// we can store the ORGANIZER as event owner
2949
									$event['owner'] = $uid;
2950
								}
2951
							}
2952
							break;
2953
2954
						case 'ORGANIZER':
2955
							// remember evtl. set PARTSTAT from ORGANIZER, as TB sets it on ORGANIZER not PARTICIPANT!
2956
							$organizer_uid = $uid;
2957
							$organizer_status = $status;
2958
							if (isset($vcardData['participants'][$uid]))
2959
							{
2960
								$status = $vcardData['participants'][$uid];
2961
								calendar_so::split_status($status, $quantity, $role);
2962
								$vcardData['participants'][$uid] =
2963
									calendar_so::combine_status($status, $quantity, 'CHAIR');
2964
							}
2965
							if (!$this->calendarOwner && is_numeric($uid))
2966
							{
2967
								// we can store the ORGANIZER as event owner
2968
								$event['owner'] = $uid;
2969
							}
2970
							else
2971
							{
2972
								// we must insert a CHAIR participant to keep the ORGANIZER
2973
								$event['owner'] = $this->user;
2974
								if (!isset($vcardData['participants'][$uid]))
2975
								{
2976
									// save the ORGANIZER as event CHAIR
2977
									$vcardData['participants'][$uid] =
2978
										calendar_so::combine_status('D', 1, 'CHAIR');
2979
								}
2980
							}
2981
					}
2982
					break;
2983
				case 'CREATED':		// will be written direct to the event
2984
					if ($event['modified']) break;
2985
					// fall through
2986
				case 'LAST-MODIFIED':	// will be written direct to the event
2987
					$event['modified'] = $attributes['value'];
2988
					break;
2989
				case 'STATUS':	// currently no EGroupware event column, but needed as Android uses it to delete single recurrences
2990
					$event['status'] = $attributes['value'];
2991
					break;
2992
2993
				// ignore all PROPS, we dont want to store like X-properties or unsupported props
2994
				case 'DTSTAMP':
2995
				case 'SEQUENCE':
2996
				case 'CREATED':
2997
				case 'LAST-MODIFIED':
2998
				case 'DTSTART':
2999
				case 'DTEND':
3000
				case 'DURATION':
3001
				case 'X-LIC-ERROR':	// parse errors from libical, makes no sense to store them
3002
					break;
3003
3004
				case 'ATTACH':
3005
					if ($attributes['params'] && !empty($attributes['params']['FMTTYPE'])) break;	// handeled by managed attachment code
3006
					// fall throught to store external attachment url
3007
				default:	// X- attribute or other by EGroupware unsupported property
3008
					//error_log(__METHOD__."() $attributes[name] = ".array2string($attributes));
3009
					// for attributes with multiple values in multiple lines, merge the values
3010
					if (isset($event['##'.$attributes['name']]))
3011
					{
3012
						//error_log(__METHOD__."() taskData['##$attribute[name]'] = ".array2string($taskData['##'.$attribute['name']]));
3013
						$attributes['values'] = array_merge(
3014
							is_array($event['##'.$attributes['name']]) ? $event['##'.$attributes['name']]['values'] : (array)$event['##'.$attributes['name']],
3015
							$attributes['values']);
3016
					}
3017
					$event['##'.$attributes['name']] = $attributes['params'] || count($attributes['values']) > 1 ?
3018
						json_encode($attributes) : $attributes['value'];
3019
3020
					// check if json_encoded attribute is to big for our table
3021
					if (($attributes['params'] || count($attributes['values']) > 1) &&
3022
						strlen($event['##'.$attributes['name']]) >
3023
							Api\Db::get_column_attribute('cal_extra_value', 'egw_cal_extra', 'calendar', 'precision'))
0 ignored issues
show
Bug Best Practice introduced by
The method EGroupware\Api\Db::get_column_attribute() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

3023
							Api\Db::/** @scrutinizer ignore-call */ 
3024
               get_column_attribute('cal_extra_value', 'egw_cal_extra', 'calendar', 'precision'))
Loading history...
3024
					{
3025
						// store content compressed (Outlook/Exchange HTML garbadge is very good compressable)
3026
						if (function_exists('gzcompress'))
3027
						{
3028
							$event['##'.$attributes['name']] = json_encode(array(
3029
								'gzcompress' => base64_encode(gzcompress($event['##'.$attributes['name']]))
3030
							));
3031
						}
3032
						// if that's not enough --> unset it, as truncating the json gives nothing
3033
						if (strlen($event['##'.$attributes['name']]) >
3034
							Api\Db::get_column_attribute('cal_extra_value', 'egw_cal_extra', 'calendar', 'precision'))
3035
						{
3036
							unset($event['##'.$attributes['name']]);
3037
						}
3038
					}
3039
					break;
3040
			}
3041
		}
3042
		// check if the entry is a birthday
3043
		// this field is only set from NOKIA clients
3044
		try {
3045
			$agendaEntryType = $component->getAttribute('X-EPOCAGENDAENTRYTYPE');
3046
			if (strtolower($agendaEntryType) == 'anniversary')
3047
			{
3048
				$event['special'] = '1';
3049
				$event['non_blocking'] = 1;
3050
				// make it a whole day event for eGW
3051
				$vcardData['end'] = $vcardData['start'] + 86399;
3052
			}
3053
			elseif (strtolower($agendaEntryType) == 'event')
3054
			{
3055
				$event['special'] = '2';
3056
			}
3057
		}
3058
		catch (Horde_Icalendar_Exception $e) {}
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
3059
3060
		$event['priority'] = 2; // default
3061
		$event['alarm'] = $alarms;
3062
3063
		// now that we know what the vard provides,
3064
		// we merge that data with the information we have about the device
3065
		while (($fieldName = array_shift($supportedFields)))
3066
		{
3067
			switch ($fieldName)
3068
			{
3069
				case 'recur_interval':
3070
				case 'recur_enddate':
3071
				case 'recur_data':
3072
				case 'recur_exception':
3073
				case 'recur_count':
3074
				case 'whole_day':
3075
					// not handled here
3076
					break;
3077
3078
				case 'recur_type':
3079
					$event['recur_type'] = $vcardData['recur_type'];
3080
					if ($event['recur_type'] != MCAL_RECUR_NONE)
3081
					{
3082
						$event['reference'] = 0;
3083
						foreach (array('recur_interval','recur_enddate','recur_data','recur_exception','recur_count') as $r)
3084
						{
3085
							if (isset($vcardData[$r]))
3086
							{
3087
								$event[$r] = $vcardData[$r];
3088
							}
3089
						}
3090
					}
3091
					break;
3092
3093
				default:
3094
					if (isset($vcardData[$fieldName]))
3095
					{
3096
						$event[$fieldName] = $vcardData[$fieldName];
3097
					}
3098
				break;
3099
			}
3100
		}
3101
		if ($event['recur_enddate'])
3102
		{
3103
			// reset recure_enddate to 00:00:00 on the last day
3104
			$rriter = calendar_rrule::event2rrule($event, false);
3105
			$last = $rriter->normalize_enddate();
3106
			if(!is_object($last))
3107
			{
3108
				if($this->log)
3109
				{
3110
					error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ .
3111
					" Unable to determine recurrence end date.  \n".array2string($event),3, $this->logfile);
3112
				}
3113
				return false;
3114
			}
3115
			$last->setTime(0, 0, 0);
3116
			//error_log(__METHOD__."() rrule=$recurence --> ".array2string($rriter)." --> enddate=".array2string($last).'='.Api\DateTime::to($last, ''));
3117
			$event['recur_enddate'] = Api\DateTime::to($last, 'server');
3118
		}
3119
		// translate COUNT into an enddate, as we only store enddates
3120
		elseif($event['recur_count'])
3121
		{
3122
			$rriter = calendar_rrule::event2rrule($event, false);
3123
			$last = $rriter->count2date($event['recur_count']);
3124
			if(!is_object($last))
3125
			{
3126
				if($this->log)
3127
				{
3128
					error_log(__FILE__.'['.__LINE__.'] '.__METHOD__,
3129
					" Unable to determine recurrence end date.  \n".array2string($event),3, $this->logfile);
3130
				}
3131
				return false;
3132
			}
3133
			$last->setTime(0, 0, 0);
3134
			$event['recur_enddate'] = Api\DateTime::to($last, 'server');
3135
			unset($event['recur_count']);
3136
		}
3137
3138
		// Apple iCal on OS X uses X-CALENDARSERVER-ACCESS: CONFIDENTIAL on VCALANDAR (not VEVENT!)
3139
		try {
3140
			if ($this->productManufacturer == 'groupdav' && $container &&
3141
				($x_calendarserver_access = $container->getAttribute('X-CALENDARSERVER-ACCESS')))
3142
			{
3143
				$event['public'] =  (int)(strtoupper($x_calendarserver_access) == 'PUBLIC');
3144
			}
3145
			//error_log(__METHOD__."() X-CALENDARSERVER-ACCESS=".array2string($x_calendarserver_access).' --> public='.array2string($event['public']));
3146
		}
3147
		catch (Horde_Icalendar_Exception $e) {}
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
3148
3149
		// if no end is given in iCal we use the default lenght from user prefs
3150
		// whole day events get one day in calendar_boupdate::save()
3151
		if (!isset($event['end']))
3152
		{
3153
			$event['end'] = $event['start'] + 60 * $this->cal_prefs['defaultlength'];
3154
		}
3155
3156
		if ($this->calendarOwner) $event['owner'] = $this->calendarOwner;
3157
3158
		// parsing ATTACH attributes for managed attachments
3159
		$event['attach-delete-by-put'] = $component->getAttributeDefault('X-EGROUPWARE-ATTACH-INCLUDED', null) === 'TRUE';
3160
		$event['attach'] = $component->getAllAttributes('ATTACH');
3161
3162
		if ($this->log)
3163
		{
3164
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" .
3165
				array2string($event)."\n",3,$this->logfile);
3166
		}
3167
		//Horde::logMessage("vevent2egw:\n" . print_r($event, true),
3168
	    //    	__FILE__, __LINE__, PEAR_LOG_DEBUG);
3169
		return $event;
3170
	}
3171
3172
	function search($_vcalData, $contentID=null, $relax=false, $charset=null)
3173
	{
3174
		if (($events = $this->icaltoegw($_vcalData, $charset)))
3175
		{
3176
			// this function only supports searching a single event
3177
			if (count($events) == 1)
3178
			{
3179
				$filter = $relax ? 'relax' : 'check';
3180
				$event = array_shift($events);
3181
				$eventId = -1;
3182
				if ($this->so->isWholeDay($event)) $event['whole_day'] = true;
3183
				if ($contentID)
3184
				{
3185
					$parts = preg_split('/:/', $contentID);
3186
					$event['id'] = $eventId = $parts[0];
3187
				}
3188
				$event['category'] = $this->find_or_add_categories($event['category'], $eventId);
3189
				return $this->find_event($event, $filter);
3190
			}
3191
			if ($this->log)
3192
			{
3193
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."() found:\n" .
3194
					array2string($events)."\n",3,$this->logfile);
3195
			}
3196
		}
3197
		return array();
3198
	}
3199
3200
	/**
3201
	 * iCal defines enddate to be a time and eg. Apple sends 1s less then next recurance, if they split events
3202
	 *
3203
	 * We need to fix that situation by moving end one day back.
3204
	 *
3205
	 * @param array& $vcardData values for keys "start" and "recur_enddate", later get's modified if neccessary
3206
	 */
3207
	static function check_fix_endate(array &$vcardData)
3208
	{
3209
		$end = new Api\DateTime($vcardData['recur_enddate']);
3210
		$start = new Api\DateTime($vcardData['start']);
3211
		$start->setDate($end->format('Y'), $end->format('m'), $end->format('d'));
3212
3213
		if ($end->format('ts') < $start->format('ts'))
3214
		{
3215
			$end->modify('-1day');
3216
			$vcardData['recur_enddate'] = $end->format('ts');
3217
			//error_log(__METHOD__."($vcardData[event_title]) fix recure_enddate to ".$end->format('Y-m-d H:i:s'));
3218
		}
3219
	}
3220
3221
	/**
3222
	 * Create a freebusy vCal for the given user(s)
3223
	 *
3224
	 * @param int $user account_id
3225
	 * @param mixed $end =null end-date, default now+1 month
3226
	 * @param boolean $utc =true if false, use severtime for dates
3227
	 * @param string $charset ='UTF-8' encoding of the vcalendar, default UTF-8
3228
	 * @param mixed $start =null default now
3229
	 * @param string $method ='PUBLISH' or eg. 'REPLY'
3230
	 * @param array $extra =null extra attributes to add
3231
	 * 	X-CALENDARSERVER-MASK-UID can be used to not include an event specified by this uid as busy
3232
	 */
3233
	function freebusy($user,$end=null,$utc=true, $charset='UTF-8', $start=null, $method='PUBLISH', array $extra=null)
3234
	{
3235
		if (!$start) $start = time();	// default now
3236
		if (!$end) $end = time() + 100*DAY_s;	// default next 100 days
3237
3238
		$vcal = new Horde_Icalendar;
3239
		$vcal->setAttribute('PRODID','-//EGroupware//NONSGML EGroupware Calendar '.$GLOBALS['egw_info']['apps']['calendar']['version'].'//'.
3240
			strtoupper($GLOBALS['egw_info']['user']['preferences']['common']['lang']));
3241
		$vcal->setAttribute('VERSION','2.0');
3242
		$vcal->setAttribute('METHOD',$method);
3243
3244
		$vfreebusy = Horde_Icalendar::newComponent('VFREEBUSY',$vcal);
3245
3246
		$attributes = array(
3247
			'DTSTAMP' => time(),
3248
			'DTSTART' => $this->date2ts($start,true),	// true = server-time
3249
			'DTEND' => $this->date2ts($end,true),	// true = server-time
3250
		);
3251
		if (!$utc)
3252
		{
3253
			foreach ($attributes as $attr => $value)
3254
			{
3255
				$attributes[$attr] = date('Ymd\THis', $value);
3256
			}
3257
		}
3258
		if (is_null($extra)) $extra = array(
3259
			'URL' => $this->freebusy_url($user),
3260
			'ORGANIZER' => 'mailto:'.$GLOBALS['egw']->accounts->id2name($user,'account_email'),
3261
		);
3262
		foreach($attributes+$extra as $attr => $value)
3263
		{
3264
			$vfreebusy->setAttribute($attr, $value);
3265
		}
3266
		$events = parent::search(array(
3267
			'start' => $start,
3268
			'end'   => $end,
3269
			'users' => $user,
3270
			'date_format' => 'server',
3271
			'show_rejected' => false,
3272
		));
3273
		if (is_array($events))
0 ignored issues
show
introduced by
The condition is_array($events) is always false.
Loading history...
3274
		{
3275
			$fbdata = array();
3276
3277
			foreach ($events as $event)
3278
			{
3279
				if ($event['non_blocking']) continue;
3280
				if ($event['uid'] === $extra['X-CALENDARSERVER-MASK-UID']) continue;
3281
				$status = $event['participants'][$user];
3282
				$quantity = $role = null;
3283
				calendar_so::split_status($status, $quantity, $role);
3284
				if ($status == 'R' || $role == 'NON-PARTICIPANT') continue;
3285
3286
				$fbtype = $status == 'T' ? 'BUSY-TENTATIVE' : 'BUSY';
3287
3288
				// hack to fix end-time to be non-inclusive
3289
				// all-day events end in our data-model at 23:59:59 (of given TZ)
3290
				if (date('is', $event['end']) == '5959') ++$event['end'];
3291
3292
				$fbdata[$fbtype][] = $event;
3293
			}
3294
			foreach($fbdata as $fbtype => $events)
3295
			{
3296
				foreach($this->aggregate_periods($events, $start, $end) as $event)
3297
				{
3298
					if ($utc)
3299
					{
3300
						$vfreebusy->setAttribute('FREEBUSY',array(array(
3301
							'start' => $event['start'],
3302
							'end' => $event['end'],
3303
						)), array('FBTYPE' => $fbtype));
3304
					}
3305
					else
3306
					{
3307
						$vfreebusy->setAttribute('FREEBUSY',array(array(
3308
							'start' => date('Ymd\THis',$event['start']),
3309
							'end' => date('Ymd\THis',$event['end']),
3310
						)), array('FBTYPE' => $fbtype));
3311
					}
3312
				}
3313
			}
3314
		}
3315
		$vcal->addComponent($vfreebusy);
3316
3317
		return $vcal->exportvCalendar($charset);
0 ignored issues
show
Unused Code introduced by
The call to Horde_Icalendar::exportvCalendar() has too many arguments starting with $charset. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

3317
		return $vcal->/** @scrutinizer ignore-call */ exportvCalendar($charset);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
3318
	}
3319
3320
	/**
3321
	 * Aggregate multiple, possibly overlapping events cliped by $start and $end
3322
	 *
3323
	 * @param array $events array with values for keys "start" and "end"
3324
	 * @param int $start
3325
	 * @param int $end
3326
	 * @return array of array with values for keys "start" and "end"
3327
	 */
3328
	public function aggregate_periods(array $events, $start, $end)
3329
	{
3330
		// sort by start datetime
3331
		uasort($events, function($a, $b)
3332
		{
3333
			$diff = $a['start'] - $b['start'];
3334
3335
			return $diff == 0 ? 0 : ($diff < 0 ? -1 : 1);
3336
		});
3337
3338
		$fbdata = array();
3339
		foreach($events as $event)
3340
		{
3341
			error_log(__METHOD__."(..., $start, $end) event[start]=$event[start], event[end]=$event[end], fbdata=".array2string($fbdata));
3342
			if ($event['end'] <= $start || $event['start'] >= $end) continue;
3343
3344
			if (!$fbdata)
0 ignored issues
show
Bug Best Practice introduced by
The expression $fbdata of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
3345
			{
3346
				$fbdata[] = array(
3347
					'start' => $event['start'] < $start ? $start : $event['start'],
3348
					'end' => $event['end'],
3349
				);
3350
				continue;
3351
			}
3352
			$last =& $fbdata[count($fbdata)-1];
3353
3354
			if ($last['end'] >= $event['start'])
3355
			{
3356
				if ($last['end'] < $event['end'])
3357
				{
3358
					$last['end'] = $event['end'];
3359
				}
3360
			}
3361
			else
3362
			{
3363
				$fbdata[] = array(
3364
					'start' => $event['start'],
3365
					'end' => $event['end'],
3366
				);
3367
			}
3368
		}
3369
		$last =& $fbdata[count($fbdata)-1];
3370
3371
		if ($last['end'] > $end) $last['end'] = $end;
3372
3373
		error_log(__METHOD__."(..., $start, $end) returning ".array2string($fbdata));
3374
		return $fbdata;
3375
	}
3376
}
3377