Completed
Push — 14.2 ( 5702a1...375f73 )
by Nathan
38:22
created

calendar_ical::update()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 10
nc 5
nop 7
dl 0
loc 20
rs 8.8571
c 0
b 0
f 0
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
 * @version $Id$
13
 */
14
15
/**
16
 * iCal import and export via Horde iCalendar classes
17
 *
18
 * @ToDo: NOT changing default-timezone as it messes up timezone calculation of timestamps eg. in calendar_boupdate::send_update
19
 * 	(currently fixed by restoring server-timezone in calendar_boupdate::send_update)
20
 */
21
class calendar_ical extends calendar_boupdate
22
{
23
	/**
24
	 * @var array $supportedFields array containing the supported fields of the importing device
25
	 */
26
	var $supportedFields;
27
28
	var $recur_days_1_0 = array(
29
		MCAL_M_MONDAY    => 'MO',
30
		MCAL_M_TUESDAY   => 'TU',
31
		MCAL_M_WEDNESDAY => 'WE',
32
		MCAL_M_THURSDAY  => 'TH',
33
		MCAL_M_FRIDAY    => 'FR',
34
		MCAL_M_SATURDAY  => 'SA',
35
		MCAL_M_SUNDAY    => 'SU',
36
	);
37
	/**
38
	 * @var array $status_egw2ical conversation of the participant status egw => ical
39
	 */
40
	var $status_egw2ical = array(
41
		'U' => 'NEEDS-ACTION',
42
		'A' => 'ACCEPTED',
43
		'R' => 'DECLINED',
44
		'T' => 'TENTATIVE',
45
		'D' => 'DELEGATED'
46
	);
47
	/**
48
	 * @var array conversation of the participant status ical => egw
49
	 */
50
	var $status_ical2egw = array(
51
		'NEEDS-ACTION' => 'U',
52
		'NEEDS ACTION' => 'U',
53
		'ACCEPTED'     => 'A',
54
		'DECLINED'     => 'R',
55
		'TENTATIVE'    => 'T',
56
		'DELEGATED'    => 'D',
57
		'X-UNINVITED'  => 'G', // removed
58
	);
59
60
	/**
61
	 * @var array $priority_egw2ical conversion of the priority egw => ical
62
	 */
63
	var $priority_egw2ical = array(
64
		0 => 0,		// undefined
65
		1 => 9,		// low
66
		2 => 5,		// normal
67
		3 => 1,		// high
68
	);
69
70
	/**
71
	 * @var array $priority_ical2egw conversion of the priority ical => egw
72
	 */
73
	var $priority_ical2egw = array(
74
		0 => 0,		// undefined
75
		9 => 1,	8 => 1, 7 => 1, 6 => 1,	// low
76
		5 => 2,		// normal
77
		4 => 3, 3 => 3, 2 => 3, 1 => 3,	// high
78
	);
79
80
	/**
81
	 * @var array $priority_egw2funambol conversion of the priority egw => funambol
82
	 */
83
	var $priority_egw2funambol = array(
84
		0 => 1,		// undefined (mapped to normal since undefined does not exist)
85
		1 => 0,		// low
86
		2 => 1,		// normal
87
		3 => 2,		// high
88
	);
89
90
	/**
91
	 * @var array $priority_funambol2egw conversion of the priority funambol => egw
92
	 */
93
	var $priority_funambol2egw = array(
94
		0 => 1,		// low
95
		1 => 2,		// normal
96
		2 => 3,		// high
97
	);
98
99
	/**
100
	 * manufacturer and name of the sync-client
101
	 *
102
	 * @var string
103
	 */
104
	var $productManufacturer = 'file';
105
	var $productName = '';
106
107
	/**
108
	 * user preference: import all-day events as non blocking
109
	 *
110
	 * @var boolean
111
	 */
112
	var $nonBlockingAllday = false;
113
114
	/**
115
	 * user preference: attach UID entries to the DESCRIPTION
116
	 *
117
	 * @var boolean
118
	 */
119
	var $uidExtension = false;
120
121
	/**
122
	 * user preference: calendar to synchronize with
123
	 *
124
	 * @var int
125
	 */
126
	var $calendarOwner = 0;
127
128
	/**
129
	 * user preference: Use this timezone for import from and export to device
130
	 *
131
	 * === false => use event's TZ
132
	 * === null  => export in UTC
133
	 * string    => device TZ
134
	 *
135
	 * @var string|boolean
136
	 */
137
	var $tzid = null;
138
139
	/**
140
	 * Device CTCap Properties
141
	 *
142
	 * @var array
143
	 */
144
	var $clientProperties;
145
146
	/**
147
	 * vCalendar Instance for parsing
148
	 *
149
	 * @var array
150
	 */
151
	var $vCalendar;
152
153
	/**
154
	 * Addressbook BO instance
155
	 *
156
	 * @var array
157
	 */
158
	var $addressbook;
159
160
	/**
161
	 * Set Logging
162
	 *
163
	 * @var boolean
164
	 */
165
	var $log = false;
166
	var $logfile="/tmp/log-vcal";
167
168
	/**
169
	 * Conflict callback
170
	 * If set, conflict checking will be enabled, and the event as well as 
171
	 * conflicts are passed as parameters to this callback
172
	 */
173
	var $conflict_callback = null;
174
	
175
	/**
176
	 * Constructor
177
	 *
178
	 * @param array $_clientProperties		client properties
179
	 */
180 View Code Duplication
	function __construct(&$_clientProperties = array())
181
	{
182
		parent::__construct();
183
		if ($this->log) $this->logfile = $GLOBALS['egw_info']['server']['temp_dir']."/log-vcal";
184
		$this->clientProperties = $_clientProperties;
185
		$this->vCalendar = new Horde_Icalendar;
186
		$this->addressbook = new addressbook_bo;
187
	}
188
189
190
	/**
191
	 * Exports one calendar event to an iCalendar item
192
	 *
193
	 * @param int|array $events (array of) cal_id or array of the events with timestamps in server time
194
	 * @param string $version ='1.0' could be '2.0' too
195
	 * @param string $method ='PUBLISH'
196
	 * @param int $recur_date =0	if set export the next recurrence at or after the timestamp,
197
	 *                          default 0 => export whole series (or events, if not recurring)
198
	 * @param string $principalURL ='' Used for CalDAV exports
199
	 * @param string $charset ='UTF-8' encoding of the vcalendar, default UTF-8
200
	 * @param int|string $current_user =0 uid of current user to only export that one as participant for method=REPLY
201
	 * @return string|boolean string with iCal or false on error (e.g. no permission to read the event)
202
	 */
203
	function &exportVCal($events, $version='1.0', $method='PUBLISH', $recur_date=0, $principalURL='', $charset='UTF-8', $current_user=0)
204
	{
205
		if ($this->log)
206
		{
207
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
208
				"($version, $method, $recur_date, $principalURL, $charset)\n",
209
				3, $this->logfile);
210
		}
211
		$egwSupportedFields = array(
212
			'CLASS'			=> 'public',
213
			'SUMMARY'		=> 'title',
214
			'DESCRIPTION'	=> 'description',
215
			'LOCATION'		=> 'location',
216
			'DTSTART'		=> 'start',
217
			'DTEND'			=> 'end',
218
			'ATTENDEE'		=> 'participants',
219
			'ORGANIZER'		=> 'owner',
220
			'RRULE'			=> 'recur_type',
221
			'EXDATE'		=> 'recur_exception',
222
			'PRIORITY'		=> 'priority',
223
			'TRANSP'		=> 'non_blocking',
224
			'CATEGORIES'	=> 'category',
225
			'UID'			=> 'uid',
226
			'RECURRENCE-ID' => 'recurrence',
227
			'SEQUENCE'		=> 'etag',
228
			'STATUS'		=> 'status',
229
			'ATTACH'        => 'attachments',
230
		);
231
232
		if (!is_array($this->supportedFields)) $this->setSupportedFields();
233
234
		if ($this->productManufacturer == '' )
235
		{	// syncevolution is broken
236
			$version = '2.0';
237
		}
238
239
		$vcal = new Horde_Icalendar;
240
		$vcal->setAttribute('PRODID','-//EGroupware//NONSGML EGroupware Calendar '.$GLOBALS['egw_info']['apps']['calendar']['version'].'//'.
241
			strtoupper($GLOBALS['egw_info']['user']['preferences']['common']['lang']));
242
		$vcal->setAttribute('VERSION', $version);
243
		if ($method) $vcal->setAttribute('METHOD', $method);
244
		$events_exported = false;
245
246
		if (!is_array($events)) $events = array($events);
247
248
		$vtimezones_added = array();
249
		foreach ($events as $event)
250
		{
251
			$organizerURL = '';
252
			$organizerCN = false;
253
			$recurrence = $this->date2usertime($recur_date);
254
			$tzid = null;
255
256
			if ((!is_array($event) || empty($event['tzid']) && ($event = $event['id'])) &&
257
				!($event = $this->read($event, $recurrence, false, 'server')))
258
			{
259
				if ($this->read($event, $recurrence, true, 'server'))
260
				{
261
					if ($this->check_perms(EGW_ACL_FREEBUSY, $event, 0, 'server'))
262
					{
263
						$this->clear_private_infos($event, array($this->user, $event['owner']));
264
					}
265
					else
266
					{
267
						if ($this->log)
268
						{
269
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
270
								'() User does not have the permission to read event ' . $event['id']. "\n",
271
								3,$this->logfile);
272
						}
273
						return -1; // Permission denied
274
					}
275
				}
276
				else
277
				{
278
					$retval = false;  // Entry does not exist
279
					if ($this->log)
280
					{
281
						error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
282
							"() Event $event not found.\n",
283
							3, $this->logfile);
284
					}
285
				}
286
				continue;
287
			}
288
289
			if ($this->log)
290
			{
291
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
292
					'() export event UID: ' . $event['uid'] . ".\n",
293
					3, $this->logfile);
294
			}
295
296 View Code Duplication
			if ($this->tzid)
297
			{
298
				// explicit device timezone
299
				$tzid = $this->tzid;
300
			}
301
			elseif ($this->tzid === false)
302
			{
303
				// use event's timezone
304
				$tzid = $event['tzid'];
305
			}
306
307 View Code Duplication
			if (!isset(self::$tz_cache[$event['tzid']]))
308
			{
309
				self::$tz_cache[$event['tzid']] = calendar_timezones::DateTimeZone($event['tzid']);
310
			}
311
312
			if ($this->so->isWholeDay($event)) $event['whole_day'] = true;
313
314
			if ($this->log)
315
			{
316
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
317
					'(' . $event['id']. ',' . $recurrence . ")\n" .
318
					array2string($event)."\n",3,$this->logfile);
319
			}
320
321
			if ($recurrence)
322
			{
323
				if (!($master = $this->read($event['id'], 0, true, 'server'))) continue;
324
325
				if (!isset($this->supportedFields['participants']))
326
				{
327
					$days = $this->so->get_recurrence_exceptions($master, $tzid, 0, 0, 'tz_rrule');
328 View Code Duplication
					if (isset($days[$recurrence]))
329
					{
330
						$recurrence = $days[$recurrence]; // use remote representation
331
					}
332
					else
333
					{
334
						// We don't need status only exceptions
335
						if ($this->log)
336
						{
337
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
338
								"(, $recurrence) Gratuitous pseudo exception, skipped ...\n",
339
								3,$this->logfile);
340
						}
341
						continue; // unsupported status only exception
342
					}
343
				}
344
				else
345
				{
346
					$days = $this->so->get_recurrence_exceptions($master, $tzid, 0, 0, 'rrule');
347
					if ($this->log)
348
					{
349
						error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" .
350
							array2string($days)."\n",3,$this->logfile);
351
					}
352
					$recurrence = $days[$recurrence]; // use remote representation
353
				}
354
				// force single event
355
				foreach (array('recur_enddate','recur_interval','recur_exception','recur_data','recur_date','id','etag') as $name)
356
				{
357
					unset($event[$name]);
358
				}
359
				$event['recur_type'] = MCAL_RECUR_NONE;
360
			}
361
362
			// check if tzid of event (not only recuring ones) is already added to export
363
			if ($tzid && $tzid != 'UTC' && !in_array($tzid,$vtimezones_added))
364
			{
365
				// check if we have vtimezone component data for tzid of event, if not default to user timezone (default to server tz)
366
				if (calendar_timezones::add_vtimezone($vcal, $tzid) ||
367
					!in_array($tzid = egw_time::$user_timezone->getName(), $vtimezones_added) &&
368
						calendar_timezones::add_vtimezone($vcal, $tzid))
369
				{
370
					$vtimezones_added[] = $tzid;
371
					if (!isset(self::$tz_cache[$tzid]))
372
					{
373
						self::$tz_cache[$tzid] = calendar_timezones::DateTimeZone($tzid);
374
					}
375
				}
376
			}
377 View Code Duplication
			if ($this->productManufacturer != 'file' && $this->uidExtension)
378
			{
379
				// Append UID to DESCRIPTION
380
				if (!preg_match('/\[UID:.+\]/m', $event['description'])) {
381
					$event['description'] .= "\n[UID:" . $event['uid'] . "]";
382
				}
383
			}
384
385
			$vevent = Horde_Icalendar::newComponent('VEVENT', $vcal);
386
			$parameters = $attributes = $values = array();
387
388
			if ($this->productManufacturer == 'sonyericsson')
389
			{
390
				$eventDST = date('I', $event['start']);
391
				if ($eventDST)
392
				{
393
					$attributes['X-SONYERICSSON-DST'] = 4;
394
				}
395
			}
396
397
			if ($event['recur_type'] != MCAL_RECUR_NONE)
398
			{
399
				$exceptions = array();
400
401
				// dont use "virtual" exceptions created by participant status for GroupDAV or file export
402
				if (!in_array($this->productManufacturer,array('file','groupdav')))
403
				{
404
					$filter = isset($this->supportedFields['participants']) ? 'rrule' : 'tz_rrule';
405
					$exceptions = $this->so->get_recurrence_exceptions($event, $tzid, 0, 0, $filter);
406
					if ($this->log)
407
					{
408
						error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."(EXCEPTIONS)\n" .
409
							array2string($exceptions)."\n",3,$this->logfile);
410
					}
411
				}
412
				elseif (is_array($event['recur_exception']))
413
				{
414
					$exceptions = array_unique($event['recur_exception']);
415
					sort($exceptions);
416
				}
417
				$event['recur_exception'] = $exceptions;
418
			}
419
			foreach ($egwSupportedFields as $icalFieldName => $egwFieldName)
420
			{
421
				if (!isset($this->supportedFields[$egwFieldName]))
422
				{
423
					if ($this->log)
424
					{
425
						error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
426
							'(' . $event['id'] . ") [$icalFieldName] not supported\n",
427
							3,$this->logfile);
428
					}
429
					continue;
430
				}
431
				$values[$icalFieldName] = array();
432
				switch ($icalFieldName)
433
				{
434
					case 'ATTENDEE':
435
						foreach ((array)$event['participants'] as $uid => $status)
436
						{
437
							$quantity = $role = null;
438
							calendar_so::split_status($status, $quantity, $role);
439
							// do not include event owner/ORGANIZER as participant in his own calendar, if he is only participant
440
							if (count($event['participants']) == 1 && $event['owner'] == $uid) continue;
441
442
							if (!($info = $this->resource_info($uid))) continue;
443
444
							if (in_array($status, array('X','E'))) continue;	// dont include deleted participants
445
446
							if ($this->log)
447
							{
448
								error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ .
449
									'()attendee:' . array2string($info) ."\n",3,$this->logfile);
450
							}
451
							$participantCN = str_replace(array('\\', ',', ';', ':'),
452
												array('\\\\', '\\,', '\\;', '\\:'),
453
												trim(empty($info['cn']) ? $info['name'] : $info['cn']));
454
							if ($version == '1.0')
455
							{
456
								$participantURL = trim('"' . $participantCN . '"' . (empty($info['email']) ? '' : ' <' . $info['email'] .'>'));
457
							}
458
							else
459
							{
460
								$participantURL = empty($info['email']) ? '' : 'mailto:' . $info['email'];
461
							}
462
							// RSVP={TRUE|FALSE}	// resonse expected, not set in eGW => status=U
463
							$rsvp = $status == 'U' ? 'TRUE' : 'FALSE';
464
							if ($role == 'CHAIR')
465
							{
466
								$organizerURL = $participantURL;
467
								$rsvp = '';
468
								$organizerCN = $participantCN;
469
								$organizerUID = ($info['type'] != 'e' ? (string)$uid : '');
470
							}
471
							// iCal method=REPLY only exports replying / current user, except external organiser / chair above
472
							if ($method == 'REPLY' && $current_user && (string)$current_user !== (string)$uid)
473
							{
474
								continue;
475
							}
476
							// PARTSTAT={NEEDS-ACTION|ACCEPTED|DECLINED|TENTATIVE|DELEGATED|COMPLETED|IN-PROGRESS} everything from delegated is NOT used by eGW atm.
477
							$status = $this->status_egw2ical[$status];
478
							// CUTYPE={INDIVIDUAL|GROUP|RESOURCE|ROOM|UNKNOWN}
479
							switch ($info['type'])
480
							{
481
								case 'g':
482
									$cutype = 'GROUP';
483
									$participantURL = 'urn:uuid:'.common::generate_uid('accounts', $uid);
484
									if (!isset($event['participants'][$this->user]) &&
485
										($members = $GLOBALS['egw']->accounts->members($uid, true)) && in_array($this->user, $members))
486
									{
487
										$user = $this->resource_info($this->user);
488
										$attributes['ATTENDEE'][] = 'mailto:' . $user['email'];
489
			    						$parameters['ATTENDEE'][] = array(
490
			    							'CN'		=>	$user['name'],
491
			    							'ROLE'		=> 'REQ-PARTICIPANT',
492
											'PARTSTAT'	=> 'NEEDS-ACTION',
493
											'CUTYPE'	=> 'INDIVIDUAL',
494
											'RSVP'		=> 'TRUE',
495
											'X-EGROUPWARE-UID'	=> (string)$this->user,
496
			    						);
497
			    						$event['participants'][$this->user] = true;
498
									}
499
									break;
500
								case 'r':
501
									$participantURL = 'urn:uuid:'.common::generate_uid('resources', substr($uid, 1));
502
									$cutype = groupdav_principals::resource_is_location(substr($uid, 1)) ? 'ROOM' : 'RESOURCE';
503
									// unset resource email (email of responsible user) as iCal at least has problems,
504
									// if resonpsible is also pariticipant or organizer
505
									unset($info['email']);
506
									break;
507
								case 'u':	// account
508
								case 'c':	// contact
509
								case 'e':	// email address
510
									$cutype = 'INDIVIDUAL';
511
									break;
512
								default:
513
									$cutype = 'UNKNOWN';
514
									break;
515
							}
516
							// generate urn:uuid, if we have no other participant URL
517
							if (empty($participantURL) && $info && $info['app'])
518
							{
519
								$participantURL = 'urn:uuid:'.common::generate_uid($info['app'], substr($uid, 1));
520
							}
521
							// ROLE={CHAIR|REQ-PARTICIPANT|OPT-PARTICIPANT|NON-PARTICIPANT|X-*}
522
							$options = array();
523
							if (!empty($participantCN)) $options['CN'] = $participantCN;
524
							if (!empty($role)) $options['ROLE'] = $role;
525
							if (!empty($status)) $options['PARTSTAT'] = $status;
526
							if (!empty($cutype)) $options['CUTYPE'] = $cutype;
527
							if (!empty($rsvp)) $options['RSVP'] = $rsvp;
528
							if (!empty($info['email']) && $participantURL != 'mailto:'.$info['email'])
529
							{
530
								$options['EMAIL'] = $info['email'];	// only add EMAIL attribute, if not already URL, as eg. Akonadi is reported to have problems with it
531
							}
532
							if ($info['type'] != 'e') $options['X-EGROUPWARE-UID'] = (string)$uid;
533
							if ($quantity > 1) $options['X-EGROUPWARE-QUANTITY'] = (string)$quantity;
534
							$attributes['ATTENDEE'][] = $participantURL;
535
							$parameters['ATTENDEE'][] = $options;
536
						}
537
						break;
538
539
					case 'CLASS':
540
						if ($event['public']) continue;	// public is default, no need to export, fails CalDAVTester if added as default
541
						$attributes['CLASS'] = $event['public'] ? 'PUBLIC' : 'PRIVATE';
542
						// Apple iCal on OS X uses X-CALENDARSERVER-ACCESS: CONFIDENTIAL on VCALANDAR (not VEVENT!)
543
						if (!$event['public'] && $this->productManufacturer == 'groupdav')
544
						{
545
							$vcal->setAttribute('X-CALENDARSERVER-ACCESS', 'CONFIDENTIAL');
546
						}
547
						break;
548
549
    				case 'ORGANIZER':
550
	    				if (!$organizerURL)
551
	    				{
552
	    					$organizerCN = '"' . trim($GLOBALS['egw']->accounts->id2name($event['owner'],'account_firstname')
553
			    				. ' ' . $GLOBALS['egw']->accounts->id2name($event['owner'],'account_lastname')) . '"';
554
			    			$organizerEMail = $GLOBALS['egw']->accounts->id2name($event['owner'],'account_email');
555
			    			if ($version == '1.0')
556
			    			{
557
		    					$organizerURL = trim($organizerCN . (empty($organizerURL) ? '' : ' <' . $organizerURL .'>'));
558
			    			}
559
			    			else
560
			    			{
561
		    					$organizerURL = empty($organizerEMail) ? '' : 'mailto:' . $organizerEMail;
562
			    			}
563
			    			$organizerUID = $event['owner'];
564
		    				if (!isset($event['participants'][$event['owner']]))
565
		    				{
566
			    				$options = array(
567
									'ROLE'     => 'CHAIR',
568
									'PARTSTAT' => 'DELEGATED',
569
									'CUTYPE'   => 'INDIVIDUAL',
570
									//'RSVP'     => 'FALSE',
571
									);
572
								if (!empty($organizerCN)) $options['CN'] = $organizerCN;
573
								if (!empty($organizerEMail)) $options['EMAIL'] = $organizerEMail;
574
								if (!empty($event['owner'])) $options['X-EGROUPWARE-UID'] = $event['owner'];
575
								$attributes['ATTENDEE'][] = $organizerURL;
576
			    				$parameters['ATTENDEE'][] = $options;
577
		    				}
578
	    				}
579
    					// do NOT use ORGANIZER for events without further participants or a different organizer
580
	    				if (count($event['participants']) > 1 || !isset($event['participants'][$event['owner']]))
581
	    				{
582
		    				$attributes['ORGANIZER'] = $organizerURL;
583
		    				$parameters['ORGANIZER']['CN'] = $organizerCN;
584
		    				if (!empty($organizerUID))
585
		    				{
586
			    				$parameters['ORGANIZER']['X-EGROUPWARE-UID'] = $organizerUID;
587
		    				}
588
	    				}
589
	    				break;
590
591
					case 'DTSTART':
592
						if (empty($event['whole_day']))
593
						{
594
							$attributes['DTSTART'] = self::getDateTime($event['start'],$tzid,$parameters['DTSTART']);
595
						}
596
						break;
597
598
					case 'DTEND':
599
						if (empty($event['whole_day']))
600
						{
601
							// Hack for CalDAVTester to export duration instead of endtime
602
							if ($tzid == 'UTC' && $event['end'] - $event['start'] <= 86400)
603
								$attributes['duration'] = $event['end'] - $event['start'];
604
							else
605
							$attributes['DTEND'] = self::getDateTime($event['end'],$tzid,$parameters['DTEND']);
606
						}
607
						else
608
						{
609
							// write start + end of whole day events as dates
610
							$event['end-nextday'] = $event['end'] + 12*3600;	// we need the date of the next day, as DTEND is non-inclusive (= exclusive) in rfc2445
611
							foreach (array('start' => 'DTSTART','end-nextday' => 'DTEND') as $f => $t)
612
							{
613
								$time = new egw_time($event[$f],egw_time::$server_timezone);
614
								$arr = egw_time::to($time,'array');
615
								$vevent->setAttribute($t, array('year' => $arr['year'],'month' => $arr['month'],'mday' => $arr['day']),
616
									array('VALUE' => 'DATE'));
617
							}
618
							unset($attributes['DTSTART']);
619
							// 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
620
							$vevent->setAttribute('X-MICROSOFT-CDO-ALLDAYEVENT','TRUE');
621
						}
622
						break;
623
624
					case 'RRULE':
625
						if ($event['recur_type'] == MCAL_RECUR_NONE) break;		// no recuring event
626
						$rriter = calendar_rrule::event2rrule($event, false, $tzid);
627
						$rrule = $rriter->generate_rrule($version);
628
						if ($event['recur_enddate'])
629
						{
630
							if (!$tzid || $version != '1.0')
631
							{
632
								if (!isset(self::$tz_cache['UTC']))
633
								{
634
									self::$tz_cache['UTC'] = calendar_timezones::DateTimeZone('UTC');
635
								}
636
								$rrule['UNTIL']->setTimezone(self::$tz_cache['UTC']);
637
								$rrule['UNTIL'] = $rrule['UNTIL']->format('Ymd\THis\Z');
638
							}
639
						}
640
						if ($version == '1.0')
641
						{
642
							if ($event['recur_enddate'] && $tzid)
643
							{
644
								$rrule['UNTIL'] = self::getDateTime($rrule['UNTIL'],$tzid);
645
							}
646
							$attributes['RRULE'] = $rrule['FREQ'].' '.$rrule['UNTIL'];
647
						}
648
						else // $version == '2.0'
649
						{
650
							$attributes['RRULE'] = '';
651
							foreach($rrule as $n => $v)
0 ignored issues
show
Bug introduced by
The expression $rrule of type false|array<string,?> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
652
							{
653
								$attributes['RRULE'] .= ($attributes['RRULE']?';':'').$n.'='.$v;
654
							}
655
						}
656
						break;
657
658
					case 'EXDATE':
659
						if ($event['recur_type'] == MCAL_RECUR_NONE) break;
660
						if (!empty($event['recur_exception']))
661
						{
662
							if (empty($event['whole_day']))
663
							{
664
								foreach ($event['recur_exception'] as $key => $timestamp)
665
								{
666
									// current Horde_Icalendar 2.1.4 exports EXDATE always in UTC, so we should not set a timezone here
667
									// Apple calendar on OS X 10.11.4 uses a timezone, so does Horde eg. for Recurrence-ID
668
									$event['recur_exception'][$key] = self::getDateTime($timestamp,$tzid);//,$parameters['EXDATE']);
669
								}
670
							}
671
							else
672
							{
673
								// use 'DATE' instead of 'DATE-TIME' on whole day events
674
								foreach ($event['recur_exception'] as $id => $timestamp)
675
								{
676
									$time = new egw_time($timestamp,egw_time::$server_timezone);
677
									$time->setTimezone(self::$tz_cache[$event['tzid']]);
678
									$arr = egw_time::to($time,'array');
679
									$days[$id] = array(
680
										'year'  => $arr['year'],
681
										'month' => $arr['month'],
682
										'mday'  => $arr['day'],
683
									);
684
								}
685
								$event['recur_exception'] = $days;
686
								if ($version != '1.0') $parameters['EXDATE']['VALUE'] = 'DATE';
687
							}
688
							$vevent->setAttribute('EXDATE', $event['recur_exception'], $parameters['EXDATE']);
689
						}
690
						break;
691
692 View Code Duplication
					case 'PRIORITY':
693
						if (!$event['priority']) continue;	// 0=undefined is default, no need to export, fails CalDAVTester if our default is added
694
						if ($this->productManufacturer == 'funambol' &&
695
							(strpos($this->productName, 'outlook') !== false
696
								|| strpos($this->productName, 'pocket pc') !== false))
697
						{
698
							$attributes['PRIORITY'] = (int) $this->priority_egw2funambol[$event['priority']];
699
						}
700
						else
701
						{
702
							$attributes['PRIORITY'] = (int) $this->priority_egw2ical[$event['priority']];
703
						}
704
						break;
705
706
					case 'TRANSP':
707
						if (!$event['non_blocking']) continue;	// OPAQUE is default, no need to export, fails CalDAVTester if added as default
708
						if ($version == '1.0')
709
						{
710
							$attributes['TRANSP'] = ($event['non_blocking'] ? 1 : 0);
711
						}
712
						else
713
						{
714
							$attributes['TRANSP'] = ($event['non_blocking'] ? 'TRANSPARENT' : 'OPAQUE');
715
						}
716
						break;
717
718
					case 'STATUS':
719
						$attributes['STATUS'] = 'CONFIRMED';
720
						break;
721
722
					case 'CATEGORIES':
723
						if ($event['category'] && ($values['CATEGORIES'] = $this->get_categories($event['category'])))
724
						{
725
							if (count($values['CATEGORIES']) == 1)
726
							{
727
								$attributes['CATEGORIES'] = array_shift($values['CATEGORIES']);
728
							}
729
							else
730
							{
731
								$attributes['CATEGORIES'] = '';
732
							}
733
						}
734
						break;
735
736
					case 'RECURRENCE-ID':
737
						if ($version == '1.0')
738
						{
739
								$icalFieldName = 'X-RECURRENCE-ID';
740
						}
741
						if ($recur_date)
742
						{
743
							// We handle a pseudo exception
744 View Code Duplication
							if (empty($event['whole_day']))
745
							{
746
								$attributes[$icalFieldName] = self::getDateTime($recur_date,$tzid,$parameters[$icalFieldName]);
747
							}
748
							else
749
							{
750
								$time = new egw_time($recur_date,egw_time::$server_timezone);
751
								$time->setTimezone(self::$tz_cache[$event['tzid']]);
752
								$arr = egw_time::to($time,'array');
753
								$vevent->setAttribute($icalFieldName, array(
754
									'year' => $arr['year'],
755
									'month' => $arr['month'],
756
									'mday' => $arr['day']),
757
									array('VALUE' => 'DATE')
758
								);
759
							}
760
						}
761
						elseif ($event['recurrence'] && $event['reference'])
762
						{
763
							// $event['reference'] is a calendar_id, not a timestamp
764
							if (!($revent = $this->read($event['reference']))) break;	// referenced event does not exist
765
766 View Code Duplication
							if (empty($revent['whole_day']))
767
							{
768
								$attributes[$icalFieldName] = self::getDateTime($event['recurrence'],$tzid,$parameters[$icalFieldName]);
769
							}
770
							else
771
							{
772
								$time = new egw_time($event['recurrence'],egw_time::$server_timezone);
773
								$time->setTimezone(self::$tz_cache[$event['tzid']]);
774
								$arr = egw_time::to($time,'array');
775
								$vevent->setAttribute($icalFieldName, array(
776
									'year' => $arr['year'],
777
									'month' => $arr['month'],
778
									'mday' => $arr['day']),
779
									array('VALUE' => 'DATE')
780
								);
781
							}
782
783
							unset($revent);
784
						}
785
						break;
786
787
					case 'ATTACH':
788
						groupdav::add_attach('calendar', $event['id'], $attributes, $parameters);
789
						break;
790
791
					default:
792 View Code Duplication
						if (isset($this->clientProperties[$icalFieldName]['Size']))
793
						{
794
							$size = $this->clientProperties[$icalFieldName]['Size'];
795
							$noTruncate = $this->clientProperties[$icalFieldName]['NoTruncate'];
796
							if ($this->log && $size > 0)
797
							{
798
								error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ .
799
									"() $icalFieldName Size: $size, NoTruncate: " .
800
									($noTruncate ? 'TRUE' : 'FALSE') . "\n",3,$this->logfile);
801
							}
802
							//Horde::logMessage("vCalendar $icalFieldName Size: $size, NoTruncate: " .
803
							//	($noTruncate ? 'TRUE' : 'FALSE'), __FILE__, __LINE__, PEAR_LOG_DEBUG);
804
						}
805
						else
806
						{
807
							$size = -1;
808
							$noTruncate = false;
809
						}
810
						$value = $event[$egwFieldName];
811
						$cursize = strlen($value);
812 View Code Duplication
						if ($size > 0 && $cursize > $size)
813
						{
814
							if ($noTruncate)
815
							{
816
								if ($this->log)
817
								{
818
									error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ .
819
										"() $icalFieldName omitted due to maximum size $size\n",3,$this->logfile);
820
								}
821
								//Horde::logMessage("vCalendar $icalFieldName omitted due to maximum size $size",
822
								//	__FILE__, __LINE__, PEAR_LOG_WARNING);
823
								continue; // skip field
824
							}
825
							// truncate the value to size
826
							$value = substr($value, 0, $size - 1);
827
							if ($this->log)
828
							{
829
								error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ .
830
									"() $icalFieldName truncated to maximum size $size\n",3,$this->logfile);
831
							}
832
							//Horde::logMessage("vCalendar $icalFieldName truncated to maximum size $size",
833
							//	__FILE__, __LINE__, PEAR_LOG_INFO);
834
						}
835
						if (!empty($value) || ($size >= 0 && !$noTruncate))
836
						{
837
							$attributes[$icalFieldName] = $value;
838
						}
839
				}
840
			}
841
842
			// for CalDAV add all X-Properties previously parsed
843
			if ($this->productManufacturer == 'groupdav' || $this->productManufacturer == 'file')
844
			{
845
				foreach($event as $name => $value)
846
				{
847
					if (substr($name, 0, 2) == '##')
848
					{
849
						if (($attr = json_decode($value, true)) && is_array($attr))
850
						{
851
							$vevent->setAttribute(substr($name, 2), $attr['value'], $attr['params'], true, $attr['values']);
852
						}
853
						else
854
						{
855
							$vevent->setAttribute(substr($name, 2), $value);
856
						}
857
					}
858
				}
859
			}
860
861
			if ($this->productManufacturer == 'nokia')
862
			{
863
				if ($event['special'] == '1')
864
				{
865
					$attributes['X-EPOCAGENDAENTRYTYPE'] = 'ANNIVERSARY';
866
					$attributes['DTEND'] = $attributes['DTSTART'];
867
				}
868
				elseif ($event['special'] == '2' || !empty($event['whole_day']))
869
				{
870
					$attributes['X-EPOCAGENDAENTRYTYPE'] = 'EVENT';
871
				}
872
				else
873
				{
874
					$attributes['X-EPOCAGENDAENTRYTYPE'] = 'APPOINTMENT';
875
				}
876
			}
877
878
			if ($event['created'] || $event['modified'])
879
			{
880
				$attributes['CREATED'] = $event['created'] ? $event['created'] : $event['modified'];
881
			}
882
			if ($event['modified'])
883
			{
884
				$attributes['LAST-MODIFIED'] = $event['modified'];
885
			}
886
			$attributes['DTSTAMP'] = time();
887
			foreach ((array)$event['alarm'] as $alarmData)
888
			{
889
				// skip over alarms that don't have the minimum required info
890
				if (!isset($alarmData['offset']) && !isset($alarmData['time'])) continue;
891
892
				// skip alarms not being set for all users and alarms owned by other users
893
				if ($alarmData['all'] != true && $alarmData['owner'] != $this->user)
894
				{
895
					continue;
896
				}
897
898
				if ($alarmData['offset'])
899
				{
900
					$alarmData['time'] = $event['start'] - $alarmData['offset'];
901
				}
902
903
				$description = trim(preg_replace("/\r?\n?\\[[A-Z_]+:.*\\]/i", '', $event['description']));
904
905
				if ($version == '1.0')
906
				{
907
					if ($event['title']) $description = $event['title'];
908
					if ($description)
909
					{
910
						$values['DALARM']['snooze_time'] = '';
911
						$values['DALARM']['repeat count'] = '';
912
						$values['DALARM']['display text'] = $description;
913
						$values['AALARM']['snooze_time'] = '';
914
						$values['AALARM']['repeat count'] = '';
915
						$values['AALARM']['display text'] = $description;
916
					}
917
					$attributes['DALARM'] = self::getDateTime($alarmData['time'],$tzid,$parameters['DALARM']);
918
					$attributes['AALARM'] = self::getDateTime($alarmData['time'],$tzid,$parameters['AALARM']);
919
					// lets take only the first alarm
920
					break;
921
				}
922
				else
923
				{
924
					// VCalendar 2.0 / RFC 2445
925
926
					// RFC requires DESCRIPTION for DISPLAY
927
					if (!$event['title'] && !$description) $description = 'Alarm';
928
929
					/* Disabling for now
930
					// Lightning infinitly pops up alarms for recuring events, if the only use an offset
931
					if ($this->productName == 'lightning' && $event['recur_type'] != MCAL_RECUR_NONE)
932
					{
933
						// return only future alarms to lightning
934
						if (($nextOccurence = $this->read($event['id'], $this->now_su + $alarmData['offset'], false, 'server')))
935
						{
936
							$alarmData['time'] = $nextOccurence['start'] - $alarmData['offset'];
937
							$alarmData['offset'] = false;
938
						}
939
						else
940
						{
941
							continue;
942
						}
943
					}*/
944
945
					// for SyncML non-whole-day events always use absolute times
946
					// (probably because some devices have no clue about timezones)
947
					// GroupDAV uses offsets, as web UI assumes alarms are relative too
948
					// (with absolute times GroupDAV clients do NOT move alarms, if events move!)
949
					if ($this->productManufacturer != 'groupdav' &&
950
						!empty($event['whole_day']) && $alarmData['offset'])
951
					{
952
						$alarmData['offset'] = false;
953
					}
954
955
					$valarm = Horde_Icalendar::newComponent('VALARM',$vevent);
956
					if ($alarmData['offset'] !== false)
957
					{
958
						$valarm->setAttribute('TRIGGER', -$alarmData['offset'],
959
							array('VALUE' => 'DURATION', 'RELATED' => 'START'));
960
					}
961
					else
962
					{
963
						$params = array('VALUE' => 'DATE-TIME');
964
						$value = self::getDateTime($alarmData['time'],$tzid,$params);
965
						$valarm->setAttribute('TRIGGER', $value, $params);
966
					}
967
					if (!empty($alarmData['uid']))
968
					{
969
						$valarm->setAttribute('UID', $alarmData['uid']);
970
						$valarm->setAttribute('X-WR-ALARMUID', $alarmData['uid']);
971
					}
972
					// set evtl. existing attributes set by iCal clients not used by EGroupware
973
					if (isset($alarmData['attrs']))
974
					{
975
						foreach($alarmData['attrs'] as $attr => $data)
976
						{
977
							$valarm->setAttribute($attr, $data['value'], $data['params']);
978
						}
979
					}
980
					// set default ACTION and DESCRIPTION, if not set by a client
981
					if (!isset($alarmData['attrs']) || !isset($alarmData['attrs']['ACTION']))
982
					{
983
						$valarm->setAttribute('ACTION','DISPLAY');
984
					}
985
					if (!isset($alarmData['attrs']) || !isset($alarmData['attrs']['DESCRIPTION']))
986
					{
987
						$valarm->setAttribute('DESCRIPTION',$event['title'] ? $event['title'] : $description);
988
					}
989
					$vevent->addComponent($valarm);
990
				}
991
			}
992
993
			foreach ($attributes as $key => $value)
994
			{
995
				foreach (is_array($value) && $parameters[$key]['VALUE']!='DATE' ? $value : array($value) as $valueID => $valueData)
996
				{
997
					$valueData = translation::convert($valueData,translation::charset(),$charset);
998
                    $paramData = (array) translation::convert(is_array($value) ?
999
                    		$parameters[$key][$valueID] : $parameters[$key],
1000
                            translation::charset(),$charset);
1001
                    $valuesData = (array) translation::convert($values[$key],
1002
                    		translation::charset(),$charset);
1003
                    $content = $valueData . implode(';', $valuesData);
1004
1005
					if ($version == '1.0' && (preg_match('/[^\x20-\x7F]/', $content) ||
1006
						($paramData['CN'] && preg_match('/[^\x20-\x7F]/', $paramData['CN']))))
1007
					{
1008
						$paramData['CHARSET'] = $charset;
1009
						switch ($this->productManufacturer)
1010
						{
1011
							case 'groupdav':
1012
								if ($this->productName == 'kde')
1013
								{
1014
									$paramData['ENCODING'] = 'QUOTED-PRINTABLE';
1015
								}
1016
								else
1017
								{
1018
									$paramData['CHARSET'] = '';
1019
									if (preg_match('/([\000-\012\015\016\020-\037\075])/', $valueData))
1020
									{
1021
										$paramData['ENCODING'] = 'QUOTED-PRINTABLE';
1022
									}
1023
									else
1024
									{
1025
										$paramData['ENCODING'] = '';
1026
									}
1027
								}
1028
								break;
1029
							case 'funambol':
1030
								$paramData['ENCODING'] = 'FUNAMBOL-QP';
1031
						}
1032
					}
1033
					/*
1034
					if (preg_match('/([\000-\012])/', $valueData))
1035
					{
1036
						if ($this->log)
1037
						{
1038
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ .
1039
								"() Has invalid XML data: $valueData",3,$this->logfile);
1040
						}
1041
					}
1042
					*/
1043
					$vevent->setAttribute($key, $valueData, $paramData, true, $valuesData);
1044
				}
1045
			}
1046
			$vcal->addComponent($vevent);
1047
			$events_exported = true;
1048
		}
1049
1050
		$retval = $events_exported ? $vcal->exportvCalendar() : false;
1051 View Code Duplication
 		if ($this->log)
1052
 		{
1053
 			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ .
1054
				"() '$this->productManufacturer','$this->productName'\n",3,$this->logfile);
1055
 			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ .
1056
				"()\n".array2string($retval)."\n",3,$this->logfile);
1057
 		}
1058
		return $retval;
1059
	}
1060
1061
	/**
1062
	 * Get DateTime value for a given time and timezone
1063
	 *
1064
	 * @param int|string|DateTime $time in server-time as returned by calendar_bo for $data_format='server'
1065
	 * @param string $tzid TZID of event or 'UTC' or NULL for palmos timestamps in usertime
1066
	 * @param array &$params=null parameter array to set TZID
1067
	 * @return mixed attribute value to set: integer timestamp if $tzid == 'UTC' otherwise Ymd\THis string IN $tzid
1068
	 */
1069
	static function getDateTime($time,$tzid,array &$params=null)
1070
	{
1071
		if (empty($tzid) || $tzid == 'UTC')
1072
		{
1073
			return egw_time::to($time,'ts');
1074
		}
1075
		if (!is_a($time,'DateTime'))
1076
		{
1077
			$time = new egw_time($time,egw_time::$server_timezone);
1078
		}
1079
		if (!isset(self::$tz_cache[$tzid]))
1080
		{
1081
			self::$tz_cache[$tzid] = calendar_timezones::DateTimeZone($tzid);
1082
		}
1083
		$time->setTimezone(self::$tz_cache[$tzid]);
0 ignored issues
show
Bug introduced by
It seems like $time is not always an object, but can also be of type integer|string. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
1084
		$params['TZID'] = $tzid;
1085
1086
		return $time->format('Ymd\THis');
1087
	}
1088
1089
	/**
1090
	 * Number of events imported in last call to importVCal
1091
	 *
1092
	 * @var int
1093
	 */
1094
	var $events_imported;
1095
1096
	/**
1097
	 * Import an iCal
1098
	 *
1099
	 * @param string|resource $_vcalData
1100
	 * @param int $cal_id=-1 must be -1 for new entries!
1101
	 * @param string $etag=null if an etag is given, it has to match the current etag or the import will fail
1102
	 * @param boolean $merge=false	merge data with existing entry
1103
	 * @param int $recur_date=0 if set, import the recurrence at this timestamp,
1104
	 *                          default 0 => import whole series (or events, if not recurring)
1105
	 * @param string $principalURL='' Used for CalDAV imports
1106
	 * @param int $user=null account_id of owner, default null
1107
	 * @param string $charset  The encoding charset for $text. Defaults to
1108
	 *                         utf-8 for new format, iso-8859-1 for old format.
1109
	 * @param string $caldav_name=null name from CalDAV client or null (to use default)
1110
	 * @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"
1111
	 */
1112
	function importVCal($_vcalData, $cal_id=-1, $etag=null, $merge=false, $recur_date=0, $principalURL='', $user=null, $charset=null, $caldav_name=null,$skip_notification=false)
1113
	{
1114
		//error_log(__METHOD__."(, $cal_id, $etag, $merge, $recur_date, $principalURL, $user, $charset, $caldav_name)");
1115
		$this->events_imported = 0;
1116
		$replace = $delete_exceptions= false;
1117
1118
		if (!is_array($this->supportedFields)) $this->setSupportedFields();
1119
1120
		if (!($events = $this->icaltoegw($_vcalData, $principalURL, $charset)))
1121
		{
1122
			return false;
1123
		}
1124
		if (!is_array($events)) $cal_id = -1;	// just to be sure, as iterator does NOT allow array access (eg. $events[0])
1125
1126
		if ($cal_id > 0)
1127
		{
1128
			if (count($events) == 1)
1129
			{
1130
				$replace = $recur_date == 0;
1131
				$events[0]['id'] = $cal_id;
1132
				if (!is_null($etag)) $events[0]['etag'] = (int) $etag;
1133
				if ($recur_date) $events[0]['recurrence'] = $recur_date;
1134
			}
1135
			elseif (($foundEvent = $this->find_event(array('id' => $cal_id), 'exact')) &&
1136
					($eventId = array_shift($foundEvent)) &&
1137
					($egwEvent = $this->read($eventId)))
1138
			{
1139
				foreach ($events as $k => $event)
1140
				{
1141
					if (!isset($event['uid'])) $events[$k]['uid'] = $egwEvent['uid'];
1142
				}
1143
			}
1144
		}
1145
1146
		// check if we are importing an event series with exceptions in CalDAV
1147
		// only first event / series master get's cal_id from URL
1148
		// other events are exceptions and need to be checked if they are new
1149
		// and for real (not status only) exceptions their recurrence-id need
1150
		// to be included as recur_exception to the master
1151
		if ($this->productManufacturer == 'groupdav' && $cal_id > 0 &&
1152
			$events[0]['recur_type'] != MCAL_RECUR_NONE)
1153
		{
1154
			calendar_groupdav::fix_series($events);
1155
		}
1156
1157
		if ($this->tzid)
1158
		{
1159
			$tzid = $this->tzid;
1160
		}
1161
		else
1162
		{
1163
			$tzid = egw_time::$user_timezone->getName();
1164
		}
1165
1166
		date_default_timezone_set($tzid);
1167
1168
		$msg = null;
1169
		foreach ($events as $event)
1170
		{
1171
			if (!is_array($event)) continue; // the iterator may return false
1172
			++$this->events_imported;
1173
1174
			if ($this->so->isWholeDay($event)) $event['whole_day'] = true;
1175
			if (is_array($event['category']))
1176
			{
1177
				$event['category'] = $this->find_or_add_categories($event['category'],
1178
					isset($event['id']) ? $event['id'] : -1);
1179
			}
1180
			if ($this->log)
1181
			{
1182
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
1183
					."($cal_id, $etag, $recur_date, $principalURL, $user, $charset)\n"
1184
					. array2string($event)."\n",3,$this->logfile);
1185
			}
1186
1187
			$updated_id = false;
1188
1189
			if ($replace)
1190
			{
1191
				$event_info['type'] = $event['recur_type'] == MCAL_RECUR_NONE ?
1192
					'SINGLE' : 'SERIES-MASTER';
1193
				$event_info['acl_edit'] = $this->check_perms(EGW_ACL_EDIT, $cal_id);
1194
				if (($event_info['stored_event'] = $this->read($cal_id, 0, false, 'server')) &&
1195
					$event_info['stored_event']['recur_type'] != MCAL_RECUR_NONE &&
1196
					($event_info['stored_event']['recur_type'] != $event['recur_type']
1197
					|| $event_info['stored_event']['recur_interval'] != $event['recur_interval']
1198
					|| $event_info['stored_event']['recur_data'] != $event['recur_data']
1199
					|| $event_info['stored_event']['start'] != $event['start']))
1200
				{
1201
					// handle the old exceptions
1202
					$recur_exceptions = $this->so->get_related($event_info['stored_event']['uid']);
1203
					foreach ($recur_exceptions as $id)
1204
					{
1205
						if ($delete_exceptions)
1206
						{
1207
							$this->delete($id,0,false,$skip_notification);
1208
						}
1209
						else
1210
						{
1211
							if (!($exception = $this->read($id))) continue;
1212
							$exception['uid'] = common::generate_uid('calendar', $id);
1213
							$exception['reference'] = $exception['recurrence'] = 0;
1214
							$this->update($exception, true,true,false,true,$msg,$skip_notification);
1215
						}
1216
					}
1217
				}
1218
			}
1219
			else
1220
			{
1221
				$event_info = $this->get_event_info($event);
1222
			}
1223
1224
			// common adjustments for existing events
1225
			if (is_array($event_info['stored_event']))
1226
			{
1227
				if ($this->log)
1228
				{
1229
					error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
1230
						. "(UPDATE Event)\n"
1231
						. array2string($event_info['stored_event'])."\n",3,$this->logfile);
1232
				}
1233
				if (empty($event['uid']))
1234
				{
1235
					$event['uid'] = $event_info['stored_event']['uid']; // restore the UID if it was not delivered
1236
				}
1237
				elseif (empty($event['id']))
1238
				{
1239
					$event['id'] = $event_info['stored_event']['id']; // CalDAV does only provide UIDs
1240
				}
1241
				if (is_array($event['participants']))
1242
				{
1243
					// if the client does not return a status, we restore the original one
1244
					foreach ($event['participants'] as $uid => $status)
1245
					{
1246
						if ($status[0] == 'X')
1247
						{
1248
							if (isset($event_info['stored_event']['participants'][$uid]))
1249
							{
1250
								if ($this->log)
1251
								{
1252
									error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
1253
										"() Restore status for $uid\n",3,$this->logfile);
1254
								}
1255
								$event['participants'][$uid] = $event_info['stored_event']['participants'][$uid];
1256
							}
1257
							else
1258
							{
1259
								$event['participants'][$uid] = calendar_so::combine_status('U');
1260
							}
1261
						}
1262
					}
1263
				}
1264
				// unset old X-* attributes stored in custom-fields
1265
				foreach ($event_info['stored_event'] as $key => $value)
1266
				{
1267
					if ($key[0] == '#' && $key[1] == '#' && !isset($event[$key]))
1268
					{
1269
						$event[$key] = '';
1270
					}
1271
				}
1272
				if ($merge)
1273
				{
1274
					if ($this->log)
1275
					{
1276
						error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
1277
							"()[MERGE]\n",3,$this->logfile);
1278
					}
1279
					// overwrite with server data for merge
1280
					foreach ($event_info['stored_event'] as $key => $value)
1281
					{
1282
						switch ($key)
1283
						{
1284
							case 'participants_types':
1285
								continue;
1286
1287
							case 'participants':
1288
								foreach ($event_info['stored_event']['participants'] as $uid => $status)
1289
								{
1290
									// Is a participant and no longer present in the event?
1291
									if (!isset($event['participants'][$uid]))
1292
									{
1293
										// Add it back in
1294
										$event['participants'][$uid] = $status;
1295
									}
1296
								}
1297
								break;
1298
1299
							default:
1300
								if (!empty($value)) $event[$key] = $value;
1301
						}
1302
					}
1303
				}
1304
				else
1305
				{
1306
					// no merge
1307
					if(!isset($this->supportedFields['category']) || !isset($event['category']))
1308
					{
1309
						$event['category'] = $event_info['stored_event']['category'];
1310
					}
1311
					if (!isset($this->supportedFields['participants'])
1312
						|| !$event['participants']
1313
						|| !is_array($event['participants'])
1314
						|| !count($event['participants']))
1315
					{
1316
						if ($this->log)
1317
						{
1318
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
1319
							"() No participants\n",3,$this->logfile);
1320
						}
1321
1322
						// If this is an updated meeting, and the client doesn't support
1323
						// participants OR the event no longer contains participants, add them back
1324
						unset($event['participants']);
1325
					}
1326
					// since we export now all participants in CalDAV as urn:uuid, if they have no email,
1327
					// we dont need and dont want that special treatment anymore, as it keeps client from changing resources
1328
					elseif ($this->productManufacturer != 'groupdav')
1329
					{
1330 View Code Duplication
						foreach ($event_info['stored_event']['participants'] as $uid => $status)
1331
						{
1332
							// Is it a resource and no longer present in the event?
1333
							if ($uid[0] == 'r' && !isset($event['participants'][$uid]))
1334
							{
1335
								if ($this->log)
1336
								{
1337
									error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
1338
										"() Restore resource $uid to status $status\n",3,$this->logfile);
1339
								}
1340
								// Add it back in
1341
								$event['participants'][$uid] = $status;
1342
							}
1343
						}
1344
					}
1345
1346
					/* Modifying an existing event with timezone different from default timezone of user
1347
					 * to a whole-day event (no timezone allowed according to iCal rfc)
1348
					 * --> code to modify start- and end-time here creates a one day longer event!
1349
					 * Skipping that code, creates the event correct in default timezone of user
1350
 					if (!empty($event['whole_day']) && $event['tzid'] != $event_info['stored_event']['tzid'])
1351
					{
1352
						// Adjust dates to original TZ
1353
						$time = new egw_time($event['start'],egw_time::$server_timezone);
1354
						$time =& $this->so->startOfDay($time, $event_info['stored_event']['tzid']);
1355
						$event['start'] = egw_time::to($time,'server');
1356
						//$time = new egw_time($event['end'],egw_time::$server_timezone);
1357
						//$time =& $this->so->startOfDay($time, $event_info['stored_event']['tzid']);
1358
						//$time->setTime(23, 59, 59);
1359
						$time->modify('+'.round(($event['end']-$event['start'])/DAY_s).' day');
1360
						$event['end'] = egw_time::to($time,'server');
1361
						if ($event['recur_type'] != MCAL_RECUR_NONE)
1362
						{
1363
							foreach ($event['recur_exception'] as $key => $day)
1364
							{
1365
								$time = new egw_time($day,egw_time::$server_timezone);
1366
								$time =& $this->so->startOfDay($time, $event_info['stored_event']['tzid']);
1367
								$event['recur_exception'][$key] = egw_time::to($time,'server');
1368
							}
1369
						}
1370
						elseif ($event['recurrence'])
1371
						{
1372
							$time = new egw_time($event['recurrence'],egw_time::$server_timezone);
1373
							$time =& $this->so->startOfDay($time, $event_info['stored_event']['tzid']);
1374
							$event['recurrence'] = egw_time::to($time,'server');
1375
						}
1376
						error_log(__METHOD__."() TZ adjusted {$event_info['stored_event']['tzid']} --> {$event['tzid']} event=".array2string($event));
1377
					}*/
1378
1379
					calendar_rrule::rrule2tz($event, $event_info['stored_event']['start'],
1380
						$event_info['stored_event']['tzid']);
1381
1382
					$event['tzid'] = $event_info['stored_event']['tzid'];
1383
					// avoid that iCal changes the organizer, which is not allowed
1384
					$event['owner'] = $event_info['stored_event']['owner'];
1385
				}
1386
				$event['caldav_name'] = $event_info['stored_event']['caldav_name'];
1387
1388
				// as we no longer export event owner/ORGANIZER as only participant, we have to re-add owner as participant
1389
				// to not loose him, as EGroupware knows events without owner/ORGANIZER as participant
1390
				if (isset($event_info['stored_event']['participants'][$event['owner']]) && !isset($event['participants'][$event['owner']]))
1391
				{
1392
					$event['participants'][$event['owner']] = $event_info['stored_event']['participants'][$event['owner']];
1393
				}
1394
			}
1395
			else // common adjustments for new events
1396
			{
1397
				unset($event['id']);
1398
				if ($caldav_name) $event['caldav_name'] = $caldav_name;
1399
				// set non blocking all day depending on the user setting
1400
				if (!empty($event['whole_day']) && $this->nonBlockingAllday)
1401
				{
1402
					$event['non_blocking'] = 1;
1403
				}
1404
1405
				if (!is_null($user))
1406
				{
1407
					if ($user > 0 && $this->check_perms(EGW_ACL_ADD, 0, $user))
1408
					{
1409
						$event['owner'] = $user;
1410
					}
1411
					elseif ($user > 0)
1412
					{
1413
						date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']);
1414
						return 0; // no permission
1415
					}
1416
					else
1417
					{
1418
						// add group or resource invitation
1419
						$event['owner'] = $this->user;
1420
						if (!isset($event['participants'][$this->user]))
1421
						{
1422
							$event['participants'][$this->user] = calendar_so::combine_status('A', 1, 'CHAIR');
1423
						}
1424
						// for resources check which new-status to give (eg. with direct booking permision 'A' instead 'U')
1425
						$event['participants'][$user] = calendar_so::combine_status(
1426
							$user < 0 || !isset($this->resources[$user[0]]['new_status']) ? 'U' :
1427
							ExecMethod($this->resources[$user[0]]['new_status'], substr($user, 1)));
1428
					}
1429
				}
1430
				// check if an owner is set and the current user has add rights
1431
				// for that owners calendar; if not set the current user
1432 View Code Duplication
				elseif (!isset($event['owner'])
1433
					|| !$this->check_perms(EGW_ACL_ADD, 0, $event['owner']))
1434
				{
1435
					$event['owner'] = $this->user;
1436
				}
1437
1438
				if (!$event['participants']
1439
					|| !is_array($event['participants'])
1440
					|| !count($event['participants'])
1441
					// for new events, allways add owner as participant. Users expect to participate too, if they invite further participants.
1442
					// They can now only remove themselfs, if that is desired, after storing the event first.
1443
					|| !isset($event['participants'][$event['owner']]))
1444
				{
1445
					$status = calendar_so::combine_status($event['owner'] == $this->user ? 'A' : 'U', 1, 'CHAIR');
1446
					if (!is_array($event['participants'])) $event['participants'] = array();
1447
					$event['participants'][$event['owner']] = $status;
1448
				}
1449
				else
1450
				{
1451
					foreach ($event['participants'] as $uid => $status)
1452
					{
1453
						// if the client did not give us a proper status => set default
1454
						if ($status[0] == 'X')
1455
						{
1456
							if ($uid == $event['owner'])
1457
							{
1458
								$event['participants'][$uid] = calendar_so::combine_status('A', 1, 'CHAIR');
1459
							}
1460
							else
1461
							{
1462
								$event['participants'][$uid] = calendar_so::combine_status('U');
1463
							}
1464
						}
1465
					}
1466
				}
1467
			}
1468
1469
			// update alarms depending on the given event type
1470
			if (count($event['alarm']) > 0 || isset($this->supportedFields['alarm']))
1471
			{
1472
				switch ($event_info['type'])
1473
				{
1474
					case 'SINGLE':
1475
					case 'SERIES-MASTER':
1476
					case 'SERIES-EXCEPTION':
1477
					case 'SERIES-EXCEPTION-PROPAGATE':
1478
						if (isset($event['alarm']))
1479
						{
1480
							$this->sync_alarms($event, (array)$event_info['stored_event']['alarm'], $this->user);
1481
						}
1482
						break;
1483
1484
					case 'SERIES-PSEUDO-EXCEPTION':
1485
						// nothing to do here
1486
						break;
1487
				}
1488
			}
1489
1490 View Code Duplication
			if ($this->log)
1491
			{
1492
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . '('
1493
					. $event_info['type'] . ")\n"
1494
					. array2string($event)."\n",3,$this->logfile);
1495
			}
1496
1497
			// Android (any maybe others) delete recurrences by setting STATUS: CANCELLED
1498
			// as we ignore STATUS we have to delete the recurrence by calling delete
1499
			if (in_array($event_info['type'], array('SERIES-EXCEPTION', 'SERIES-EXCEPTION-PROPAGATE', 'SERIES-PSEUDO-EXCEPTION')) &&
1500
				$event['status'] == 'CANCELLED')
1501
			{
1502
				if (!$this->delete($event['id'] ? $event['id'] : $cal_id, $event['recurrence'],false,$skip_notification))
1503
				{
1504
					// delete fails (because no rights), reject recurrence
1505
					$this->set_status($event['id'] ? $event['id'] : $cal_id, $this->user, 'R', $event['recurrence'],false,true,$skip_notification);
1506
				}
1507
				continue;
1508
			}
1509
1510
			// save event depending on the given event type
1511
			switch ($event_info['type'])
1512
			{
1513
				case 'SINGLE':
1514
					if ($this->log)
1515
					{
1516
						error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
1517
							"(): event SINGLE\n",3,$this->logfile);
1518
					}
1519
1520
					// update the event
1521
					if ($event_info['acl_edit'])
1522
					{
1523
						// Force SINGLE
1524
						$event['reference'] = 0;
1525
						$event_to_store = $event; // prevent $event from being changed by the update method
1526
						$this->server2usertime($event_to_store);
1527
						$updated_id = $this->update($event_to_store, true,true,false,true,$msg,$skip_notification);
1528
						unset($event_to_store);
1529
					}
1530
					break;
1531
1532
				case 'SERIES-MASTER':
1533
					if ($this->log)
1534
					{
1535
						error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
1536
							"(): event SERIES-MASTER\n",3,$this->logfile);
1537
					}
1538
1539
					// remove all known pseudo exceptions and update the event
1540
					if ($event_info['acl_edit'])
1541
					{
1542
						$filter = isset($this->supportedFields['participants']) ? 'map' : 'tz_map';
1543
						$days = $this->so->get_recurrence_exceptions($event_info['stored_event'], $this->tzid, 0, 0, $filter);
0 ignored issues
show
Bug introduced by
It seems like $event_info['stored_event'] can also be of type boolean; however, calendar_so::get_recurrence_exceptions() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1544
						if ($this->log)
1545
						{
1546
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."(EXCEPTIONS MAPPING):\n" .
1547
								array2string($days)."\n",3,$this->logfile);
1548
						}
1549 View Code Duplication
						if (is_array($days))
1550
						{
1551
							$recur_exceptions = array();
1552
1553
							foreach ($event['recur_exception'] as $recur_exception)
1554
							{
1555
								if (isset($days[$recur_exception]))
1556
								{
1557
									$recur_exceptions[] = $days[$recur_exception];
1558
								}
1559
							}
1560
							$event['recur_exception'] = $recur_exceptions;
1561
						}
1562
1563
						$event_to_store = $event; // prevent $event from being changed by the update method
1564
						$this->server2usertime($event_to_store);
1565
						$updated_id = $this->update($event_to_store, true,true,false,true,$msg,$skip_notification);
1566
						unset($event_to_store);
1567
					}
1568
					break;
1569
1570
				case 'SERIES-EXCEPTION':
1571
				case 'SERIES-EXCEPTION-PROPAGATE':
1572
					if ($this->log)
1573
					{
1574
						error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
1575
							"(): event SERIES-EXCEPTION\n",3,$this->logfile);
1576
					}
1577
1578
					// update event
1579
					if ($event_info['acl_edit'])
1580
					{
1581
						if (isset($event_info['stored_event']['id']))
1582
						{
1583
							// We update an existing exception
1584
							$event['id'] = $event_info['stored_event']['id'];
1585
							$event['category'] = $event_info['stored_event']['category'];
1586
						}
1587
						else
1588
						{
1589
							// We create a new exception
1590
							unset($event['id']);
1591
							unset($event_info['stored_event']);
1592
							$event['recur_type'] = MCAL_RECUR_NONE;
1593
							if (empty($event['recurrence']))
1594
							{
1595
								// find an existing exception slot
1596
								$occurence = $exception = false;
1597
								foreach ($event_info['master_event']['recur_exception'] as $exception)
1598
								{
1599
									if ($exception > $event['start']) break;
1600
									$occurence = $exception;
1601
								}
1602
								if (!$occurence)
1603
								{
1604
									if (!$exception)
1605
									{
1606
										// use start as dummy recurrence
1607
										$event['recurrence'] = $event['start'];
1608
									}
1609
									else
1610
									{
1611
										$event['recurrence'] = $exception;
1612
									}
1613
								}
1614
								else
1615
								{
1616
									$event['recurrence'] = $occurence;
1617
								}
1618
							}
1619
							else
1620
							{
1621
								$event_info['master_event']['recur_exception'] =
1622
									array_unique(array_merge($event_info['master_event']['recur_exception'],
1623
										array($event['recurrence'])));
1624
							}
1625
1626
							$event['reference'] = $event_info['master_event']['id'];
1627
							$event['category'] = $event_info['master_event']['category'];
1628
							$event['owner'] = $event_info['master_event']['owner'];
1629
							$event_to_store = $event_info['master_event']; // prevent the master_event from being changed by the update method
1630
							$this->server2usertime($event_to_store);
1631
							$this->update($event_to_store, true,true,false,true,$msg,$skip_notification);
1632
							unset($event_to_store);
1633
						}
1634
1635
						$event_to_store = $event; // prevent $event from being changed by update method
1636
						$this->server2usertime($event_to_store);
1637
						$updated_id = $this->update($event_to_store, true,true,false,true,$msg,$skip_notification);
1638
						unset($event_to_store);
1639
					}
1640
					break;
1641
1642
				case 'SERIES-PSEUDO-EXCEPTION':
1643
					if ($this->log)
1644
					{
1645
						error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
1646
							"(): event SERIES-PSEUDO-EXCEPTION\n",3,$this->logfile);
1647
					}
1648
					//Horde::logMessage('importVCAL event SERIES-PSEUDO-EXCEPTION',
1649
					//	__FILE__, __LINE__, PEAR_LOG_DEBUG);
1650
1651
					if ($event_info['acl_edit'])
1652
					{
1653
						// truncate the status only exception from the series master
1654
						$recur_exceptions = array();
1655 View Code Duplication
						foreach ($event_info['master_event']['recur_exception'] as $recur_exception)
1656
						{
1657
							if ($recur_exception != $event['recurrence'])
1658
							{
1659
								$recur_exceptions[] = $recur_exception;
1660
							}
1661
						}
1662
						$event_info['master_event']['recur_exception'] = $recur_exceptions;
1663
1664
						// save the series master with the adjusted exceptions
1665
						$event_to_store = $event_info['master_event']; // prevent the master_event from being changed by the update method
1666
						$this->server2usertime($event_to_store);
1667
						$updated_id = $this->update($event_to_store, true, true, false, false,$msg,$skip_notification);
1668
						unset($event_to_store);
1669
					}
1670
1671
					break;
1672
			}
1673
1674
			// read stored event into info array for fresh stored (new) events
1675 View Code Duplication
			if (!is_array($event_info['stored_event']) && $updated_id > 0)
1676
			{
1677
				$event_info['stored_event'] = $this->read($updated_id, 0, false, 'server');
1678
			}
1679
1680
			if (isset($event['participants']))
1681
			{
1682
				// update status depending on the given event type
1683
				switch ($event_info['type'])
1684
				{
1685
					case 'SINGLE':
1686
					case 'SERIES-MASTER':
1687
					case 'SERIES-EXCEPTION':
1688
					case 'SERIES-EXCEPTION-PROPAGATE':
1689 View Code Duplication
						if (is_array($event_info['stored_event'])) // status update requires a stored event
1690
						{
1691
							if ($event_info['acl_edit'])
1692
							{
1693
								// update all participants if we have the right to do that
1694
								$this->update_status($event, $event_info['stored_event'],0,$skip_notification);
1695
							}
1696
							elseif (isset($event['participants'][$this->user]) || isset($event_info['stored_event']['participants'][$this->user]))
1697
							{
1698
								// update the users status only
1699
								$this->set_status($event_info['stored_event']['id'], $this->user,
1700
									($event['participants'][$this->user] ? $event['participants'][$this->user] : 'R'), 0, true,true,$skip_notification);
1701
							}
1702
						}
1703
						break;
1704
1705
					case 'SERIES-PSEUDO-EXCEPTION':
1706
						if (is_array($event_info['master_event'])) // status update requires a stored master event
1707
						{
1708
							$recurrence = $this->date2usertime($event['recurrence']);
1709 View Code Duplication
							if ($event_info['acl_edit'])
1710
							{
1711
								// update all participants if we have the right to do that
1712
								$this->update_status($event, $event_info['stored_event'], $recurrence,$skip_notification);
0 ignored issues
show
Bug introduced by
It seems like $event_info['stored_event'] can also be of type boolean; however, calendar_boupdate::update_status() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1713
							}
1714
							elseif (isset($event['participants'][$this->user]) || isset($event_info['master_event']['participants'][$this->user]))
1715
							{
1716
								// update the users status only
1717
								$this->set_status($event_info['master_event']['id'], $this->user,
1718
									($event['participants'][$this->user] ? $event['participants'][$this->user] : 'R'), $recurrence, true,true,$skip_notification);
1719
							}
1720
						}
1721
						break;
1722
				}
1723
			}
1724
1725
			// choose which id to return to the client
1726
			switch ($event_info['type'])
1727
			{
1728
				case 'SINGLE':
1729
				case 'SERIES-MASTER':
1730
				case 'SERIES-EXCEPTION':
1731
					$return_id = is_array($event_info['stored_event']) ? $event_info['stored_event']['id'] : false;
1732
					break;
1733
1734 View Code Duplication
				case 'SERIES-PSEUDO-EXCEPTION':
1735
					$return_id = is_array($event_info['master_event']) ? $event_info['master_event']['id'] . ':' . $event['recurrence'] : false;
1736
					break;
1737
1738 View Code Duplication
				case 'SERIES-EXCEPTION-PROPAGATE':
1739
					if ($event_info['acl_edit'] && is_array($event_info['stored_event']))
1740
					{
1741
						// we had sufficient rights to propagate the status only exception to a real one
1742
						$return_id = $event_info['stored_event']['id'];
1743
					}
1744
					else
1745
					{
1746
						// we did not have sufficient rights to propagate the status only exception to a real one
1747
						// we have to keep the SERIES-PSEUDO-EXCEPTION id and keep the event untouched
1748
						$return_id = $event_info['master_event']['id'] . ':' . $event['recurrence'];
1749
					}
1750
					break;
1751
			}
1752
1753
			// handle ATTACH attribute for managed attachments
1754
			if ($updated_id && groupdav::handle_attach('calendar', $updated_id, $event['attach'], $event['attach-delete-by-put']) === false)
1755
			{
1756
				$return_id = null;
1757
			}
1758
1759 View Code Duplication
			if ($this->log)
1760
			{
1761
				$event_info['stored_event'] = $this->read($event_info['stored_event']['id']);
1762
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()[$updated_id]\n" .
1763
					array2string($event_info['stored_event'])."\n",3,$this->logfile);
1764
			}
1765
		}
1766
		date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']);
1767
1768
		return $updated_id === 0 ? 0 : $return_id;
1769
	}
1770
1771
	/**
1772
	 * Override parent update function to handle conflict checking callback, if set
1773
	 * 
1774
	 * @param array &$event event-array, on return some values might be changed due to set defaults
1775
	 * @param boolean $ignore_conflicts =false just ignore conflicts or do a conflict check and return the conflicting events.
1776
	 *	Set to false if $this->conflict_callback is set
1777
	 * @param boolean $touch_modified =true NOT USED ANYMORE (was only used in old csv-import), modified&modifier is always updated!
1778
	 * @param boolean $ignore_acl =false should we ignore the acl
1779
	 * @param boolean $updateTS =true update the content history of the event
1780
	 * @param array &$messages=null messages about because of missing ACL removed participants or categories
1781
	 * @param boolean $skip_notification =false true: send NO notifications, default false = send them
1782
	 * @return mixed on success: int $cal_id > 0, on error or conflicts false.
1783
	 *	Conflicts are passed to $this->conflict_callback
1784
	 */
1785
	public function update(&$event,$ignore_conflicts=false,$touch_modified=true,$ignore_acl=false,$updateTS=true,&$messages=null, $skip_notification=false)
1786
	{
1787
		if($this->conflict_callback !== null)
1788
		{
1789
			// calendar_ical overrides search(), which breaks conflict checking
1790
			// so we make sure to use the original from parent
1791
			static $bo;
1792
			if(!$bo)
1793
			{
1794
				$bo = new calendar_boupdate();
1795
			}
1796
			$conflicts = $bo->conflicts($event);
0 ignored issues
show
Bug introduced by
The method conflicts() does not seem to exist on object<calendar_boupdate>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1797
			if(is_array($conflicts) && count($conflicts) > 0)
1798
			{
1799
				call_user_func_array($this->conflict_callback, array(&$event, &$conflicts));
1800
				return false;
1801
			}
1802
		}
1803
		return parent::update($event, $ignore_conflicts, $touch_modified, $ignore_acl, $updateTS, $messages, $skip_notification);
1804
	}
1805
	
1806
	/**
1807
	 * Sync alarms of current user: add alarms added on client and remove the ones removed
1808
	 *
1809
	 * @param array& $event
1810
	 * @param array $old_alarms
1811
	 * @param int $user account_id of user to create alarm for
1812
	 * @return int number of modified alarms
1813
	 */
1814
	public function sync_alarms(array &$event, array $old_alarms, $user)
1815
	{
1816
		if ($this->debug) error_log(__METHOD__."(".array2string($event).', old_alarms='.array2string($old_alarms).", $user,)");
1817
		$modified = 0;
1818
		foreach($event['alarm'] as &$alarm)
1819
		{
1820
			// check if alarm is already stored or from other users
1821
			foreach($old_alarms as $id => $old_alarm)
1822
			{
1823
				// not current users alarm --> ignore
1824 View Code Duplication
				if (!$old_alarm['all'] && $old_alarm['owner'] != $user)
1825
				{
1826
					unset($old_alarm[$id]);
1827
					continue;
1828
				}
1829
				// alarm found --> stop
1830
				if (empty($alarm['uid']) && $alarm['offset'] == $old_alarm['offset'] || $alarm['uid'] && $alarm['uid'] == $old_alarm['uid'])
1831
				{
1832
					unset($old_alarms[$id]);
1833
					break;
1834
				}
1835
			}
1836
			// alarm not found --> add it
1837
			if ($alarm['offset'] != $old_alarm['offset'] || $old_alarm['owner'] != $user && !$alarm['all'])
1838
			{
1839
				$alarm['owner'] = $user;
1840 View Code Duplication
				if (!isset($alarm['time'])) $alarm['time'] = $event['start'] - $alarm['offset'];
1841
				if ($alarm['time'] < time()) calendar_so::shift_alarm($event, $alarm);
1842
				if ($this->debug) error_log(__METHOD__."() adding new alarm from client ".array2string($alarm));
1843
				if ($event['id']) $alarm['id'] = $this->save_alarm($event['id'], $alarm);
1844
				++$modified;
1845
			}
1846
			// existing alarm --> update it
1847
			elseif ($alarm['offset'] == $old_alarm['offset'] && ($old_alarm['owner'] == $user || $old_alarm['all']))
0 ignored issues
show
Bug introduced by
The variable $old_alarm seems to be defined by a foreach iteration on line 1821. Are you sure the iterator is never empty, otherwise this variable is not defined?

It seems like you are relying on a variable being defined by an iteration:

foreach ($a as $b) {
}

// $b is defined here only if $a has elements, for example if $a is array()
// then $b would not be defined here. To avoid that, we recommend to set a
// default value for $b.


// Better
$b = 0; // or whatever default makes sense in your context
foreach ($a as $b) {
}

// $b is now guaranteed to be defined here.
Loading history...
1848
			{
1849 View Code Duplication
				if (!isset($alarm['time'])) $alarm['time'] = $event['start'] - $alarm['offset'];
1850
				if ($alarm['time'] < time()) calendar_so::shift_alarm($event, $alarm);
1851
				$alarm = array_merge($old_alarm, $alarm);
1852
				if ($this->debug) error_log(__METHOD__."() updating existing alarm from client ".array2string($alarm));
1853
				$alarm['id'] = $this->save_alarm($event['id'], $alarm);
1854
				++$modified;
1855
			}
1856
		}
1857
		// remove all old alarms left from current user
1858
		foreach($old_alarms as $id => $old_alarm)
1859
		{
1860
			// not current users alarm --> ignore
1861 View Code Duplication
			if (!$old_alarm['all'] && $old_alarm['owner'] != $user)
1862
			{
1863
				unset($old_alarm[$id]);
1864
				continue;
1865
			}
1866
			if ($this->debug) error_log(__METHOD__."() deleting alarm '$id' deleted on client ".array2string($old_alarm));
1867
			$this->delete_alarm($id);
1868
			++$modified;
1869
		}
1870
		return $modified;
1871
	}
1872
1873
	/**
1874
	 * get the value of an attribute by its name
1875
	 *
1876
	 * @param array $components
1877
	 * @param string $name eg. 'DTSTART'
1878
	 * @param string $what ='value'
1879
	 * @return mixed
1880
	 */
1881
	static function _get_attribute($components,$name,$what='value')
1882
	{
1883
		foreach ($components as $attribute)
1884
		{
1885
			if ($attribute['name'] == $name)
1886
			{
1887
				return !$what ? $attribute : $attribute[$what];
1888
			}
1889
		}
1890
		return false;
1891
	}
1892
1893
	/**
1894
	 * Parsing a valarm component preserving all attributes unknown to EGw
1895
	 *
1896
	 * @param array &$alarms on return alarms parsed
1897
	 * @param Horde_Icalendar_Valarm $valarm valarm component
1898
	 * @param int $duration in seconds to be able to convert RELATED=END
1899
	 * @return int number of parsed alarms
1900
	 */
1901
	static function valarm2egw(&$alarms, Horde_Icalendar_Valarm $valarm, $duration)
1902
	{
1903
		$alarm = array();
1904
		foreach ($valarm->getAllAttributes() as $vattr)
1905
		{
1906
			switch ($vattr['name'])
1907
			{
1908
				case 'TRIGGER':
1909
					$vtype = (isset($vattr['params']['VALUE']))
1910
						? $vattr['params']['VALUE'] : 'DURATION'; //default type
1911
					switch ($vtype)
1912
					{
1913
						case 'DURATION':
1914
							if (isset($vattr['params']['RELATED']) && $vattr['params']['RELATED'] == 'END')
1915
							{
1916
								$alarm['offset'] = $duration -$vattr['value'];
1917
							}
1918
							elseif (isset($vattr['params']['RELATED']) && $vattr['params']['RELATED'] != 'START')
1919
							{
1920
								error_log("Unsupported VALARM offset anchor ".$vattr['params']['RELATED']);
1921
								return;
1922
							}
1923
							else
1924
							{
1925
								$alarm['offset'] = -$vattr['value'];
1926
							}
1927
							break;
1928
						case 'DATE-TIME':
1929
							$alarm['time'] = $vattr['value'];
1930
							break;
1931
						default:
1932
							error_log('VALARM/TRIGGER: unsupported value type:' . $vtype);
1933
					}
1934
					break;
1935
1936
				case 'UID':
1937
				case 'X-WR-ALARMUID':
1938
					$alarm['uid'] = $vattr['value'];
1939
					break;
1940
1941
				default:	// store all other attributes, so we dont loose them
1942
					$alarm['attrs'][$vattr['name']] = array(
1943
						'params' => $vattr['params'],
1944
						'value'  => $vattr['value'],
1945
					);
1946
			}
1947
		}
1948
		if (isset($alarm['offset']) || isset($alarm['time']))
1949
		{
1950
			//error_log(__METHOD__."(..., ".$valarm->exportvCalendar().", $duration) alarm=".array2string($alarm));
1951
			$alarms[] = $alarm;
1952
			return 1;
1953
		}
1954
		return 0;
1955
	}
1956
1957
	function setSupportedFields($_productManufacturer='', $_productName='')
1958
	{
1959
		$state =& $_SESSION['SyncML.state'];
1960
		if (isset($state))
1961
		{
1962
			$deviceInfo = $state->getClientDeviceInfo();
1963
		}
1964
1965
		// store product manufacturer and name for further usage
1966
		if ($_productManufacturer)
1967
		{
1968
				$this->productManufacturer = strtolower($_productManufacturer);
1969
				$this->productName = strtolower($_productName);
1970
		}
1971
1972
		if (isset($deviceInfo) && is_array($deviceInfo))
1973
		{
1974
			/*
1975
			if ($this->log)
1976
			{
1977
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
1978
					'() ' . array2string($deviceInfo) . "\n",3,$this->logfile);
1979
			}
1980
			*/
1981
			if (isset($deviceInfo['uidExtension']) &&
1982
				$deviceInfo['uidExtension'])
1983
			{
1984
				$this->uidExtension = true;
1985
			}
1986
			if (isset($deviceInfo['nonBlockingAllday']) &&
1987
				$deviceInfo['nonBlockingAllday'])
1988
			{
1989
				$this->nonBlockingAllday = true;
1990
			}
1991
			if (isset($deviceInfo['tzid']) &&
1992
				$deviceInfo['tzid'])
1993
			{
1994
				switch ($deviceInfo['tzid'])
1995
				{
1996
					case -1:
1997
						$this->tzid = false; // use event's TZ
1998
						break;
1999
					case -2:
2000
						$this->tzid = null; // use UTC for export
2001
						break;
2002
					default:
2003
						$this->tzid = $deviceInfo['tzid'];
2004
				}
2005
			}
2006
			elseif (strpos($this->productName, 'palmos') !== false)
2007
			{
2008
				// for palmos we have to use user-time and NO timezone
2009
				$this->tzid = false;
2010
			}
2011
2012 View Code Duplication
			if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['calendar_owner']))
2013
			{
2014
				$owner = $GLOBALS['egw_info']['user']['preferences']['syncml']['calendar_owner'];
2015
				switch ($owner)
2016
				{
2017
					case 'G':
2018
					case 'P':
2019
					case 0:
2020
					case -1:
2021
						$owner = $this->user;
2022
						break;
2023
					default:
2024
						if ((int)$owner && $this->check_perms(EGW_ACL_EDIT, 0, $owner))
2025
						{
2026
							$this->calendarOwner = $owner;
2027
						}
2028
				}
2029
			}
2030 View Code Duplication
			if (!isset($this->productManufacturer) ||
2031
				 $this->productManufacturer == '' ||
2032
				 $this->productManufacturer == 'file')
2033
			{
2034
				$this->productManufacturer = strtolower($deviceInfo['manufacturer']);
2035
			}
2036 View Code Duplication
			if (!isset($this->productName) || $this->productName == '')
2037
			{
2038
				$this->productName = strtolower($deviceInfo['model']);
2039
			}
2040
		}
2041
2042
		$defaultFields['minimal'] = array(
2043
			'public'			=> 'public',
2044
			'description'		=> 'description',
2045
			'end'				=> 'end',
2046
			'start'				=> 'start',
2047
			'location'			=> 'location',
2048
			'recur_type'		=> 'recur_type',
2049
			'recur_interval'	=> 'recur_interval',
2050
			'recur_data'		=> 'recur_data',
2051
			'recur_enddate'		=> 'recur_enddate',
2052
			'recur_exception'	=> 'recur_exception',
2053
			'title'				=> 'title',
2054
			'alarm'				=> 'alarm',
2055
			'whole_day'			=> 'whole_day',
2056
		);
2057
2058
		$defaultFields['basic'] = $defaultFields['minimal'] + array(
2059
			'priority'			=> 'priority',
2060
		);
2061
2062
		$defaultFields['nexthaus'] = $defaultFields['basic'] + array(
2063
			'participants'		=> 'participants',
2064
			'uid'				=> 'uid',
2065
		);
2066
2067
		$defaultFields['s60'] = $defaultFields['basic'] + array(
2068
			'category'			=> 'category',
2069
			'recurrence'			=> 'recurrence',
2070
			'uid'				=> 'uid',
2071
		);
2072
2073
		$defaultFields['synthesis'] = $defaultFields['basic'] + array(
2074
			'participants'		=> 'participants',
2075
			'owner'				=> 'owner',
2076
			'category'			=> 'category',
2077
			'non_blocking'		=> 'non_blocking',
2078
			'uid'				=> 'uid',
2079
			'recurrence'		=> 'recurrence',
2080
			'etag'				=> 'etag',
2081
		);
2082
2083
		$defaultFields['funambol'] = $defaultFields['basic'] + array(
2084
			'participants'		=> 'participants',
2085
			'owner'				=> 'owner',
2086
			'category'			=> 'category',
2087
			'non_blocking'		=> 'non_blocking',
2088
		);
2089
2090
		$defaultFields['evolution'] = $defaultFields['basic'] + array(
2091
			'participants'		=> 'participants',
2092
			'owner'				=> 'owner',
2093
			'category'			=> 'category',
2094
			'uid'				=> 'uid',
2095
		);
2096
2097
		$defaultFields['full'] = $defaultFields['basic'] + array(
2098
			'participants'		=> 'participants',
2099
			'owner'				=> 'owner',
2100
			'category'			=> 'category',
2101
			'non_blocking'		=> 'non_blocking',
2102
			'uid'				=> 'uid',
2103
			'recurrence'		=> 'recurrence',
2104
			'etag'				=> 'etag',
2105
			'status'			=> 'status',
2106
		);
2107
2108
2109
		switch ($this->productManufacturer)
2110
		{
2111
			case 'nexthaus corporation':
2112
			case 'nexthaus corp':
2113
				switch ($this->productName)
2114
				{
2115
					default:
0 ignored issues
show
Unused Code introduced by
default: $this->supp...'nexthaus']; break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
2116
						$this->supportedFields = $defaultFields['nexthaus'];
2117
						break;
2118
				}
2119
				break;
2120
2121
			// multisync does not provide anymore information then the manufacturer
2122
			// we suppose multisync with evolution
2123
			case 'the multisync project':
2124
				switch ($this->productName)
2125
				{
2126
					default:
0 ignored issues
show
Unused Code introduced by
default: $this->supp...ds['basic']; break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
2127
						$this->supportedFields = $defaultFields['basic'];
2128
						break;
2129
				}
2130
				break;
2131
2132
			case 'siemens':
2133
				switch ($this->productName)
2134
				{
2135
					case 'sx1':
2136
						$this->supportedFields = $defaultFields['minimal'];
2137
						break;
2138
					default:
2139
						error_log("Unknown Siemens phone '$_productName', using minimal set");
2140
						$this->supportedFields = $defaultFields['minimal'];
2141
						break;
2142
				}
2143
				break;
2144
2145
			case 'nokia':
2146
				switch ($this->productName)
2147
				{
2148
					case 'e61':
2149
						$this->supportedFields = $defaultFields['minimal'];
2150
						break;
2151
					case 'e51':
2152
					case 'e90':
2153
					case 'e71':
2154
					case 'e72-1':
2155
					case 'e75-1':
2156
					case 'e66':
2157
					case '6120c':
2158
					case 'nokia 6131':
2159
					case 'n97':
2160
					case 'n97 mini':
2161
					case '5800 xpressmusic':
2162
						$this->supportedFields = $defaultFields['s60'];
2163
						break;
2164
					default:
2165
						if ($this->productName[0] == 'e')
2166
						{
2167
							$model = 'E90';
2168
							$this->supportedFields = $defaultFields['s60'];
2169
						}
2170
						else
2171
						{
2172
							$model = 'E61';
2173
							$this->supportedFields = $defaultFields['minimal'];
2174
						}
2175
						error_log("Unknown Nokia phone '$_productName', assuming same as '$model'");
2176
						break;
2177
				}
2178
				break;
2179
2180
			case 'sonyericsson':
2181
			case 'sony ericsson':
2182
				switch ($this->productName)
2183
				{
2184
					case 'd750i':
2185
					case 'p910i':
2186
					case 'g705i':
2187
					case 'w890i':
2188
						$this->supportedFields = $defaultFields['basic'];
2189
						break;
2190
					default:
2191
						error_log("Unknown Sony Ericsson phone '$this->productName' assuming d750i");
2192
						$this->supportedFields = $defaultFields['basic'];
2193
						break;
2194
				}
2195
				break;
2196
2197
			case 'synthesis ag':
2198
				switch ($this->productName)
2199
				{
2200
					case 'sysync client pocketpc std':
2201
					case 'sysync client pocketpc pro':
2202
					case 'sysync client iphone contacts':
2203
					case 'sysync client iphone contacts+todoz':
2204
					default:
2205
						$this->supportedFields = $defaultFields['synthesis'];
2206
						break;
2207
				}
2208
				break;
2209
2210
			//Syncevolution compatibility
2211
			case 'patrick ohly':
2212
				$this->supportedFields = $defaultFields['evolution'];
2213
				break;
2214
2215
			case '': // seems syncevolution 0.5 doesn't send a manufacturer
2216
				error_log("No vendor name, assuming syncevolution 0.5");
2217
				$this->supportedFields = $defaultFields['evolution'];
2218
				break;
2219
2220
			case 'file':	// used outside of SyncML, eg. by the calendar itself ==> all possible fields
2221
				if ($this->cal_prefs['export_timezone'])
2222
				{
2223
					$this->tzid = $this->cal_prefs['export_timezone'];
2224
				}
2225
				else	// not set or '0' = use event TZ
2226
				{
2227
					$this->tzid = false; // use event's TZ
2228
				}
2229
				$this->supportedFields = $defaultFields['full'];
2230
				break;
2231
2232
			case 'full':
2233
			case 'groupdav':		// all GroupDAV access goes through here
2234
				$this->tzid = false; // use event's TZ
2235
				switch ($this->productName)
2236
				{
2237
					default:
0 ignored issues
show
Unused Code introduced by
default: $this->supp...edFields['whole_day']); does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
2238
						$this->supportedFields = $defaultFields['full'];
2239
						unset($this->supportedFields['whole_day']);
2240
				}
2241
				break;
2242
2243
			case 'funambol':
2244
				$this->supportedFields = $defaultFields['funambol'];
2245
				break;
2246
2247
			// the fallback for SyncML
2248
			default:
2249
				error_log("Unknown calendar SyncML client: manufacturer='$this->productManufacturer'  product='$this->productName'");
2250
				$this->supportedFields = $defaultFields['synthesis'];
2251
		}
2252
2253 View Code Duplication
		if ($this->log)
2254
		{
2255
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2256
				'(' . $this->productManufacturer .
2257
				', '. $this->productName .', ' .
2258
				($this->tzid ? $this->tzid : egw_time::$user_timezone->getName()) .
2259
				', ' . $this->calendarOwner . ")\n" , 3, $this->logfile);
2260
		}
2261
2262
		//Horde::logMessage('setSupportedFields(' . $this->productManufacturer . ', '
2263
		//	. $this->productName .', ' .
2264
		//	($this->tzid ? $this->tzid : egw_time::$user_timezone->getName()) .')',
2265
		//	__FILE__, __LINE__, PEAR_LOG_DEBUG);
2266
	}
2267
2268
	/**
2269
	 * Convert vCalendar data in EGw events
2270
	 *
2271
	 * @param string|resource $_vcalData
2272
	 * @param string $principalURL ='' Used for CalDAV imports
2273
	 * @param string $charset  The encoding charset for $text. Defaults to
2274
     *                         utf-8 for new format, iso-8859-1 for old format.
2275
	 * @return Iterator|array|boolean Iterator if resource given or array of events on success, false on failure
2276
	 */
2277
	function icaltoegw($_vcalData, $principalURL='', $charset=null)
2278
	{
2279
		if ($this->log)
2280
		{
2281
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."($principalURL, $charset)\n" .
2282
				array2string($_vcalData)."\n",3,$this->logfile);
2283
		}
2284
2285
		if (!is_array($this->supportedFields)) $this->setSupportedFields();
2286
2287
		// we use egw_ical_iterator only on resources, as calling importVCal() accesses single events like an array (eg. $events[0])
2288
		if (is_resource($_vcalData))
2289
		{
2290
			return new egw_ical_iterator($_vcalData,'VCALENDAR',$charset,array($this,'_ical2egw_callback'),array($this->tzid,$principalURL));
2291
		}
2292
2293
		if ($this->tzid)
2294
		{
2295
			$tzid = $this->tzid;
2296
		}
2297
		else
2298
		{
2299
			$tzid = egw_time::$user_timezone->getName();
2300
		}
2301
2302
		date_default_timezone_set($tzid);
2303
2304
		$events = array();
2305
		$vcal = new Horde_Icalendar;
2306
		if ($charset && $charset != 'utf-8')
2307
		{
2308
			$_vcalData = translation::convert($_vcalData, $charset, 'utf-8');
2309
		}
2310
		if (!$vcal->parsevCalendar($_vcalData, 'VCALENDAR'))
2311
		{
2312
			if ($this->log)
2313
			{
2314
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2315
					"(): No vCalendar Container found!\n",3,$this->logfile);
2316
			}
2317
			date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']);
2318
			return false;
2319
		}
2320
		foreach ($vcal->getComponents() as $component)
2321
		{
2322
			if (($event = $this->_ical2egw_callback($component,$this->tzid,$principalURL,$vcal)))
0 ignored issues
show
Bug introduced by
It seems like $this->tzid can also be of type boolean; however, calendar_ical::_ical2egw_callback() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
2323
			{
2324
				$events[] = $event;
2325
			}
2326
		}
2327
		date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']);
2328
2329
		return $events;
2330
	}
2331
2332
	/**
2333
	 * Callback for egw_ical_iterator to convert Horde_iCalendar_Vevent to EGw event array
2334
	 *
2335
	 * @param Horde_iCalendar $component
2336
	 * @param string $tzid timezone
2337
	 * @param string $principalURL ='' Used for CalDAV imports
2338
	 * @param Horde_Icalendar $container =null container to access attributes on container
2339
	 * @return array|boolean event array or false if $component is no Horde_Icalendar_Vevent
2340
	 */
2341
	function _ical2egw_callback(Horde_Icalendar $component, $tzid, $principalURL='', Horde_Icalendar $container=null)
2342
	{
2343
		//unset($component->_container); _debug_array($component);
2344
2345
		if ($this->log)
2346
		{
2347
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.'() '.get_class($component)." found\n",3,$this->logfile);
2348
		}
2349
2350
		if (!is_a($component, 'Horde_Icalendar_Vevent') ||
2351
			!($event = $this->vevent2egw($component, $container ? $container->getAttributeDefault('VERSION', '2.0') : '2.0',
2352
				$this->supportedFields, $principalURL, null, $container)))
2353
		{
2354
			return false;
2355
		}
2356
		//common adjustments
2357
		if ($this->productManufacturer == '' && $this->productName == '' && !empty($event['recur_enddate']))
2358
		{
2359
			// syncevolution needs an adjusted recur_enddate
2360
			$event['recur_enddate'] = (int)$event['recur_enddate'] + 86400;
2361
		}
2362
		if ($event['recur_type'] != MCAL_RECUR_NONE)
2363
		{
2364
			// No reference or RECURRENCE-ID for the series master
2365
			$event['reference'] = $event['recurrence'] = 0;
2366
		}
2367
2368
		// handle the alarms
2369
		$alarms = $event['alarm'];
2370
		foreach ($component->getComponents() as $valarm)
2371
		{
2372
			if (is_a($valarm, 'Horde_Icalendar_Valarm'))
2373
			{
2374
				self::valarm2egw($alarms, $valarm, $event['end'] - $event['start']);
2375
			}
2376
		}
2377
		$event['alarm'] = $alarms;
2378
		if ($tzid || empty($event['tzid']))
2379
		{
2380
			$event['tzid'] = $tzid;
2381
		}
2382
		return $event;
2383
	}
2384
2385
	/**
2386
	 * Parse a VEVENT
2387
	 *
2388
	 * @param array $component			VEVENT
2389
	 * @param string $version			vCal version (1.0/2.0)
2390
	 * @param array $supportedFields	supported fields of the device
2391
	 * @param string $principalURL =''	Used for CalDAV imports, no longer used in favor of groupdav_principals::url2uid()
2392
	 * @param string $check_component ='Horde_Icalendar_Vevent'
2393
	 * @param Horde_Icalendar $container =null container to access attributes on container
2394
	 * @return array|boolean			event on success, false on failure
2395
	 */
2396
	function vevent2egw($component, $version, $supportedFields, $principalURL='', $check_component='Horde_Icalendar_Vevent', Horde_Icalendar $container=null)
2397
	{
2398
		unset($principalURL);	// not longer used, but required in function signature
2399
2400 View Code Duplication
		if ($check_component && !is_a($component, $check_component))
2401
		{
2402
			if ($this->log)
2403
			{
2404
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.'()' .
2405
					get_class($component)." found\n",3,$this->logfile);
2406
			}
2407
			return false;
2408
		}
2409
2410 View Code Duplication
		if (!empty($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length']))
2411
		{
2412
			$minimum_uid_length = $GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'];
2413
		}
2414
		else
2415
		{
2416
			$minimum_uid_length = 8;
2417
		}
2418
2419
		$isDate = false;
2420
		$event		= array();
2421
		$alarms		= array();
2422
		$vcardData	= array(
2423
			'recur_type'		=> MCAL_RECUR_NONE,
2424
			'recur_exception'	=> array(),
2425
			'priority'          => 0,	// iCalendar default is 0=undefined, not EGroupware 5=normal
2426
		);
2427
		// we need to parse DTSTART, DTEND or DURATION (in that order!) first
2428
		foreach (array_merge(
2429
			$component->getAllAttributes('DTSTART'),
0 ignored issues
show
Bug introduced by
The method getAllAttributes cannot be called on $component (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
2430
			$component->getAllAttributes('DTEND'),
0 ignored issues
show
Bug introduced by
The method getAllAttributes cannot be called on $component (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
2431
			$component->getAllAttributes('DURATION')) as $attributes)
0 ignored issues
show
Bug introduced by
The method getAllAttributes cannot be called on $component (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
2432
		{
2433
			switch ($attributes['name'])
2434
			{
2435
				case 'DTSTART':
2436
					if (isset($attributes['params']['VALUE'])
2437
							&& $attributes['params']['VALUE'] == 'DATE')
2438
					{
2439
						$isDate = true;
2440
					}
2441
					$dtstart_ts = is_numeric($attributes['value']) ? $attributes['value'] : $this->date2ts($attributes['value']);
2442
					$vcardData['start']	= $dtstart_ts;
2443
2444
					if ($this->tzid)
2445
					{
2446
						$event['tzid'] = $this->tzid;
2447
					}
2448
					else
2449
					{
2450
						if (!empty($attributes['params']['TZID']))
2451
						{
2452
							// import TZID, if PHP understands it (we only care about TZID of starttime,
2453
							// as we store only a TZID for the whole event)
2454
							try
2455
							{
2456
								$tz = calendar_timezones::DateTimeZone($attributes['params']['TZID']);
2457
								// sometimes we do not get an egw_time object but no exception is thrown
2458
								// may be php 5.2.x related. occurs when a NokiaE72 tries to open Outlook invitations
2459
								if ($tz instanceof DateTimeZone)
2460
								{
2461
									$event['tzid'] = $tz->getName();
2462
								}
2463
								else
2464
								{
2465
									error_log(__METHOD__ . '() unknown TZID='
2466
										. $attributes['params']['TZID'] . ', defaulting to timezone "'
2467
										. date_default_timezone_get() . '".'.array2string($tz));
2468
									$event['tzid'] = date_default_timezone_get();	// default to current timezone
2469
								}
2470
							}
2471
							catch(Exception $e)
2472
							{
2473
								error_log(__METHOD__ . '() unknown TZID='
2474
									. $attributes['params']['TZID'] . ', defaulting to timezone "'
2475
									. date_default_timezone_get() . '".'.$e->getMessage());
2476
								$event['tzid'] = date_default_timezone_get();	// default to current timezone
2477
							}
2478
						}
2479
						else
2480
						{
2481
							// Horde seems not to distinguish between an explicit UTC time postfixed with Z and one without
2482
							// assuming for now UTC to pass CalDAVTester tests
2483
							// ToDo: fix Horde_Icalendar to return UTC for timestamp postfixed with Z
2484
							$event['tzid'] = 'UTC';
2485
						}
2486
					}
2487
					break;
2488
2489
				case 'DTEND':
2490
					$dtend_ts = is_numeric($attributes['value']) ? $attributes['value'] : $this->date2ts($attributes['value']);
2491
					if (date('H:i:s',$dtend_ts) == '00:00:00')
2492
					{
2493
						$dtend_ts -= 1;
2494
					}
2495
					$vcardData['end']	= $dtend_ts;
2496
					break;
2497
2498
				case 'DURATION':	// clients can use DTSTART+DURATION, instead of DTSTART+DTEND
2499
					if (!isset($vcardData['end']))
2500
					{
2501
						$vcardData['end'] = $vcardData['start'] + $attributes['value'];
2502
					}
2503
					else
2504
					{
2505
						error_log(__METHOD__."() find DTEND AND DURATION --> ignoring DURATION");
2506
					}
2507
					break;
2508
			}
2509
		}
2510
		if (!isset($vcardData['start']))
2511
		{
2512
			if ($this->log)
2513
			{
2514
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2515
					. "() DTSTART missing!\n",3,$this->logfile);
2516
			}
2517
			return false; // not a valid entry
2518
		}
2519
		// lets see what we can get from the vcard
2520
		foreach ($component->getAllAttributes() as $attributes)
0 ignored issues
show
Bug introduced by
The method getAllAttributes cannot be called on $component (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
2521
		{
2522
			switch ($attributes['name'])
2523
			{
2524
				case 'X-MICROSOFT-CDO-ALLDAYEVENT':
2525
					if (isset($supportedFields['whole_day']))
2526
					{
2527
						$event['whole_day'] = (isset($attributes['value'])?strtoupper($attributes['value'])=='TRUE':true);
2528
					}
2529
					break;
2530
				case 'AALARM':
2531
				case 'DALARM':
2532
					$alarmTime = $attributes['value'];
2533
					$alarms[$alarmTime] = array(
2534
						'time' => $alarmTime
2535
					);
2536
					break;
2537
				case 'CLASS':
2538
					$vcardData['public'] = (int)(strtolower($attributes['value']) == 'public');
2539
					break;
2540
				case 'DESCRIPTION':
2541
					$vcardData['description'] = str_replace("\r\n", "\n", $attributes['value']);
2542
					$matches = null;
2543
					if (preg_match('/\s*\[UID:(.+)?\]/Usm', $attributes['value'], $matches))
2544
					{
2545
						if (!isset($vcardData['uid'])
2546
								&& strlen($matches[1]) >= $minimum_uid_length)
2547
						{
2548
							$vcardData['uid'] = $matches[1];
2549
						}
2550
					}
2551
					break;
2552
				case 'RECURRENCE-ID':
2553
				case 'X-RECURRENCE-ID':
2554
					$vcardData['recurrence'] = $attributes['value'];
2555
					break;
2556
				case 'LOCATION':
2557
					$vcardData['location']	= str_replace("\r\n", "\n", $attributes['value']);
2558
					break;
2559
				case 'RRULE':
2560
					$recurence = $attributes['value'];
2561
					$vcardData['recur_interval'] = 1;
2562
					$type = preg_match('/FREQ=([^;: ]+)/i',$recurence,$matches) ? $matches[1] : $recurence[0];
2563
					// vCard 2.0 values for all types
2564
					if (preg_match('/UNTIL=([0-9TZ]+)/',$recurence,$matches))
2565
					{
2566
						$vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($matches[1]);
0 ignored issues
show
Bug introduced by
The method _parseDateTime cannot be called on $this->vCalendar (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
2567
						// If it couldn't be parsed, treat it as not set
2568
						if(is_string($vcardData['recur_enddate']))
2569
						{
2570
							unset($vcardData['recur_enddate']);
2571
						}
2572
					}
2573
					elseif (preg_match('/COUNT=([0-9]+)/',$recurence,$matches))
2574
					{
2575
						$vcardData['recur_count'] = (int)$matches[1];
2576
					}
2577
					if (preg_match('/INTERVAL=([0-9]+)/',$recurence,$matches))
2578
					{
2579
						$vcardData['recur_interval'] = (int) $matches[1] ? (int) $matches[1] : 1;
2580
					}
2581
					$vcardData['recur_data'] = 0;
2582
					switch($type)
2583
					{
2584 View Code Duplication
						case 'D':	// 1.0
2585
							$recurenceMatches = null;
2586
							if (preg_match('/D(\d+) #(\d+)/', $recurence, $recurenceMatches))
2587
							{
2588
								$vcardData['recur_interval'] = $recurenceMatches[1];
2589
								$vcardData['recur_count'] = $recurenceMatches[2];
2590
							}
2591
							elseif (preg_match('/D(\d+) (.*)/', $recurence, $recurenceMatches))
2592
							{
2593
								$vcardData['recur_interval'] = $recurenceMatches[1];
2594
								$vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime(trim($recurenceMatches[2]));
0 ignored issues
show
Bug introduced by
The method _parseDateTime cannot be called on $this->vCalendar (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
2595
							}
2596
							else break;
2597
							// fall-through
2598
						case 'DAILY':	// 2.0
2599
							$vcardData['recur_type'] = MCAL_RECUR_DAILY;
2600
							if (stripos($recurence, 'BYDAY') === false) break;
2601
							// hack to handle TYPE=DAILY;BYDAY= as WEEKLY, which is true as long as there's no interval
2602
							// fall-through
2603
						case 'W':
2604
						case 'WEEKLY':
2605
							$days = array();
2606
							if (preg_match('/W(\d+) *((?i: [AEFHMORSTUW]{2})+)?( +([^ ]*))$/',$recurence, $recurenceMatches))		// 1.0
2607
							{
2608
								$vcardData['recur_interval'] = $recurenceMatches[1];
2609
								if (empty($recurenceMatches[2]))
2610
								{
2611
									$days[0] = strtoupper(substr(date('D', $vcardData['start']),0,2));
2612
								}
2613
								else
2614
								{
2615
									$days = explode(' ',trim($recurenceMatches[2]));
2616
								}
2617
2618
								$repeatMatches = null;
2619
								if (preg_match('/#(\d+)/',$recurenceMatches[4],$repeatMatches))
2620
								{
2621
									if ($repeatMatches[1]) $vcardData['recur_count'] = $repeatMatches[1];
2622
								}
2623
								else
2624
								{
2625
									$vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($recurenceMatches[4]);
0 ignored issues
show
Bug introduced by
The method _parseDateTime cannot be called on $this->vCalendar (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
2626
								}
2627
2628
								$recur_days = $this->recur_days_1_0;
2629
							}
2630
							elseif (preg_match('/BYDAY=([^;: ]+)/',$recurence,$recurenceMatches))	// 2.0
2631
							{
2632
								$days = explode(',',$recurenceMatches[1]);
2633
								$recur_days = $this->recur_days;
2634
							}
2635 View Code Duplication
							else	// no day given, use the day of dtstart
2636
							{
2637
								$vcardData['recur_data'] |= 1 << (int)date('w',$vcardData['start']);
2638
								$vcardData['recur_type'] = MCAL_RECUR_WEEKLY;
2639
							}
2640
							if ($days)
2641
							{
2642
								foreach ($recur_days as $id => $day)
2643
								{
2644
									if (in_array(strtoupper(substr($day,0,2)),$days))
2645
									{
2646
										$vcardData['recur_data'] |= $id;
2647
									}
2648
								}
2649
								$vcardData['recur_type'] = MCAL_RECUR_WEEKLY;
2650
							}
2651
							break;
2652
2653
						case 'M':
2654
							if (preg_match('/MD(\d+)(?: [^ ]+)? #(\d+)/', $recurence, $recurenceMatches))
2655
							{
2656
								$vcardData['recur_type'] = MCAL_RECUR_MONTHLY_MDAY;
2657
								$vcardData['recur_interval'] = $recurenceMatches[1];
2658
								$vcardData['recur_count'] = $recurenceMatches[2];
2659
							}
2660
							elseif (preg_match('/MD(\d+)(?: [^ ]+)? ([0-9TZ]+)/',$recurence, $recurenceMatches))
2661
							{
2662
								$vcardData['recur_type'] = MCAL_RECUR_MONTHLY_MDAY;
2663
								$vcardData['recur_interval'] = $recurenceMatches[1];
2664
								$vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($recurenceMatches[2]);
0 ignored issues
show
Bug introduced by
The method _parseDateTime cannot be called on $this->vCalendar (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
2665
							}
2666
							elseif (preg_match('/MP(\d+) (.*) (.*) (.*)/',$recurence, $recurenceMatches))
2667
							{
2668
								$vcardData['recur_type'] = MCAL_RECUR_MONTHLY_WDAY;
2669
								$vcardData['recur_interval'] = $recurenceMatches[1];
2670
								if (preg_match('/#(\d+)/',$recurenceMatches[4],$recurenceMatches))
2671
								{
2672
									$vcardData['recur_count'] = $recurenceMatches[1];
2673
								}
2674
								else
2675
								{
2676
									$vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime(trim($recurenceMatches[4]));
0 ignored issues
show
Bug introduced by
The method _parseDateTime cannot be called on $this->vCalendar (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
2677
								}
2678
							}
2679
							break;
2680
2681 View Code Duplication
						case 'Y':		// 1.0
2682
							if (preg_match('/YM(\d+)(?: [^ ]+)? #(\d+)/', $recurence, $recurenceMatches))
2683
							{
2684
								$vcardData['recur_interval'] = $recurenceMatches[1];
2685
								$vcardData['recur_count'] = $recurenceMatches[2];
2686
							}
2687
							elseif (preg_match('/YM(\d+)(?: [^ ]+)? ([0-9TZ]+)/',$recurence, $recurenceMatches))
2688
							{
2689
								$vcardData['recur_interval'] = $recurenceMatches[1];
2690
								$vcardData['recur_enddate'] = $this->vCalendar->_parseDateTime($recurenceMatches[2]);
0 ignored issues
show
Bug introduced by
The method _parseDateTime cannot be called on $this->vCalendar (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
2691
							} else break;
2692
							// fall-through
2693
						case 'YEARLY':	// 2.0
2694
							if (strpos($recurence, 'BYDAY') === false)
2695
							{
2696
								$vcardData['recur_type'] = MCAL_RECUR_YEARLY;
2697
								break;
2698
							}
2699
							// handle FREQ=YEARLY;BYDAY= as FREQ=MONTHLY;BYDAY= with 12*INTERVAL
2700
							$vcardData['recur_interval'] = $vcardData['recur_interval'] ?
2701
								12*$vcardData['recur_interval'] : 12;
2702
							// fall-through
2703
						case 'MONTHLY':
2704
							// does currently NOT parse BYDAY or BYMONTH, it has to be specified/identical to DTSTART
2705
							$vcardData['recur_type'] = strpos($recurence,'BYDAY') !== false ?
2706
								MCAL_RECUR_MONTHLY_WDAY : MCAL_RECUR_MONTHLY_MDAY;
2707
							break;
2708
					}
2709
					break;
2710
				case 'EXDATE':
2711
					if (!$attributes['value']) break;
2712
					if ((isset($attributes['params']['VALUE'])
2713
							&& $attributes['params']['VALUE'] == 'DATE') ||
2714
						(!isset($attributes['params']['VALUE']) && $isDate))
2715
					{
2716
						$days = array();
2717
						$hour = date('H', $vcardData['start']);
2718
						$minutes = date('i', $vcardData['start']);
2719
						$seconds = date('s', $vcardData['start']);
2720
						foreach ($attributes['values'] as $day)
2721
						{
2722
							$days[] = mktime(
2723
								$hour,
2724
								$minutes,
2725
								$seconds,
2726
								$day['month'],
2727
								$day['mday'],
2728
								$day['year']);
2729
						}
2730
						$vcardData['recur_exception'] = array_merge($vcardData['recur_exception'], $days);
2731
					}
2732
					else
2733
					{
2734
						$vcardData['recur_exception'] = array_merge($vcardData['recur_exception'], $attributes['values']);
2735
					}
2736
					break;
2737
				case 'SUMMARY':
2738
					$vcardData['title'] = str_replace("\r\n", "\n", $attributes['value']);
2739
					break;
2740
				case 'UID':
2741
					if (strlen($attributes['value']) >= $minimum_uid_length)
2742
					{
2743
						$event['uid'] = $vcardData['uid'] = $attributes['value'];
2744
					}
2745
					break;
2746
				case 'TRANSP':
2747
					if ($version == '1.0')
2748
					{
2749
						$vcardData['non_blocking'] = ($attributes['value'] == 1);
2750
					}
2751
					else
2752
					{
2753
						$vcardData['non_blocking'] = ($attributes['value'] == 'TRANSPARENT');
2754
					}
2755
					break;
2756 View Code Duplication
				case 'PRIORITY':
2757
					if ($this->productManufacturer == 'funambol' &&
2758
						(strpos($this->productName, 'outlook') !== false
2759
							|| strpos($this->productName, 'pocket pc') !== false))
2760
					{
2761
						$vcardData['priority'] = (int) $this->priority_funambol2egw[$attributes['value']];
2762
					}
2763
					else
2764
					{
2765
						$vcardData['priority'] = (int) $this->priority_ical2egw[$attributes['value']];
2766
					}
2767
					break;
2768
				case 'CATEGORIES':
2769
					if ($attributes['value'])
2770
					{
2771
						$vcardData['category'] = explode(',', $attributes['value']);
2772
					}
2773
					else
2774
					{
2775
						$vcardData['category'] = array();
2776
					}
2777
					break;
2778
				case 'ORGANIZER':
2779
					$event['organizer'] = $attributes['value'];	// no egw field, but needed in AS
2780 View Code Duplication
					if (strtolower(substr($event['organizer'],0,7)) == 'mailto:')
2781
					{
2782
						$event['organizer'] = substr($event['organizer'],7);
2783
					}
2784
					if (!empty($attributes['params']['CN']))
2785
					{
2786
						$event['organizer'] = $attributes['params']['CN'].' <'.$event['organizer'].'>';
2787
					}
2788
					// fall throught
2789
				case 'ATTENDEE':
2790
					if (isset($attributes['params']['PARTSTAT']))
2791
				    {
2792
				    	$attributes['params']['STATUS'] = $attributes['params']['PARTSTAT'];
2793
				    }
2794
				    if (isset($attributes['params']['STATUS']))
2795
					{
2796
						$status = $this->status_ical2egw[strtoupper($attributes['params']['STATUS'])];
2797
						if (empty($status)) $status = 'X';
2798
					}
2799
					else
2800
					{
2801
						$status = 'X'; // client did not return the status
2802
					}
2803
					$uid = $email = $cn = '';
2804
					$quantity = 1;
2805
					$role = 'REQ-PARTICIPANT';
2806
					if (!empty($attributes['params']['ROLE']))
2807
					{
2808
						$role = $attributes['params']['ROLE'];
2809
					}
2810
					// CN explicit given --> use it
2811
					if (strtolower(substr($attributes['value'], 0, 7)) == 'mailto:' &&
2812
						!empty($attributes['params']['CN']))
2813
					{
2814
						$email = substr($attributes['value'], 7);
2815
						$cn = $attributes['params']['CN'];
2816
					}
2817
					// try parsing email and cn from attendee
2818
					elseif (preg_match('/mailto:([@.a-z0-9_-]+)|mailto:"?([.a-z0-9_ -]*)"?[ ]*<([@.a-z0-9_-]*)>/i',
2819
						$attributes['value'],$matches))
2820
					{
2821
						$email = $matches[1] ? $matches[1] : $matches[3];
2822
						$cn = isset($matches[2]) ? $matches[2]: '';
2823
					}
2824
					elseif (!empty($attributes['value']) &&
2825
						preg_match('/"?([.a-z0-9_ -]*)"?[ ]*<([@.a-z0-9_-]*)>/i',
2826
						$attributes['value'],$matches))
2827
					{
2828
						$cn = $matches[1];
2829
						$email = $matches[2];
2830
					}
2831
					elseif (strpos($attributes['value'],'@') !== false)
2832
					{
2833
						$email = $attributes['value'];
2834
					}
2835
					// 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)
2836
					if (!$uid && !empty($attributes['params']['X-EGROUPWARE-UID']) &&
2837
						($res_info = $this->resource_info($attributes['params']['X-EGROUPWARE-UID'])) &&
2838
						!strcasecmp($res_info['email'], $email))
2839
					{
2840
						$uid = $attributes['params']['X-EGROUPWARE-UID'];
2841
						if (!empty($attributes['params']['X-EGROUPWARE-QUANTITY']))
2842
						{
2843
							$quantity = $attributes['params']['X-EGROUPWARE-QUANTITY'];
2844
						}
2845
						if ($this->log)
2846
						{
2847
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2848
								. "(): Found X-EGROUPWARE-UID: '$uid'\n",3,$this->logfile);
2849
						}
2850
					}
2851
					elseif ($attributes['value'] == 'Unknown')
2852
					{
2853
							// we use the current user
2854
							$uid = $this->user;
2855
					}
2856
					// check principal url from CalDAV here after X-EGROUPWARE-UID and to get optional X-EGROUPWARE-QUANTITY
2857
					if (!$uid) $uid = groupdav_principals::url2uid($attributes['value'], null, $cn);
2858
2859
					// try to find an email address
2860
					if (!$uid && $email && ($uid = $GLOBALS['egw']->accounts->name2id($email, 'account_email')))
2861
					{
2862
						// we use the account we found
2863
						if ($this->log)
2864
						{
2865
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2866
								. "() Found account: '$uid', '$cn', '$email'\n",3,$this->logfile);
2867
						}
2868
					}
2869
					if (!$uid)
2870
					{
2871
						$searcharray = array();
2872
						// search for provided email address ...
2873
						if ($email)
2874
						{
2875
							$searcharray = array('email' => $email, 'email_home' => $email);
2876
						}
2877
						// ... and for provided CN
2878
						if (!empty($attributes['params']['CN']))
2879
						{
2880
							$cn = str_replace(array('\\,', '\\;', '\\:', '\\\\'),
2881
										array(',', ';', ':', '\\'),
2882
										$attributes['params']['CN']);
2883 View Code Duplication
							if ($cn[0] == '"' && substr($cn,-1) == '"')
2884
							{
2885
								$cn = substr($cn,1,-1);
2886
							}
2887
							// not searching for $cn, as match can be not unique or without an email address
2888
							// --> notification will fail, better store just as email
2889
						}
2890
2891
						if ($this->log)
2892
						{
2893
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2894
								. "() Search participant: '$cn', '$email'\n",3,$this->logfile);
2895
						}
2896
2897
						//elseif (//$attributes['params']['CUTYPE'] == 'GROUP'
2898
						if (preg_match('/(.*) '. lang('Group') . '/', $cn, $matches))
2899
						{
2900
							// we found a group
2901
							if ($this->log)
2902
							{
2903
								error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2904
									. "() Found group: '$matches[1]', '$cn', '$email'\n",3,$this->logfile);
2905
							}
2906
							if (($uid =  $GLOBALS['egw']->accounts->name2id($matches[1], 'account_lid', 'g')))
2907
							{
2908
								//Horde::logMessage("vevent2egw: group participant $uid",
2909
								//			__FILE__, __LINE__, PEAR_LOG_DEBUG);
2910
								if (!isset($vcardData['participants'][$this->user]) &&
2911
									$status != 'X' && $status != 'U')
2912
								{
2913
									// User tries to reply to the group invitiation
2914
									if (($members = $GLOBALS['egw']->accounts->members($uid, true)) &&
2915
										in_array($this->user, $members))
2916
									{
2917
										//Horde::logMessage("vevent2egw: set status to " . $status,
2918
										//		__FILE__, __LINE__, PEAR_LOG_DEBUG);
2919
										$vcardData['participants'][$this->user] =
2920
											calendar_so::combine_status($status,$quantity,$role);
2921
									}
2922
								}
2923
								$status = 'U'; // keep the group
2924
							}
2925
							else continue; // can't find this group
2926
						}
2927
						elseif (empty($searcharray))
2928
						{
2929
							continue;	// participants without email AND CN --> ignore it
2930
						}
2931
						elseif ((list($data) = $this->addressbook->search($searcharray,
0 ignored issues
show
Bug introduced by
The method search cannot be called on $this->addressbook (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
2932
							array('id','egw_addressbook.account_id as account_id','n_fn'),
2933
							'egw_addressbook.account_id IS NOT NULL DESC, n_fn IS NOT NULL DESC',
2934
							'','',false,'OR')))
2935
						{
2936
							// found an addressbook entry
2937
							$uid = $data['account_id'] ? (int)$data['account_id'] : 'c'.$data['id'];
2938
							if ($this->log)
2939
							{
2940
								error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2941
									. "() Found addressbook entry: '$uid', '$cn', '$email'\n",3,$this->logfile);
2942
							}
2943
						}
2944
						else
2945
						{
2946
							if (!$email)
2947
							{
2948
								$email = '[email protected]';	// set dummy email to store the CN
2949
							}
2950
							$uid = 'e'. ($cn ? $cn . ' <' . $email . '>' : $email);
2951
							if ($this->log)
2952
							{
2953
								error_log(__FILE__.'['.__LINE__.'] '.__METHOD__
2954
									. "() Not Found, create dummy: '$uid', '$cn', '$email'\n",3,$this->logfile);
2955
							}
2956
						}
2957
					}
2958
					switch($attributes['name'])
2959
					{
2960
						case 'ATTENDEE':
2961
							if (!isset($attributes['params']['ROLE']) &&
2962
								isset($event['owner']) && $event['owner'] == $uid)
2963
							{
2964
								$attributes['params']['ROLE'] = 'CHAIR';
2965
							}
2966
							if (!isset($vcardData['participants'][$uid]) ||
2967
									$vcardData['participants'][$uid][0] != 'A')
2968
							{
2969
								// keep role 'CHAIR' from an external organizer, even if he is a regular participant with a different role
2970
								// as this is currently the only way to store an external organizer and send him iMip responses
2971
								$q = $r = null;
2972
								if (isset($vcardData['participants'][$uid]) && ($s=$vcardData['participants'][$uid]) &&
2973
									calendar_so::split_status($s, $q, $r) && $r == 'CHAIR')
2974
								{
2975
									$role = 'CHAIR';
2976
								}
2977
								// for multiple entries the ACCEPT wins
2978
								// add quantity and role
2979
								$vcardData['participants'][$uid] =
2980
									calendar_so::combine_status($status, $quantity, $role);
2981
2982
								try {
2983
									if (!$this->calendarOwner && is_numeric($uid) && $role == 'CHAIR')
2984
										$component->getAttribute('ORGANIZER');
0 ignored issues
show
Bug introduced by
The method getAttribute cannot be called on $component (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
2985
								}
2986
								catch(Horde_Icalendar_Exception $e)
0 ignored issues
show
Bug introduced by
The class Horde_Icalendar_Exception does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
2987
								{
2988
									// we can store the ORGANIZER as event owner
2989
									$event['owner'] = $uid;
2990
								}
2991
							}
2992
							break;
2993
2994
						case 'ORGANIZER':
2995
							if (isset($vcardData['participants'][$uid]))
2996
							{
2997
								$status = $vcardData['participants'][$uid];
2998
								calendar_so::split_status($status, $quantity, $role);
2999
								$vcardData['participants'][$uid] =
3000
									calendar_so::combine_status($status, $quantity, 'CHAIR');
3001
							}
3002
							if (!$this->calendarOwner && is_numeric($uid))
3003
							{
3004
								// we can store the ORGANIZER as event owner
3005
								$event['owner'] = $uid;
3006
							}
3007
							else
3008
							{
3009
								// we must insert a CHAIR participant to keep the ORGANIZER
3010
								$event['owner'] = $this->user;
3011
								if (!isset($vcardData['participants'][$uid]))
3012
								{
3013
									// save the ORGANIZER as event CHAIR
3014
									$vcardData['participants'][$uid] =
3015
										calendar_so::combine_status('D', 1, 'CHAIR');
3016
								}
3017
							}
3018
					}
3019
					break;
3020
				case 'CREATED':		// will be written direct to the event
3021
					if ($event['modified']) break;
3022
					// fall through
3023
				case 'LAST-MODIFIED':	// will be written direct to the event
3024
					$event['modified'] = $attributes['value'];
3025
					break;
3026
				case 'STATUS':	// currently no EGroupware event column, but needed as Android uses it to delete single recurrences
3027
					$event['status'] = $attributes['value'];
3028
					break;
3029
3030
				// ignore all PROPS, we dont want to store like X-properties or unsupported props
3031
				case 'DTSTAMP':
3032
				case 'SEQUENCE':
3033
				case 'CREATED':
3034
				case 'LAST-MODIFIED':
3035
				case 'DTSTART':
3036
				case 'DTEND':
3037
				case 'DURATION':
3038
				case 'X-LIC-ERROR':	// parse errors from libical, makes no sense to store them
3039
					break;
3040
3041
				case 'ATTACH':
3042
					if ($attributes['params'] && !empty($attributes['params']['FMTTYPE'])) break;	// handeled by managed attachment code
3043
					// fall throught to store external attachment url
3044 View Code Duplication
				default:	// X- attribute or other by EGroupware unsupported property
3045
					//error_log(__METHOD__."() $attributes[name] = ".array2string($attributes));
3046
					// for attributes with multiple values in multiple lines, merge the values
3047
					if (isset($event['##'.$attributes['name']]))
3048
					{
3049
						//error_log(__METHOD__."() taskData['##$attribute[name]'] = ".array2string($taskData['##'.$attribute['name']]));
3050
						$attributes['values'] = array_merge(
3051
							is_array($event['##'.$attributes['name']]) ? $event['##'.$attributes['name']]['values'] : (array)$event['##'.$attributes['name']],
3052
							$attributes['values']);
3053
					}
3054
					$event['##'.$attributes['name']] = $attributes['params'] || count($attributes['values']) > 1 ?
3055
						json_encode($attributes) : $attributes['value'];
3056
					break;
3057
			}
3058
		}
3059
		// check if the entry is a birthday
3060
		// this field is only set from NOKIA clients
3061
		try {
3062
			$agendaEntryType = $component->getAttribute('X-EPOCAGENDAENTRYTYPE');
0 ignored issues
show
Bug introduced by
The method getAttribute cannot be called on $component (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
3063
			if (strtolower($agendaEntryType) == 'anniversary')
3064
			{
3065
				$event['special'] = '1';
3066
				$event['non_blocking'] = 1;
3067
				// make it a whole day event for eGW
3068
				$vcardData['end'] = $vcardData['start'] + 86399;
3069
			}
3070
			elseif (strtolower($agendaEntryType) == 'event')
3071
			{
3072
				$event['special'] = '2';
3073
			}
3074
		}
3075
		catch (Horde_Icalendar_Exception $e) {}
0 ignored issues
show
Bug introduced by
The class Horde_Icalendar_Exception does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
3076
3077
		$event['priority'] = 2; // default
3078
		$event['alarm'] = $alarms;
3079
3080
		// now that we know what the vard provides,
3081
		// we merge that data with the information we have about the device
3082
		while (($fieldName = array_shift($supportedFields)))
3083
		{
3084
			switch ($fieldName)
3085
			{
3086
				case 'recur_interval':
3087
				case 'recur_enddate':
3088
				case 'recur_data':
3089
				case 'recur_exception':
3090
				case 'recur_count':
3091
				case 'whole_day':
3092
					// not handled here
3093
					break;
3094
3095
				case 'recur_type':
3096
					$event['recur_type'] = $vcardData['recur_type'];
3097
					if ($event['recur_type'] != MCAL_RECUR_NONE)
3098
					{
3099
						$event['reference'] = 0;
3100
						foreach (array('recur_interval','recur_enddate','recur_data','recur_exception','recur_count') as $r)
3101
						{
3102
							if (isset($vcardData[$r]))
3103
							{
3104
								$event[$r] = $vcardData[$r];
3105
							}
3106
						}
3107
					}
3108
					break;
3109
3110
				default:
3111
					if (isset($vcardData[$fieldName]))
3112
					{
3113
						$event[$fieldName] = $vcardData[$fieldName];
3114
					}
3115
				break;
3116
			}
3117
		}
3118
		if ($event['recur_enddate'])
3119
		{
3120
			// reset recure_enddate to 00:00:00 on the last day
3121
			$rriter = calendar_rrule::event2rrule($event, false);
3122
			$last = $rriter->normalize_enddate();
3123 View Code Duplication
			if(!is_object($last))
3124
			{
3125
				if($this->log)
3126
				{
3127
					error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ .
3128
					" Unable to determine recurrence end date.  \n".array2string($event),3, $this->logfile);
3129
				}
3130
				return false;
3131
			}
3132
			$last->setTime(0, 0, 0);
3133
			//error_log(__METHOD__."() rrule=$recurence --> ".array2string($rriter)." --> enddate=".array2string($last).'='.egw_time::to($last, ''));
3134
			$event['recur_enddate'] = egw_time::to($last, 'server');
3135
		}
3136
		// translate COUNT into an enddate, as we only store enddates
3137
		elseif($event['recur_count'])
3138
		{
3139
			$rriter = calendar_rrule::event2rrule($event, false);
3140
			$last = $rriter->count2date($event['recur_count']);
3141
			if(!is_object($last))
3142
			{
3143
				if($this->log)
3144
				{
3145
					error_log(__FILE__.'['.__LINE__.'] '.__METHOD__,
3146
					" Unable to determine recurrence end date.  \n".array2string($event),3, $this->logfile);
3147
				}
3148
				return false;
3149
			}
3150
			$last->setTime(0, 0, 0);
3151
			$event['recur_enddate'] = egw_time::to($last, 'server');
3152
			unset($event['recur_count']);
3153
		}
3154
3155
		// Apple iCal on OS X uses X-CALENDARSERVER-ACCESS: CONFIDENTIAL on VCALANDAR (not VEVENT!)
3156
		try {
3157
			if ($this->productManufacturer == 'groupdav' && $container &&
3158
				($x_calendarserver_access = $container->getAttribute('X-CALENDARSERVER-ACCESS')))
3159
			{
3160
				$event['public'] =  (int)(strtoupper($x_calendarserver_access) == 'PUBLIC');
3161
			}
3162
			//error_log(__METHOD__."() X-CALENDARSERVER-ACCESS=".array2string($x_calendarserver_access).' --> public='.array2string($event['public']));
3163
		}
3164
		catch (Horde_Icalendar_Exception $e) {}
0 ignored issues
show
Bug introduced by
The class Horde_Icalendar_Exception does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
3165
3166
		// if no end is given in iCal we use the default lenght from user prefs
3167
		// whole day events get one day in calendar_boupdate::save()
3168
		if (!isset($event['end']))
3169
		{
3170
			$event['end'] = $event['start'] + 60 * $this->cal_prefs['defaultlength'];
3171
		}
3172
3173
		if ($this->calendarOwner) $event['owner'] = $this->calendarOwner;
3174
3175
		// parsing ATTACH attributes for managed attachments
3176
		$event['attach-delete-by-put'] = $component->getAttributeDefault('X-EGROUPWARE-ATTACH-INCLUDED', null) === 'TRUE';
0 ignored issues
show
Bug introduced by
The method getAttributeDefault cannot be called on $component (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
3177
		$event['attach'] = $component->getAllAttributes('ATTACH');
0 ignored issues
show
Bug introduced by
The method getAllAttributes cannot be called on $component (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
3178
3179
		if ($this->log)
3180
		{
3181
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" .
3182
				array2string($event)."\n",3,$this->logfile);
3183
		}
3184
		//Horde::logMessage("vevent2egw:\n" . print_r($event, true),
3185
        //    	__FILE__, __LINE__, PEAR_LOG_DEBUG);
3186
		return $event;
3187
	}
3188
3189
	function search($_vcalData, $contentID=null, $relax=false, $charset=null)
3190
	{
3191
		if (($events = $this->icaltoegw($_vcalData, $charset)))
3192
		{
3193
			// this function only supports searching a single event
3194
			if (count($events) == 1)
3195
			{
3196
				$filter = $relax ? 'relax' : 'check';
3197
				$event = array_shift($events);
3198
				$eventId = -1;
3199
				if ($this->so->isWholeDay($event)) $event['whole_day'] = true;
3200
				if ($contentID)
3201
				{
3202
					$parts = preg_split('/:/', $contentID);
3203
					$event['id'] = $eventId = $parts[0];
3204
				}
3205
				$event['category'] = $this->find_or_add_categories($event['category'], $eventId);
3206
				return $this->find_event($event, $filter);
3207
			}
3208
			if ($this->log)
3209
			{
3210
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."() found:\n" .
3211
					array2string($events)."\n",3,$this->logfile);
3212
			}
3213
		}
3214
		return array();
3215
	}
3216
3217
	/**
3218
	 * Create a freebusy vCal for the given user(s)
3219
	 *
3220
	 * @param int $user account_id
3221
	 * @param mixed $end =null end-date, default now+1 month
3222
	 * @param boolean $utc =true if false, use severtime for dates
3223
	 * @param string $charset ='UTF-8' encoding of the vcalendar, default UTF-8
3224
	 * @param mixed $start =null default now
3225
	 * @param string $method ='PUBLISH' or eg. 'REPLY'
3226
	 * @param array $extra =null extra attributes to add
3227
	 * 	X-CALENDARSERVER-MASK-UID can be used to not include an event specified by this uid as busy
3228
	 */
3229
	function freebusy($user,$end=null,$utc=true, $charset='UTF-8', $start=null, $method='PUBLISH', array $extra=null)
3230
	{
3231
		if (!$start) $start = time();	// default now
3232
		if (!$end) $end = time() + 100*DAY_s;	// default next 100 days
3233
3234
		$vcal = new Horde_Icalendar;
3235
		$vcal->setAttribute('PRODID','-//EGroupware//NONSGML EGroupware Calendar '.$GLOBALS['egw_info']['apps']['calendar']['version'].'//'.
3236
			strtoupper($GLOBALS['egw_info']['user']['preferences']['common']['lang']));
3237
		$vcal->setAttribute('VERSION','2.0');
3238
		$vcal->setAttribute('METHOD',$method);
3239
3240
		$vfreebusy = Horde_Icalendar::newComponent('VFREEBUSY',$vcal);
3241
3242
		$attributes = array(
3243
			'DTSTAMP' => time(),
3244
			'DTSTART' => $this->date2ts($start,true),	// true = server-time
3245
			'DTEND' => $this->date2ts($end,true),	// true = server-time
3246
		);
3247
		if (!$utc)
3248
		{
3249
			foreach ($attributes as $attr => $value)
3250
			{
3251
				$attributes[$attr] = date('Ymd\THis', $value);
3252
			}
3253
		}
3254
		if (is_null($extra)) $extra = array(
3255
			'URL' => $this->freebusy_url($user),
3256
			'ORGANIZER' => 'mailto:'.$GLOBALS['egw']->accounts->id2name($user,'account_email'),
3257
		);
3258
		foreach($attributes+$extra as $attr => $value)
3259
		{
3260
			$vfreebusy->setAttribute($attr, $value);
3261
		}
3262
		$fbdata = parent::search(array(
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (search() instead of freebusy()). Are you sure this is correct? If so, you might want to change this to $this->search().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
3263
			'start' => $start,
3264
			'end'   => $end,
3265
			'users' => $user,
3266
			'date_format' => 'server',
3267
			'show_rejected' => false,
3268
		));
3269
		if (is_array($fbdata))
3270
		{
3271
			foreach ($fbdata as $event)
3272
			{
3273
				if ($event['non_blocking']) continue;
3274
				if ($event['uid'] === $extra['X-CALENDARSERVER-MASK-UID']) continue;
3275
				if ($event['participants'][$user] == 'R') continue;
3276
3277
				$fbtype = $event['participants'][$user] == 'T' ? 'BUSY-TENTATIVE' : 'BUSY';
3278
3279
				if ($utc)
3280
				{
3281
					$vfreebusy->setAttribute('FREEBUSY',array(array(
3282
						'start' => $event['start'],
3283
						'end' => $event['end'],
3284
					)), array('FBTYPE' => $fbtype));
3285
				}
3286
				else
3287
				{
3288
					$vfreebusy->setAttribute('FREEBUSY',array(array(
3289
						'start' => date('Ymd\THis',$event['start']),
3290
						'end' => date('Ymd\THis',$event['end']),
3291
					)), array('FBTYPE' => $fbtype));
3292
				}
3293
			}
3294
		}
3295
		$vcal->addComponent($vfreebusy);
3296
3297
		return $vcal->exportvCalendar($charset);
3298
	}
3299
}
3300