Passed
Push — 16.1 ( 004f52...4b9eb7 )
by Nathan
22:12
created

calendar_bo::__construct()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 61
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 36
nc 8
nop 0
dl 0
loc 61
rs 7.399
c 0
b 0
f 0

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * EGroupware - Calendar's buisness-object - access only
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) 2004-16 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
use EGroupware\Api;
15
use EGroupware\Api\Link;
16
use EGroupware\Api\Acl;
17
18
if (!defined('ACL_TYPE_IDENTIFER'))	// used to mark ACL-values for the debug_message methode
19
{
20
	define('ACL_TYPE_IDENTIFER','***ACL***');
21
}
22
23
define('HOUR_s',60*60);
0 ignored issues
show
Coding Style introduced by
This constant is not in uppercase (expected 'HOUR_S').
Loading history...
24
define('DAY_s',24*HOUR_s);
0 ignored issues
show
Coding Style introduced by
This constant is not in uppercase (expected 'DAY_S').
Loading history...
25
define('WEEK_s',7*DAY_s);
0 ignored issues
show
Coding Style introduced by
This constant is not in uppercase (expected 'WEEK_S').
Loading history...
26
27
/**
28
 * Required (!) include, as we use the MCAL_* constants, BEFORE instanciating (and therefore autoloading) the class
29
 */
30
require_once(EGW_INCLUDE_ROOT.'/calendar/inc/class.calendar_so.inc.php');
31
32
/**
33
 * Class to access all calendar data
34
 *
35
 * For updating calendar data look at the bocalupdate class, which extends this class.
36
 *
37
 * The new UI, BO and SO classes have a strikt definition, in which time-zone they operate:
38
 *  UI only operates in user-time, so there have to be no conversation at all !!!
39
 *  BO's functions take and return user-time only (!), they convert internaly everything to servertime, because
40
 *  SO operates only in server-time
41
 *
42
 * As this BO class deals with dates/times of several types and timezone, each variable should have a postfix
43
 * appended, telling with type it is: _s = seconds, _su = secs in user-time, _ss = secs in server-time, _h = hours
44
 *
45
 * All new BO code (should be true for eGW in general) NEVER use any $_REQUEST ($_POST or $_GET) vars itself.
46
 * Nor does it store the state of any UI-elements (eg. cat-id selectbox). All this is the task of the UI class(es) !!!
47
 *
48
 * All permanent debug messages of the calendar-code should done via the debug-message method of this class !!!
49
 */
50
class calendar_bo
51
{
52
	/**
53
	 * Gives read access to the calendar, but all events the user is not participating are private!
54
	 * Used by addressbook.
55
	 */
56
	const ACL_READ_FOR_PARTICIPANTS = Acl::CUSTOM1;
57
	/**
58
	 * Right to see free/busy data only
59
	 */
60
	const ACL_FREEBUSY = Acl::CUSTOM2;
61
	/**
62
	 * Allows to invite an other user (if configured to be used!)
63
	 */
64
	const ACL_INVITE = Acl::CUSTOM3;
65
66
	/**
67
	 * @var int $debug name of method to debug or level of debug-messages:
68
	 *	False=Off as higher as more messages you get ;-)
69
	 *	1 = function-calls incl. parameters to general functions like search, read, write, delete
70
	 *	2 = function-calls to exported helper-functions like check_perms
71
	 *	4 = function-calls to exported conversation-functions like date2ts, date2array, ...
72
	 *	5 = function-calls to private functions
73
	 */
74
	var $debug=false;
75
76
	/**
77
	 * @var int $now timestamp in server-time
78
	 */
79
	var $now;
80
81
	/**
82
	 * @var int $now_su timestamp of actual user-time
83
	 */
84
	var $now_su;
85
86
	/**
87
	 * @var array $cal_prefs calendar-specific prefs
88
	 */
89
	var $cal_prefs;
90
91
	/**
92
	 * @var array $common_prefs common preferences
93
	 */
94
	var $common_prefs;
95
	/**
96
	 * Custom fields read from the calendar config
97
	 *
98
	 * @var array
99
	 */
100
	var $customfields = array();
101
	/**
102
	 * @var int $user nummerical id of the current user-id
103
	 */
104
	var $user=0;
105
106
	/**
107
	 * @var array $grants grants of the current user, array with user-id / ored-ACL-rights pairs
108
	 */
109
	var $grants=array();
110
111
	/**
112
	 * @var array $verbose_status translated 1-char status values to a verbose name, run through lang() by the constructor
113
	 */
114
	var $verbose_status = array(
115
		'A' => 'Accepted',
116
		'R' => 'Rejected',
117
		'T' => 'Tentative',
118
		'U' => 'No Response',
119
		'D' => 'Delegated',
120
		'G' => 'Group invitation',
121
	);
122
	/**
123
	 * @var array recur_types translates MCAL recur-types to verbose labels
124
	 */
125
	var $recur_types = Array(
126
		MCAL_RECUR_NONE         => 'No recurrence',
127
		MCAL_RECUR_DAILY        => 'Daily',
128
		MCAL_RECUR_WEEKLY       => 'Weekly',
129
		MCAL_RECUR_MONTHLY_WDAY => 'Monthly (by day)',
130
		MCAL_RECUR_MONTHLY_MDAY => 'Monthly (by date)',
131
		MCAL_RECUR_YEARLY       => 'Yearly'
132
	);
133
	/**
134
	 * @var array recur_days translates MCAL recur-days to verbose labels
135
	 */
136
	var $recur_days = array(
137
		MCAL_M_MONDAY    => 'Monday',
138
		MCAL_M_TUESDAY   => 'Tuesday',
139
		MCAL_M_WEDNESDAY => 'Wednesday',
140
		MCAL_M_THURSDAY  => 'Thursday',
141
		MCAL_M_FRIDAY    => 'Friday',
142
		MCAL_M_SATURDAY  => 'Saturday',
143
		MCAL_M_SUNDAY    => 'Sunday',
144
	);
145
	/**
146
	 * Standard iCal attendee roles
147
	 *
148
	 * @var array
149
	 */
150
	var $roles = array(
151
		'REQ-PARTICIPANT' => 'Requested',
152
		'CHAIR'           => 'Chair',
153
		'OPT-PARTICIPANT' => 'Optional',
154
		'NON-PARTICIPANT' => 'None',
155
	);
156
	/**
157
	 * Alarm times
158
	 *
159
	 * @var array
160
	 */
161
	var $alarms = array(
162
		300 => '5 Minutes',
163
		600 => '10 Minutes',
164
		900 => '15 Minutes',
165
		1800 => '30 Minutes',
166
		3600 => '1 Hour',
167
		7200 => '2 Hours',
168
		43200 => '12 Hours',
169
		86400 => '1 Day',
170
		172800 => '2 Days',
171
		604800 => '1 Week',
172
	);
173
	/**
174
	 * @var array $resources registered scheduling resources of the calendar (gets cached in the session for performance reasons)
175
	 */
176
	var $resources;
177
	/**
178
	 * @var array $cached_event here we do some caching to read single events only once
179
	 */
180
	protected static $cached_event = array();
181
	protected static $cached_event_date_format = false;
182
	protected static $cached_event_date = 0;
183
184
	/**
185
	 * Instance of the socal class
186
	 *
187
	 * @var calendar_so
188
	 */
189
	var $so;
190
	/**
191
	 * Instance of the categories class
192
	 *
193
	 * @var Api\Categories
194
	 */
195
	var $categories;
196
	/**
197
	 * Config values for "calendar", only used for horizont, regular calendar config is under phpgwapi
198
	 *
199
	 * @var array
200
	 */
201
	var $config;
202
203
	/**
204
	 * Does a user require an extra invite grant, to be able to invite an other user, default no
205
	 *
206
	 * @var string 'all', 'groups' or null
207
	 */
208
	public $require_acl_invite = null;
209
210
	/**
211
	 * Warnings to show in regular UI
212
	 *
213
	 * @var array
214
	 */
215
	var $warnings = array();
216
217
	/**
218
	 * Constructor
219
	 */
220
	function __construct()
221
	{
222
		if ($this->debug > 0) $this->debug_message('calendar_bo::bocal() started',True);
223
224
		$this->so = new calendar_so();
225
226
		$this->common_prefs =& $GLOBALS['egw_info']['user']['preferences']['common'];
227
		$this->cal_prefs =& $GLOBALS['egw_info']['user']['preferences']['calendar'];
228
229
		$this->now = time();
230
		$this->now_su = Api\DateTime::server2user($this->now,'ts');
231
232
		$this->user = $GLOBALS['egw_info']['user']['account_id'];
233
234
		$this->grants = $GLOBALS['egw']->acl->get_grants('calendar');
235
236
		if (!is_array($this->resources = Api\Cache::getSession('calendar', 'resources')))
237
		{
238
			$this->resources = array();
239
			foreach(Api\Hooks::process('calendar_resources') as $app => $data)
240
			{
241
				if ($data && $data['type'])
242
				{
243
					$this->resources[$data['type']] = $data + array('app' => $app);
244
				}
245
			}
246
			$this->resources['e'] = array(
247
				'type' => 'e',
248
				'info' => __CLASS__.'::email_info',
249
				'app'  => 'email',
250
			);
251
			$this->resources['l'] = array(
252
				'type' => 'l',// one char type-identifier for this resources
253
				'info' => __CLASS__ .'::mailing_lists',// info method, returns array with id, type & name for a given id
254
				'app' => 'Distribution list'
255
			);
256
			$this->resources[''] = array(
257
				'type' => '',
258
				'app' => 'api-accounts',
259
			);
260
			$this->resources['l'] = array(
261
				'type' => 'l',// one char type-identifier for this resources
262
				'info' => __CLASS__ .'::mailing_lists',// info method, returns array with id, type & name for a given id
263
				'app' => 'Distribution list'
264
			);
265
			Api\Cache::setSession('calendar', 'resources', $this->resources);
266
		}
267
		//error_log(__METHOD__ . " registered resources=". array2string($this->resources));
268
269
		$this->config = Api\Config::read('calendar');	// only used for horizont, regular calendar config is under phpgwapi
270
		$this->require_acl_invite = $GLOBALS['egw_info']['server']['require_acl_invite'];
271
272
		$this->categories = new Api\Categories($this->user,'calendar');
273
274
		$this->customfields = Api\Storage\Customfields::get('calendar');
275
276
		foreach($this->alarms as $secs => &$label)
277
		{
278
			$label = self::secs2label($secs);
279
		}
280
	}
281
282
	/**
283
	 * Generate translated label for a given number of seconds
284
	 *
285
	 * @param int $secs
286
	 * @return string
287
	 */
288
	static public function secs2label($secs)
289
	{
290
		if ($secs <= 3600)
291
		{
292
			$label = lang('%1 minutes', $secs/60);
293
		}
294
		elseif($secs <= 86400)
295
		{
296
			$label = lang('%1 hours', $secs/3600);
297
		}
298
		else
299
		{
300
			$label = lang('%1 days', $secs/86400);
301
		}
302
		return $label;
303
	}
304
305
	/**
306
	 * returns info about email addresses as participants
307
	 *
308
	 * @param int|array $ids single contact-id or array of id's
309
	 * @return array
310
	 */
311
	static function email_info($ids)
312
	{
313
		if (!$ids) return null;
314
315
		$data = array();
316
		foreach((array)$ids as $id)
317
		{
318
			$email = $id;
319
			$name = '';
320
			$matches = null;
321 View Code Duplication
			if (preg_match('/^(.*) *<([a-z0-9_.@-]{8,})>$/iU',$email,$matches))
322
			{
323
				$name = $matches[1];
324
				$email = $matches[2];
325
			}
326
			$data[] = array(
327
				'res_id' => $id,
328
				'email' => $email,
329
				'rights' => self::ACL_READ_FOR_PARTICIPANTS,
330
				'name' => $name,
331
			);
332
		}
333
		//error_log(__METHOD__.'('.array2string($ids).')='.array2string($data).' '.function_backtrace());
334
		return $data;
335
	}
336
337
	/**
338
	 * returns info about mailing lists as participants
339
	 *
340
	 * @param int|array $ids single mailing list ID or array of id's
341
	 * @return array
342
	 */
343
	static function mailing_lists($ids)
344
	{
345
		if(!is_array($ids))
346
		{
347
			$ids = array($ids);
348
		}
349
		$data = array();
350
351
		// Email list
352
		$contacts_obj = new Api\Contacts();
353
		$bo = new calendar_bo();
354
		foreach($ids as $id)
355
		{
356
			$list = $contacts_obj->read_list((int)$id);
357
358
			$data[] = array(
359
				'res_id' => $id,
360
				'rights' => self::ACL_READ_FOR_PARTICIPANTS,
361
				'name' => $list['list_name'],
362
				'resources' => $bo->enum_mailing_list('l'.$id, false, false)
363
			);
364
		}
365
366
		return $data;
367
	}
368
369
	/**
370
	 * Enumerates the contacts in a contact list, and returns the list of contact IDs
371
	 *
372
	 * This is used to enable mailing lists as owner/participant
373
	 *
374
	 * @param string $id Mailing list participant ID, which is the mailing list
375
	 *	ID prefixed with 'l'
376
	 * @param boolean $ignore_acl = false Flag to skip ACL checks
377
	 * @param boolean $use_freebusy =true should freebusy rights are taken into account, default true, can be set to false eg. for a search
378
	 *
379
	 * @return array
380
	 */
381
	public function enum_mailing_list($id, $ignore_acl= false, $use_freebusy = true)
382
	{
383
		$contact_list = array();
384
		$contacts = new Api\Contacts();
385
		if($contacts->check_list((int)substr($id,1), ACL::READ) || (int)substr($id,1) < 0)
386
		{
387
			$options = array('list' => substr($id,1));
388
			$lists = $contacts->search('',true,'','','',false,'AND',false,$options);
389
			if(!$lists)
390
			{
391
				return $contact_list;
392
			}
393
			foreach($lists as &$contact)
0 ignored issues
show
Bug introduced by
The expression $lists of type boolean|object<EGroupwar...\Db2DataIterator>|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
394
			{
395
				// Check for user account
396
				if (($account_id = $GLOBALS['egw']->accounts->name2id($contact['id'],'person_id')))
397
				{
398
					$contact = ''.$account_id;
399
				}
400
				else
401
				{
402
					$contact = 'c'.$contact['id'];
403
				}
404 View Code Duplication
				if ($ignore_acl || $this->check_perms(ACL::READ|self::ACL_READ_FOR_PARTICIPANTS|($use_freebusy?self::ACL_FREEBUSY:0),0,$contact))
405
				{
406
					if ($contact && !in_array($contact,$contact_list))	// already added?
407
					{
408
						$contact_list[] = $contact;
409
					}
410
				}
411
			}
412
		}
413
		return $contact_list;
414
	}
415
416
	/**
417
	 * Add group-members as participants with status 'G'
418
	 *
419
	 * @param array $event event-array
420
	 * @return int number of added participants
421
	 */
422
	function enum_groups(&$event)
423
	{
424
		$added = 0;
425
		foreach(array_keys($event['participants']) as $uid)
426
		{
427
			if (is_numeric($uid) && $GLOBALS['egw']->accounts->get_type($uid) == 'g' &&
428
				($members = $GLOBALS['egw']->accounts->members($uid, true)))
429
			{
430
				foreach($members as $member)
431
				{
432
					if (!isset($event['participants'][$member]))
433
					{
434
						$event['participants'][$member] = 'G';
435
						++$added;
436
					}
437
				}
438
			}
439
		}
440
		return $added;
441
	}
442
443
	/**
444
	 * Resolve users to add memberships for users and members for groups
445
	 *
446
	 * @param int|array $_users
447
	 * @param boolean $no_enum_groups =true
448
	 * @param boolean $ignore_acl =false
449
	 * @param boolean $use_freebusy =true should freebusy rights are taken into account, default true, can be set to false eg. for a search
450
	 * @return array of user-ids
451
	 */
452
	private function resolve_users($_users, $no_enum_groups=true, $ignore_acl=false, $use_freebusy=true)
453
	{
454
		if (!is_array($_users))
455
		{
456
			$_users = $_users ? array($_users) : array();
457
		}
458
		// only query calendars of users, we have READ-grants from
459
		$users = array();
460
		foreach($_users as $user)
461
		{
462
			$user = trim($user);
463
			
464
			// Handle email lists
465
			if(!is_numeric($user) && $user[0] == 'l')
466
			{
467
				foreach($this->enum_mailing_list($user, $ignore_acl, $use_freebusy) as $contact)
468
				{
469
					if ($contact && !in_array($contact,$users))	// already added?
470
					{
471
						$users[] = $contact;
472
					}
473
				}
474
				continue;
475
			}
476
			if ($ignore_acl || $this->check_perms(ACL::READ|self::ACL_READ_FOR_PARTICIPANTS|($use_freebusy?self::ACL_FREEBUSY:0),0,$user))
477
			{
478
				if ($user && !in_array($user,$users))	// already added?
479
				{
480
					// General expansion check
481
					if (!is_numeric($user) && $this->resources[$user[0]]['info'])
482
					{
483
						$info = $this->resource_info($user);
484
						if($info && $info['resources'])
485
						{
486
							foreach($info['resources'] as $_user)
487
							{
488
								if($_user && !in_array($_user, $users))
489
								{
490
									$users[] = $_user;
491
								}
492
							}
493
							continue;
494
						}
495
					}
496
					$users[] = $user;
497
				}
498
			}
499
			elseif ($GLOBALS['egw']->accounts->get_type($user) != 'g')
500
			{
501
				continue;	// for non-groups (eg. users), we stop here if we have no read-rights
502
			}
503
			// the further code is only for real users
504
			if (!is_numeric($user)) continue;
505
506
			// for groups we have to include the members
507
			if ($GLOBALS['egw']->accounts->get_type($user) == 'g')
508
			{
509
				if ($no_enum_groups) continue;
510
511
				$members = $GLOBALS['egw']->accounts->members($user, true);
512 View Code Duplication
				if (is_array($members))
513
				{
514
					foreach($members as $member)
515
					{
516
						// use only members which gave the user a read-grant
517
						if (!in_array($member, $users) &&
518
							($ignore_acl || $this->check_perms(Acl::READ|($use_freebusy?self::ACL_FREEBUSY:0),0,$member)))
519
						{
520
							$users[] = $member;
521
						}
522
					}
523
				}
524
			}
525 View Code Duplication
			else	// for users we have to include all the memberships, to get the group-events
526
			{
527
				$memberships = $GLOBALS['egw']->accounts->memberships($user, true);
528
				if (is_array($memberships))
529
				{
530
					foreach($memberships as $group)
531
					{
532
						if (!in_array($group,$users))
533
						{
534
							$users[] = $group;
535
						}
536
					}
537
				}
538
			}
539
		}
540
		return $users;
541
	}
542
543
	/**
544
	 * Searches / lists calendar entries, including repeating ones
545
	 *
546
	 * @param array $params array with the following keys
547
	 *	start date startdate of the search/list, defaults to today
548
	 *	end   date enddate of the search/list, defaults to start + one day
549
	 *	users  int|array integer user-id or array of user-id's to use, defaults to the current user
550
	 *  cat_id int|array category-id or array of cat-id's (incl. all sub-categories), default 0 = all
551
	 *	filter string all (not rejected), accepted, unknown, tentative, rejected, hideprivate or everything (incl. rejected, deleted)
552
	 *	query string pattern so search for, if unset or empty all matching entries are returned (no search)
553
	 *		Please Note: a search never returns repeating events more then once AND does not honor start+end date !!!
554
	 *	daywise boolean on True it returns an array with YYYYMMDD strings as keys and an array with events
555
	 *		(events spanning multiple days are returned each day again (!)) otherwise it returns one array with
556
	 *		the events (default), not honored in a search ==> always returns an array of events!
557
	 *	date_format string date-formats: 'ts'=timestamp (default), 'array'=array, or string with format for date
558
	 *  offset boolean|int false (default) to return all entries or integer offset to return only a limited result
559
	 *  enum_recuring boolean if true or not set (default) or daywise is set, each recurence of a recuring events is returned,
560
	 *		otherwise the original recuring event (with the first start- + enddate) is returned
561
	 *  num_rows int number of entries to return, default or if 0, max_entries from the prefs
562
	 *  order column-names plus optional DESC|ASC separted by comma
563
	 *  ignore_acl if set and true no check_perms for a general Acl::READ grants is performed
564
	 *  enum_groups boolean if set and true, group-members will be added as participants with status 'G'
565
	 *  cols string|array columns to select, if set an iterator will be returned
566
	 *  append string to append to the query, eg. GROUP BY
567
	 *  cfs array if set, query given custom fields or all for empty array, none are returned, if not set (default)
568
	 *  master_only boolean default false, true only take into account participants/status from master (for AS)
569
	 * @param string $sql_filter =null sql to be and'ed into query (fully quoted), default none
570
	 * @return iterator|array|boolean array of events or array with YYYYMMDD strings / array of events pairs (depending on $daywise param)
571
	 *	or false if there are no read-grants from _any_ of the requested users or iterator/recordset if cols are given
572
	 */
573
	function &search($params,$sql_filter=null)
574
	{
575
		$params_in = $params;
576
577
		$params['sql_filter'] = $sql_filter;	// dont allow to set it via UI or xmlrpc
578
579
		// check if any resource wants to hook into
580
		foreach($this->resources as $data)
581
		{
582
			if (isset($data['search_filter']))
583
			{
584
				$params = ExecMethod($data['search_filter'],$params);
0 ignored issues
show
Deprecated Code introduced by
The function ExecMethod() has been deprecated with message: use autoloadable class-names, instanciate and call method or use static methods

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

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

Loading history...
585
			}
586
		}
587
588
		if (!isset($params['users']) || !$params['users'] ||
589
			count($params['users']) == 1 && isset($params['users'][0]) && !$params['users'][0])	// null or '' casted to an array
590
		{
591
			// for a search use all account you have read grants from
592
			$params['users'] = $params['query'] ? array_keys($this->grants) : $this->user;
593
		}
594
		// resolve users to add memberships for users and members for groups
595
		// for search, do NOT use freebusy rights, as it would allow to probe the content of event entries
596
		$users = $this->resolve_users($params['users'], $params['filter'] == 'no-enum-groups', $params['ignore_acl'], empty($params['query']));
597
598
		// supply so with private_grants, to not query them again from the database
599
		if (!empty($params['query']))
600
		{
601
			$params['private_grants'] = array();
602
			foreach($this->grants as $user => $rights)
603
			{
604
				if ($rights & Acl::PRIVAT) $params['private_grants'][] = $user;
605
			}
606
		}
607
608
		// replace (by so not understood filter 'no-enum-groups' with 'default' filter
609
		if ($params['filter'] == 'no-enum-groups')
610
		{
611
			$params['filter'] = 'default';
612
		}
613
		// if we have no grants from the given user(s), we directly return no events / an empty array,
614
		// as calling the so-layer without users would give the events of all users (!)
615
		if (!count($users) && !$params['ignore_acl'])
616
		{
617
			return false;
618
		}
619
		if (isset($params['start'])) $start = $this->date2ts($params['start']);
620
621
		if (isset($params['end']))
622
		{
623
			$end = $this->date2ts($params['end']);
624
			$this->check_move_horizont($end);
625
		}
626
		$daywise = !isset($params['daywise']) ? False : !!$params['daywise'];
627
		$params['enum_recuring'] = $enum_recuring = $daywise || !isset($params['enum_recuring']) || !!$params['enum_recuring'];
628
		$cat_id = isset($params['cat_id']) ? $params['cat_id'] : 0;
629
		$filter = isset($params['filter']) ? $params['filter'] : 'all';
630
		$offset = isset($params['offset']) && $params['offset'] !== false ? (int) $params['offset'] : false;
631
		// socal::search() returns rejected group-invitations, as only the user not also the group is rejected
632
		// as we cant remove them efficiantly in SQL, we kick them out here, but only if just one user is displayed
633
		$users_in = (array)$params_in['users'];
634
		$remove_rejected_by_user = !in_array($filter,array('all','rejected','everything')) &&
635
			count($users_in) == 1 && $users_in[0] > 0 ? $users_in[0] : null;
636
		//error_log(__METHOD__.'('.array2string($params_in).", $sql_filter) params[users]=".array2string($params['users']).' --> remove_rejected_by_user='.array2string($remove_rejected_by_user));
637
638
		if ($this->debug && ($this->debug > 1 || $this->debug == 'search'))
639
		{
640
			$this->debug_message('calendar_bo::search(%1) start=%2, end=%3, daywise=%4, cat_id=%5, filter=%6, query=%7, offset=%8, num_rows=%9, order=%10, sql_filter=%11)',
641
				True,$params,$start,$end,$daywise,$cat_id,$filter,$params['query'],$offset,(int)$params['num_rows'],$params['order'],$params['sql_filter']);
642
		}
643
		// date2ts(,true) converts to server time, db2data converts again to user-time
644
		$events =& $this->so->search(isset($start) ? $this->date2ts($start,true) : null,isset($end) ? $this->date2ts($end,true) : null,
645
			$users,$cat_id,$filter,$offset,(int)$params['num_rows'],$params,$remove_rejected_by_user);
646
647
		if (isset($params['cols']))
648
		{
649
			return $events;
650
		}
651
		$this->total = $this->so->total;
0 ignored issues
show
Bug introduced by
The property total does not seem to exist in calendar_so.

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...
652
		$this->db2data($events,isset($params['date_format']) ? $params['date_format'] : 'ts');
653
654
		//echo "<p align=right>remove_rejected_by_user=$remove_rejected_by_user, filter=$filter, params[users]=".print_r($param['users'])."</p>\n";
655
		foreach($events as $id => $event)
656
		{
657
			if ($params['enum_groups'] && $this->enum_groups($event))
658
			{
659
				$events[$id] = $event;
660
			}
661
			$matches = null;
662
			if (!(int)$event['id'] && preg_match('/^([a-z_]+)([0-9]+)$/',$event['id'],$matches))
663
			{
664
				$is_private = self::integration_get_private($matches[1],$matches[2],$event);
665
			}
666
			else
667
			{
668
				$is_private = !$this->check_perms(Acl::READ,$event);
669
			}
670
			if (!$params['ignore_acl'] && ($is_private || (!$event['public'] && $filter == 'hideprivate')))
671
			{
672
				$this->clear_private_infos($events[$id],$users);
673
			}
674
		}
675
676
		if ($daywise)
677
		{
678
			if ($this->debug && ($this->debug > 2 || $this->debug == 'search'))
679
			{
680
				$this->debug_message('socalendar::search daywise sorting from %1 to %2 of %3',False,$start,$end,$events);
681
			}
682
			// create empty entries for each day in the reported time
683
			for($ts = $start; $ts <= $end; $ts += DAY_s) // good enough for array creation, but see while loop below.
684
			{
685
				$daysEvents[$this->date2string($ts)] = array();
686
			}
687
			foreach($events as $k => $event)
688
			{
689
				$e_start = max($this->date2ts($event['start']),$start);
690
				// $event['end']['raw']-1 to allow events to end on a full hour/day without the need to enter it as minute=59
691
				$e_end   = min($this->date2ts($event['end'])-1,$end);
692
693
				// add event to each day in the reported time
694
				$ts = $e_start;
695
				//  $ts += DAY_s in a 'for' loop does not work for daylight savings in week view
696
				// because the day is longer than DAY_s: Fullday events will be added twice.
697
				$ymd = null;
698
				while ($ts <= $e_end)
699
				{
700
					$daysEvents[$ymd = $this->date2string($ts)][] =& $events[$k];
701
					$ts = strtotime("+1 day",$ts);
702
				}
703
				if ($ymd != ($last = $this->date2string($e_end)))
704
				{
705
					$daysEvents[$last][] =& $events[$k];
706
				}
707
			}
708
			$events =& $daysEvents;
709
			if ($this->debug && ($this->debug > 2 || $this->debug == 'search'))
710
			{
711
				$this->debug_message('socalendar::search daywise events=%1',False,$events);
712
			}
713
		}
714
		if ($this->debug && ($this->debug > 0 || $this->debug == 'search'))
715
		{
716
			$this->debug_message('calendar_bo::search(%1)=%2',True,$params,$events);
717
		}
718
		//error_log(__METHOD__."() returning ".count($events)." entries, total=$this->total ".function_backtrace());
719
		return $events;
720
	}
721
722
	/**
723
	 * Get integration data for a given app of a part (value for a certain key) of it
724
	 *
725
	 * @param string $app
726
	 * @param string $part
727
	 * @return array
728
	 */
729
	static function integration_get_data($app,$part=null)
730
	{
731
		static $integration_data=null;
732
733
		if (!isset($integration_data))
734
		{
735
			$integration_data = calendar_so::get_integration_data();
736
		}
737
738
		if (!isset($integration_data[$app])) return null;
739
740
		return $part ? $integration_data[$app][$part] : $integration_data[$app];
741
	}
742
743
	/**
744
	 * Get private attribute for an integration event
745
	 *
746
	 * Attribute 'is_private' is either a boolean value, eg. false to make all events of $app public
747
	 * or an ExecMethod callback with parameters $id,$event
748
	 *
749
	 * @param string $app
750
	 * @param int|string $id
751
	 * @return string
752
	 */
753
	static function integration_get_private($app,$id,$event)
754
	{
755
		$app_data = self::integration_get_data($app,'is_private');
756
757
		// no method, fall back to link title
758
		if (is_null($app_data))
759
		{
760
			$is_private = !Link::title($app,$id);
761
		}
762
		// boolean value to make all events of $app public (false) or private (true)
763
		elseif (is_bool($app_data))
764
		{
765
			$is_private = $app_data;
766
		}
767
		else
768
		{
769
			$is_private = (bool)ExecMethod2($app_data,$id,$event);
0 ignored issues
show
Deprecated Code introduced by
The function ExecMethod2() has been deprecated with message: use autoloadable class-names, instanciate and call method or use static methods

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

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

Loading history...
770
		}
771
		//echo '<p>'.__METHOD__."($app,$id,) app_data=".array2string($app_data).' returning '.array2string($is_private)."</p>\n";
772
		return $is_private;
773
	}
774
775
	/**
776
	 * Clears all non-private info from a privat event
777
	 *
778
	 * That function only returns the infos allowed to be viewed by people without Acl::PRIVAT grants
779
	 *
780
	 * @param array &$event
781
	 * @param array $allowed_participants ids of the allowed participants, eg. the ones the search is over or eg. the owner of the calendar
782
	 */
783
	function clear_private_infos(&$event,$allowed_participants = array())
784
	{
785
		if ($event == false) return;
786
		if (!is_array($event['participants'])) error_log(__METHOD__.'('.array2string($event).', '.array2string($allowed_participants).') NO PARTICIPANTS '.function_backtrace());
787
788
		$event = array(
789
			'id'    => $event['id'],
790
			'start' => $event['start'],
791
			'end'   => $event['end'],
792
			'whole_day' => $event['whole_day'],
793
			'tzid'  => $event['tzid'],
794
			'title' => lang('private'),
795
			'modified'	=> $event['modified'],
796
			'owner'		=> $event['owner'],
797
			'uid'	=> $event['uid'],
798
			'etag'	=> $event['etag'],
799
			'participants' => array_intersect_key($event['participants'],array_flip($allowed_participants)),
800
			'public'=> 0,
801
			'category' => $event['category'],	// category is visible anyway, eg. by using planner by cat
802
			'non_blocking' => $event['non_blocking'],
803
			'caldav_name' => $event['caldav_name'],
804
		// we need full recurrence information, as they are relevant free/busy information
805
		)+($event['recur_type'] ? array(
806
			'recur_type'     => $event['recur_type'],
807
			'recur_interval' => $event['recur_interval'],
808
			'recur_data'     => $event['recur_data'],
809
			'recur_enddate'  => $event['recur_enddate'],
810
			'recur_exception'=> $event['recur_exception'],
811
		):array(
812
			'reference'      => $event['reference'],
813
			'recurrence'     => $event['recurrence'],
814
		));
815
	}
816
817
	/**
818
	 * check and evtl. move the horizont (maximum date for unlimited recuring events) to a new date
819
	 *
820
	 * @internal automaticaly called by search
821
	 * @param mixed $_new_horizont time to set the horizont to (user-time)
822
	 */
823
	function check_move_horizont($_new_horizont)
824
	{
825
		if ((int) $this->debug >= 2 || $this->debug == 'check_move_horizont')
826
		{
827
			$this->debug_message('calendar_bo::check_move_horizont(%1) horizont=%2',true,$_new_horizont,(int)$this->config['horizont']);
828
		}
829
		$new_horizont = $this->date2ts($_new_horizont,true);	// now we are in server-time, where this function operates
830
831
		if ($new_horizont <= $this->config['horizont'])	// no move necessary
832
		{
833
			if ($this->debug == 'check_move_horizont') $this->debug_message('calendar_bo::check_move_horizont(%1) horizont=%2 is bigger ==> nothing to do',true,$new_horizont,(int)$this->config['horizont']);
834
			return;
835
		}
836
		if (!empty($GLOBALS['egw_info']['server']['calendar_horizont']))
837
		{
838
			$maxdays = abs($GLOBALS['egw_info']['server']['calendar_horizont']);
839
		}
840
		if (empty($maxdays)) $maxdays = 1000; // old default
841
		if ($new_horizont > time()+$maxdays*DAY_s)		// some user tries to "look" more then the maximum number of days in the future
842
		{
843
			if ($this->debug == 'check_move_horizont') $this->debug_message('calendar_bo::check_move_horizont(%1) horizont=%2 new horizont more then %3 days from now --> ignoring it',true,$new_horizont,(int)$this->config['horizont'],$maxdays);
844
			$this->warnings['horizont'] = lang('Requested date %1 outside allowed range of %2 days: recurring events obmitted!', Api\DateTime::to($new_horizont,true), $maxdays);
845
			return;
846
		}
847
		if ($new_horizont < time()+31*DAY_s)
848
		{
849
			$new_horizont = time()+31*DAY_s;
850
		}
851
		$old_horizont = $this->config['horizont'];
852
		$this->config['horizont'] = $new_horizont;
853
854
		// create further recurrences for all recurring and not yet (at the old horizont) ended events
855
		if (($recuring = $this->so->unfinished_recuring($old_horizont)))
856
		{
857
			@set_time_limit(0);	// disable time-limit, in case it takes longer to calculate the recurrences
858
			foreach($this->read(array_keys($recuring)) as $cal_id => $event)
0 ignored issues
show
Bug introduced by
The expression $this->read(array_keys($recuring)) of type boolean|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
859
			{
860
				if ($this->debug == 'check_move_horizont')
861
				{
862
					$this->debug_message('calendar_bo::check_move_horizont(%1): calling set_recurrences(%2,%3)',true,$new_horizont,$event,$old_horizont);
863
				}
864
				// insert everything behind max(cal_start), which can be less then $old_horizont because of bugs in the past
865
				$this->set_recurrences($event,Api\DateTime::server2user($recuring[$cal_id]+1));	// set_recurences operates in user-time!
866
			}
867
		}
868
		// update the horizont
869
		Api\Config::save_value('horizont',$this->config['horizont'],'calendar');
870
871
		if ($this->debug == 'check_move_horizont') $this->debug_message('calendar_bo::check_move_horizont(%1) new horizont=%2, exiting',true,$new_horizont,(int)$this->config['horizont']);
872
	}
873
874
	/**
875
	 * set all recurrences for an event until the defined horizont $this->config['horizont']
876
	 *
877
	 * This methods operates in usertime, while $this->config['horizont'] is in servertime!
878
	 *
879
	 * @param array $event
880
	 * @param mixed $start =0 minimum start-time for new recurrences or !$start = since the start of the event
881
	 */
882
	function set_recurrences($event,$start=0)
883
	{
884 View Code Duplication
		if ($this->debug && ((int) $this->debug >= 2 || $this->debug == 'set_recurrences' || $this->debug == 'check_move_horizont'))
885
		{
886
			$this->debug_message('calendar_bo::set_recurrences(%1,%2)',true,$event,$start);
887
		}
888
		// check if the caller gave us enough information and if not read it from the DB
889
		if (!isset($event['participants']) || !isset($event['start']) || !isset($event['end']))
890
		{
891
			list(,$event_read) = each($this->so->read($event['id']));
0 ignored issues
show
Bug introduced by
$this->so->read($event['id']) cannot be passed to each() as the parameter $array expects a reference.
Loading history...
892
			if (!isset($event['participants']))
893
			{
894
				$event['participants'] = $event_read['participants'];
895
			}
896
			if (!isset($event['start']) || !isset($event['end']))
897
			{
898
				$event['start'] = $this->date2usertime($event_read['start']);
899
				$event['end'] = $this->date2usertime($event_read['end']);
900
			}
901
		}
902
		if (!$start) $start = $event['start'];
903
		$start_obj = new Api\DateTime($start);
904
		$read_start = new Api\DateTime($event_read['start']);
905
906
		$events = array();
907
		$this->insert_all_recurrences($event,$start,$this->date2usertime($this->config['horizont']),$events);
908
909
		$exceptions = array();
910
		foreach((array)$event['recur_exception'] as $exception)
911
		{
912
			$exceptions[] = Api\DateTime::to($exception, true);	// true = date
913
		}
914
		foreach($events as $event)
915
		{
916
			$is_exception = in_array(Api\DateTime::to($event['start'], true), $exceptions);
917
			$start = $this->date2ts($event['start'],true);
918
			if ($event['whole_day'])
919
			{
920
				$start = new Api\DateTime($event['start'], Api\DateTime::$server_timezone);
921
				$start->setTime(0,0,0);
922
				$start = $start->format('ts');
923
				$time = $this->so->startOfDay(new Api\DateTime($event['end'], Api\DateTime::$user_timezone));
924
				$time->setTime(23, 59, 59);
925
				$end = $this->date2ts($time,true);
926
			}
927
			else
928
			{
929
				$end = $this->date2ts($event['end'],true);
930
			}
931
			//error_log(__METHOD__."() start=".Api\DateTime::to($start).", is_exception=".array2string($is_exception));
932
			$this->so->recurrence($event['id'], $start, $end, $event['participants'], $is_exception);
933
		}
934
	}
935
936
	/**
937
	 * Convert data read from the db, eg. convert server to user-time
938
	 *
939
	 * Also make sure all timestamps comming from DB as string are converted to integer,
940
	 * to avoid misinterpretation by Api\DateTime as Ymd string.
941
	 *
942
	 * @param array &$events array of event-arrays (reference)
943
	 * @param $date_format ='ts' date-formats: 'ts'=timestamp, 'server'=timestamp in server-time, 'array'=array or string with date-format
944
	 */
945
	function db2data(&$events,$date_format='ts')
946
	{
947
		if (!is_array($events)) echo "<p>calendar_bo::db2data(\$events,$date_format) \$events is no array<br />\n".function_backtrace()."</p>\n";
948
		foreach ($events as &$event)
949
		{
950
			// convert timezone id of event to tzid (iCal id like 'Europe/Berlin')
951 View Code Duplication
			if (empty($event['tzid']) && (!$event['tz_id'] || !($event['tzid'] = calendar_timezones::id2tz($event['tz_id']))))
952
			{
953
				$event['tzid'] = Api\DateTime::$server_timezone->getName();
954
			}
955
			// database returns timestamps as string, convert them to integer
956
			// to avoid misinterpretation by Api\DateTime as Ymd string
957
			// (this will fail on 32bit systems for times > 2038!)
958
			$event['start'] = (int)$event['start'];	// this is for isWholeDay(), which also calls Api\DateTime
959
			$event['end'] = (int)$event['end'];
960
			$event['whole_day'] = self::isWholeDay($event);
961
			if ($event['whole_day'] && $date_format != 'server')
962
			{
963
				// Adjust dates to user TZ
964
				$stime =& $this->so->startOfDay(new Api\DateTime((int)$event['start'], Api\DateTime::$server_timezone), $event['tzid']);
965
				$event['start'] = Api\DateTime::to($stime, $date_format);
966
				$time =& $this->so->startOfDay(new Api\DateTime((int)$event['end'], Api\DateTime::$server_timezone), $event['tzid']);
967
				$time->setTime(23, 59, 59);
968
				$event['end'] = Api\DateTime::to($time, $date_format);
969
				if (!empty($event['recurrence']))
970
				{
971
					$time =& $this->so->startOfDay(new Api\DateTime((int)$event['recurrence'], Api\DateTime::$server_timezone), $event['tzid']);
972
					$event['recurrence'] = Api\DateTime::to($time, $date_format);
973
				}
974
				if (!empty($event['recur_enddate']))
975
				{
976
					$time =& $this->so->startOfDay(new Api\DateTime((int)$event['recur_enddate'], Api\DateTime::$server_timezone), $event['tzid']);
977
					$time->setTime(23, 59, 59);
978
					$event['recur_enddate'] = Api\DateTime::to($time, $date_format);
979
				}
980
				$timestamps = array('modified','created','deleted');
981
			}
982 View Code Duplication
			else
983
			{
984
				$timestamps = array('start','end','modified','created','recur_enddate','recurrence','recur_date','deleted');
985
			}
986
			// we convert here from the server-time timestamps to user-time and (optional) to a different date-format!
987
			foreach ($timestamps as $ts)
988
			{
989
				if (!empty($event[$ts]))
990
				{
991
					$event[$ts] = $this->date2usertime((int)$event[$ts],$date_format);
992
				}
993
			}
994
			// same with the recur exceptions
995
			if (isset($event['recur_exception']) && is_array($event['recur_exception']))
996
			{
997
				foreach($event['recur_exception'] as &$date)
998
				{
999
					if ($event['whole_day'] && $date_format != 'server')
1000
					{
1001
						// Adjust dates to user TZ
1002
						$time =& $this->so->startOfDay(new Api\DateTime((int)$date, Api\DateTime::$server_timezone), $event['tzid']);
1003
						$date = Api\DateTime::to($time, $date_format);
1004
					}
1005
					else
1006
					{
1007
						$date = $this->date2usertime((int)$date,$date_format);
1008
					}
1009
				}
1010
			}
1011
			// same with the alarms
1012 View Code Duplication
			if (isset($event['alarm']) && is_array($event['alarm']))
1013
			{
1014
				foreach($event['alarm'] as &$alarm)
1015
				{
1016
					$alarm['time'] = $this->date2usertime((int)$alarm['time'],$date_format);
1017
				}
1018
			}
1019
		}
1020
	}
1021
1022
	/**
1023
	 * convert a date from server to user-time
1024
	 *
1025
	 * @param int $ts timestamp in server-time
1026
	 * @param string $date_format ='ts' date-formats: 'ts'=timestamp, 'server'=timestamp in server-time, 'array'=array or string with date-format
1027
	 * @return mixed depending of $date_format
1028
	 */
1029
	function date2usertime($ts,$date_format='ts')
1030
	{
1031
		if (empty($ts) || $date_format == 'server') return $ts;
1032
1033
		return Api\DateTime::server2user($ts,$date_format);
1034
	}
1035
1036
	/**
1037
	 * Reads a calendar-entry
1038
	 *
1039
	 * @param int|array|string $ids id or array of id's of the entries to read, or string with a single uid
1040
	 * @param mixed $date =null date to specify a single event of a series
1041
	 * @param boolean $ignore_acl should we ignore the acl, default False for a single id, true for multiple id's
1042
	 * @param string $date_format ='ts' date-formats: 'ts'=timestamp, 'server'=timestamp in servertime, 'array'=array, or string with date-format
1043
	 * @param array|int $clear_private_infos_users =null if not null, return events with self::ACL_FREEBUSY too,
1044
	 * 	but call clear_private_infos() with the given users
1045
	 * @return boolean|array event or array of id => event pairs, false if the acl-check went wrong, null if $ids not found
1046
	 */
1047
	function read($ids,$date=null,$ignore_acl=False,$date_format='ts',$clear_private_infos_users=null)
1048
	{
1049
		if (!$ids) return false;
1050
1051
		if ($date) $date = $this->date2ts($date);
1052
1053
		$return = null;
1054
1055
		$check = $clear_private_infos_users ? self::ACL_FREEBUSY : Acl::READ;
1056
		if ($ignore_acl || is_array($ids) || ($return = $this->check_perms($check,$ids,0,$date_format,$date)))
1057
		{
1058
			if (is_array($ids) || !isset(self::$cached_event['id']) || self::$cached_event['id'] != $ids ||
1059
				self::$cached_event_date_format != $date_format ||
1060
				self::$cached_event['recur_type'] != MCAL_RECUR_NONE && self::$cached_event_date != $date)
1061
			{
1062
				$events = $this->so->read($ids,$date ? $this->date2ts($date,true) : 0);
1063
1064
				if ($events)
1065
				{
1066
					$this->db2data($events,$date_format);
1067
1068
					if (is_array($ids))
1069
					{
1070
						$return =& $events;
1071
					}
1072
					else
1073
					{
1074
						self::$cached_event = array_shift($events);
1075
						self::$cached_event_date_format = $date_format;
0 ignored issues
show
Documentation Bug introduced by
The property $cached_event_date_format was declared of type boolean, but $date_format is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
1076
						self::$cached_event_date = $date;
1077
						$return = self::$cached_event;
1078
					}
1079
				}
1080
			}
1081
			else
1082
			{
1083
				$return = self::$cached_event;
1084
			}
1085
		}
1086
		if ($clear_private_infos_users && !is_array($ids) && !$this->check_perms(Acl::READ,$return))
1087
		{
1088
			$this->clear_private_infos($return, (array)$clear_private_infos_users);
1089
		}
1090
		if ($this->debug && ($this->debug > 1 || $this->debug == 'read'))
1091
		{
1092
			$this->debug_message('calendar_bo::read(%1,%2,%3,%4,%5)=%6',True,$ids,$date,$ignore_acl,$date_format,$clear_private_infos_users,$return);
1093
		}
1094
		return $return;
1095
	}
1096
1097
	/**
1098
	 * Inserts all repetions of $event in the timespan between $start and $end into $events
1099
	 *
1100
	 * The new entries are just appended to $events, so $events is no longer sorted by startdate !!!
1101
	 *
1102
	 * Recurrences get calculated by rrule iterator implemented in calendar_rrule class.
1103
	 *
1104
	 * @param array $event repeating event whos repetions should be inserted
1105
	 * @param mixed $start start-date
1106
	 * @param mixed $end end-date
1107
	 * @param array $events where the repetions get inserted
1108
	 * @param array $recur_exceptions with date (in Ymd) as key (and True as values), seems not to be used anymore
1109
	 */
1110
	function insert_all_recurrences($event,$_start,$end,&$events)
1111
	{
1112 View Code Duplication
		if ((int) $this->debug >= 3 || $this->debug == 'set_recurrences' || $this->debug == 'check_move_horizont' || $this->debug == 'insert_all_recurrences')
1113
		{
1114
			$this->debug_message(__METHOD__.'(%1,%2,%3,&$events)',true,$event,$_start,$end);
1115
		}
1116
		$end_in = $end;
1117
1118
		$start = $this->date2ts($_start);
1119
		$event_start_ts = $this->date2ts($event['start']);
1120
		$event_length = $this->date2ts($event['end']) - $event_start_ts;	// we use a constant event-length, NOT a constant end-time!
1121
1122
		// if $end is before recur_enddate, use it instead
1123
		if (!$event['recur_enddate'] || $this->date2ts($event['recur_enddate']) > $this->date2ts($end))
1124
		{
1125
			//echo "<p>recur_enddate={$event['recur_enddate']}=".Api\DateTime::to($event['recur_enddate'])." > end=$end=".Api\DateTime::to($end)." --> using end instead of recur_enddate</p>\n";
1126
			// insert at least the event itself, if it's behind the horizont
1127
			$event['recur_enddate'] = $this->date2ts($end) < $this->date2ts($event['end']) ? $event['end'] : $end;
1128
		}
1129
		$event['recur_enddate'] = is_a($event['recur_enddate'],'DateTime') ?
1130
				$event['recur_enddate'] :
1131
				new Api\DateTime($event['recur_enddate'], calendar_timezones::DateTimeZone($event['tzid']));
1132
		// unset exceptions, as we need to add them as recurrence too, but marked as exception
1133
		unset($event['recur_exception']);
1134
		// loop over all recurrences and insert them, if they are after $start
1135
 		$rrule = calendar_rrule::event2rrule($event, !$event['whole_day'], Api\DateTime::$user_timezone->getName());	// true = we operate in usertime, like the rest of calendar_bo
1136
		foreach($rrule as $time)
1137
		{
1138
			$time->setUser();	// $time is in timezone of event, convert it to usertime used here
1139
			if($event['whole_day'])
1140
			{
1141
				// All day events are processed in server timezone
1142
				$time->setServer();
1143
				$time->setTime(0,0,0);
1144
			}
1145
			if (($ts = $this->date2ts($time)) < $start-$event_length)
1146
			{
1147
				//echo "<p>".$time." --> ignored as $ts < $start-$event_length</p>\n";
1148
				continue;	// to early or original event (returned by interator too)
1149
			}
1150
1151
			$ts_end = $ts + $event_length;
1152
			// adjust ts_end for whole day events in case it does not fit due to
1153
			// spans over summer/wintertime adjusted days
1154
			if($event['whole_day'] && ($arr_end = $this->date2array($ts_end)) &&
1155
				!($arr_end['hour'] == 23 && $arr_end['minute'] == 59 && $arr_end['second'] == 59))
1156
			{
1157
				$arr_end['hour'] = 23;
1158
				$arr_end['minute'] = 59;
1159
				$arr_end['second'] = 59;
1160
				$ts_end_guess = $this->date2ts($arr_end);
1161
				if($ts_end_guess - $ts_end > DAY_s/2)
1162
				{
1163
					$ts_end = $ts_end_guess - DAY_s; // $ts_end_guess was one day too far in the future
1164
				}
1165
				else
1166
				{
1167
					$ts_end = $ts_end_guess; // $ts_end_guess was ok
1168
				}
1169
			}
1170
1171
			$event['start'] = $ts;
1172
			$event['end'] = $ts_end;
1173
			$events[] = $event;
1174
		}
1175
		if ($this->debug && ((int) $this->debug > 2 || $this->debug == 'set_recurrences' || $this->debug == 'check_move_horizont' || $this->debug == 'insert_all_recurrences'))
1176
		{
1177
			$event['start'] = $event_start_ts;
1178
			$event['end'] = $event_start_ts + $event_length;
1179
			$this->debug_message(__METHOD__.'(%1,start=%2,end=%3,events) events=%5',True,$event,$_start,$end_in,$events);
1180
		}
1181
	}
1182
1183
	/**
1184
	 * Adds one repetion of $event for $date_ymd to the $events array, after adjusting its start- and end-time
1185
	 *
1186
	 * @param array $events array in which the event gets inserted
1187
	 * @param array $event event to insert, it has start- and end-date of the first recurrence, not of $date_ymd
1188
	 * @param int|string $date_ymd of the date of the event
1189
	 */
1190
	function add_adjusted_event(&$events,$event,$date_ymd)
1191
	{
1192
		$event_in = $event;
1193
		// calculate the new start- and end-time
1194
		$length_s = $this->date2ts($event['end']) - $this->date2ts($event['start']);
1195
		$event_start_arr = $this->date2array($event['start']);
1196
1197
		$date_arr = $this->date2array((string) $date_ymd);
1198
		$date_arr['hour'] = $event_start_arr['hour'];
1199
		$date_arr['minute'] = $event_start_arr['minute'];
1200
		$date_arr['second'] = $event_start_arr['second'];
1201
		unset($date_arr['raw']);	// else date2ts would use it
1202
		$event['start'] = $this->date2ts($date_arr);
1203
		$event['end'] = $event['start'] + $length_s;
1204
1205
		$events[] = $event;
1206
1207
		if ($this->debug && ($this->debug > 2 || $this->debug == 'add_adjust_event'))
1208
		{
1209
			$this->debug_message('calendar_bo::add_adjust_event(,%1,%2) as %3',True,$event_in,$date_ymd,$event);
1210
		}
1211
	}
1212
1213
	/**
1214
	 * Fetch information about a resource
1215
	 *
1216
	 * We do some caching here, as the resource itself might not do it.
1217
	 *
1218
	 * @param string $uid string with one-letter resource-type and numerical resource-id, eg. "r19"
1219
	 * @return array|boolean array with keys res_id,cat_id,name,useable (name definied by max_quantity in $this->resources),rights,responsible or false if $uid is not found
1220
	 */
1221
	function resource_info($uid)
1222
	{
1223
		static $res_info_cache = array();
1224
1225
		if (!is_scalar($uid)) throw new Api\Exception\WrongParameter(__METHOD__.'('.array2string($uid).') parameter must be scalar');
1226
1227
		if (!isset($res_info_cache[$uid]))
1228
		{
1229
			if (is_numeric($uid))
1230
			{
1231
				$info = array(
1232
					'res_id'    => $uid,
1233
					'email' => $GLOBALS['egw']->accounts->id2name($uid,'account_email'),
1234
					'name'  => trim($GLOBALS['egw']->accounts->id2name($uid,'account_firstname'). ' ' .
1235
					$GLOBALS['egw']->accounts->id2name($uid,'account_lastname')),
1236
					'type'  => $GLOBALS['egw']->accounts->get_type($uid),
1237
					'app'   => 'accounts',
1238
				);
1239
			}
1240
			else
1241
			{
1242
				list($info) = $this->resources[$uid[0]]['info'] ? ExecMethod($this->resources[$uid[0]]['info'],substr($uid,1)) : false;
0 ignored issues
show
Deprecated Code introduced by
The function ExecMethod() has been deprecated with message: use autoloadable class-names, instanciate and call method or use static methods

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

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

Loading history...
1243
				if ($info)
1244
				{
1245
					$info['type'] = $uid[0];
1246 View Code Duplication
					if (!$info['email'] && $info['responsible'])
1247
					{
1248
						$info['email'] = $GLOBALS['egw']->accounts->id2name($info['responsible'],'account_email');
1249
					}
1250
					$info['app'] = $this->resources[$uid[0]]['app'];
1251
				}
1252
			}
1253
			$res_info_cache[$uid] = $info;
1254
		}
1255
		if ($this->debug && ($this->debug > 2 || $this->debug == 'resource_info'))
1256
		{
1257
			$this->debug_message('calendar_bo::resource_info(%1) = %2',True,$uid,$res_info_cache[$uid]);
1258
		}
1259
		return $res_info_cache[$uid];
1260
	}
1261
1262
	/**
1263
	 * Checks if the current user has the necessary ACL rights
1264
	 *
1265
	 * The check is performed on an event or generally on the cal of an other user
1266
	 *
1267
	 * Note: Participating in an event is considered as haveing read-access on that event,
1268
	 *	even if you have no general read-grant from that user.
1269
	 *
1270
	 * @param int $needed necessary ACL right: Acl::{READ|EDIT|DELETE}
1271
	 * @param mixed $event event as array or the event-id or 0 for a general check
1272
	 * @param int $other uid to check (if event==0) or 0 to check against $this->user
1273
	 * @param string $date_format ='ts' date-format used for reading: 'ts'=timestamp, 'array'=array, 'string'=iso8601 string for xmlrpc
1274
	 * @param mixed $date_to_read =null date used for reading, internal param for the caching
1275
	 * @param int $user =null for which user to check, default current user
1276
	 * @return boolean true permission granted, false for permission denied or null if event not found
1277
	 */
1278
	function check_perms($needed,$event=0,$other=0,$date_format='ts',$date_to_read=null,$user=null)
1279
	{
1280
		if (!$user) $user = $this->user;
1281 View Code Duplication
		if ($user == $this->user)
1282
		{
1283
			$grants = $this->grants;
1284
		}
1285
		else
1286
		{
1287
			$grants = $GLOBALS['egw']->acl->get_grants('calendar',true,$user);
1288
		}
1289
1290
		if ($other && !is_numeric($other))
1291
		{
1292
			$resource = $this->resource_info($other);
1293
			return $needed & $resource['rights'];
1294
		}
1295
		if (is_int($event) && $event == 0)
1296
		{
1297
			$owner = $other ? $other : $user;
1298
		}
1299
		else
1300
		{
1301
			if (!is_array($event))
1302
			{
1303
				$event = $this->read($event,$date_to_read,true,$date_format);	// = no ACL check !!!
1304
			}
1305
			if (!is_array($event))
1306
			{
1307
				if ($this->xmlrpc)
1308
				{
1309
					$GLOBALS['server']->xmlrpc_error($GLOBALS['xmlrpcerr']['not_exist'],$GLOBALS['xmlrpcstr']['not_exist']);
1310
				}
1311
				return null;	// event not found
1312
			}
1313
			$owner = $event['owner'];
1314
			$private = !$event['public'];
1315
		}
1316
		$grant = $grants[$owner];
1317
1318
		// now any ACL rights (but invite rights!) implicate FREEBUSY rights (at least READ has to include FREEBUSY)
1319
		if ($grant & ~self::ACL_INVITE) $grant |= self::ACL_FREEBUSY;
1320
1321
		if (is_array($event) && ($needed == Acl::READ || $needed == self::ACL_FREEBUSY))
1322
		{
1323
			// Check if the $user is one of the participants or has a read-grant from one of them
1324
			// in that case he has an implicite READ grant for that event
1325
			//
1326
			if ($event['participants'] && is_array($event['participants']))
1327
			{
1328
				foreach(array_keys($event['participants']) as $uid)
1329
				{
1330
					if ($uid == $user || $uid < 0 && in_array($user, (array)$GLOBALS['egw']->accounts->members($uid,true)))
1331
					{
1332
						// if we are a participant, we have an implicite FREEBUSY, READ and PRIVAT grant
1333
						$grant |= self::ACL_FREEBUSY | Acl::READ | Acl::PRIVAT;
1334
						break;
1335
					}
1336
					elseif ($grants[$uid] & Acl::READ)
1337
					{
1338
						// if we have a READ grant from a participant, we dont give an implicit privat grant too
1339
						$grant |= Acl::READ;
1340
						// we cant break here, as we might be a participant too, and would miss the privat grant
1341
					}
1342
					elseif (!is_numeric($uid))
1343
					{
1344
						// if the owner only grants self::ACL_FREEBUSY we are not interested in the recources explicit rights
1345
						if ($grant == self::ACL_FREEBUSY) continue;
1346
						// if we have a resource as participant
1347
						$resource = $this->resource_info($uid);
1348
						$grant |= $resource['rights'];
1349
					}
1350
				}
1351
			}
1352
		}
1353
		if ($GLOBALS['egw']->accounts->get_type($owner) == 'g' && $needed == Acl::ADD)
1354
		{
1355
			$access = False;	// a group can't be the owner of an event
1356
		}
1357
		else
1358
		{
1359
			$access = $user == $owner || $grant & $needed
1360
				&& ($needed == self::ACL_FREEBUSY || !$private || $grant & Acl::PRIVAT);
1361
		}
1362
		// do NOT allow users to purge deleted events, if we dont have 'userpurge' enabled
1363
		if ($access && $needed == Acl::DELETE && $event['deleted'] &&
1364
			!$GLOBALS['egw_info']['user']['apps']['admin'] &&
1365
			$GLOBALS['egw_info']['server']['calendar_delete_history'] != 'userpurge')
1366
		{
1367
			$access = false;
1368
		}
1369
		if ($this->debug && ($this->debug > 2 || $this->debug == 'check_perms'))
1370
		{
1371
			$this->debug_message('calendar_bo::check_perms(%1,%2,other=%3,%4,%5,user=%6)=%7',True,ACL_TYPE_IDENTIFER.$needed,$event,$other,$date_format,$date_to_read,$user,$access);
1372
		}
1373
		//error_log(__METHOD__."($needed,".array2string($event).",$other,...,$user) returning ".array2string($access));
1374
		return $access;
1375
	}
1376
1377
	/**
1378
	 * Converts several date-types to a timestamp and optionally converts user- to server-time
1379
	 *
1380
	 * @param mixed $date date to convert, should be one of the following types
1381
	 *	string (!) in form YYYYMMDD or iso8601 YYYY-MM-DDThh:mm:ss or YYYYMMDDThhmmss
1382
	 *	int already a timestamp
1383
	 *	array with keys 'second', 'minute', 'hour', 'day' or 'mday' (depricated !), 'month' and 'year'
1384
	 * @param boolean $user2server =False conversion between user- and server-time; default False == Off
1385
	 */
1386
	static function date2ts($date,$user2server=False)
1387
	{
1388
		return $user2server ? Api\DateTime::user2server($date,'ts') : Api\DateTime::to($date,'ts');
1389
	}
1390
1391
	/**
1392
	 * Converts a date to an array and optionally converts server- to user-time
1393
	 *
1394
	 * @param mixed $date date to convert
1395
	 * @param boolean $server2user conversation between user- and server-time default False == Off
1396
	 * @return array with keys 'second', 'minute', 'hour', 'day', 'month', 'year', 'raw' (timestamp) and 'full' (Ymd-string)
1397
	 */
1398
	static function date2array($date,$server2user=False)
1399
	{
1400
		return $server2user ? Api\DateTime::server2user($date,'array') : Api\DateTime::to($date,'array');
1401
	}
1402
1403
	/**
1404
	 * Converts a date as timestamp or array to a date-string and optionaly converts server- to user-time
1405
	 *
1406
	 * @param mixed $date integer timestamp or array with ('year','month',..,'second') to convert
1407
	 * @param boolean $server2user conversation between user- and server-time default False == Off, not used if $format ends with \Z
1408
	 * @param string $format ='Ymd' format of the date to return, eg. 'Y-m-d\TH:i:sO' (2005-11-01T15:30:00+0100)
1409
	 * @return string date formatted according to $format
1410
	 */
1411
	static function date2string($date,$server2user=False,$format='Ymd')
1412
	{
1413
		return $server2user ? Api\DateTime::server2user($date,$format) : Api\DateTime::to($date,$format);
1414
	}
1415
1416
	/**
1417
	 * Formats a date given as timestamp or array
1418
	 *
1419
	 * @param mixed $date integer timestamp or array with ('year','month',..,'second') to convert
1420
	 * @param string|boolean $format ='' default common_prefs[dateformat], common_prefs[timeformat], false=time only, true=date only
1421
	 * @return string the formated date (incl. time)
1422
	 */
1423
	static function format_date($date,$format='')
1424
	{
1425
		return Api\DateTime::to($date,$format);
0 ignored issues
show
Bug introduced by
It seems like $format defined by parameter $format on line 1423 can also be of type boolean; however, EGroupware\Api\DateTime::to() does only seem to accept string, 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...
1426
	}
1427
1428
	/**
1429
	 * Gives out a debug-message with certain parameters
1430
	 *
1431
	 * All permanent debug-messages in the calendar should be done by this function !!!
1432
	 *	(In future they may be logged or sent as xmlrpc-faults back.)
1433
	 *
1434
	 * Permanent debug-message need to make sure NOT to give secret information like passwords !!!
1435
	 *
1436
	 * This function do NOT honor the setting of the debug variable, you may use it like
1437
	 * if ($this->debug > N) $this->debug_message('Error ;-)');
1438
	 *
1439
	 * The parameters get formated depending on their type. ACL-values need a ACL_TYPE_IDENTIFER prefix.
1440
	 *
1441
	 * @param string $msg message with parameters/variables like lang(), eg. '%1'
1442
	 * @param boolean $backtrace =True include a function-backtrace, default True=On
1443
	 *	should only be set to False=Off, if your code ensures a call with backtrace=On was made before !!!
1444
	 * @param mixed $param a variable number of parameters, to be inserted in $msg
1445
	 *	arrays get serialized with print_r() !
1446
	 */
1447
	static function debug_message($msg,$backtrace=True)
1448
	{
1449
		static $acl2string = array(
1450
			0               => 'ACL-UNKNOWN',
1451
			Acl::READ    => 'ACL_READ',
1452
			Acl::ADD     => 'ACL_ADD',
1453
			Acl::EDIT    => 'ACL_EDIT',
1454
			Acl::DELETE  => 'ACL_DELETE',
1455
			Acl::PRIVAT => 'ACL_PRIVATE',
1456
			self::ACL_FREEBUSY => 'ACL_FREEBUSY',
1457
		);
1458
		for($i = 2; $i < func_num_args(); ++$i)
1459
		{
1460
			$param = func_get_arg($i);
1461
1462
			if (is_null($param))
1463
			{
1464
				$param='NULL';
1465
			}
1466
			else
1467
			{
1468
				switch(gettype($param))
1469
				{
1470
					case 'string':
1471
						if (substr($param,0,strlen(ACL_TYPE_IDENTIFER))== ACL_TYPE_IDENTIFER)
1472
						{
1473
							$param = (int) substr($param,strlen(ACL_TYPE_IDENTIFER));
1474
							$param = (isset($acl2string[$param]) ? $acl2string[$param] : $acl2string[0])." ($param)";
1475
						}
1476
						else
1477
						{
1478
							$param = "'$param'";
1479
						}
1480
						break;
1481
					case 'EGroupware\\Api\\DateTime':
1482
					case 'egw_time':
1483
					case 'datetime':
1484
						$p = $param;
1485
						unset($param);
1486
						$param = $p->format('l, Y-m-d H:i:s').' ('.$p->getTimeZone()->getName().')';
1487
						break;
1488
					case 'array':
1489
					case 'object':
1490
						$param = array2string($param);
1491
						break;
1492
					case 'boolean':
1493
						$param = $param ? 'True' : 'False';
1494
						break;
1495
					case 'integer':
1496
						if ($param >= mktime(0,0,0,1,1,2000)) $param = adodb_date('Y-m-d H:i:s',$param)." ($param)";
1497
						break;
1498
				}
1499
			}
1500
			$msg = str_replace('%'.($i-1),$param,$msg);
1501
		}
1502
		error_log($msg);
1503
		if ($backtrace) error_log(function_backtrace(1));
1504
	}
1505
1506
	/**
1507
	 * Formats one or two dates (range) as long date (full monthname), optionaly with a time
1508
	 *
1509
	 * @param mixed $_first first date
1510
	 * @param mixed $last =0 last date if != 0 (default)
1511
	 * @param boolean $display_time =false should a time be displayed too
1512
	 * @param boolean $display_day =false should a day-name prefix the date, eg. monday June 20, 2006
1513
	 * @return string with formated date
1514
	 */
1515
	function long_date($_first,$last=0,$display_time=false,$display_day=false)
1516
	{
1517
		$first = $this->date2array($_first);
1518
		if ($last)
1519
		{
1520
			$last = $this->date2array($last);
1521
		}
1522
		$datefmt = $this->common_prefs['dateformat'];
1523
		$timefmt = $this->common_prefs['timeformat'] == 12 ? 'h:i a' : 'H:i';
1524
1525
		$month_before_day = strtolower($datefmt[0]) == 'm' ||
1526
			strtolower($datefmt[2]) == 'm' && $datefmt[4] == 'd';
1527
1528
		if ($display_day)
1529
		{
1530
			$range = lang(adodb_date('l',$first['raw'])).($this->common_prefs['dateformat'][0] != 'd' ? ' ' : ', ');
1531
		}
1532
		for ($i = 0; $i < 5; $i += 2)
1533
		{
1534
			switch($datefmt[$i])
1535
			{
1536
				case 'd':
1537
					$range .= $first['day'] . ($datefmt[1] == '.' ? '.' : '');
1538
					if ($first['month'] != $last['month'] || $first['year'] != $last['year'])
1539
					{
1540
						if (!$month_before_day)
1541
						{
1542
							$range .= ' '.lang(strftime('%B',$first['raw']));
1543
						}
1544
						if ($first['year'] != $last['year'] && $datefmt[0] != 'Y')
1545
						{
1546
							$range .= ($datefmt[0] != 'd' ? ', ' : ' ') . $first['year'];
1547
						}
1548
						if ($display_time)
1549
						{
1550
							$range .= ' '.adodb_date($timefmt,$first['raw']);
1551
						}
1552
						if (!$last)
1553
						{
1554
							return $range;
1555
						}
1556
						$range .= ' - ';
1557
1558
						if ($first['year'] != $last['year'] && $datefmt[0] == 'Y')
1559
						{
1560
							$range .= $last['year'] . ', ';
1561
						}
1562
1563
						if ($month_before_day)
1564
						{
1565
							$range .= lang(strftime('%B',$last['raw']));
1566
						}
1567
					}
1568
					else
1569
					{
1570
						if ($display_time)
1571
						{
1572
							$range .= ' '.adodb_date($timefmt,$first['raw']);
1573
						}
1574
						$range .= ' - ';
1575
					}
1576
					$range .= ' ' . $last['day'] . ($datefmt[1] == '.' ? '.' : '');
1577
					break;
1578
				case 'm':
1579
				case 'M':
1580
					$range .= ' '.lang(strftime('%B',$month_before_day ? $first['raw'] : $last['raw'])) . ' ';
1581
					break;
1582
				case 'Y':
1583
					if ($datefmt[0] != 'm')
1584
					{
1585
						$range .= ' ' . ($datefmt[0] == 'Y' ? $first['year'].($datefmt[2] == 'd' ? ', ' : ' ') : $last['year'].' ');
1586
					}
1587
					break;
1588
			}
1589
		}
1590
		if ($display_time && $last)
1591
		{
1592
			$range .= ' '.adodb_date($timefmt,$last['raw']);
1593
		}
1594
		if ($datefmt[4] == 'Y' && $datefmt[0] == 'm')
1595
		{
1596
			$range .= ', ' . $last['year'];
1597
		}
1598
		return $range;
1599
	}
1600
1601
	/**
1602
	 * Displays a timespan, eg. $both ? "10:00 - 13:00: 3h" (10:00 am - 1 pm: 3h) : "10:00 3h" (10:00 am 3h)
1603
	 *
1604
	 * @param int $start_m start time in minutes since 0h
1605
	 * @param int $end_m end time in minutes since 0h
1606
	 * @param boolean $both =false display the end-time too, duration is always displayed
1607
	 */
1608
	function timespan($start_m,$end_m,$both=false)
1609
	{
1610
		$duration = $end_m - $start_m;
1611
		if ($end_m == 24*60-1) ++$duration;
1612
		$duration = floor($duration/60).lang('h').($duration%60 ? $duration%60 : '');
1613
1614
		$timespan = $t = Api\DateTime::to('20000101T'.sprintf('%02d',$start_m/60).sprintf('%02d',$start_m%60).'00', false);
1615
1616
		if ($both)	// end-time too
1617
		{
1618
			$timespan .= ' - '.Api\DateTime::to('20000101T'.sprintf('%02d',$end_m/60).sprintf('%02d',$end_m%60).'00', false);
1619
			// dont double am/pm if they are the same in both times
1620
			if ($this->common_prefs['timeformat'] == 12 && substr($timespan,-2) == substr($t,-2))
1621
			{
1622
				$timespan = str_replace($t,substr($t,0,-3),$timespan);
1623
			}
1624
			$timespan .= ':';
1625
		}
1626
		return $timespan . ' ' . $duration;
1627
	}
1628
1629
	/**
1630
	* Converts a participant into a (readable) user- or resource-name
1631
	*
1632
	* @param string|int $id id of user or resource
1633
	* @param string|boolean $use_type =false type-letter or false
1634
	* @param boolean $append_email =false append email (Name <email>)
1635
	* @return string with name
1636
	*/
1637
	function participant_name($id,$use_type=false, $append_email=false)
1638
	{
1639
		static $id2lid = array();
1640
		static $id2email = array();
1641
1642
		if ($use_type && $use_type != 'u') $id = $use_type.$id;
1643
1644
		if (!isset($id2lid[$id]))
1645
		{
1646
			if (!is_numeric($id))
1647
			{
1648
				$id2lid[$id] = '#'.$id;
1649
				if (($info = $this->resource_info($id)))
1650
				{
1651
					$id2lid[$id] = $info['name'] ? $info['name'] : $info['email'];
1652
					if ($info['name']) $id2email[$id] = $info['email'];
1653
				}
1654
			}
1655
			else
1656
			{
1657
				$id2lid[$id] = Api\Accounts::username($id);
1658
				$id2email[$id] = $GLOBALS['egw']->accounts->id2name($id,'account_email');
1659
			}
1660
		}
1661
		return $id2lid[$id].(($append_email || $id[0] == 'e') && $id2email[$id] ? ' <'.$id2email[$id].'>' : '');
1662
	}
1663
1664
	/**
1665
	* Converts participants array of an event into array of (readable) participant-names with status
1666
	*
1667
	* @param array $event event-data
1668
	* @param boolean $long_status =false should the long/verbose status or an icon be use
1669
	* @param boolean $show_group_invitation =false show group-invitations (status == 'G') or not (default)
1670
	* @return array with id / names with status pairs
1671
	*/
1672
	function participants($event,$long_status=false,$show_group_invitation=false)
1673
	{
1674
		//error_log(__METHOD__.__LINE__.array2string($event['participants']));
1675
		$names = array();
1676
		foreach((array)$event['participants'] as $id => $status)
1677
		{
1678
			if (!is_string($status)) continue;
1679
			$quantity = $role = null;
1680
			calendar_so::split_status($status,$quantity,$role);
1681
1682
			if ($status == 'G' && !$show_group_invitation) continue;	// dont show group-invitation
1683
1684
			$lang_status = lang($this->verbose_status[$status]);
1685
			if (!$long_status)
1686
			{
1687
				switch($status[0])
1688
				{
1689
					case 'A':	// accepted
1690
						$status = Api\Html::image('calendar','accepted',$lang_status);
1691
						break;
1692
					case 'R':	// rejected
1693
						$status = Api\Html::image('calendar','rejected',$lang_status);
1694
						break;
1695
					case 'T':	// tentative
1696
						$status = Api\Html::image('calendar','tentative',$lang_status);
1697
						break;
1698
					case 'U':	// no response = unknown
1699
						$status = Api\Html::image('calendar','needs-action',$lang_status);
1700
						break;
1701
					case 'D':	// delegated
1702
						$status = Api\Html::image('calendar','forward',$lang_status);
1703
						break;
1704
					case 'G':	// group invitation
1705
						// Todo: Image, seems not to be used
1706
						$status = '('.$lang_status.')';
1707
						break;
1708
				}
1709
			}
1710
			else
1711
			{
1712
				$status = '('.$lang_status.')';
1713
			}
1714
			$names[$id] = Api\Html::htmlspecialchars($this->participant_name($id)).($quantity > 1 ? ' ('.$quantity.')' : '').' '.$status;
1715
1716
			// add role, if not a regular participant
1717
			if ($role != 'REQ-PARTICIPANT')
1718
			{
1719
				if (isset($this->roles[$role]))
1720
				{
1721
					$role = lang($this->roles[$role]);
1722
				}
1723
				// allow to use cats as roles (beside regular iCal ones)
1724
				elseif (substr($role,0,6) == 'X-CAT-' && ($cat_id = (int)substr($role,6)) > 0)
1725
				{
1726
					$role = $GLOBALS['egw']->categories->id2name($cat_id);
1727
				}
1728
				else
1729
				{
1730
					$role = lang(str_replace('X-','',$role));
1731
				}
1732
				$names[$id] .= ' '.$role;
1733
			}
1734
		}
1735
		natcasesort($names);
1736
1737
		return $names;
1738
	}
1739
1740
	/**
1741
	* Converts category string of an event into array of (readable) category-names
1742
	*
1743
	* @param string $category cat-id (multiple id's commaseparated)
1744
	* @param int $color color of the category, if multiple cats, the color of the last one with color is returned
1745
	* @return array with id / names
1746
	*/
1747
	function categories($category,&$color)
1748
	{
1749
		static $id2cat = array();
1750
		$cats = array();
1751
		$color = 0;
1752
1753
		foreach(explode(',',$category) as $cat_id)
1754
		{
1755
			if (!$cat_id) continue;
1756
1757
			if (!isset($id2cat[$cat_id]))
1758
			{
1759
				$id2cat[$cat_id] = Api\Categories::read($cat_id);
1760
			}
1761
			$cat = $id2cat[$cat_id];
1762
1763
			$parts = null;
1764
			if (is_array($cat['data']) && !empty($cat['data']['color']))
1765
			{
1766
				$color = $cat['data']['color'];
1767
			}
1768
			elseif(preg_match('/(#[0-9A-Fa-f]{6})/', $cat['description'], $parts))
1769
			{
1770
				$color = $parts[1];
1771
			}
1772
			$cats[$cat_id] = stripslashes($cat['name']);
1773
		}
1774
		return $cats;
1775
	}
1776
1777
	/**
1778
	 *  This is called only by list_cals().  It was moved here to remove fatal error in php5 beta4
1779
	 */
1780
	private static function _list_cals_add($id,&$users,&$groups)
1781
	{
1782
		$name = Api\Accounts::username($id);
1783
		if (!($egw_name = $GLOBALS['egw']->accounts->id2name($id)))
1784
		{
1785
			return;	// do not return no longer existing accounts which eg. still mentioned in acl
1786
		}
1787
		if (($type = $GLOBALS['egw']->accounts->get_type($id)) == 'g')
1788
		{
1789
			$arr = &$groups;
1790
		}
1791
		else
1792
		{
1793
			$arr = &$users;
1794
		}
1795
		$arr[$id] = array(
1796
			'grantor' => $id,
1797
			'value'   => ($type == 'g' ? 'g_' : '') . $id,
1798
			'name'    => $name,
1799
			'sname'	  => $egw_name
1800
		);
1801
	}
1802
1803
	/**
1804
	 * generate list of user- / group-calendars for the selectbox in the header
1805
	 *
1806
	 * @return array alphabeticaly sorted array with users first and then groups: array('grantor'=>$id,'value'=>['g_'.]$id,'name'=>$name)
1807
	 */
1808
	function list_cals()
1809
	{
1810
		return self::list_calendars($GLOBALS['egw_info']['user']['account_id'], $this->grants);
1811
	}
1812
1813
	/**
1814
	 * generate list of user- / group-calendars or a given user
1815
	 *
1816
	 * @param int $user account_id of user to generate list for
1817
	 * @param array $grants =null calendar grants from user, or null to query them from acl class
1818
	 */
1819
	public static function list_calendars($user, array $grants=null)
1820
	{
1821
		if (is_null($grants)) $grants = $GLOBALS['egw']->acl->get_grants('calendar', true, $user);
1822
1823
		$users = $groups = array();
1824
		foreach(array_keys($grants) as $id)
1825
		{
1826
			self::_list_cals_add($id,$users,$groups);
1827
		}
1828
		if (($memberships = $GLOBALS['egw']->accounts->memberships($user, true)))
1829
		{
1830
			foreach($memberships as $group)
1831
			{
1832
				self::_list_cals_add($group,$users,$groups);
1833
1834
				if (($account_perms = $GLOBALS['egw']->acl->get_ids_for_location($group,Acl::READ,'calendar')))
1835
				{
1836
					foreach($account_perms as $id)
1837
					{
1838
						self::_list_cals_add($id,$users,$groups);
1839
					}
1840
				}
1841
			}
1842
		}
1843
		usort($users, array(__CLASS__, 'name_cmp'));
1844
		usort($groups, array(__CLASS__, 'name_cmp'));
1845
1846
		return array_merge($users, $groups);	// users first and then groups, both alphabeticaly
1847
	}
1848
1849
	/**
1850
	 * Compare function for sort by value of key 'name'
1851
	 *
1852
	 * @param array $a
1853
	 * @param array $b
1854
	 * @return int
1855
	 */
1856
	public static function name_cmp(array $a, array $b)
1857
	{
1858
		return strnatcasecmp($a['name'], $b['name']);
1859
	}
1860
1861
	/**
1862
	 * Convert the recurrence-information of an event, into a human readable string
1863
	 *
1864
	 * @param array $event
1865
	 * @return string
1866
	 */
1867
	function recure2string($event)
1868
	{
1869
		if (!is_array($event)) return false;
1870
		return (string)calendar_rrule::event2rrule($event);
1871
	}
1872
1873
	/**
1874
	 * Read the holidays for a given $year
1875
	 *
1876
	 * The holidays get cached in the session (performance), so changes in holidays or birthdays do NOT affect a current session!!!
1877
	 *
1878
	 * @param int $year =0 year, defaults to 0 = current year
1879
	 * @return array indexed with Ymd of array of holidays. A holiday is an array with the following fields:
1880
	 *	name: string
1881
	 *  title: optional string with description
1882
	 *	day: numerical day in month
1883
	 *	month: numerical month
1884
	 *	occurence: numerical year or 0 for every year
1885
	 */
1886
	function read_holidays($year=0)
1887
	{
1888
		if (!$year) $year = (int) date('Y',$this->now_su);
1889
1890
		$holidays = calendar_holidays::read(
1891
				!empty($GLOBALS['egw_info']['server']['ical_holiday_url']) ?
1892
				$GLOBALS['egw_info']['server']['ical_holiday_url'] :
1893
				$GLOBALS['egw_info']['user']['preferences']['common']['country'], $year);
1894
1895
		// search for birthdays
1896
		if ($GLOBALS['egw_info']['server']['hide_birthdays'] != 'yes')
1897
		{
1898
			$contacts = new Api\Contacts();
1899
			foreach($contacts->get_addressbooks() as $owner => $name)
1900
			{
1901
				$holidays += $contacts->read_birthdays($owner, $year);
1902
			}
1903
		}
1904
1905
		if ((int) $this->debug >= 2 || $this->debug == 'read_holidays')
1906
		{
1907
			$this->debug_message('calendar_bo::read_holidays(%1)=%2',true,$year,$holidays);
1908
		}
1909
		return $holidays;
1910
	}
1911
1912
	/**
1913
	 * Get translated calendar event fields, presenting as link title options
1914
	 *
1915
	 * @param type $event
1916
	 * @return array array of selected calendar fields
1917
	 */
1918
	public static function get_link_options ($event = array())
1919
	{
1920
		unset($event);	// not used, but required by function signature
1921
		$options = array (
1922
			'end' => lang('End date'),
1923
			'id' => lang('ID'),
1924
			'owner' => lang('Event owner'),
1925
			'category' => lang('Category'),
1926
			'location' => lang('Location'),
1927
			'creator' => lang('Creator'),
1928
			'participants' => lang('Participants')
1929
		);
1930
		return $options;
1931
	}
1932
1933
	/**
1934
	 * get title for an event identified by $event
1935
	 *
1936
	 * Is called as hook to participate in the linking
1937
	 *
1938
	 * @param int|array $entry int cal_id or array with event
1939
	 * @param string|boolean string with title, null if not found or false if not read perms
1940
	 */
1941
	function link_title($event)
1942
	{
1943
		if (!is_array($event) && strpos($event, '-') !== false)
1944
		{
1945
			list($id, $recur) = explode('-', $event, 2);
1946
			$event = $this->read($id, $recur);
1947
		}
1948
		else if (!is_array($event) && (int) $event > 0)
1949
		{
1950
			$event = $this->read($event);
1951
		}
1952
		if (!is_array($event))
1953
		{
1954
			return $event;
1955
		}
1956
		$type = explode(',',$this->cal_prefs['link_title']);
1957
		if (is_array($type))
1958
		{
1959
			foreach ($type as &$val)
1960
			{
1961
				switch ($val)
1962
				{
1963
					case 'end':
1964
					case 'modified':
1965
						$extra_fields [$val] = $this->format_date($event[$val]);
1966
						break;
1967
					case 'participants':
1968
						foreach (array_keys($event[$val]) as $key)
1969
						{
1970
							$extra_fields [$val] = Api\Accounts::id2name($key, 'account_fullname');
1971
						}
1972
						break;
1973
					case 'modifier':
1974
					case 'creator':
1975
					case 'owner':
1976
						$extra_fields [$val] = Api\Accounts::id2name($event[$val], 'account_fullname');
1977
						break;
1978
					default:
1979
						$extra_fields [] = $event[$val];
1980
				}
1981
			}
1982
			$str_fields = implode(', ',$extra_fields);
1983
			if (is_array($extra_fields)) return $this->format_date($event['start']) . ': ' . $event['title'] . ($str_fields? ', ' . $str_fields:'');
1984
		}
1985
		return $this->format_date($event['start']) . ': ' . $event['title'];
1986
	}
1987
1988
	/**
1989
	 * query calendar for events matching $pattern
1990
	 *
1991
	 * Is called as hook to participate in the linking
1992
	 *
1993
	 * @param string $pattern pattern to search
1994
	 * @return array with cal_id - title pairs of the matching entries
1995
	 */
1996
	function link_query($pattern, Array &$options = array())
1997
	{
1998
		$result = array();
1999
		$query = array(
2000
			'query'	=>	$pattern,
2001
			'offset' =>	$options['start'],
2002
			'order' => 'cal_start DESC',
2003
		);
2004
		if($options['num_rows']) {
2005
			$query['num_rows'] = $options['num_rows'];
2006
		}
2007
		foreach((array) $this->search($query) as $event)
2008
		{
2009
			$result[$event['id']] = $this->link_title($event);
2010
		}
2011
		$options['total'] = $this->total;
2012
		return $result;
2013
	}
2014
2015
	/**
2016
	 * Check access to the file store
2017
	 *
2018
	 * @param int $id id of entry
2019
	 * @param int $check Acl::READ for read and Acl::EDIT for write or delete access
2020
	 * @param string $rel_path =null currently not used in calendar
2021
	 * @param int $user =null for which user to check, default current user
2022
	 * @return boolean true if access is granted or false otherwise
2023
	 */
2024
	function file_access($id,$check,$rel_path,$user=null)
2025
	{
2026
		unset($rel_path);	// not used, but required by function signature
2027
2028
		return $this->check_perms($check,$id,0,'ts',null,$user);
2029
	}
2030
2031
	/**
2032
	 * sets the default prefs, if they are not already set (on a per pref. basis)
2033
	 *
2034
	 * It sets a flag in the app-session-data to be called only once per session
2035
	 */
2036
	function check_set_default_prefs()
2037
	{
2038
		if ($this->cal_prefs['interval'] && ($set = Api\Cache::getSession('calendar', 'default_prefs_set')))
2039
		{
2040
			return;
2041
		}
2042
		Api\Cache::setSession('calendar', 'default_prefs_set', 'set');
2043
2044
		$default_prefs =& $GLOBALS['egw']->preferences->default['calendar'];
2045
		$forced_prefs  =& $GLOBALS['egw']->preferences->forced['calendar'];
2046
2047
		$subject = lang('Calendar Event') . ' - $$action$$: $$startdate$$ $$title$$'."\n";
2048
		$values = array(
2049
			'notifyAdded'     => $subject . lang ('You have a meeting scheduled for %1','$$startdate$$'),
2050
			'notifyCanceled'  => $subject . lang ('Your meeting scheduled for %1 has been canceled','$$startdate$$'),
2051
			'notifyModified'  => $subject . lang ('Your meeting that had been scheduled for %1 has been rescheduled to %2','$$olddate$$','$$startdate$$'),
2052
			'notifyDisinvited'=> $subject . lang ('You have been disinvited from the meeting at %1','$$startdate$$'),
2053
			'notifyResponse'  => $subject . lang ('On %1 %2 %3 your meeting request for %4','$$date$$','$$fullname$$','$$action$$','$$startdate$$'),
2054
			'notifyAlarm'     => lang('Alarm for %1 at %2 in %3','$$title$$','$$startdate$$','$$location$$')."\n".lang ('Here is your requested alarm.'),
2055
			'interval'        => 30,
2056
		);
2057
		foreach($values as $var => $default)
2058
		{
2059
			$type = substr($var,0,6) == 'notify' ? 'forced' : 'default';
2060
2061
			// only set, if neither default nor forced pref exists
2062
			if ((!isset($default_prefs[$var]) || (string)$default_prefs[$var] === '') && (!isset($forced_prefs[$var]) || (string)$forced_prefs[$var] === ''))
2063
			{
2064
				$GLOBALS['egw']->preferences->add('calendar',$var,$default,'default');	// always store default, even if we have a forced too
2065
				if ($type == 'forced') $GLOBALS['egw']->preferences->add('calendar',$var,$default,'forced');
2066
				$this->cal_prefs[$var] = $default;
2067
				$need_save = True;
2068
			}
2069
		}
2070
		if ($need_save)
2071
		{
2072
			$GLOBALS['egw']->preferences->save_repository(False,'default');
2073
			$GLOBALS['egw']->preferences->save_repository(False,'forced');
2074
		}
2075
	}
2076
2077
	/**
2078
	 * Get the freebusy URL of a user
2079
	 *
2080
	 * @param int|string $user account_id or account_lid
2081
	 * @param string $pw =null password
2082
	 */
2083
	static function freebusy_url($user='',$pw=null)
2084
	{
2085
		if (is_numeric($user)) $user = $GLOBALS['egw']->accounts->id2name($user);
2086
2087
		$credentials = '';
2088
2089
		if ($pw)
2090
		{
2091
			$credentials = '&password='.urlencode($pw);
2092
		}
2093
		elseif ($GLOBALS['egw_info']['user']['preferences']['calendar']['freebusy'] == 2)
2094
		{
2095
			$credentials = $GLOBALS['egw_info']['user']['account_lid']
2096
				. ':' . $GLOBALS['egw_info']['user']['passwd'];
2097
			$credentials = '&cred=' . base64_encode($credentials);
2098
		}
2099
		return (!$GLOBALS['egw_info']['server']['webserver_url'] || $GLOBALS['egw_info']['server']['webserver_url'][0] == '/' ?
2100
			($_SERVER['HTTPS'] ? 'https://' : 'http://').$_SERVER['HTTP_HOST'] : '').
2101
			$GLOBALS['egw_info']['server']['webserver_url'].'/calendar/freebusy.php/?user='.urlencode($user).$credentials;
2102
	}
2103
2104
	/**
2105
	 * Check if the event is the whole day
2106
	 *
2107
	 * @param array $event event
2108
	 * @return boolean true if whole day event, false othwerwise
2109
	 */
2110
	public static function isWholeDay($event)
2111
	{
2112
		// check if the event is the whole day
2113
		$start = self::date2array($event['start']);
2114
		$end = self::date2array($event['end']);
2115
2116
		return !$start['hour'] && !$start['minute'] && $end['hour'] == 23 && $end['minute'] == 59;
2117
	}
2118
2119
	/**
2120
	 * Get the etag for an entry
2121
	 *
2122
	 * As all update routines (incl. set_status and add/delete alarms) update (series master) modified timestamp,
2123
	 * we do NOT need any special handling for series master anymore
2124
	 *
2125
	 * @param array|int|string $entry array with event or cal_id, or cal_id:recur_date for virtual exceptions
2126
	 * @param string &$schedule_tag=null on return schedule-tag (egw_cal.cal_id:egw_cal.cal_etag, no participant modifications!)
2127
	 * @return string|boolean string with etag or false
2128
	 */
2129
	function get_etag($entry, &$schedule_tag=null)
2130
	{
2131
		if (!is_array($entry))
2132
		{
2133
			list($id,$recur_date) = explode(':',$entry);
2134
			$entry = $this->read($id, $recur_date, true, 'server');
2135
		}
2136
		$etag = $schedule_tag = $entry['id'].':'.$entry['etag'];
2137
		$etag .= ':'.$entry['modified'];
2138
2139
		//error_log(__METHOD__ . "($entry[id],$client_share_uid_excpetions) entry=".array2string($entry)." --> etag=$etag");
2140
		return $etag;
2141
	}
2142
2143
	/**
2144
	 * Query ctag for calendar
2145
	 *
2146
	 * @param int|string|array $user integer user-id or array of user-id's to use, defaults to the current user
2147
	 * @param string $filter ='owner' all (not rejected), accepted, unknown, tentative, rejected or hideprivate
2148
	 * @param boolean $master_only =false only check recurance master (egw_cal_user.recur_date=0)
2149
	 * @return integer
2150
	 */
2151
	public function get_ctag($user, $filter='owner', $master_only=false)
2152
	{
2153
		if ($this->debug > 1) $startime = microtime(true);
2154
2155
		// resolve users to add memberships for users and members for groups
2156
		$users = $this->resolve_users($user);
2157
		$ctag = $users ? $this->so->get_ctag($users, $filter == 'owner', $master_only) : 0;	// no rights, return 0 as ctag (otherwise we get SQL error!)
2158
2159
		if ($this->debug > 1) error_log(__METHOD__. "($user, '$filter', $master_only) = $ctag = ".date('Y-m-d H:i:s',$ctag)." took ".(microtime(true)-$startime)." secs");
2160
		return $ctag;
2161
	}
2162
2163
	/**
2164
	 * Hook for infolog  to set some extra data and links
2165
	 *
2166
	 * @param array $data event-array preset by infolog plus
2167
	 * @param int $data[id] cal_id
2168
	 * @return array with key => value pairs to set in new event and link_app/link_id arrays
2169
	 */
2170
	function infolog_set($data)
2171
	{
2172
		if (!($calendar = $this->read($data['id'])))
2173
		{
2174
			return array();
2175
		}
2176
2177
		$content = array(
2178
			'info_cat'       => $GLOBALS['egw']->categories->check_list(Acl::READ, $calendar['category']),
2179
			'info_priority'  => $calendar['priority'] ,
2180
			'info_public'    => $calendar['public'] != 'private',
2181
			'info_subject'   => $calendar['title'],
2182
			'info_des'       => $calendar['description'],
2183
			'info_location'  => $calendar['location'],
2184
			'info_startdate' => $calendar['range_start'],
2185
			//'info_enddate' => $calendar['range_end'] ? $calendar['range_end'] : $calendar['uid']
2186
			'info_contact'   => 'calendar:'.$data['id'],
2187
		);
2188
2189
		unset($content['id']);
2190
		// Add calendar link to infolog entry
2191
		$content['link_app'][] = $calendar['info_link']['app'];
2192
		$content['link_id'][]  = $calendar['info_link']['id'];
2193
		// Copy claendar's links
2194
		foreach(Link::get_links('calendar',$calendar['id'],'','link_lastmod DESC',true) as $link)
2195
		{
2196
			if ($link['app'] != Link::VFS_APPNAME)
2197
			{
2198
				$content['link_app'][] = $link['app'];
2199
				$content['link_id'][]  = $link['id'];
2200
			}
2201
			if ($link['app'] == 'addressbook')	// prefering contact as primary contact over calendar entry set above
2202
			{
2203
				$content['info_contact'] = 'addressbook:'.$link['id'];
2204
			}
2205
		}
2206
		// Copy same custom fields
2207
		foreach(array_keys(Api\Storage\Customfields::get('infolog')) as $name)
2208
		{
2209
			if ($this->customfields[$name]) $content['#'.$name] = $calendar['#'.$name];
2210
		}
2211
		//error_log(__METHOD__.'('.array2string($data).') calendar='.array2string($calendar).' returning '.array2string($content));
2212
		return $content;
2213
	}
2214
2215
	/**
2216
	 * Hook for timesheet to set some extra data and links
2217
	 *
2218
	 * @param array $data
2219
	 * @param int $data[id] cal_id:recurrence
2220
	 * @return array with key => value pairs to set in new timesheet and link_app/link_id arrays
2221
	 */
2222
	function timesheet_set($data)
2223
	{
2224
		$set = array();
2225
		list($id,$recurrence) = explode(':',$data['id']);
2226
		if ((int)$id && ($event = $this->read($id,$recurrence)))
2227
		{
2228
			$set['ts_start'] = $event['start'];
2229
			$set['ts_title'] = $this->link_title($event);
2230
			$set['start_time'] = Api\DateTime::to($event['start'],'H:i');
2231
			$set['ts_description'] = $event['description'];
2232
			if ($this->isWholeDay($event)) $event['end']++;	// whole day events are 1sec short
2233
			$set['ts_duration']	= ($event['end'] - $event['start']) / 60;
2234
			$set['ts_quantity'] = ($event['end'] - $event['start']) / 3600;
2235
			$set['end_time'] = null;	// unset end-time
2236
			$set['cat_id'] = (int)$event['category'];
2237
2238 View Code Duplication
			foreach(Link::get_links('calendar',$id,'','link_lastmod DESC',true) as $link)
2239
			{
2240
				if ($link['app'] != 'timesheet' && $link['app'] != Link::VFS_APPNAME)
2241
				{
2242
					$set['link_app'][] = $link['app'];
2243
					$set['link_id'][]  = $link['id'];
2244
				}
2245
			}
2246
		}
2247
		return $set;
2248
	}
2249
}
2250