Completed
Push — 14.2 ( 375f73...2f6b0f )
by Nathan
54:05 queued 24:07
created

calendar_boupdate::check_cat_acl()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 24
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 12
nc 6
nop 2
dl 0
loc 24
rs 6.7272
c 0
b 0
f 0
1
<?php
2
/**
3
 * EGroupware - Calendar's buisness-object - access + update
4
 *
5
 * @link http://www.egroupware.org
6
 * @package calendar
7
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
8
 * @author Joerg Lehrke <[email protected]>
9
 * @copyright (c) 2005-15 by RalfBecker-At-outdoor-training.de
10
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
11
 * @version $Id$
12
 */
13
14
// types of messsages send by calendar_boupdate::send_update
15
define('MSG_DELETED',0);
16
define('MSG_MODIFIED',1);
17
define('MSG_ADDED',2);
18
define('MSG_REJECTED',3);
19
define('MSG_TENTATIVE',4);
20
define('MSG_ACCEPTED',5);
21
define('MSG_ALARM',6);
22
define('MSG_DISINVITE',7);
23
define('MSG_DELEGATED',8);
24
25
/**
26
 * Class to access AND manipulate all calendar data (business object)
27
 *
28
 * The new UI, BO and SO classes have a strikt definition, in which time-zone they operate:
29
 *  UI only operates in user-time, so there have to be no conversation at all !!!
30
 *  BO's functions take and return user-time only (!), they convert internaly everything to servertime, because
31
 *  SO operates only on server-time
32
 *
33
 * As this BO class deals with dates/times of several types and timezone, each variable should have a postfix
34
 * appended, telling with type it is: _s = seconds, _su = secs in user-time, _ss = secs in server-time, _h = hours
35
 *
36
 * All new BO code (should be true for eGW in general) NEVER use any $_REQUEST ($_POST or $_GET) vars itself.
37
 * Nor does it store the state of any UI-elements (eg. cat-id selectbox). All this is the task of the UI class(es) !!!
38
 *
39
 * All permanent debug messages of the calendar-code should done via the debug-message method of the bocal class !!!
40
 */
41
42
class calendar_boupdate extends calendar_bo
43
{
44
	/**
45
	 * Category ACL allowing to add a given category
46
	 */
47
	const CAT_ACL_ADD = 512;
48
	/**
49
	 * Category ACL allowing to change status of a participant
50
	 */
51
	const CAT_ACL_STATUS = 1024;
52
53
	/**
54
	 * name of method to debug or level of debug-messages:
55
	 *	False=Off as higher as more messages you get ;-)
56
	 *	1 = function-calls incl. parameters to general functions like search, read, write, delete
57
	 *	2 = function-calls to exported helper-functions like check_perms
58
	 *	4 = function-calls to exported conversation-functions like date2ts, date2array, ...
59
	 *	5 = function-calls to private functions
60
	 * @var mixed
61
	 */
62
	var $debug;
63
64
	/**
65
	 * Set Logging
66
	 *
67
	 * @var boolean
68
	 */
69
	var $log = false;
70
	var $logfile = '/tmp/log-calendar-boupdate';
71
72
	/**
73
	 * Cached timezone data
74
	 *
75
	 * @var array id => data
76
	 */
77
	protected static $tz_cache = array();
78
79
	/**
80
	 * Constructor
81
	 */
82
	function __construct()
83
	{
84
		if ($this->debug > 0) $this->debug_message('calendar_boupdate::__construct() started',True);
85
86
		parent::__construct();	// calling the parent constructor
87
88
		if ($this->debug > 0) $this->debug_message('calendar_boupdate::__construct() finished',True);
89
	}
90
91
	/**
92
	 * updates or creates an event, it (optionaly) checks for conflicts and sends the necessary notifications
93
	 *
94
	 * @param array &$event event-array, on return some values might be changed due to set defaults
95
	 * @param boolean $ignore_conflicts =false just ignore conflicts or do a conflict check and return the conflicting events
96
	 * @param boolean $touch_modified =true NOT USED ANYMORE (was only used in old csv-import), modified&modifier is always updated!
97
	 * @param boolean $ignore_acl =false should we ignore the acl
98
	 * @param boolean $updateTS =true update the content history of the event
99
	 * @param array &$messages=null messages about because of missing ACL removed participants or categories
100
	 * @param boolean $skip_notification =false true: send NO notifications, default false = send them
101
	 * @return mixed on success: int $cal_id > 0, on error false or array with conflicting events (only if $check_conflicts)
102
	 * 		Please note: the events are not garantied to be readable by the user (no read grant or private)!
103
	 *
104
	 * @ToDo current conflict checking code does NOT cope quantity-wise correct with multiple non-overlapping
105
	 * 	events overlapping the event to store: the quantity sum is used, even as the events dont overlap!
106
	 *
107
	 * ++++++++ ++++++++
108
	 * +      + +  B   +	If A get checked for conflicts, the check used for resource quantity is
109
	 * +      + ++++++++
110
	 * +  A   +				quantity(A,resource)+quantity(B,resource)+quantity(C,resource) > maxBookable(resource)
111
	 * +      + ++++++++
112
	 * +      + +  C   +	which is clearly wrong for everything with a maximum quantity > 1
113
	 * ++++++++ ++++++++
114
	 */
115
	function update(&$event,$ignore_conflicts=false,$touch_modified=true,$ignore_acl=false,$updateTS=true,&$messages=null, $skip_notification=false)
116
	{
117
		//error_log(__METHOD__."(".array2string($event).",$ignore_conflicts,$touch_modified,$ignore_acl)");
118
		if (!is_array($messages)) $messages = $messages ? (array)$messages : array();
119
120
		if ($this->debug > 1 || $this->debug == 'update')
121
		{
122
			$this->debug_message('calendar_boupdate::update(%1,ignore_conflict=%2,touch_modified=%3,ignore_acl=%4)',
123
				false,$event,$ignore_conflicts,$touch_modified,$ignore_acl);
124
		}
125
		// check some minimum requirements:
126
		// - new events need start, end and title
127
		// - updated events cant set start, end or title to empty
128
		if (!$event['id'] && (!$event['start'] || !$event['end'] || !$event['title']) ||
129
			$event['id'] && (isset($event['start']) && !$event['start'] || isset($event['end']) && !$event['end'] ||
130
			isset($event['title']) && !$event['title']))
131
		{
132
			$messages[] = lang('Required information (start, end, title, ...) missing!');
133
			return false;
134
		}
135
136
		$status_reset_to_unknown = false;
137
138
		if (($new_event = !$event['id']))	// some defaults for new entries
139
		{
140
			// if no owner given, set user to owner
141
			if (!$event['owner']) $event['owner'] = $this->user;
142
			// set owner as participant if none is given
143
			if (!is_array($event['participants']) || !count($event['participants']))
144
			{
145
				$status = calendar_so::combine_status($event['owner'] == $this->user ? 'A' : 'U', 1, 'CHAIR');
146
				$event['participants'] = array($event['owner'] => $status);
147
			}
148
		}
149
150
		// check if user has the permission to update / create the event
151
		if (!$ignore_acl && (!$new_event && !$this->check_perms(EGW_ACL_EDIT,$event['id']) ||
152
			$new_event && !$this->check_perms(EGW_ACL_EDIT,0,$event['owner'])) &&
153
			!$this->check_perms(EGW_ACL_ADD,0,$event['owner']))
154
		{
155
			$messages[] = lang('Access to calendar of %1 denied!',common::grab_owner_name($event['owner']));
156
			return false;
157
		}
158
		if ($new_event)
159
		{
160
			$old_event = array();
161
		}
162
		else
163
		{
164
			$old_event = $this->read((int)$event['id'],null,$ignore_acl);
165
		}
166
167
		// do we need to check, if user is allowed to invite the invited participants
168
		if ($this->require_acl_invite && ($removed = $this->remove_no_acl_invite($event,$old_event)))
169
		{
170
			// report removed participants back to user
171
			foreach($removed as $key => $account_id)
172
			{
173
				$removed[$key] = $this->participant_name($account_id);
174
			}
175
			$messages[] = lang('%1 participants removed because of missing invite grants',count($removed)).
176
				': '.implode(', ',$removed);
177
		}
178
		// check category based ACL
179
		if ($event['category'])
180
		{
181 View Code Duplication
			if (!is_array($event['category'])) $event['category'] = explode(',',$event['category']);
182
			if (!$old_event || !isset($old_event['category']))
183
			{
184
				$old_event['category'] = array();
185
			}
186 View Code Duplication
			elseif (!is_array($old_event['category']))
187
			{
188
				$old_event['category'] = explode(',', $old_event['category']);
189
			}
190
			foreach($event['category'] as $key => $cat_id)
191
			{
192
				// check if user is allowed to update event categories
193
				if ((!$old_event || !in_array($cat_id,$old_event['category'])) &&
194
					self::has_cat_right(self::CAT_ACL_ADD,$cat_id,$this->user) === false)
195
				{
196
					unset($event['category'][$key]);
197
					// report removed category to user
198
					$removed_cats[$cat_id] = $this->categories->id2name($cat_id);
199
					continue;	// no further check, as cat was removed
200
				}
201
				// for new or moved events check status of participants, if no category status right --> set all status to 'U' = unknown
202
				if (!$status_reset_to_unknown &&
203
					self::has_cat_right(self::CAT_ACL_STATUS,$cat_id,$this->user) === false &&
204
					(!$old_event || $old_event['start'] != $event['start'] || $old_event['end'] != $event['end']))
205
				{
206
					foreach((array)$event['participants'] as $uid => $status)
207
					{
208
						$q = $r = null;
209
						calendar_so::split_status($status,$q,$r);
210
						if ($status != 'U')
211
						{
212
							$event['participants'][$uid] = calendar_so::combine_status('U',$q,$r);
213
							// todo: report reset status to user
214
						}
215
					}
216
					$status_reset_to_unknown = true;	// once is enough
217
				}
218
			}
219
			if ($removed_cats)
220
			{
221
				$messages[] = lang('Category %1 removed because of missing rights',implode(', ',$removed_cats));
222
			}
223
			if ($status_reset_to_unknown)
224
			{
225
				$messages[] = lang('Status of participants set to unknown because of missing category rights');
226
			}
227
		}
228
		// check for conflicts only happens !$ignore_conflicts AND if start + end date are given
229
		if (!$ignore_conflicts && !$event['non_blocking'] && isset($event['start']) && isset($event['end']))
230
		{
231
			$types_with_quantity = array();
232
			foreach($this->resources as $type => $data)
233
			{
234
				if ($data['max_quantity']) $types_with_quantity[] = $type;
235
			}
236
			// get all NOT rejected participants and evtl. their quantity
237
			$quantity = $users = array();
238
			foreach($event['participants'] as $uid => $status)
239
			{
240
				calendar_so::split_status($status,$q,$r);
241
				if ($status[0] == 'R') continue;	// ignore rejected participants
242
243 View Code Duplication
				if ($uid < 0)	// group, check it's members too
244
				{
245
					$users += (array)$GLOBALS['egw']->accounts->members($uid,true);
246
					$users = array_unique($users);
247
				}
248
				$users[] = $uid;
249
				if (in_array($uid[0],$types_with_quantity))
250
				{
251
					$quantity[$uid] = $q;
252
				}
253
			}
254
			//$start = microtime(true);
255
			$overlapping_events =& $this->search(array(
256
				'start' => $event['start'],
257
				'end'   => $event['end'],
258
				'users' => $users,
259
				'ignore_acl' => true,	// otherwise we get only events readable by the user
260
				'enum_groups' => true,	// otherwise group-events would not block time
261
				'query' => array(
262
					'cal_non_blocking' => 0,
263
				),
264
				'no_integration' => true,	// do NOT use integration of other apps
265
			));
266
			//error_log(__METHOD__."() conflict check took ".number_format(microtime(true)-$start, 3).'s');
267
			if ($this->debug > 2 || $this->debug == 'update')
268
			{
269
				$this->debug_message('calendar_boupdate::update() checking for potential overlapping events for users %1 from %2 to %3',false,$users,$event['start'],$event['end']);
270
			}
271
			$max_quantity = $possible_quantity_conflicts = $conflicts = array();
272 View Code Duplication
			foreach((array) $overlapping_events as $k => $overlap)
273
			{
274
				if ($overlap['id'] == $event['id'] ||	// that's the event itself
275
					$overlap['id'] == $event['reference'] ||	// event is an exception of overlap
276
					$overlap['non_blocking'])			// that's a non_blocking event
277
				{
278
					continue;
279
				}
280
				if ($this->debug > 3 || $this->debug == 'update')
281
				{
282
					$this->debug_message('calendar_boupdate::update() checking overlapping event %1',false,$overlap);
283
				}
284
				// check if the overlap is with a rejected participant or within the allowed quantity
285
				$common_parts = array_intersect($users,array_keys($overlap['participants']));
286
				foreach($common_parts as $n => $uid)
287
				{
288
					$status = $overlap['participants'][$uid];
289
					calendar_so::split_status($status, $q, $r);
290
					if ($status == 'R')
291
					{
292
						unset($common_parts[$n]);
293
						continue;
294
					}
295
					if (is_numeric($uid) || !in_array($uid[0],$types_with_quantity))
296
					{
297
						continue;	// no quantity check: quantity allways 1 ==> conflict
298
					}
299
					if (!isset($max_quantity[$uid]))
300
					{
301
						$res_info = $this->resource_info($uid);
302
						$max_quantity[$uid] = $res_info[$this->resources[$uid[0]]['max_quantity']];
303
					}
304
					$quantity[$uid] += $q;
305
					if ($quantity[$uid] <= $max_quantity[$uid])
306
					{
307
						$possible_quantity_conflicts[$uid][] =& $overlapping_events[$k];	// an other event can give the conflict
308
						unset($common_parts[$n]);
309
						continue;
310
					}
311
					// now we have a quantity conflict for $uid
312
				}
313
				if (count($common_parts))
314
				{
315
					if ($this->debug > 3 || $this->debug == 'update')
316
					{
317
						$this->debug_message('calendar_boupdate::update() conflicts with the following participants found %1',false,$common_parts);
318
					}
319
					$conflicts[$overlap['id'].'-'.$this->date2ts($overlap['start'])] =& $overlapping_events[$k];
320
				}
321
			}
322
			// check if we are withing the allowed quantity and if not add all events using that resource
323
			// seems this function is doing very strange things, it gives empty conflicts
324 View Code Duplication
			foreach($max_quantity as $uid => $max)
325
			{
326
				if ($quantity[$uid] > $max)
327
				{
328
					foreach((array)$possible_quantity_conflicts[$uid] as $conflict)
329
					{
330
						$conflicts[$conflict['id'].'-'.$this->date2ts($conflict['start'])] =& $possible_quantity_conflicts[$k];
0 ignored issues
show
Bug introduced by
The variable $k seems to be defined by a foreach iteration on line 272. 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...
331
					}
332
				}
333
			}
334
			unset($possible_quantity_conflicts);
335
336 View Code Duplication
			if (count($conflicts))
337
			{
338
				foreach($conflicts as $key => $conflict)
339
				{
340
						$conflict['participants'] = array_intersect_key((array)$conflict['participants'],$event['participants']);
341
					if (!$this->check_perms(EGW_ACL_READ,$conflict))
342
					{
343
						$conflicts[$key] = array(
344
							'id'    => $conflict['id'],
345
							'title' => lang('busy'),
346
							'participants' => $conflict['participants'],
347
							'start' => $conflict['start'],
348
							'end'   => $conflict['end'],
349
						);
350
					}
351
				}
352
				if ($this->debug > 2 || $this->debug == 'update')
353
				{
354
					$this->debug_message('calendar_boupdate::update() %1 conflicts found %2',false,count($conflicts),$conflicts);
355
				}
356
				return $conflicts;
357
			}
358
		}
359
360
		//echo "saving $event[id]="; _debug_array($event);
361
		$event2save = $event;
362
363
		if (!($cal_id = $this->save($event, $ignore_acl, $updateTS)))
364
		{
365
			return $cal_id;
366
		}
367
368
		$event = $this->read($cal_id);	// we re-read the event, in case only partial information was update and we need the full info for the notifies
369
		//echo "new $cal_id="; _debug_array($event);
370
371
		if($old_event['deleted'] && $event['deleted'] == null)
372
		{
373
			// Restored, bring back links
374
			egw_link::restore('calendar', $cal_id);
375
		}
376
		if ($this->log_file)
0 ignored issues
show
Bug introduced by
The property log_file does not seem to exist. Did you mean logfile?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
377
		{
378
			$this->log2file($event2save,$event,$old_event);
379
		}
380
		// send notifications
381
		if(!$skip_notification)
382
		{
383
			if ($new_event)
384
			{
385
				$this->send_update(MSG_ADDED,$event['participants'],'',$event);
386
			}
387
			else // update existing event
388
			{
389
				$this->check4update($event,$old_event);
390
			}
391
		}
392
393
		// notify the link-class about the update, as other apps may be subscribt to it
394
		egw_link::notify_update('calendar',$cal_id,$event);
395
396
		return $cal_id;
397
	}
398
	
399
	/**
400
	 * Check given event for conflicts and return them
401
	 *
402
	 * For recurring events we check a configurable fixed number of recurrences
403
	 * and for a fixed maximum time (default 3s).
404
	 *
405
	 * Conflict check skips past events/recurrences and is always limited by recurrence horizont,
406
	 * as it would only report non-recurring events after.
407
	 *
408
	 * @param array $event
409
	 * @param Api\DateTime& $checked_excluding =null time until which (excluding) recurrences have been checked
410
	 * @return array or events
411
	 */
412
	function conflicts(array $event, &$checked_excluding=null)
413
	{
414
		$types_with_quantity = array();
415
		foreach($this->resources as $type => $data)
416
		{
417
			if ($data['max_quantity']) $types_with_quantity[] = $type;
418
		}
419
		// get all NOT rejected participants and evtl. their quantity
420
		$quantity = $users = array();
421
		foreach($event['participants'] as $uid => $status)
422
		{
423
			$q = $r = null;
424
			calendar_so::split_status($status,$q,$r);
425
			if ($status[0] == 'R') continue;	// ignore rejected participants
426
427 View Code Duplication
			if ($uid < 0)	// group, check it's members too
428
			{
429
				$users = array_unique(array_merge($users, (array)$GLOBALS['egw']->accounts->members($uid,true)));
430
			}
431
			$users[] = $uid;
432
			if (in_array($uid[0],$types_with_quantity))
433
			{
434
				$quantity[$uid] = $q;
435
			}
436
		}
437
		$max_quantity = $possible_quantity_conflicts = $conflicts = array();
438
439
		if ($event['recur_type'])
440
		{
441
			$recurences = calendar_rrule::event2rrule($event);
442
		}
443
		else
444
		{
445
			$recurences = array(new egw_time((int)$event['start']));
446
		}
447
		$checked_excluding = null;
448
		$max_checked = $GLOBALS['egw_info']['server']['conflict_max_checked'];
449
		if (($max_check_time = (float)$GLOBALS['egw_info']['server']['conflict_max_check_time']) < 1.0)
450
		{
451
			$max_check_time = 3.0;
452
		}
453
		$checked = 0;
454
		$start = microtime(true);
455
		$duration = $event['end']-$event['start'];
456
		foreach($recurences as $date)
457
		{
458
			$startts = $date->format('ts');
459
460
			// skip past events or recurrences
461
			if ($startts+$duration < $this->now_su) continue;
462
463
			// abort check if configured limits are exceeded
464
			if ($event['recur_type'] &&
465
				(++$checked > $max_checked && $max_checked > 0 || // maximum number of checked recurrences exceeded
466
				microtime(true) > $start+$max_check_time ||	// max check time exceeded
467
				$startts > $this->config['horizont']))	// we are behind horizon for which recurrences are rendered
468
			{
469
				if ($this->debug > 2 || $this->debug == 'conflicts')
470
				{
471
					$this->debug_message(__METHOD__.'() conflict check limited to %1 recurrences, %2 seconds, until (excluding) %3',
472
						$checked, microtime(true)-$start, $date);
473
				}
474
				$checked_excluding = $date;
475
				break;
476
			}
477
			$overlapping_events =& $this->search(array(
478
				'start' => $startts,
479
				'end'   => $startts+$duration,
480
				'users' => $users,
481
				'ignore_acl' => true,	// otherwise we get only events readable by the user
482
				'enum_groups' => true,	// otherwise group-events would not block time
483
				'query' => array(
484
					'cal_non_blocking' => 0,
485
				),
486
				'no_integration' => true,	// do NOT use integration of other apps
487
			));
488
			if ($this->debug > 2 || $this->debug == 'conflicts')
489
			{
490
				$this->debug_message(__METHOD__.'() checking for potential overlapping events for users %1 from %2 to %3',false,$users,$startts,$startts+$duration);
491
			}
492 View Code Duplication
			foreach((array) $overlapping_events as $k => $overlap)
493
			{
494
				if ($overlap['id'] == $event['id'] ||	// that's the event itself
495
					$overlap['id'] == $event['reference'] ||	// event is an exception of overlap
496
					$overlap['non_blocking'])			// that's a non_blocking event
497
				{
498
					continue;
499
				}
500
				if ($this->debug > 3 || $this->debug == 'conflicts')
501
				{
502
					$this->debug_message(__METHOD__.'() checking overlapping event %1',false,$overlap);
503
				}
504
				// check if the overlap is with a rejected participant or within the allowed quantity
505
				$common_parts = array_intersect($users,array_keys($overlap['participants']));
506
				foreach($common_parts as $n => $uid)
507
				{
508
					$status = $overlap['participants'][$uid];
509
					calendar_so::split_status($status, $q, $r);
510
					if ($status == 'R')
511
					{
512
						unset($common_parts[$n]);
513
						continue;
514
					}
515
					if (is_numeric($uid) || !in_array($uid[0],$types_with_quantity))
516
					{
517
						continue;	// no quantity check: quantity allways 1 ==> conflict
518
					}
519
					if (!isset($max_quantity[$uid]))
520
					{
521
						$res_info = $this->resource_info($uid);
522
						$max_quantity[$uid] = $res_info[$this->resources[$uid[0]]['max_quantity']];
523
					}
524
					$quantity[$uid] += $q;
525
					if ($quantity[$uid] <= $max_quantity[$uid])
526
					{
527
						$possible_quantity_conflicts[$uid][] =& $overlapping_events[$k];	// an other event can give the conflict
528
						unset($common_parts[$n]);
529
						continue;
530
					}
531
					// now we have a quantity conflict for $uid
532
				}
533
				if (count($common_parts))
534
				{
535
					if ($this->debug > 3 || $this->debug == 'conflicts')
536
					{
537
						$this->debug_message(__METHOD__.'() conflicts with the following participants found %1',false,$common_parts);
538
					}
539
					$conflicts[$overlap['id'].'-'.$this->date2ts($overlap['start'])] =& $overlapping_events[$k];
540
				}
541
			}
542
		}
543
		//error_log(__METHOD__."() conflict check took ".number_format(microtime(true)-$start, 3).'s');
544
		// check if we are withing the allowed quantity and if not add all events using that resource
545
		// seems this function is doing very strange things, it gives empty conflicts
546 View Code Duplication
		foreach($max_quantity as $uid => $max)
547
		{
548
			if ($quantity[$uid] > $max)
549
			{
550
				foreach((array)$possible_quantity_conflicts[$uid] as $conflict)
551
				{
552
					$conflicts[$conflict['id'].'-'.$this->date2ts($conflict['start'])] =& $possible_quantity_conflicts[$k];
0 ignored issues
show
Bug introduced by
The variable $k seems to be defined by a foreach iteration on line 492. 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...
553
				}
554
			}
555
		}
556
		unset($possible_quantity_conflicts);
557
558 View Code Duplication
		if (count($conflicts))
559
		{
560
			foreach($conflicts as $key => $conflict)
561
			{
562
					$conflict['participants'] = array_intersect_key((array)$conflict['participants'],$event['participants']);
563
				if (!$this->check_perms(Acl::READ,$conflict))
564
				{
565
					$conflicts[$key] = array(
566
						'id'    => $conflict['id'],
567
						'title' => lang('busy'),
568
						'participants' => $conflict['participants'],
569
						'start' => $conflict['start'],
570
						'end'   => $conflict['end'],
571
					);
572
				}
573
			}
574
			if ($this->debug > 2 || $this->debug == 'conflicts')
575
			{
576
				$this->debug_message(__METHOD__.'() %1 conflicts found %2',false,count($conflicts),$conflicts);
577
			}
578
		}
579
		return $conflicts;
580
	}
581
582
	/**
583
	 * Remove participants current user has no right to invite
584
	 *
585
	 * @param array &$event new event
586
	 * @param array $old_event =null old event with already invited participants
587
	 * @return array removed participants because of missing invite grants
588
	 */
589
	public function remove_no_acl_invite(array &$event,array $old_event=null)
590
	{
591
		if (!$this->require_acl_invite)
592
		{
593
			return array();	// nothing to check, everyone can invite everyone else
594
		}
595
		if ($event['id'] && is_null($old_event))
596
		{
597
			$old_event = $this->read($event['id']);
598
		}
599
		$removed = array();
600
		foreach(array_keys((array)$event['participants']) as $uid)
601
		{
602
			if ((is_null($old_event) || !isset($old_event['participants'][$uid])) && !$this->check_acl_invite($uid))
603
			{
604
				unset($event['participants'][$uid]);	// remove participant
605
				$removed[] = $uid;
606
			}
607
		}
608
		//echo "<p>".__METHOD__."($event[title],".($old_event?'$old_event':'NULL').") returning ".array2string($removed)."</p>";
609
		return $removed;
610
	}
611
612
	/**
613
	 * Check if current user is allowed to invite a given participant
614
	 *
615
	 * @param int|string $uid
616
	 * @return boolean
617
	 */
618
	public function check_acl_invite($uid)
619
	{
620
		if (!is_numeric($uid)) return true;	// nothing implemented for resources so far
621
622
		if (!$this->require_acl_invite)
623
		{
624
			$ret = true;	// no grant required
625
		}
626
		elseif ($this->require_acl_invite == 'groups' && $GLOBALS['egw']->accounts->get_type($uid) != 'g')
627
		{
628
			$ret = true;	// grant only required for groups
629
		}
630
		else
631
		{
632
			$ret = $this->check_perms(EGW_ACL_INVITE,0,$uid);
633
		}
634
		//error_log(__METHOD__."($uid) = ".array2string($ret));
635
		//echo "<p>".__METHOD__."($uid) require_acl_invite=$this->require_acl_invite returning ".array2string($ret)."</p>\n";
636
		return $ret;
637
	}
638
639
	/**
640
	 * Check for added, modified or deleted participants AND notify them
641
	 *
642
	 * @param array $new_event the updated event
643
	 * @param array $old_event the event before the update
644
	 * @todo check if there is a real change, not assume every save is a change
645
	 */
646
	function check4update($new_event,$old_event)
647
	{
648
		//error_log(__METHOD__."($new_event[title])");
649
		$modified = $added = $deleted = array();
650
651
		//echo "<p>calendar_boupdate::check4update() new participants = ".print_r($new_event['participants'],true).", old participants =".print_r($old_event['participants'],true)."</p>\n";
652
653
		// Find modified and deleted participants ...
654
		foreach($old_event['participants'] as $old_userid => $old_status)
655
		{
656
			if(isset($new_event['participants'][$old_userid]))
657
			{
658
				$modified[$old_userid] = $new_event['participants'][$old_userid];
659
			}
660
			else
661
			{
662
				$deleted[$old_userid] = $old_status;
663
			}
664
		}
665
		// Find new participants ...
666
		foreach(array_keys((array)$new_event['participants']) as $new_userid)
667
		{
668
			if(!isset($old_event['participants'][$new_userid]))
669
			{
670
				$added[$new_userid] = 'U';
671
			}
672
		}
673
		//echo "<p>calendar_boupdate::check4update() added=".print_r($added,true).", modified=".print_r($modified,true).", deleted=".print_r($deleted,true)."</p>\n";
674
		if(count($added) || count($modified) || count($deleted))
675
		{
676
			if(count($added))
677
			{
678
				$this->send_update(MSG_ADDED,$added,$old_event,$new_event);
679
			}
680
			if(count($modified))
681
			{
682
				$this->send_update(MSG_MODIFIED,$modified,$old_event,$new_event);
683
			}
684
			if(count($deleted))
685
			{
686
				$this->send_update(MSG_DISINVITE,$deleted,$new_event);
687
			}
688
		}
689
	}
690
691
	/**
692
	 * checks if $userid has requested (in $part_prefs) updates for $msg_type
693
	 *
694
	 * @param int $userid numerical user-id
695
	 * @param array $part_prefs preferces of the user $userid
696
	 * @param int &$msg_type type of the notification: MSG_ADDED, MSG_MODIFIED, MSG_ACCEPTED, ...
697
	 * @param array $old_event Event before the change
698
	 * @param array $new_event Event after the change
699
	 * @param string $role we treat CHAIR like event owners
700
	 * @param string $status of current user
701
	 * @return boolean true = update requested, false otherwise
702
	 */
703
	public static function update_requested($userid, $part_prefs, &$msg_type, $old_event ,$new_event, $role, $status=null)
704
	{
705
		if ($msg_type == MSG_ALARM)
706
		{
707
			return True;	// always True for now
708
		}
709
		$want_update = 0;
710
711
		// the following switch falls through all cases, as each included the following too
712
		//
713
		$msg_is_response = $msg_type == MSG_REJECTED || $msg_type == MSG_ACCEPTED || $msg_type == MSG_TENTATIVE || $msg_type == MSG_DELEGATED;
714
715
		switch($ru = $part_prefs['calendar']['receive_updates'])
716
		{
717
			case 'responses':
718
				++$want_update;
719
			case 'modifications':
720
				if (!$msg_is_response)
721
				{
722
					++$want_update;
723
				}
724
			case 'time_change_4h':
725
			case 'time_change':
726
			default:
727
				if (is_array($new_event) && is_array($old_event))
728
				{
729
					$diff = max(abs(self::date2ts($old_event['start'])-self::date2ts($new_event['start'])),
730
						abs(self::date2ts($old_event['end'])-self::date2ts($new_event['end'])));
731
					$check = $ru == 'time_change_4h' ? 4 * 60 * 60 - 1 : 0;
732
					if ($msg_type == MSG_MODIFIED && $diff > $check)
733
					{
734
						++$want_update;
735
					}
736
				}
737
			case 'add_cancel':
0 ignored issues
show
Unused Code introduced by
case 'add_cancel': i...pdate; } 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...
738
				if ($msg_is_response && ($old_event['owner'] == $userid || $role == 'CHAIR') ||
739
					$msg_type == MSG_DELETED || $msg_type == MSG_ADDED || $msg_type == MSG_DISINVITE)
740
				{
741
					++$want_update;
742
				}
743
				break;
744
			case 'no':
745
				// always notify externals chairs
746
				// EGroupware owner only get notified about responses, if pref is NOT "no"
747
				if (!is_numeric($userid) && $role == 'CHAIR' &&
748
					($msg_is_response || in_array($msg_type, array(MSG_ADDED, MSG_DELETED))))
749
				{
750
					switch($msg_type)
751
					{
752
						case MSG_DELETED:	// treat deleting event as rejection to organizer
753
							$msg_type = MSG_REJECTED;
754
							break;
755
						case MSG_ADDED:		// new events use added, but organizer needs status
756
							switch($status[0])
757
							{
758
								case 'A': $msg_type = MSG_ACCEPTED; break;
759
								case 'R': $msg_type = MSG_REJECTED; break;
760
								case 'T': $msg_type = MSG_TENTATIVE; break;
761
								case 'D': $msg_type = MSG_DELEGATED; break;
762
							}
763
							break;
764
					}
765
					++$want_update;
766
				}
767
				break;
768
		}
769
		//error_log(__METHOD__."(userid=$userid, receive_updates='$ru', msg_type=$msg_type, ..., role='$role') msg_is_response=$msg_is_response --> want_update=$want_update");
770
		return $want_update > 0;
771
	}
772
773
	/**
774
	 * Check calendar prefs, if a given user (integer account_id) or email (user or externals) should get notified
775
	 *
776
	 * @param int|string $user_or_email
777
	 * @param string $ical_method ='REQUEST'
778
	 * @param string $role ='REQ-PARTICIPANT'
779
	 * @return boolean true if user requested to be notified, false if not
780
	 */
781
	static public function email_update_requested($user_or_email, $ical_method='REQUEST', $role='REQ-PARTICIPANT')
782
	{
783
		// check if email is from a user
784
		if (is_numeric($user_or_email))
785
		{
786
			$account_id = $user_or_email;
787
		}
788
		else
789
		{
790
			$account_id = $GLOBALS['egw']->accounts->name2id($user_or_email, 'account_email');
791
		}
792
		if ($account_id)
793
		{
794
			$pref_obj = new preferences($account_id);
795
			$prefs = $pref_obj->read_repository();
796
		}
797
		else
798
		{
799
			$prefs = array(
800
				'calendar' => array(
801
					'receive_updates' => $GLOBALS['egw_info']['user']['preferences']['calendar']['notify_externals'],
802
				)
803
			);
804
		}
805
		switch($ical_method)
806
		{
807
			default:
808
			case 'REQUEST':
809
				$msg_type = MSG_ADDED;
810
				break;
811
			case 'REPLY':
812
				$msg_type = MSG_ACCEPTED;
813
				break;
814
			case 'CANCEL':
815
				$msg_type = MSG_DELETED;
816
				break;
817
		}
818
		$ret = self::update_requested($account_id, $prefs, $msg_type, array(), array(), $role);
819
		//error_log(__METHOD__."('$user_or_email', '$ical_method', '$role') account_id=$account_id --> updated_requested returned ".array2string($ret));
820
		return $ret;
821
	}
822
823
	/**
824
	 * Get iCal/iMip method from internal nummeric msg-type plus optional notification message and verbose name
825
	 *
826
	 * @param int $msg_type see MSG_* defines
827
	 * @param string& $action=null on return verbose name
828
	 * @param string& $msg=null on return notification message
829
	 */
830
	function msg_type2ical_method($msg_type, &$action=null, &$msg=null)
831
	{
832
		switch($msg_type)
833
		{
834
			case MSG_DELETED:
835
				$action = 'Canceled';
836
				$pref = 'Canceled';
837
				$method = 'CANCEL';
838
				break;
839
			case MSG_MODIFIED:
840
				$action = 'Modified';
841
				$pref = 'Modified';
842
				$method = 'REQUEST';
843
				break;
844
			case MSG_DISINVITE:
845
				$action = 'Disinvited';
846
				$pref = 'Disinvited';
847
				$method = 'CANCEL';
848
				break;
849
			case MSG_ADDED:
850
				$action = 'Added';
851
				$pref = 'Added';
852
				$method = 'REQUEST';
853
				break;
854
			case MSG_REJECTED:
855
				$action = 'Rejected';
856
				$pref = 'Response';
857
				$method = 'REPLY';
858
				break;
859
			case MSG_TENTATIVE:
860
				$action = 'Tentative';
861
				$pref = 'Response';
862
				$method = 'REPLY';
863
				break;
864
			case MSG_ACCEPTED:
865
				$action = 'Accepted';
866
				$pref = 'Response';
867
				$method = 'REPLY';
868
				break;
869
			case MSG_DELEGATED:
870
				$action = 'Delegated';
871
				$pref = 'Response';
872
				$method = 'REPLY';
873
				break;
874
			case MSG_ALARM:
875
				$action = 'Alarm';
876
				$pref = 'Alarm';
877
				break;
878
			default:
879
				$method = 'PUBLISH';
880
		}
881
		$msg = $this->cal_prefs['notify'.$pref];
882
		if (empty($msg))
883
		{
884
			$msg = $this->cal_prefs['notifyAdded'];	// use a default
885
		}
886
		//error_log(__METHOD__."($msg_type) action='$action', $msg='$msg' returning '$method'");
887
		return $method;
888
	}
889
890
	/**
891
	 * sends update-messages to certain participants of an event
892
	 *
893
	 * @param int $msg_type type of the notification: MSG_ADDED, MSG_MODIFIED, MSG_ACCEPTED, ...
894
	 * @param array $to_notify numerical user-ids as keys (!) (value is not used)
895
	 * @param array $old_event Event before the change
896
	 * @param array $new_event =null Event after the change
897
	 * @param int $user =0 User who started the notify, default current user
898
	 * @return bool true/false
899
	 */
900
	function send_update($msg_type,$to_notify,$old_event,$new_event=null,$user=0)
901
	{
902
		//error_log(__METHOD__."($msg_type,".array2string($to_notify).",...) ".array2string($new_event));
903
		if (!is_array($to_notify))
904
		{
905
			$to_notify = array();
906
		}
907
		$disinvited = $msg_type == MSG_DISINVITE ? array_keys($to_notify) : array();
908
909
		$owner = $old_event ? $old_event['owner'] : $new_event['owner'];
910
		if ($owner && !isset($to_notify[$owner]) && $msg_type != MSG_ALARM)
911
		{
912
			$to_notify[$owner] = 'OCHAIR';	// always include the event-owner
913
		}
914
915
		// ignore events in the past (give a tolerance of 10 seconds for the script)
916
		if($old_event && $this->date2ts($old_event['start']) < ($this->now_su - 10))
917
		{
918
			return False;
919
		}
920
		// check if default timezone is set correctly to server-timezone (ical-parser messes with it!!!)
921
		if ($GLOBALS['egw_info']['server']['server_timezone'] && ($tz = date_default_timezone_get()) != $GLOBALS['egw_info']['server']['server_timezone'])
922
		{
923
			$restore_tz = $tz;
924
			date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']);
925
		}
926
		$temp_user = $GLOBALS['egw_info']['user'];	// save user-date of the enviroment to restore it after
927
928
		if (!$user)
929
		{
930
			$user = $temp_user['account_id'];
931
		}
932
		$lang = $GLOBALS['egw_info']['user']['preferences']['common']['lang'];
933 View Code Duplication
		if ($GLOBALS['egw']->preferences->account_id != $user)
934
		{
935
			$GLOBALS['egw']->preferences->__construct($user);
936
			$GLOBALS['egw_info']['user']['preferences'] = $GLOBALS['egw']->preferences->read_repository();
937
		}
938
		$senderid = $this->user;
939
		$event = $msg_type == MSG_ADDED || $msg_type == MSG_MODIFIED ? $new_event : $old_event;
940
941
		// add all group-members to the notification, unless they are already participants
942
		foreach($to_notify as $userid => $statusid)
943
		{
944
			if (is_numeric($userid) && $GLOBALS['egw']->accounts->get_type($userid) == 'g' &&
945
				($members = $GLOBALS['egw']->accounts->member($userid)))
946
			{
947
				foreach($members as $member)
948
				{
949
					$member = $member['account_id'];
950
					if (!isset($to_notify[$member]))
951
					{
952
						$to_notify[$member] = 'G';	// Group-invitation
953
					}
954
				}
955
			}
956
		}
957
		$user_prefs = $GLOBALS['egw_info']['user']['preferences'];
958
		$startdate = new egw_time($event['start']);
959
		$enddate = new egw_time($event['end']);
960
		$modified = new egw_time($event['modified']);
961
		if ($old_event) $olddate = new egw_time($old_event['start']);
962
		//error_log(__METHOD__."() date_default_timezone_get()=".date_default_timezone_get().", user-timezone=".egw_time::$user_timezone->getName().", startdate=".$startdate->format().", enddate=".$enddate->format().", updated=".$modified->format().", olddate=".($olddate ? $olddate->format() : ''));
963
		$owner_prefs = $ics = null;
964
		foreach($to_notify as $userid => $statusid)
965
		{
966
			$res_info = $quantity = $role = null;
967
			calendar_so::split_status($statusid, $quantity, $role);
968
			if ($this->debug > 0) error_log(__METHOD__." trying to notify $userid, with $statusid ($role)");
969
970
			if (!is_numeric($userid))
971
			{
972
				$res_info = $this->resource_info($userid);
973
974
				// check if responsible of a resource has read rights on event (might be private!)
975
				if ($res_info['app'] == 'resources' && $res_info['responsible'] &&
976
					!$this->check_perms(EGW_ACL_READ, $event, 0, 'ts', null, $res_info['responsible']))
977
				{
978
					// --> use only details from (private-)cleared event only containing resource ($userid)
979
					// reading server timezone, to be able to use cleared event for iCal generation further down
980
					$cleared_event = $this->read($event['id'], null, true, 'server');
981
					$this->clear_private_infos($cleared_event, array($userid));
982
				}
983
				$userid = $res_info['responsible'];
984
985
				if (!isset($userid))
986
				{
987
					if (empty($res_info['email'])) continue;	// no way to notify
988
					// check if event-owner wants non-EGroupware users notified
989
					if (is_null($owner_prefs))
990
					{
991
						$preferences = new preferences($owner);
992
						$owner_prefs = $preferences->read_repository();
993
					}
994
					if ($role != 'CHAIR' &&		// always notify externals CHAIRs
995
						(empty($owner_prefs['calendar']['notify_externals']) ||
996
						$owner_prefs['calendar']['notify_externals'] == 'no'))
997
					{
998
						continue;
999
					}
1000
					$userid = $res_info['email'];
1001
				}
1002
			}
1003
1004
			if ($statusid == 'R' || $GLOBALS['egw']->accounts->get_type($userid) == 'g')
1005
			{
1006
				continue;	// dont notify rejected participants or groups
1007
			}
1008
1009
			if($userid != $GLOBALS['egw_info']['user']['account_id'] ||
1010
				($userid == $GLOBALS['egw_info']['user']['account_id'] &&
1011
					$user_prefs['calendar']['receive_own_updates']==1) ||
1012
				$msg_type == MSG_ALARM)
1013
			{
1014
				$tfn = $tln = $lid = null; //cleanup of lastname and fullname (in case they are set in a previous loop)
1015
				if (is_numeric($userid))
1016
				{
1017
					$preferences = new preferences($userid);
1018
					$GLOBALS['egw_info']['user']['preferences'] = $part_prefs = $preferences->read_repository();
1019
					$GLOBALS['egw']->accounts->get_account_name($userid,$lid,$tfn,$tln);
1020
					$fullname = common::display_fullname('',$tfn,$tln);
1021
				}
1022
				else	// external email address: use preferences of event-owner, plus some hardcoded settings (eg. ical notification)
1023
				{
1024
					if (is_null($owner_prefs))
1025
					{
1026
						$preferences = new preferences($owner);
1027
						$GLOBALS['egw_info']['user']['preferences'] = $owner_prefs = $preferences->read_repository();
1028
					}
1029
					$part_prefs = $owner_prefs;
1030
					$part_prefs['calendar']['receive_updates'] = $owner_prefs['calendar']['notify_externals'];
1031
					$part_prefs['calendar']['update_format'] = 'ical';	// use ical format
1032
					$fullname = $res_info && !empty($res_info['name']) ? $res_info['name'] : $userid;
1033
				}
1034
				$m_type = $msg_type;
1035
				if (!self::update_requested($userid, $part_prefs, $m_type, $old_event, $new_event, $role,
0 ignored issues
show
Bug introduced by
It seems like $new_event defined by parameter $new_event on line 900 can also be of type null; however, calendar_boupdate::update_requested() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1036
					$event['participants'][$GLOBALS['egw_info']['user']['account_id']]))
1037
				{
1038
					continue;
1039
				}
1040
				$action = $notify_msg = null;
1041
				$method = $this->msg_type2ical_method($m_type, $action, $notify_msg);
1042
1043
				if ($lang !== $part_prefs['common']['lang'])
1044
				{
1045
					translation::init();
1046
					$lang = $part_prefs['common']['lang'];
1047
				}
1048
				$event_arr = null;
1049
				$details = $this->_get_event_details(isset($cleared_event) ? $cleared_event : $event,
1050
					$action, $event_arr, $disinvited);
1051
				$details['to-fullname'] = $fullname;
1052
				if (isset($tfn)) $details['to-firstname'] = $tfn;
1053
				if (isset($tln)) $details['to-lastname'] = $tln;
1054
1055
				// event is in user-time of current user, now we need to calculate the tz-difference to the notified user and take it into account
1056 View Code Duplication
				if (!isset($part_prefs['common']['tz'])) $part_prefs['common']['tz'] = $GLOBALS['egw_info']['server']['server_timezone'];
1057
				$timezone = new DateTimeZone($part_prefs['common']['tz']);
1058
				$timeformat = $part_prefs['common']['timeformat'];
1059
				switch($timeformat)
1060
				{
1061
			  		case '24':
1062
						$timeformat = 'H:i';
1063
						break;
1064
					case '12':
1065
						$timeformat = 'h:i a';
1066
						break;
1067
				}
1068
				$timeformat = $part_prefs['common']['dateformat'] . ', ' . $timeformat;
1069
1070
				$startdate->setTimezone($timezone);
1071
				$details['startdate'] = $startdate->format($timeformat);
1072
1073
				$enddate->setTimezone($timezone);
1074
				$details['enddate'] = $enddate->format($timeformat);
1075
1076
				$modified->setTimezone($timezone);
1077
				$details['updated'] = $modified->format($timeformat) . ', ' . common::grab_owner_name($event['modifier']);
1078
1079
				if ($old_event != False)
1080
				{
1081
					$olddate->setTimezone($timezone);
1082
					$details['olddate'] = $olddate->format($timeformat);
1083
				}
1084
				//error_log(__METHOD__."() userid=$userid, timezone=".$timezone->getName().", startdate=$details[startdate], enddate=$details[enddate], updated=$details[updated], olddate=$details[olddate]");
1085
1086
				list($subject,$notify_body) = explode("\n",$GLOBALS['egw']->preferences->parse_notify($notify_msg,$details),2);
1087
				// alarm is NOT an iCal method, therefore we have to use extened (no iCal)
1088
				switch($msg_type == MSG_ALARM ? 'extended' : $part_prefs['calendar']['update_format'])
1089
				{
1090
					case 'ical':
1091
						if (is_null($ics) || $m_type != $msg_type)	// need different ical for organizer notification
1092
						{
1093
							$calendar_ical = new calendar_ical();
1094
							$calendar_ical->setSupportedFields('full');	// full iCal fields+event TZ
1095
							// we need to pass $event[id] so iCal class reads event again,
1096
							// as event is in user TZ, but iCal class expects server TZ!
1097
							$ics = $calendar_ical->exportVCal(array(isset($cleared_event) ? $cleared_event : $event['id']),
1098
								'2.0', $method, 0, '', 'utf-8', $method == 'REPLY' ? $user : 0);
1099
							unset($calendar_ical);
1100
						}
1101
						$attachment = array(
1102
							'string' => $ics,
1103
							'filename' => 'cal.ics',
1104
							'encoding' => '8bit',
1105
							'type' => 'text/calendar; method='.$method,
1106
						);
1107
						if ($m_type != $msg_type) unset($ics);
1108
						$subject = isset($cleared_event) ? $cleared_event['title'] : $event['title'];
1109
						// fall through
1110
					case 'extended':
1111
1112
						$details_body = lang('Event Details follow').":\n";
1113
						foreach($event_arr as $key => $val)
1114
						{
1115
							if(!empty($details[$key]))
1116
							{
1117
								switch($key)
1118
								{
1119
							 		case 'access':
1120
									case 'priority':
1121
									case 'link':
1122
									case 'description':
1123
									case 'title':
1124
										break;
1125
									default:
1126
										$details_body .= sprintf("%-20s %s\n",$val['field'].':',$details[$key]);
1127
										break;
1128
							 	}
1129
							}
1130
						}
1131
						break;
1132
				}
1133
				// send via notification_app
1134
				if($GLOBALS['egw_info']['apps']['notifications']['enabled'])
1135
				{
1136
					try {
1137
						//error_log(__METHOD__."() notifying $userid from $senderid: $subject");
1138
						$notification = new notifications();
1139
						$notification->set_receivers(array($userid));
1140
						$notification->set_sender($senderid);
1141
						$notification->set_subject($subject);
1142
						// as we want ical body to be just description, we can NOT set links, as they get appended to body
1143
						if ($part_prefs['calendar']['update_format'] != 'ical')
1144
						{
1145
							$notification->set_message($notify_body."\n\n".$details['description']."\n\n".$details_body);
1146
							$notification->set_links(array($details['link_arr']));
1147
						}
1148
						else
1149
						{
1150
							// iCal: description need to be separated from body by fancy separator
1151
							$notification->set_message($notify_body."\n\n".$details_body."\n*~*~*~*~*~*~*~*~*~*\n\n".$details['description']);
1152
						}
1153
						// popup notifiactions: set subject, different message (without separator) and (always) links
1154
						$notification->set_popupsubject($subject);
1155
						$notification->set_popupmessage($notify_body."\n\n".$details['description']."\n\n".$details_body);
1156
						$notification->set_popuplinks(array($details['link_arr']));
1157
1158
						if(is_array($attachment)) { $notification->set_attachments(array($attachment)); }
1159
						$notification->send();
1160
					}
1161
					catch (Exception $exception) {
1162
						error_log(__METHOD__.' error while notifying user '.$userid.':'.$exception->getMessage());
1163
						continue;
1164
					}
1165
				}
1166
				else
1167
				{
1168
					error_log(__METHOD__.' cannot send any notifications because notifications is not installed');
1169
				}
1170
			}
1171
		}
1172
		// restore the enviroment (preferences->read_repository() sets the timezone!)
1173
		$GLOBALS['egw_info']['user'] = $temp_user;
1174 View Code Duplication
		if ($GLOBALS['egw']->preferences->account_id != $temp_user['account_id'])
1175
		{
1176
			$GLOBALS['egw']->preferences->__construct($temp_user['account_id']);
1177
			$GLOBALS['egw_info']['user']['preferences'] = $GLOBALS['egw']->preferences->read_repository();
1178
			//echo "<p>".__METHOD__."() restored enviroment of #$temp_user[account_id] $temp_user[account_fullname]: tz={$GLOBALS['egw_info']['user']['preferences']['common']['tz']}</p>\n";
1179
		}
1180 View Code Duplication
		if ($lang !== $GLOBALS['egw_info']['user']['preferences']['common']['lang'])
1181
		{
1182
			translation::init();
1183
		}
1184
		// restore timezone, in case we had to reset it to server-timezone
1185
		if ($restore_tz) date_default_timezone_set($restore_tz);
1186
1187
		return true;
1188
	}
1189
1190
	function get_update_message($event,$added)
1191
	{
1192
		$nul = null;
1193
		$details = $this->_get_event_details($event,$added ? lang('Added') : lang('Modified'),$nul);
1194
1195
		$notify_msg = $this->cal_prefs[$added || empty($this->cal_prefs['notifyModified']) ? 'notifyAdded' : 'notifyModified'];
1196
1197
		return explode("\n",$GLOBALS['egw']->preferences->parse_notify($notify_msg,$details),2);
1198
	}
1199
1200
	/**
1201
	 * Function called via async service, when an alarm is to be send
1202
	 *
1203
	 * @param array $alarm array with keys owner, cal_id, all
1204
	 * @return boolean
1205
	 */
1206
	function send_alarm($alarm)
1207
	{
1208
		//echo "<p>bocalendar::send_alarm("; print_r($alarm); echo ")</p>\n";
1209
		$GLOBALS['egw_info']['user']['account_id'] = $this->owner = $alarm['owner'];
1210
1211
		$event_time_user = egw_time::server2user($alarm['time'] + $alarm['offset']);	// alarm[time] is in server-time, read requires user-time
1212
		if (!$alarm['owner'] || !$alarm['cal_id'] || !($event = $this->read($alarm['cal_id'],$event_time_user)))
1213
		{
1214
			return False;	// event not found
1215
		}
1216
		if ($alarm['all'])
1217
		{
1218
			$to_notify = $event['participants'];
1219
		}
1220
		elseif ($this->check_perms(EGW_ACL_READ,$event))	// checks agains $this->owner set to $alarm[owner]
1221
		{
1222
			$to_notify[$alarm['owner']] = 'A';
1223
		}
1224
		else
1225
		{
1226
			return False;	// no rights
1227
		}
1228
		// need to load calendar translations and set currentapp, so calendar can reload a different lang
1229
		translation::add_app('calendar');
1230
		$GLOBALS['egw_info']['flags']['currentapp'] = 'calendar';
1231
1232
		$ret = $this->send_update(MSG_ALARM,$to_notify,$event,False,$alarm['owner']);
1233
1234
		// create a new alarm for recuring events for the next event, if one exists
1235
		if ($event['recur_type'] != MCAL_RECUR_NONE && ($event = $this->read($alarm['cal_id'],$event_time_user+1)))
1236
		{
1237
			$alarm['time'] = $this->date2ts($event['start']) - $alarm['offset'];
1238
			unset($alarm['times']);
1239
			unset($alarm['next']);
1240
			//error_log(__METHOD__."() moving alarm to next recurrence ".array2string($alarm));
1241
			$this->save_alarm($alarm['cal_id'], $alarm, false);	// false = do NOT update timestamp, as nothing changed for iCal clients
1242
		}
1243
		return $ret;
1244
	}
1245
1246
	/**
1247
	 * saves an event to the database, does NOT do any notifications, see calendar_boupdate::update for that
1248
	 *
1249
	 * This methode converts from user to server time and handles the insertion of users and dates of repeating events
1250
	 *
1251
	 * @param array $event
1252
	 * @param boolean $ignore_acl =false should we ignore the acl
1253
	 * @param boolean $updateTS =true update the content history of the event
1254
	 * DEPRECATED: we allways (have to) update timestamp, as they are required for sync!
1255
	 * @return int|boolean $cal_id > 0 or false on error (eg. permission denied)
1256
	 */
1257
	function save($event,$ignore_acl=false,$updateTS=true)
1258
	{
1259
		unset($updateTS);
1260
		//error_log(__METHOD__.'('.array2string($event).", $ignore_acl, $updateTS)");
1261
1262
		// check if user has the permission to update / create the event
1263 View Code Duplication
		if (!$ignore_acl && ($event['id'] && !$this->check_perms(EGW_ACL_EDIT,$event['id']) ||
1264
			!$event['id'] && !$this->check_perms(EGW_ACL_EDIT,0,$event['owner']) &&
1265
			!$this->check_perms(EGW_ACL_ADD,0,$event['owner'])))
1266
		{
1267
			return false;
1268
		}
1269
1270
		if ($event['id'])
1271
		{
1272
			// invalidate the read-cache if it contains the event we store now
1273
			if ($event['id'] == self::$cached_event['id']) self::$cached_event = array();
1274
			$old_event = $this->read($event['id'], $event['recurrence'], false, 'server');
1275
		}
1276
		else
1277
		{
1278
			$old_event = null;
1279
		}
1280
1281
		if (!isset($event['whole_day'])) $event['whole_day'] = $this->isWholeDay($event);
1282
		$save_event = $event;
1283
		if ($event['whole_day'])
1284
		{
1285
			if (!empty($event['start']))
1286
			{
1287
				$time = $this->so->startOfDay(new egw_time($event['start'], egw_time::$user_timezone));
1288
				$event['start'] = egw_time::to($time, 'ts');
1289
				$save_event['start'] = $time;
1290
			}
1291 View Code Duplication
			if (!empty($event['end']))
1292
			{
1293
				$time = new egw_time($event['end'], egw_time::$user_timezone);
1294
				$time->setTime(23, 59, 59);
1295
				$event['end'] = egw_time::to($time, 'ts');
1296
				$save_event['end'] = $time;
1297
			}
1298 View Code Duplication
			if (!empty($event['recurrence']))
1299
			{
1300
				$time = $this->so->startOfDay(new egw_time($event['recurrence'], egw_time::$user_timezone));
1301
				$event['recurrence'] = egw_time::to($time, 'ts');
1302
			}
1303 View Code Duplication
			if (!empty($event['recur_enddate']))
1304
			{
1305
				$time = $this->so->startOfDay(new egw_time($event['recur_enddate'], egw_time::$user_timezone));
1306
				$event['recur_enddate'] = egw_time::to($time, 'ts');
1307
				$time->setUser();
1308
				$save_event['recur_enddate'] = egw_time::to($time, 'ts');
1309
			}
1310
			$timestamps = array('modified','created');
1311
			// all-day events are handled in server time
1312
			$event['tzid'] = $save_event['tzid'] = egw_time::$server_timezone->getName();
1313
		}
1314 View Code Duplication
		else
1315
		{
1316
			$timestamps = array('start','end','modified','created','recur_enddate','recurrence');
1317
		}
1318
		// we run all dates through date2ts, to adjust to server-time and the possible date-formats
1319
		foreach($timestamps as $ts)
1320
		{
1321
			// we convert here from user-time to timestamps in server-time!
1322
			if (isset($event[$ts])) $event[$ts] = $event[$ts] ? $this->date2ts($event[$ts],true) : 0;
1323
		}
1324
		// convert tzid name to integer tz_id, of set user default
1325 View Code Duplication
		if (empty($event['tzid']) || !($event['tz_id'] = calendar_timezones::tz2id($event['tzid'])))
1326
		{
1327
			$event['tz_id'] = calendar_timezones::tz2id($event['tzid'] = egw_time::$user_timezone->getName());
1328
		}
1329
		// same with the recur exceptions
1330
		if (isset($event['recur_exception']) && is_array($event['recur_exception']))
1331
		{
1332
			foreach($event['recur_exception'] as &$date)
1333
			{
1334
				if ($event['whole_day'])
1335
				{
1336
					$time = $this->so->startOfDay(new egw_time($date, egw_time::$user_timezone));
1337
					$date = egw_time::to($time, 'ts');
1338
				}
1339
				else
1340
				{
1341
					$date = $this->date2ts($date,true);
1342
				}
1343
			}
1344
			unset($date);
1345
		}
1346
		// same with the alarms
1347
		if (isset($event['alarm']) && is_array($event['alarm']) && isset($event['start']))
1348
		{
1349
			foreach($event['alarm'] as $id => &$alarm)
1350
			{
1351
				// remove alarms belonging to not longer existing or rejected participants
1352
				if ($alarm['owner'] && isset($event['participants']))
1353
				{
1354
					$status = $event['participants'][$alarm['owner']];
1355
					if (!isset($status) || calendar_so::split_status($status) === 'R')
1356
					{
1357
						unset($event['alarm'][$id]);
1358
						$this->so->delete_alarm($id);
1359
						error_log(__LINE__.': '.__METHOD__."(".array2string($event).") deleting alarm=".array2string($alarm).", $status=".array2string($alarm));
1360
					}
1361
				}
1362
			}
1363
		}
1364
		// update all existing alarm times, in case alarm got moved and alarms are not include in $event
1365
		if ($old_event && is_array($old_event['alarm']) && isset($event['start']))
1366
		{
1367
			foreach($old_event['alarm'] as $id => &$alarm)
1368
			{
1369
				if (!isset($event['alarm'][$id]))
1370
				{
1371
					$alarm['time'] = $event['start'] - $alarm['offset'];
1372
					if ($alarm['time'] < time()) calendar_so::shift_alarm($event, $alarm);
1373
						// remove (not store) alarms belonging to not longer existing or rejected participants
1374
					$status = isset($event['participants']) ? $event['participants'][$alarm['owner']] :
1375
						$old_event['participants'][$alarm['owner']];
1376
					if (!$alarm['owner'] || isset($status) && calendar_so::split_status($status) !== 'R')
1377
					{
1378
						$this->so->save_alarm($event['id'], $alarm);
1379
						error_log(__LINE__.': '.__METHOD__."() so->save_alarm($event[id], ".array2string($alarm).")");
1380
					}
1381
					else
1382
					{
1383
						$this->so->delete_alarm($id);
1384
						error_log(__LINE__.': '.__METHOD__."(".array2string($event).") deleting alarm=".array2string($alarm).", $status=".array2string($alarm));
1385
					}
1386
				}
1387
			}
1388
		}
1389
1390
		// always update modification time (ctag depends on it!)
1391
		$event['modified'] = $this->now;
1392
		$event['modifier'] = $this->user;
1393
1394
		if (empty($event['id']) && (!isset($event['created']) || $event['created'] > $this->now))
1395
		{
1396
			$event['created'] = $this->now;
1397
			$event['creator'] = $this->user;
1398
		}
1399
		$set_recurrences = false;
1400
		$set_recurrences_start = 0;
1401
		if (($cal_id = $this->so->save($event,$set_recurrences,$set_recurrences_start,0,$event['etag'])) && $set_recurrences && $event['recur_type'] != MCAL_RECUR_NONE)
1402
		{
1403
			$save_event['id'] = $cal_id;
1404
			// unset participants to enforce the default stati for all added recurrences
1405
			unset($save_event['participants']);
1406
			$this->set_recurrences($save_event, $set_recurrences_start);
1407
		}
1408
		if ($updateTS) $GLOBALS['egw']->contenthistory->updateTimeStamp('calendar', $cal_id, $event['id'] ? 'modify' : 'add', $this->now);
1409
1410
		// create links for new participants from addressbook, if configured
1411
		if ($cal_id && $GLOBALS['egw_info']['server']['link_contacts'] && $event['participants'])
1412
		{
1413
			foreach($event['participants'] as $uid => $status)
1414
			{
1415
				$user_type = $user_id = null;
1416
				calendar_so::split_user($uid, $user_type, $user_id);
1417
				if ($user_type == 'c' && (!$old_event || !isset($old_event['participants'][$uid])))
1418
				{
1419
					egw_link::link('calendar', $cal_id, 'addressbook', $user_id);
1420
				}
1421
			}
1422
		}
1423
1424
		// Update history
1425
		$tracking = new calendar_tracking($this);
0 ignored issues
show
Unused Code introduced by
The call to calendar_tracking::__construct() has too many arguments starting with $this.

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

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

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1426
		if (empty($event['id']) && !empty($cal_id)) $event['id']=$cal_id;
1427
		$tracking->track($event, $old_event);
1428
1429
		return $cal_id;
1430
	}
1431
1432
	/**
1433
	 * Check if the current user has the necessary ACL rights to change the status of $uid
1434
	 *
1435
	 * For contacts we use edit rights of the owner of the event (aka. edit rights of the event).
1436
	 *
1437
	 * @param int|string $uid account_id or 1-char type-identifer plus id (eg. c15 for addressbook entry #15)
1438
	 * @param array|int $event event array or id of the event
1439
	 * @return boolean
1440
	 */
1441
	function check_status_perms($uid,$event)
1442
	{
1443
		if ($uid[0] == 'c' || $uid[0] == 'e')	// for contact we use the owner of the event
1444
		{
1445
			if (!is_array($event) && !($event = $this->read($event))) return false;
1446
1447
			return $this->check_perms(EGW_ACL_EDIT,0,$event['owner']);
1448
		}
1449
		// check if we have a category acl for the event or not (null)
1450
		$access = $this->check_cat_acl(self::CAT_ACL_STATUS,$event);
1451
		if (!is_null($access))
1452
		{
1453
			return $access;
1454
		}
1455
		// no access or denied access because of category acl --> regular check
1456
		if (!is_numeric($uid))	// this is eg. for resources (r123)
1457
		{
1458
			$resource = $this->resource_info($uid);
1459
1460
			return EGW_ACL_EDIT & $resource['rights'];
1461
		}
1462
		if (!is_array($event) && !($event = $this->read($event))) return false;
1463
1464
		// regular user and groups (need to check memberships too)
1465 View Code Duplication
		if (!isset($event['participants'][$uid]))
1466
		{
1467
			$memberships = $GLOBALS['egw']->accounts->memberships($uid,true);
1468
		}
1469
		$memberships[] = $uid;
1470
		return array_intersect($memberships, array_keys($event['participants'])) && $this->check_perms(EGW_ACL_EDIT,0,$uid);
1471
	}
1472
1473
	/**
1474
	 * Check if current user has a certain right on the categories of an event
1475
	 *
1476
	 * Not having the given right for a single category, means not having it!
1477
	 *
1478
	 * @param int $right self::CAT_ACL_{ADD|STATUS}
1479
	 * @param int|array $event
1480
	 * @return boolean true if use has the right, false if not
1481
	 * @return boolean false=access denied because of cat acl, true access granted because of cat acl,
1482
	 * 	null = cat has no acl
1483
	 */
1484
	function check_cat_acl($right,$event)
1485
	{
1486
		if (!is_array($event)) $event = $this->read($event);
1487
1488
		$ret = null;
1489
		if ($event['category'])
1490
		{
1491
			foreach(is_array($event['category']) ? $event['category'] : explode(',',$event['category']) as $cat_id)
1492
			{
1493
				$access = self::has_cat_right($right,$cat_id,$this->user);
1494
				if ($access === true)
1495
				{
1496
					$ret = true;
1497
					break;
1498
				}
1499
				if ($access === false)
1500
				{
1501
					$ret = false;	// cat denies access --> check further cats
1502
				}
1503
			}
1504
		}
1505
		//echo "<p>".__METHOD__."($event[id]: $event[title], $right) = ".array2string($ret)."</p>\n";
1506
		return $ret;
1507
	}
1508
1509
	/**
1510
	 * Array with $cat_id => $rights pairs for current user (no entry means, cat is not limited by ACL!)
1511
	 *
1512
	 * @var array
1513
	 */
1514
	private static $cat_rights_cache;
1515
1516
	/**
1517
	 * Get rights for a given category id
1518
	 *
1519
	 * @param int $cat_id =null null to return array with all cats
1520
	 * @return array with account_id => right pairs
1521
	 */
1522
	public static function get_cat_rights($cat_id=null)
1523
	{
1524
		if (!isset(self::$cat_rights_cache))
1525
		{
1526
			self::$cat_rights_cache = egw_cache::getSession('calendar','cat_rights',
1527
				array($GLOBALS['egw']->acl,'get_location_grants'),array('L%','calendar'));
1528
		}
1529
		//echo "<p>".__METHOD__."($cat_id) = ".array2string($cat_id ? self::$cat_rights_cache['L'.$cat_id] : self::$cat_rights_cache)."</p>\n";
1530
		return $cat_id ? self::$cat_rights_cache['L'.$cat_id] : self::$cat_rights_cache;
1531
	}
1532
1533
	/**
1534
	 * Set rights for a given single category and user
1535
	 *
1536
	 * @param int $cat_id
1537
	 * @param int $user
1538
	 * @param int $rights self::CAT_ACL_{ADD|STATUS} or'ed together
1539
	 */
1540
	public static function set_cat_rights($cat_id,$user,$rights)
1541
	{
1542
		//echo "<p>".__METHOD__."($cat_id,$user,$rights)</p>\n";
1543
		if (!isset(self::$cat_rights_cache)) self::get_cat_rights($cat_id);
1544
1545
		if ((int)$rights != (int)self::$cat_rights_cache['L'.$cat_id][$user])
1546
		{
1547
			if ($rights)
1548
			{
1549
				self::$cat_rights_cache['L'.$cat_id][$user] = $rights;
1550
				$GLOBALS['egw']->acl->add_repository('calendar','L'.$cat_id,$user,$rights);
1551
			}
1552
			else
1553
			{
1554
				unset(self::$cat_rights_cache['L'.$cat_id][$user]);
1555
				if (!self::$cat_rights_cache['L'.$cat_id]) unset(self::$cat_rights_cache['L'.$cat_id]);
1556
				$GLOBALS['egw']->acl->delete_repository('calendar','L'.$cat_id,$user);
1557
			}
1558
			egw_cache::setSession('calendar','cat_rights',self::$cat_rights_cache);
1559
		}
1560
	}
1561
1562
	/**
1563
	 * Check if current user has a given right on a category (if it's restricted!)
1564
	 *
1565
	 * @param int $cat_id
1566
	 * @return boolean false=access denied because of cat acl, true access granted because of cat acl,
1567
	 * 	null = cat has no acl
1568
	 */
1569
	public static function has_cat_right($right,$cat_id,$user)
1570
	{
1571
		static $cache=null;
1572
1573
		if (!isset($cache[$cat_id]))
1574
		{
1575
			$all = $own = 0;
1576
			$cat_rights = self::get_cat_rights($cat_id);
1577
			if (!is_null($cat_rights))
1578
			{
1579
				static $memberships=null;
1580
				if (is_null($memberships))
1581
				{
1582
					$memberships = $GLOBALS['egw']->accounts->memberships($user,true);
1583
					$memberships[] = $user;
1584
				}
1585
				foreach($cat_rights as $uid => $value)
1586
				{
1587
					$all |= $value;
1588
					if (in_array($uid,$memberships)) $own |= $value;
1589
				}
1590
			}
1591
			foreach(array(self::CAT_ACL_ADD,self::CAT_ACL_STATUS) as $mask)
1592
			{
1593
				$cache[$cat_id][$mask] = !($all & $mask) ? null : !!($own & $mask);
1594
			}
1595
		}
1596
		//echo "<p>".__METHOD__."($right,$cat_id) all=$all, own=$own returning ".array2string($cache[$cat_id][$right])."</p>\n";
1597
		return $cache[$cat_id][$right];
1598
	}
1599
1600
	/**
1601
	 * set the status of one participant for a given recurrence or for all recurrences since now (includes recur_date=0)
1602
	 *
1603
	 * @param int|array $event event-array or id of the event
1604
	 * @param string|int $uid account_id or 1-char type-identifer plus id (eg. c15 for addressbook entry #15)
1605
	 * @param int|char $status numeric status (defines) or 1-char code: 'R', 'U', 'T' or 'A'
1606
	 * @param int $recur_date =0 date to change, or 0 = all since now
1607
	 * @param boolean $ignore_acl =false do not check the permisions for the $uid, if true
1608
	 * @param boolean $updateTS =true update the content history of the event
1609
	 * DEPRECATED: we allways (have to) update timestamp, as they are required for sync!
1610
	 * @param boolean $skip_notification =false true: do not send notification messages
1611
	 * @return int number of changed recurrences
1612
	 */
1613
	function set_status($event,$uid,$status,$recur_date=0,$ignore_acl=false,$updateTS=true,$skip_notification=false)
1614
	{
1615
		unset($updateTS);
1616
1617
		$cal_id = is_array($event) ? $event['id'] : $event;
1618
		//echo "<p>calendar_boupdate::set_status($cal_id,$uid,$status,$recur_date)</p>\n";
1619
		if (!$cal_id || (!$ignore_acl && !$this->check_status_perms($uid,$event)))
1620
		{
1621
			return false;
1622
		}
1623
		$quantity = $role = null;
1624
		calendar_so::split_status($status, $quantity, $role);
1625
		if ($this->log)
1626
		{
1627
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
1628
				"($cal_id, $uid, $status, $recur_date)\n",3,$this->logfile);
1629
		}
1630
		$old_event = $this->read($cal_id, $recur_date, false, 'server');
1631
		if (($Ok = $this->so->set_status($cal_id,is_numeric($uid)?'u':$uid[0],
1632
				is_numeric($uid)?$uid:substr($uid,1),$status,
1633
				$recur_date?$this->date2ts($recur_date,true):0,$role)))
1634
		{
1635
			if ($status == 'R')	// remove alarms belonging to rejected participants
1636
			{
1637
				foreach(isset($event['alarm']) ? $event['alarm'] : $old_event['alarm'] as $id => $alarm)
1638
				{
1639
					if ((string)$alarm['owner'] === (string)$uid)
1640
					{
1641
						$this->so->delete_alarm($id);
1642
						//error_log(__LINE__.': '.__METHOD__."(".array2string($event).", '$uid', '$status', ...) deleting alarm=".array2string($alarm).", $status=".array2string($alarm));
1643
					}
1644
				}
1645
			}
1646
			if ($updateTS)
1647
			{
1648
				$GLOBALS['egw']->contenthistory->updateTimeStamp('calendar', $cal_id, 'modify', $this->now);
1649
			}
1650
1651
			static $status2msg = array(
1652
				'R' => MSG_REJECTED,
1653
				'T' => MSG_TENTATIVE,
1654
				'A' => MSG_ACCEPTED,
1655
				'D' => MSG_DELEGATED,
1656
			);
1657
			if (isset($status2msg[$status]) && !$skip_notification)
1658
			{
1659
				if (!is_array($event)) $event = $this->read($cal_id);
1660
				if (isset($recur_date)) $event = $this->read($event['id'],$recur_date); //re-read the actually edited recurring event
1661
				$this->send_update($status2msg[$status],$event['participants'],$event);
0 ignored issues
show
Bug introduced by
It seems like $event can also be of type boolean; however, calendar_boupdate::send_update() 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...
1662
			}
1663
1664
			// Update history
1665
			$event = $this->read($cal_id, $recur_date, false, 'server');
1666
			$tracking = new calendar_tracking($this);
0 ignored issues
show
Unused Code introduced by
The call to calendar_tracking::__construct() has too many arguments starting with $this.

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

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

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1667
			$tracking->track($event, $old_event);
1668
1669
		}
1670
		return $Ok;
1671
	}
1672
1673
	/**
1674
	 * update the status of all participant for a given recurrence or for all recurrences since now (includes recur_date=0)
1675
	 *
1676
	 * @param array $new_event event-array with the new stati
1677
	 * @param array $old_event event-array with the old stati
1678
	 * @param int $recur_date =0 date to change, or 0 = all since now
1679
	 * @param boolean $skip_notification Do not send notifications.  Parameter passed on to set_status().
1680
	 */
1681
	function update_status($new_event, $old_event , $recur_date=0, $skip_notification=false)
1682
	{
1683
		if (!isset($new_event['participants'])) return;
1684
1685
		// check the old list against the new list
1686
		foreach ($old_event['participants'] as $userid => $status)
1687
  		{
1688
            if (!isset($new_event['participants'][$userid])){
1689
            	// Attendee will be deleted this way
1690
            	$new_event['participants'][$userid] = 'G';
1691
            }
1692
            elseif ($new_event['participants'][$userid] == $status)
1693
            {
1694
            	// Same status -- nothing to do.
1695
            	unset($new_event['participants'][$userid]);
1696
            }
1697
		}
1698
		// write the changes
1699
		foreach ($new_event['participants'] as $userid => $status)
1700
		{
1701
			$this->set_status($old_event, $userid, $status, $recur_date, true, false,$skip_notification);
1702
		}
1703
    }
1704
1705
	/**
1706
	 * deletes an event
1707
	 *
1708
	 * @param int $cal_id id of the event to delete
1709
	 * @param int $recur_date =0 if a single event from a series should be deleted, its date
1710
	 * @param boolean $ignore_acl =false true for no ACL check, default do ACL check
1711
	 * @param boolean $skip_notification =false
1712
	 * @param boolean $delete_exceptions =true true: delete, false: keep exceptions (with new UID)
1713
	 * @param int &$exceptions_kept=null on return number of kept exceptions
1714
	 * @return boolean true on success, false on error (usually permission denied)
1715
	 */
1716
	function delete($cal_id, $recur_date=0, $ignore_acl=false, $skip_notification=false,
1717
		$delete_exceptions=true, &$exceptions_kept=null)
1718
	{
1719
		//error_log(__METHOD__."(cal_id=$cal_id, recur_date=$recur_date, ignore_acl=$ignore_acl, skip_notifications=$skip_notification)");
1720
		if (!($event = $this->read($cal_id,$recur_date)) ||
1721
			!$ignore_acl && !$this->check_perms(EGW_ACL_DELETE,$event))
1722
		{
1723
			return false;
1724
		}
1725
1726
		// Don't send notification if the event has already been deleted
1727
		if(!$event['deleted'] && !$skip_notification)
1728
		{
1729
			$this->send_update(MSG_DELETED,$event['participants'],$event);
1730
		}
1731
1732
		if (!$recur_date || $event['recur_type'] == MCAL_RECUR_NONE)
1733
		{
1734
			$config = config::read('phpgwapi');
1735
			if(!$config['calendar_delete_history'] || $event['deleted'])
1736
			{
1737
				$this->so->delete($cal_id);
1738
1739
				// delete all links to the event
1740
				egw_link::unlink(0,'calendar',$cal_id);
1741
			}
1742
			elseif ($config['calendar_delete_history'])
1743
			{
1744
				// mark all links to the event as deleted, but keep them
1745
				egw_link::unlink(0,'calendar',$cal_id,'','','',true);
1746
1747
				$event['deleted'] = $this->now;
1748
				$this->save($event, $ignore_acl);
1749
				// Actually delete alarms
1750
				if (isset($event['alarm']) && is_array($event['alarm']))
1751
				{
1752
					foreach($event['alarm'] as $id => $alarm)
1753
					{
1754
						$this->delete_alarm($id);
1755
					}
1756
				}
1757
			}
1758
			$GLOBALS['egw']->contenthistory->updateTimeStamp('calendar', $cal_id, 'delete', $this->now);
1759
1760
			// delete or keep (with new uid) exceptions of a recurring event
1761
			if ($event['recur_type'] != MCAL_RECUR_NONE)
1762
			{
1763
				$exceptions_kept = 0;
1764
				foreach ($this->so->get_related($event['uid']) as $id)
1765
				{
1766
					if ($delete_exceptions)
1767
					{
1768
						$this->delete($id, 0, $ignore_acl, true);
1769
					}
1770
					else
1771
					{
1772
						if (!($exception = $this->read($id))) continue;
1773
						$exception['uid'] = common::generate_uid('calendar', $id);
1774
						$exception['reference'] = $exception['recurrence'] = 0;
1775
						$this->update($exception, true, true, false, true, $msg=null, true);
0 ignored issues
show
Bug introduced by
$msg = null cannot be passed to update() as the parameter $messages expects a reference.
Loading history...
1776
						++$exceptions_kept;
1777
					}
1778
				}
1779
			}
1780
		}
1781
		else	// delete an exception
1782
		{
1783
			// check if deleted recurrance has alarms (because it's the next recurrance) --> move it to next recurrance
1784
			if ($event['alarm'])
1785
			{
1786
				$next_recurrance = null;
1787
				foreach($event['alarm'] as &$alarm)
1788
				{
1789
					if (($alarm['time'] == $recur_date) || ($alarm['time']+$alarm['offset'] == $recur_date))
1790
					{
1791
						//error_log(__METHOD__.__LINE__.'->'.array2string($recur_date));
1792
						//error_log(__METHOD__.__LINE__.array2string($event));
1793
						if (is_null($next_recurrance))
1794
						{
1795
							$checkdate = $recur_date;
1796
							//if ($alarm['time']+$alarm['offset'] == $recur_date) $checkdate = $recur_date + $alarm['offset'];
1797
							if (($e = $this->read($cal_id,$checkdate+1)))
1798
							{
1799
								$next_recurrance = $this->date2ts($e['start']);
1800
							}
1801
						}
1802
						$alarm['time'] = $this->date2ts($next_recurrance, true);	// user to server-time
1803
						$alarm['cal_id'] = $cal_id;
1804
						unset($alarm['times']);
1805
						unset($alarm['next']);
1806
						$this->so->save_alarm($event['id'], $alarm);
1807
					}
1808
				}
1809
			}
1810
			// need to read series master, as otherwise existing exceptions will be lost!
1811
			$recur_date = $this->date2ts($event['start']);
1812
			//if ($event['alarm']) $alarmbuffer = $event['alarm'];
1813
			$event = $this->read($cal_id);
1814
			//if (isset($alarmbuffer)) $event['alarm'] = $alarmbuffer;
1815
			$event['recur_exception'][] = $recur_date;
1816
			$this->save($event);// updates the content-history
1817
		}
1818
		if ($event['reference'])
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
1819
		{
1820
			// evtl. delete recur_exception $event['recurrence'] from event with cal_id=$event['reference']
1821
		}
1822
		return true;
1823
	}
1824
1825
	/**
1826
	 * helper for send_update and get_update_message
1827
	 * @internal
1828
	 * @param array $event
1829
	 * @param string $action
1830
	 * @param array $event_arr
1831
	 * @param array $disinvited
1832
	 * @return array
1833
	 */
1834
	function _get_event_details($event,$action,&$event_arr,$disinvited=array())
1835
	{
1836
		$details = array(			// event-details for the notify-msg
1837
			'id'          => $event['id'],
1838
			'action'      => lang($action),
1839
		);
1840
		$event_arr = $this->event2array($event);
1841
		foreach($event_arr as $key => $val)
1842
		{
1843
			if ($key == 'recur_type') $key = 'repetition';
1844
			$details[$key] = $val['data'];
1845
		}
1846
		$details['participants'] = $details['participants'] ? implode("\n",$details['participants']) : '';
1847
1848
		$event_arr['link']['field'] = lang('URL');
1849
		$eventStart_arr = $this->date2array($event['start']); // give this as 'date' to the link to pick the right recurrence for the participants state
1850
		$link = $GLOBALS['egw_info']['server']['webserver_url'].'/index.php?menuaction=calendar.calendar_uiforms.edit&cal_id='.$event['id'].'&date='.$eventStart_arr['full'].'&no_popup=1';
1851
		// if url is only a path, try guessing the rest ;-)
1852 View Code Duplication
		if ($link[0] == '/')
1853
		{
1854
			$link = ($GLOBALS['egw_info']['server']['enforce_ssl'] || $_SERVER['HTTPS'] ? 'https://' : 'http://').
1855
				($GLOBALS['egw_info']['server']['hostname'] ? $GLOBALS['egw_info']['server']['hostname'] : $_SERVER['HTTP_HOST']).
1856
				$link;
1857
		}
1858
		$event_arr['link']['data'] = $details['link'] = $link;
1859
1860
		/* this is needed for notification-app
1861
		 * notification-app creates the link individual for
1862
		 * every user, so we must provide a neutral link-style
1863
		 * if calendar implements tracking in near future, this part can be deleted
1864
		 */
1865
		$link_arr = array();
1866
		$link_arr['text'] = $event['title'];
1867
		$link_arr['view'] = array(	'menuaction' => 'calendar.calendar_uiforms.edit',
1868
									'cal_id' => $event['id'],
1869
									'date' => $eventStart_arr['full'],
1870
									);
1871
		$link_arr['popup'] = '750x400';
1872
		$details['link_arr'] = $link_arr;
1873
1874
		$dis = array();
1875
		foreach($disinvited as $uid)
1876
		{
1877
			$dis[] = $this->participant_name($uid);
1878
		}
1879
		$details['disinvited'] = implode(', ',$dis);
1880
		return $details;
1881
	}
1882
1883
	/**
1884
	 * create array with name, translated name and readable content of each attributes of an event
1885
	 *
1886
	 * old function, so far only used by send_update (therefor it's in bocalupdate and not bocal)
1887
	 *
1888
	 * @param array $event event to use
1889
	 * @returns array of attributes with fieldname as key and array with the 'field'=translated name 'data' = readable content (for participants this is an array !)
1890
	 */
1891
	function event2array($event)
1892
	{
1893
		$var['title'] = Array(
1894
			'field'		=> lang('Title'),
1895
			'data'		=> $event['title']
1896
		);
1897
1898
		$var['description'] = Array(
1899
			'field'	=> lang('Description'),
1900
			'data'	=> $event['description']
1901
		);
1902
1903
		foreach(explode(',',$event['category']) as $cat_id)
1904
		{
1905
			list($cat) = $GLOBALS['egw']->categories->return_single($cat_id);
1906
			$cat_string[] = stripslashes($cat['name']);
1907
		}
1908
		$var['category'] = Array(
1909
			'field'	=> lang('Category'),
1910
			'data'	=> implode(', ',$cat_string)
1911
		);
1912
1913
		$var['location'] = Array(
1914
			'field'	=> lang('Location'),
1915
			'data'	=> $event['location']
1916
		);
1917
1918
		$var['startdate'] = Array(
1919
			'field'	=> lang('Start Date/Time'),
1920
			'data'	=> $this->format_date($event['start']),
1921
		);
1922
1923
		$var['enddate'] = Array(
1924
			'field'	=> lang('End Date/Time'),
1925
			'data'	=> $this->format_date($event['end']),
1926
		);
1927
1928
		$pri = Array(
1929
			0   => lang('None'),
1930
			1	=> lang('Low'),
1931
			2	=> lang('Normal'),
1932
			3	=> lang('High')
1933
		);
1934
		$var['priority'] = Array(
1935
			'field'	=> lang('Priority'),
1936
			'data'	=> $pri[$event['priority']]
1937
		);
1938
1939
		$var['owner'] = Array(
1940
			'field'	=> lang('Owner'),
1941
			'data'	=> common::grab_owner_name($event['owner'])
1942
		);
1943
1944
		$var['updated'] = Array(
1945
			'field'	=> lang('Updated'),
1946
			'data'	=> $this->format_date($event['modtime']).', '.common::grab_owner_name($event['modifier'])
1947
		);
1948
1949
		$var['access'] = Array(
1950
			'field'	=> lang('Access'),
1951
			'data'	=> $event['public'] ? lang('Public') : lang('Private')
1952
		);
1953
1954
		if (isset($event['participants']) && is_array($event['participants']) && !empty($event['participants']))
1955
		{
1956
			$participants = $this->participants($event,true);
1957
		}
1958
		$var['participants'] = Array(
1959
			'field'	=> lang('Participants'),
1960
			'data'	=> $participants
1961
		);
1962
1963
		// Repeated Events
1964
		$var['recur_type'] = Array(
1965
			'field'	=> lang('Repetition'),
1966
			'data'	=> ($event['recur_type'] != MCAL_RECUR_NONE) ? $this->recure2string($event) : '',
1967
		);
1968
		return $var;
1969
	}
1970
1971
	/**
1972
	 * log all updates to a file
1973
	 *
1974
	 * @param array $event2save event-data before calling save
1975
	 * @param array $event_saved event-data read back from the DB
1976
	 * @param array $old_event =null event-data in the DB before calling save
1977
	 * @param string $type ='update'
1978
	 */
1979
	function log2file($event2save,$event_saved,$old_event=null,$type='update')
1980
	{
1981
		if (!($f = fopen($this->log_file,'a')))
0 ignored issues
show
Bug introduced by
The property log_file does not seem to exist. Did you mean logfile?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1982
		{
1983
			echo "<p>error opening '$this->log_file' !!!</p>\n";
0 ignored issues
show
Bug introduced by
The property log_file does not seem to exist. Did you mean logfile?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1984
			return false;
1985
		}
1986
		fwrite($f,$type.': '.common::grab_owner_name($this->user).': '.date('r')."\n");
1987
		fwrite($f,"Time: time to save / saved time read back / old time before save\n");
1988
		foreach(array('start','end') as $name)
1989
		{
1990
			fwrite($f,$name.': '.(isset($event2save[$name]) ? $this->format_date($event2save[$name]) : 'not set').' / '.
1991
				$this->format_date($event_saved[$name]) .' / '.
1992
				(is_null($old_event) ? 'no old event' : $this->format_date($old_event[$name]))."\n");
1993
		}
1994
		foreach(array('event2save','event_saved','old_event') as $name)
1995
		{
1996
			fwrite($f,$name.' = '.print_r($$name,true));
1997
		}
1998
		fwrite($f,"\n");
1999
		fclose($f);
2000
2001
		return true;
2002
	}
2003
2004
	/**
2005
	 * Check alarms and move them if needed
2006
	 *
2007
	 * Used when the start time has changed, and alarms need to be updated
2008
	 *
2009
	 * @param array $event
2010
	 * @param array $old_event
2011
	 * @param egw_time $instance_date For recurring events, this is the date we
2012
	 *	are dealing with
2013
	 */
2014
	function check_move_alarms(Array &$event, Array $old_event = null, egw_time $instance_date = null)
2015
	{
2016
		if ($old_event !== null && $event['start'] == $old_event['start']) return;
2017
2018
		$time = new egw_time($event['start']);
2019
		if(!is_array($event['alarm']))
2020
		{
2021
			$event['alarm'] = $this->so->read_alarms($event['id']);
2022
		}
2023
2024
		foreach($event['alarm'] as $id => &$alarm)
2025
		{
2026
			if($event['recur_type'] != MCAL_RECUR_NONE)
2027
			{
2028
				calendar_so::shift_alarm($event, $alarm, $instance_date->format('ts'));
0 ignored issues
show
Bug introduced by
It seems like $instance_date is not always an object, but can also be of type null. 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...
2029
			}
2030 View Code Duplication
			else if ($alarm['time'] !== $time->format('ts') - $alarm['offset'])
2031
			{
2032
				$alarm['time'] = $time->format('ts') - $alarm['offset'];
2033
				$this->save_alarm($event['id'], $alarm);
2034
			}
2035
		}
2036
	}
2037
2038
	/**
2039
	 * saves a new or updated alarm
2040
	 *
2041
	 * @param int $cal_id Id of the calendar-entry
2042
	 * @param array $alarm array with fields: text, owner, enabled, ..
2043
	 * @param boolean $update_modified =true call update modified, default true
2044
	 * @return string id of the alarm, or false on error (eg. no perms)
2045
	 */
2046
	function save_alarm($cal_id, $alarm, $update_modified=true)
2047
	{
2048
		if (!$cal_id || !$this->check_perms(EGW_ACL_EDIT,$alarm['all'] ? $cal_id : 0,!$alarm['all'] ? $alarm['owner'] : 0))
2049
		{
2050
			//echo "<p>no rights to save the alarm=".print_r($alarm,true)." to event($cal_id)</p>";
2051
			return false;	// no rights to add the alarm
2052
		}
2053
		$alarm['time'] = $this->date2ts($alarm['time'],true);	// user to server-time
2054
2055
		$GLOBALS['egw']->contenthistory->updateTimeStamp('calendar', $cal_id, 'modify', $this->now);
2056
2057
		return $this->so->save_alarm($cal_id, $alarm, $update_modified);
2058
	}
2059
2060
	/**
2061
	 * delete one alarms identified by its id
2062
	 *
2063
	 * @param string $id alarm-id is a string of 'cal:'.$cal_id.':'.$alarm_nr, it is used as the job-id too
2064
	 * @return int number of alarms deleted, false on error (eg. no perms)
2065
	 */
2066
	function delete_alarm($id)
2067
	{
2068
		list(,$cal_id) = explode(':',$id);
2069
2070
		if (!($alarm = $this->so->read_alarm($id)) || !$cal_id || !$this->check_perms(EGW_ACL_EDIT,$alarm['all'] ? $cal_id : 0,!$alarm['all'] ? $alarm['owner'] : 0))
2071
		{
2072
			return false;	// no rights to delete the alarm
2073
		}
2074
2075
		$GLOBALS['egw']->contenthistory->updateTimeStamp('calendar', $cal_id, 'modify', $this->now);
2076
2077
		return $this->so->delete_alarm($id);
2078
	}
2079
2080
	/**
2081
	 * Find existing categories in database by name or add categories that do not exist yet
2082
	 * currently used for ical/sif import
2083
	 *
2084
	 * @param array $catname_list names of the categories which should be found or added
2085
	 * @param int|array $old_event =null match against existing event and expand the returned category ids
2086
	 *  by the ones the user normally does not see due to category permissions - used to preserve categories
2087
	 * @return array category ids (found, added and preserved categories)
2088
	 */
2089
	function find_or_add_categories($catname_list, $old_event=null)
2090
	{
2091
		if (is_array($old_event) || $old_event > 0)
2092
		{
2093
			// preserve categories without users read access
2094
			if (!is_array($old_event)) $old_event = $this->read($old_event);
2095
			$old_categories = explode(',',$old_event['category']);
2096
			$old_cats_preserve = array();
2097
			if (is_array($old_categories) && count($old_categories) > 0)
2098
			{
2099
				foreach ($old_categories as $cat_id)
2100
				{
2101
					if (!$this->categories->check_perms(EGW_ACL_READ, $cat_id))
2102
					{
2103
						$old_cats_preserve[] = $cat_id;
2104
					}
2105
				}
2106
			}
2107
		}
2108
2109
		$cat_id_list = array();
2110 View Code Duplication
		foreach ((array)$catname_list as $cat_name)
2111
		{
2112
			$cat_name = trim($cat_name);
2113
			$cat_id = $this->categories->name2id($cat_name, 'X-');
2114
2115
			if (!$cat_id)
2116
			{
2117
				// some SyncML clients (mostly phones) add an X- to the category names
2118
				if (strncmp($cat_name, 'X-', 2) == 0)
2119
				{
2120
					$cat_name = substr($cat_name, 2);
2121
				}
2122
				$cat_id = $this->categories->add(array('name' => $cat_name, 'descr' => $cat_name, 'access' => 'private'));
2123
			}
2124
2125
			if ($cat_id)
2126
			{
2127
				$cat_id_list[] = $cat_id;
2128
			}
2129
		}
2130
2131 View Code Duplication
		if (is_array($old_cats_preserve) && count($old_cats_preserve) > 0)
2132
		{
2133
			$cat_id_list = array_merge($cat_id_list, $old_cats_preserve);
2134
		}
2135
2136 View Code Duplication
		if (count($cat_id_list) > 1)
2137
		{
2138
			$cat_id_list = array_unique($cat_id_list);
2139
			sort($cat_id_list, SORT_NUMERIC);
2140
		}
2141
2142
		return $cat_id_list;
2143
	}
2144
2145
	function get_categories($cat_id_list)
2146
	{
2147
		if (!is_array($cat_id_list))
2148
		{
2149
			$cat_id_list = explode(',',$cat_id_list);
2150
		}
2151
		$cat_list = array();
2152
		foreach ($cat_id_list as $cat_id)
2153
		{
2154
			if ($cat_id && $this->categories->check_perms(EGW_ACL_READ, $cat_id) &&
2155
					($cat_name = $this->categories->id2name($cat_id)) && $cat_name != '--')
2156
			{
2157
				$cat_list[] = $cat_name;
2158
			}
2159
		}
2160
2161
		return $cat_list;
2162
	}
2163
2164
	/**
2165
	 * Try to find a matching db entry
2166
	 *
2167
	 * @param array $event	the vCalendar data we try to find
2168
	 * @param string filter='exact' exact	-> find the matching entry
2169
	 * 								check	-> check (consitency) for identical matches
2170
	 * 							    relax	-> be more tolerant
2171
	 *                              master	-> try to find a releated series master
2172
	 * @return array calendar_ids of matching entries
2173
	 */
2174
	function find_event($event, $filter='exact')
2175
	{
2176
		$matchingEvents = array();
2177
		$query = array();
2178
2179
		if ($this->log)
2180
		{
2181
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2182
				"($filter)[EVENT]:" . array2string($event)."\n",3,$this->logfile);
2183
		}
2184
2185
		if (!isset($event['recurrence'])) $event['recurrence'] = 0;
2186
2187
		if ($filter == 'master')
2188
		{
2189
			$query[] = 'recur_type!='. MCAL_RECUR_NONE;
2190
			$query['cal_recurrence'] = 0;
2191
		}
2192
		elseif ($filter == 'exact')
2193
		{
2194
			if ($event['recur_type'] != MCAL_RECUR_NONE)
2195
			{
2196
				$query[] = 'recur_type='.$event['recur_type'];
2197
			}
2198
			else
2199
			{
2200
				$query[] = 'recur_type IS NULL';
2201
			}
2202
			$query['cal_recurrence'] = $event['recurrence'];
2203
		}
2204
2205
		if ($event['id'])
2206
		{
2207
			if ($this->log)
2208
			{
2209
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2210
					'(' . $event['id'] . ")[EventID]\n",3,$this->logfile);
2211
			}
2212
			if (($egwEvent = $this->read($event['id'], 0, false, 'server')))
2213
			{
2214
				if ($this->log)
2215
				{
2216
					error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2217
						'()[FOUND]:' . array2string($egwEvent)."\n",3,$this->logfile);
2218
				}
2219
				if ($egwEvent['recur_type'] != MCAL_RECUR_NONE &&
2220
					(empty($event['uid']) || $event['uid'] == $egwEvent['uid']))
2221
				{
2222
					if ($filter == 'master')
2223
					{
2224
						$matchingEvents[] = $egwEvent['id']; // we found the master
2225
					}
2226
					if ($event['recur_type'] == $egwEvent['recur_type'])
2227
					{
2228
						$matchingEvents[] = $egwEvent['id']; // we found the event
2229
					}
2230
					elseif ($event['recur_type'] == MCAL_RECUR_NONE &&
2231
								$event['recurrence'] != 0)
2232
					{
2233
						$exceptions = $this->so->get_recurrence_exceptions($egwEvent, $event['tzid']);
2234 View Code Duplication
						if (in_array($event['recurrence'], $exceptions))
2235
						{
2236
							$matchingEvents[] = $egwEvent['id'] . ':' . (int)$event['recurrence'];
2237
						}
2238
					}
2239
				} elseif ($filter != 'master' && ($filter == 'exact' ||
2240
							$event['recur_type'] == $egwEvent['recur_type'] &&
2241
							strpos($egwEvent['title'], $event['title']) === 0))
2242
				{
2243
					$matchingEvents[] = $egwEvent['id']; // we found the event
2244
				}
2245
			}
2246
			if (!empty($matchingEvents) || $filter == 'exact') return $matchingEvents;
2247
		}
2248
		unset($event['id']);
2249
2250
		// No chance to find a master without [U]ID
2251
		if ($filter == 'master' && empty($event['uid'])) return $matchingEvents;
2252
2253
		// only query calendars of users, we have READ-grants from
2254
		$users = array();
2255
		foreach(array_keys($this->grants) as $user)
2256
		{
2257
			$user = trim($user);
2258 View Code Duplication
			if ($this->check_perms(EGW_ACL_READ|EGW_ACL_READ_FOR_PARTICIPANTS|EGW_ACL_FREEBUSY,0,$user))
2259
			{
2260
				if ($user && !in_array($user,$users))	// already added?
2261
				{
2262
					$users[] = $user;
2263
				}
2264
			}
2265
			elseif ($GLOBALS['egw']->accounts->get_type($user) != 'g')
2266
			{
2267
				continue;	// for non-groups (eg. users), we stop here if we have no read-rights
2268
			}
2269
			// the further code is only for real users
2270
			if (!is_numeric($user)) continue;
2271
2272
			// for groups we have to include the members
2273
			if ($GLOBALS['egw']->accounts->get_type($user) == 'g')
2274
			{
2275
				$members = $GLOBALS['egw']->accounts->member($user);
2276 View Code Duplication
				if (is_array($members))
2277
				{
2278
					foreach($members as $member)
2279
					{
2280
						// use only members which gave the user a read-grant
2281
						if (!in_array($member['account_id'],$users) &&
2282
								$this->check_perms(EGW_ACL_READ|EGW_ACL_FREEBUSY,0,$member['account_id']))
2283
						{
2284
							$users[] = $member['account_id'];
2285
						}
2286
					}
2287
				}
2288
			}
2289 View Code Duplication
			else	// for users we have to include all the memberships, to get the group-events
2290
			{
2291
				$memberships = $GLOBALS['egw']->accounts->membership($user);
2292
				if (is_array($memberships))
2293
				{
2294
					foreach($memberships as $group)
2295
					{
2296
						if (!in_array($group['account_id'],$users))
2297
						{
2298
							$users[] = $group['account_id'];
2299
						}
2300
					}
2301
				}
2302
			}
2303
		}
2304
2305
		if ($filter != 'master' && ($filter != 'exact' || empty($event['uid'])))
2306
		{
2307
			if (!empty($event['whole_day']))
2308
			{
2309
				if ($filter == 'relax')
2310
				{
2311
					$delta = 1800;
2312
				}
2313
				else
2314
				{
2315
					$delta = 60;
2316
				}
2317
2318
				// check length with some tolerance
2319
				$length = $event['end'] - $event['start'] - $delta;
2320
				$query[] = ('(cal_end-cal_start)>' . $length);
2321
				$length += 2 * $delta;
2322
				$query[] = ('(cal_end-cal_start)<' . $length);
2323
				$query[] = ('cal_start>' . ($event['start'] - 86400));
2324
				$query[] = ('cal_start<' . ($event['start'] + 86400));
2325
			}
2326
			elseif (isset($event['start']))
2327
			{
2328
				if ($filter == 'relax')
2329
				{
2330
					$query[] = ('cal_start>' . ($event['start'] - 3600));
2331
					$query[] = ('cal_start<' . ($event['start'] + 3600));
2332
				}
2333
				else
2334
				{
2335
					// we accept a tiny tolerance
2336
					$query[] = ('cal_start>' . ($event['start'] - 2));
2337
					$query[] = ('cal_start<' . ($event['start'] + 2));
2338
				}
2339
			}
2340
			if ($filter == 'relax')
2341
			{
2342
				$matchFields = array();
2343
			}
2344
			else
2345
			{
2346
				$matchFields = array('priority', 'public');
2347
			}
2348
			foreach ($matchFields as $key)
2349
			{
2350
				if (isset($event[$key])) $query['cal_'.$key] = $event[$key];
2351
			}
2352
		}
2353
2354 View Code Duplication
		if (!empty($event['uid']))
2355
		{
2356
			$query['cal_uid'] = $event['uid'];
2357
			if ($this->log)
2358
			{
2359
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2360
					'(' . $event['uid'] . ")[EventUID]\n",3,$this->logfile);
2361
			}
2362
		}
2363
2364
		if ($this->log)
2365
		{
2366
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2367
				'[QUERY]: ' . array2string($query)."\n",3,$this->logfile);
2368
		}
2369
		if (!count($users) || !($foundEvents =
2370
			$this->so->search(null, null, $users, 0, 'owner', false, 0, array('query' => $query))))
2371
		{
2372
			if ($this->log)
2373
			{
2374
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2375
				"[NO MATCH]\n",3,$this->logfile);
2376
			}
2377
			return $matchingEvents;
2378
		}
2379
2380
		$pseudos = array();
2381
2382
		foreach($foundEvents as $egwEvent)
2383
		{
2384
			if ($this->log)
2385
			{
2386
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2387
					'[FOUND]: ' . array2string($egwEvent)."\n",3,$this->logfile);
2388
			}
2389
2390
			if (in_array($egwEvent['id'], $matchingEvents)) continue;
2391
2392
			// convert timezone id of event to tzid (iCal id like 'Europe/Berlin')
2393 View Code Duplication
			if (!$egwEvent['tz_id'] || !($egwEvent['tzid'] = calendar_timezones::id2tz($egwEvent['tz_id'])))
2394
			{
2395
				$egwEvent['tzid'] = egw_time::$server_timezone->getName();
2396
			}
2397 View Code Duplication
			if (!isset(self::$tz_cache[$egwEvent['tzid']]))
2398
			{
2399
				self::$tz_cache[$egwEvent['tzid']] = calendar_timezones::DateTimeZone($egwEvent['tzid']);
2400
			}
2401
			if (!$event['tzid'])
2402
			{
2403
				$event['tzid'] = egw_time::$server_timezone->getName();
2404
			}
2405 View Code Duplication
			if (!isset(self::$tz_cache[$event['tzid']]))
2406
			{
2407
				self::$tz_cache[$event['tzid']] = calendar_timezones::DateTimeZone($event['tzid']);
2408
			}
2409
2410
			if (!empty($event['uid']))
2411
			{
2412
				if ($filter == 'master')
2413
				{
2414
					// We found the master
2415
					$matchingEvents = array($egwEvent['id']);
2416
					break;
2417
				}
2418
				if ($filter == 'exact')
2419
				{
2420
					// UID found
2421
					if (empty($event['recurrence']))
2422
					{
2423
						$egwstart = new egw_time($egwEvent['start'], egw_time::$server_timezone);
2424
						$egwstart->setTimezone(self::$tz_cache[$egwEvent['tzid']]);
2425
						$dtstart = new egw_time($event['start'], egw_time::$server_timezone);
2426
						$dtstart->setTimezone(self::$tz_cache[$event['tzid']]);
2427
						if ($egwEvent['recur_type'] == MCAL_RECUR_NONE &&
2428
							$event['recur_type'] == MCAL_RECUR_NONE ||
2429
								$egwEvent['recur_type'] != MCAL_RECUR_NONE &&
2430
									$event['recur_type'] != MCAL_RECUR_NONE)
2431
						{
2432
							if ($egwEvent['recur_type'] == MCAL_RECUR_NONE &&
2433
								$egwstart->format('Ymd') == $dtstart->format('Ymd') ||
2434
									$egwEvent['recur_type'] != MCAL_RECUR_NONE)
2435
							{
2436
								// We found an exact match
2437
								$matchingEvents = array($egwEvent['id']);
2438
								break;
2439
							}
2440
							else
2441
							{
2442
								$matchingEvents[] = $egwEvent['id'];
2443
							}
2444
						}
2445
						continue;
2446
					}
2447
					elseif ($egwEvent['recurrence'] == $event['recurrence'])
2448
					{
2449
						// We found an exact match
2450
						$matchingEvents = array($egwEvent['id']);
2451
						break;
2452
					}
2453
					if ($egwEvent['recur_type'] != MCAL_RECUR_NONE &&
2454
						$event['recur_type'] == MCAL_RECUR_NONE &&
2455
							!$egwEvent['recurrence'] && $event['recurrence'])
2456
					{
2457
						$exceptions = $this->so->get_recurrence_exceptions($egwEvent, $event['tzid']);
2458 View Code Duplication
						if (in_array($event['recurrence'], $exceptions))
2459
						{
2460
							// We found a pseudo exception
2461
							$matchingEvents = array($egwEvent['id'] . ':' . (int)$event['recurrence']);
2462
							break;
2463
						}
2464
					}
2465
					continue;
2466
				}
2467
			}
2468
2469
			// check times
2470
			if ($filter != 'relax')
2471
			{
2472
				if (empty($event['whole_day']))
2473
				{
2474
					if (abs($event['end'] - $egwEvent['end']) >= 120)
2475
					{
2476
						if ($this->log)
2477
						{
2478
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2479
							"() egwEvent length does not match!\n",3,$this->logfile);
2480
						}
2481
						continue;
2482
					}
2483
				}
2484
				else
2485
				{
2486
					if (!$this->so->isWholeDay($egwEvent))
2487
					{
2488
						if ($this->log)
2489
						{
2490
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2491
							"() egwEvent is not a whole-day event!\n",3,$this->logfile);
2492
						}
2493
						continue;
2494
					}
2495
				}
2496
			}
2497
2498
			// check for real match
2499
			$matchFields = array('title', 'description');
2500
			if ($filter != 'relax')
2501
			{
2502
				$matchFields[] = 'location';
2503
			}
2504
			foreach ($matchFields as $key)
2505
			{
2506
				if (!empty($event[$key]) && (empty($egwEvent[$key])
2507
						|| strpos(str_replace("\r\n", "\n", $egwEvent[$key]), $event[$key]) !== 0))
2508
				{
2509 View Code Duplication
					if ($this->log)
2510
					{
2511
						error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2512
							"() event[$key] differ: '" . $event[$key] .
2513
							"' <> '" . $egwEvent[$key] . "'\n",3,$this->logfile);
2514
					}
2515
					continue 2; // next foundEvent
2516
				}
2517
			}
2518
2519
			if (is_array($event['category']))
2520
			{
2521
				// check categories
2522
				$egwCategories = explode(',', $egwEvent['category']);
2523
				foreach ($egwCategories as $cat_id)
2524
				{
2525
					if ($this->categories->check_perms(EGW_ACL_READ, $cat_id) &&
2526
							!in_array($cat_id, $event['category']))
2527
					{
2528
						if ($this->log)
2529
						{
2530
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2531
							"() egwEvent category $cat_id is missing!\n",3,$this->logfile);
2532
						}
2533
						continue 2;
2534
					}
2535
				}
2536
				$newCategories = array_diff($event['category'], $egwCategories);
2537
				if (!empty($newCategories))
2538
				{
2539
					if ($this->log)
2540
					{
2541
						error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2542
							'() event has additional categories:'
2543
							. array2string($newCategories)."\n",3,$this->logfile);
2544
					}
2545
					continue;
2546
				}
2547
			}
2548
2549
			if ($filter != 'relax')
2550
			{
2551
				// check participants
2552
				if (is_array($event['participants']))
2553
				{
2554 View Code Duplication
					foreach ($event['participants'] as $attendee => $status)
2555
					{
2556
						if (!isset($egwEvent['participants'][$attendee]) &&
2557
								$attendee != $egwEvent['owner']) // ||
2558
							//(!$relax && $egw_event['participants'][$attendee] != $status))
2559
						{
2560
							if ($this->log)
2561
							{
2562
								error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2563
								"() additional event['participants']: $attendee\n",3,$this->logfile);
2564
							}
2565
							continue 2;
2566
						}
2567
						else
2568
						{
2569
							unset($egwEvent['participants'][$attendee]);
2570
						}
2571
					}
2572
					// ORGANIZER and Groups may be missing
2573
					unset($egwEvent['participants'][$egwEvent['owner']]);
2574
					foreach ($egwEvent['participants'] as $attendee => $status)
2575
					{
2576
						if (is_numeric($attendee) && $attendee < 0)
2577
						{
2578
							unset($egwEvent['participants'][$attendee]);
2579
						}
2580
					}
2581 View Code Duplication
					if (!empty($egwEvent['participants']))
2582
					{
2583
						if ($this->log)
2584
						{
2585
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2586
								'() missing event[participants]: ' .
2587
								array2string($egwEvent['participants'])."\n",3,$this->logfile);
2588
						}
2589
						continue;
2590
					}
2591
				}
2592
			}
2593
2594
			if ($event['recur_type'] == MCAL_RECUR_NONE)
2595
			{
2596
				if ($egwEvent['recur_type'] != MCAL_RECUR_NONE)
2597
				{
2598
					// We found a pseudo Exception
2599
					$pseudos[] = $egwEvent['id'] . ':' . $event['start'];
2600
					continue;
2601
				}
2602
			}
2603
			elseif ($filter != 'relax')
2604
			{
2605
				// check exceptions
2606
				// $exceptions[$remote_ts] = $egw_ts
2607
				$exceptions = $this->so->get_recurrence_exceptions($egwEvent, $event['$tzid'], 0, 0, 'map');
2608
				if (is_array($event['recur_exception']))
2609
				{
2610
					foreach ($event['recur_exception'] as $key => $day)
2611
					{
2612
						if (isset($exceptions[$day]))
2613
						{
2614
							unset($exceptions[$day]);
2615
						}
2616
						else
2617
						{
2618
							if ($this->log)
2619
							{
2620
								error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2621
								"() additional event['recur_exception']: $day\n",3,$this->logfile);
2622
							}
2623
							continue 2;
2624
						}
2625
					}
2626 View Code Duplication
					if (!empty($exceptions))
2627
					{
2628
						if ($this->log)
2629
						{
2630
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2631
								'() missing event[recur_exception]: ' .
2632
								array2string($event['recur_exception'])."\n",3,$this->logfile);
2633
						}
2634
						continue;
2635
					}
2636
				}
2637
2638
				// check recurrence information
2639
				foreach (array('recur_type', 'recur_interval', 'recur_enddate') as $key)
2640
				{
2641
					if (isset($event[$key])
2642
							&& $event[$key] != $egwEvent[$key])
2643
					{
2644 View Code Duplication
						if ($this->log)
2645
						{
2646
							error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2647
								"() events[$key] differ: " . $event[$key] .
2648
								' <> ' . $egwEvent[$key]."\n",3,$this->logfile);
2649
						}
2650
						continue 2;
2651
					}
2652
				}
2653
			}
2654
			$matchingEvents[] = $egwEvent['id']; // exact match
2655
		}
2656
2657
		if ($filter == 'exact' && !empty($event['uid']) && count($matchingEvents) > 1
2658
			|| $filter != 'master' && !empty($egwEvent['recur_type']) && empty($event['recur_type']))
2659
		{
2660
			// Unknown exception for existing series
2661
			if ($this->log)
2662
			{
2663
				error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2664
					"() new exception for series found.\n",3,$this->logfile);
2665
			}
2666
			$matchingEvents = array();
2667
		}
2668
2669
		// append pseudos as last entries
2670
		$matches = array_merge($matchingEvents, $pseudos);
2671
2672
		if ($this->log)
2673
		{
2674
			error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2675
				'[MATCHES]:' . array2string($matches)."\n",3,$this->logfile);
2676
		}
2677
		return $matches;
2678
	}
2679
2680
	/**
2681
	 * classifies an incoming event from the eGW point-of-view
2682
	 *
2683
     * exceptions: unlike other calendar apps eGW does not create an event exception
2684
     * if just the participant state changes - therefore we have to distinguish between
2685
     * real exceptions and status only exceptions
2686
     *
2687
     * @param array $event the event to check
2688
     *
2689
     * @return array
2690
     * 	type =>
2691
     * 		SINGLE a single event
2692
     * 		SERIES-MASTER the series master
2693
     * 		SERIES-EXCEPTION event is a real exception
2694
	  * 		SERIES-PSEUDO-EXCEPTION event is a status only exception
2695
	  * 		SERIES-EXCEPTION-PROPAGATE event was a status only exception in the past and is now a real exception
2696
	  * 	stored_event => if event already exists in the database array with event data or false
2697
	  * 	master_event => for event type SERIES-EXCEPTION, SERIES-PSEUDO-EXCEPTION or SERIES-EXCEPTION-PROPAGATE
2698
	  * 		the corresponding series master event array
2699
	  * 		NOTE: this param is false if event is of type SERIES-MASTER
2700
     */
2701
	function get_event_info($event)
2702
	{
2703
		$type = 'SINGLE'; // default
2704
		$master_event = false; //default
2705
		$stored_event = false;
2706
		$recurrence_event = false;
2707
		$wasPseudo = false;
2708
2709
		if (($foundEvents = $this->find_event($event, 'exact')))
2710
		{
2711
			// We found the exact match
2712
			$eventID = array_shift($foundEvents);
2713
			if (strstr($eventID, ':'))
2714
			{
2715
				$type = 'SERIES-PSEUDO-EXCEPTION';
2716
				$wasPseudo = true;
2717
				list($eventID, $date) = explode(':', $eventID);
2718
				$recur_date = $this->date2usertime($date);
2719
				$stored_event = $this->read($eventID, $recur_date, false, 'server');
2720
				$master_event = $this->read($eventID, 0, false, 'server');
2721
				$recurrence_event = $stored_event;
2722
			}
2723
			else
2724
			{
2725
				$stored_event = $this->read($eventID, 0, false, 'server');
2726
			}
2727
			if (!empty($stored_event['uid']) && empty($event['uid']))
2728
			{
2729
				$event['uid'] = $stored_event['uid']; // restore the UID if it was not delivered
2730
			}
2731
		}
2732
2733
		if ($event['recur_type'] != MCAL_RECUR_NONE)
2734
		{
2735
			$type = 'SERIES-MASTER';
2736
		}
2737
2738
		if ($type == 'SINGLE' &&
2739
			($foundEvents = $this->find_event($event, 'master')))
2740
		{
2741
			// SINGLE, SERIES-EXCEPTION OR SERIES-EXCEPTON-STATUS
2742
			foreach ($foundEvents  as $eventID)
2743
			{
2744
				// Let's try to find a related series
2745
				if ($this->log)
2746
				{
2747
					error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2748
					"()[MASTER]: $eventID\n",3,$this->logfile);
2749
				}
2750
				$type = 'SERIES-EXCEPTION';
2751
				if (($master_event = $this->read($eventID, 0, false, 'server')))
2752
				{
2753
					if (isset($stored_event['id']) &&
2754
						$master_event['id'] != $stored_event['id'])
2755
					{
2756
						break; // this is an existing exception
2757
					}
2758 View Code Duplication
					elseif (isset($event['recurrence']) &&
2759
						in_array($event['recurrence'], $master_event['recur_exception']))
2760
					{
2761
						$type = 'SERIES-PSEUDO-EXCEPTION'; // could also be a real one
2762
						$recurrence_event = $master_event;
2763
						$recurrence_event['start'] = $event['recurrence'];
2764
						$recurrence_event['end'] -= $master_event['start'] - $event['recurrence'];
2765
						break;
2766
					}
2767 View Code Duplication
					elseif (in_array($event['start'], $master_event['recur_exception']))
2768
					{
2769
						$type='SERIES-PSEUDO-EXCEPTION'; // new pseudo exception?
2770
						$recurrence_event = $master_event;
2771
						$recurrence_event['start'] = $event['start'];
2772
						$recurrence_event['end'] -= $master_event['start'] - $event['start'];
2773
						break;
2774
					}
2775
					else
2776
					{
2777
						// try to find a suitable pseudo exception date
2778
						$egw_rrule = calendar_rrule::event2rrule($master_event, false);
2779
						$egw_rrule->current = clone $egw_rrule->time;
2780
						while ($egw_rrule->valid())
2781
						{
2782
							$occurrence = egw_time::to($egw_rrule->current(), 'server');
2783
							if ($this->log)
2784
							{
2785
								error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2786
									'() try occurrence ' . $egw_rrule->current()
2787
									. " ($occurrence)\n",3,$this->logfile);
2788
							}
2789
							if ($event['start'] == $occurrence)
2790
							{
2791
								$type = 'SERIES-PSEUDO-EXCEPTION'; // let's try a pseudo exception
2792
								$recurrence_event = $master_event;
2793
								$recurrence_event['start'] = $occurrence;
2794
								$recurrence_event['end'] -= $master_event['start'] - $occurrence;
2795
								break 2;
2796
							}
2797
							if (isset($event['recurrence']) && $event['recurrence'] == $occurrence)
2798
							{
2799
								$type = 'SERIES-EXCEPTION-PROPAGATE';
2800
								if ($stored_event)
2801
								{
2802
									unset($stored_event['id']); // signal the true exception
2803
									$stored_event['recur_type'] = MCAL_RECUR_NONE;
2804
								}
2805
								break 2;
2806
							}
2807
							$egw_rrule->next_no_exception();
2808
						}
2809
					}
2810
				}
2811
			}
2812
		}
2813
2814
		// check pseudo exception propagation
2815
		if ($recurrence_event)
2816
		{
2817
			// default if we cannot find a proof for a fundamental change
2818
			// the recurrence_event is the master event with start and end adjusted to the recurrence
2819
			// check for changed data
2820
			foreach (array('start','end','uid','title','location','description',
2821
				'priority','public','special','non_blocking') as $key)
2822
			{
2823
				if (!empty($event[$key]) && $recurrence_event[$key] != $event[$key])
2824
				{
2825
					if ($wasPseudo)
2826
					{
2827
						// We started with a pseudo exception
2828
						$type = 'SERIES-EXCEPTION-PROPAGATE';
2829
					}
2830
					else
2831
					{
2832
						$type = 'SERIES-EXCEPTION';
2833
					}
2834
2835
					if ($stored_event)
2836
					{
2837
						unset($stored_event['id']); // signal the true exception
2838
						$stored_event['recur_type'] = MCAL_RECUR_NONE;
2839
					}
2840
					break;
2841
				}
2842
			}
2843
			// the event id here is always the id of the master event
2844
			// unset it to prevent confusion of stored event and master event
2845
			unset($event['id']);
2846
		}
2847
2848
		// check ACL
2849
		if (is_array($master_event))
2850
		{
2851
			$acl_edit = $this->check_perms(EGW_ACL_EDIT, $master_event['id']);
2852
		}
2853
		else
2854
		{
2855
			if (is_array($stored_event))
2856
			{
2857
				$acl_edit = $this->check_perms(EGW_ACL_EDIT, $stored_event['id']);
2858
			}
2859
			else
2860
			{
2861
				$acl_edit = true; // new event
2862
			}
2863
		}
2864
2865
		return array(
2866
			'type' => $type,
2867
			'acl_edit' => $acl_edit,
2868
			'stored_event' => $stored_event,
2869
			'master_event' => $master_event,
2870
		);
2871
    }
2872
2873
    /**
2874
     * Translates all timestamps for a given event from server-time to user-time.
2875
     * The update() and save() methods expect timestamps in user-time.
2876
     * @param &$event	the event we are working on
2877
     *
2878
     */
2879
    function server2usertime (&$event)
2880
    {
2881
		// we run all dates through date2usertime, to adjust to user-time
2882
		foreach(array('start','end','recur_enddate','recurrence') as $ts)
2883
		{
2884
			// we convert here from server-time to timestamps in user-time!
2885
			if (isset($event[$ts])) $event[$ts] = $event[$ts] ? $this->date2usertime($event[$ts]) : 0;
2886
		}
2887
		// same with the recur exceptions
2888 View Code Duplication
		if (isset($event['recur_exception']) && is_array($event['recur_exception']))
2889
		{
2890
			foreach($event['recur_exception'] as $n => $date)
2891
			{
2892
				$event['recur_exception'][$n] = $this->date2usertime($date);
2893
			}
2894
		}
2895
		// same with the alarms
2896 View Code Duplication
		if (isset($event['alarm']) && is_array($event['alarm']))
2897
		{
2898
			foreach($event['alarm'] as $id => $alarm)
2899
			{
2900
				$event['alarm'][$id]['time'] = $this->date2usertime($alarm['time']);
2901
			}
2902
		}
2903
    }
2904
	/**
2905
	 * Delete events that are more than $age years old
2906
	 *
2907
	 * Purges old events from the database
2908
	 *
2909
	 * @param int|float $age How many years old the event must be before it is deleted
2910
	 */
2911
	function purge($age)
2912
	{
2913
		if (is_numeric($age) && $age > 0)	// just make sure bogus values dont delete everything
2914
		{
2915
			$this->so->purge(time() - 365*24*3600*(float)$age);
2916
		}
2917
	}
2918
}
2919