calendar_bo::enum_groups()   B
last analyzed

Complexity

Conditions 7
Paths 5

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 9
nc 5
nop 1
dl 0
loc 19
rs 8.8333
c 0
b 0
f 0
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');
0 ignored issues
show
Documentation Bug introduced by
It seems like EGroupware\Api\DateTime:...2user($this->now, 'ts') of type EGroupware\Api\datetime is incompatible with the declared type integer of property $now_su.

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

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

Loading history...
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
			Api\Cache::setSession('calendar', 'resources', $this->resources);
261
		}
262
		//error_log(__METHOD__ . " registered resources=". array2string($this->resources));
263
264
		$this->config = Api\Config::read('calendar');	// only used for horizont, regular calendar config is under phpgwapi
265
		$this->require_acl_invite = $GLOBALS['egw_info']['server']['require_acl_invite'];
266
267
		$this->categories = new Api\Categories($this->user,'calendar');
268
269
		$this->customfields = Api\Storage\Customfields::get('calendar');
270
271
		foreach($this->alarms as $secs => &$label)
272
		{
273
			$label = self::secs2label($secs);
274
		}
275
	}
276
277
	/**
278
	 * Generate translated label for a given number of seconds
279
	 *
280
	 * @param int $secs
281
	 * @return string
282
	 */
283
	static public function secs2label($secs)
284
	{
285
		if ($secs <= 3600)
286
		{
287
			$label = lang('%1 minutes', $secs/60);
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with $secs / 60. ( Ignorable by Annotation )

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

287
			$label = /** @scrutinizer ignore-call */ lang('%1 minutes', $secs/60);

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

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

Loading history...
288
		}
289
		elseif($secs <= 86400)
290
		{
291
			$label = lang('%1 hours', $secs/3600);
292
		}
293
		else
294
		{
295
			$label = lang('%1 days', $secs/86400);
296
		}
297
		return $label;
298
	}
299
300
	/**
301
	 * returns info about email addresses as participants
302
	 *
303
	 * @param int|array $ids single contact-id or array of id's
304
	 * @return array
305
	 */
306
	static function email_info($ids)
307
	{
308
		if (!$ids) return null;
309
310
		$data = array();
311
		foreach((array)$ids as $id)
312
		{
313
			$email = $id;
314
			$name = '';
315
			$matches = null;
316
			if (preg_match('/^(.*) *<([a-z0-9_.@-]{8,})>$/iU',$email,$matches))
317
			{
318
				$name = $matches[1];
319
				$email = $matches[2];
320
			}
321
			$data[] = array(
322
				'res_id' => $id,
323
				'email' => $email,
324
				'rights' => self::ACL_READ_FOR_PARTICIPANTS | self::ACL_INVITE,
325
				'name' => $name,
326
			);
327
		}
328
		//error_log(__METHOD__.'('.array2string($ids).')='.array2string($data).' '.function_backtrace());
329
		return $data;
330
	}
331
332
	/**
333
	 * returns info about mailing lists as participants
334
	 *
335
	 * @param int|array $ids single mailing list ID or array of id's
336
	 * @return array
337
	 */
338
	static function mailing_lists($ids)
339
	{
340
		if(!is_array($ids))
341
		{
342
			$ids = array($ids);
343
		}
344
		$data = array();
345
346
		// Email list
347
		$contacts_obj = new Api\Contacts();
348
		$bo = new calendar_bo();
349
		foreach($ids as $id)
350
		{
351
			$list = $contacts_obj->read_list((int)$id);
352
353
			$data[] = array(
354
				'res_id' => $id,
355
				'rights' => self::ACL_READ_FOR_PARTICIPANTS,
356
				'name' => $list['list_name'],
357
				'resources' => $bo->enum_mailing_list('l'.$id, false, false)
358
			);
359
		}
360
361
		return $data;
362
	}
363
364
	/**
365
	 * Enumerates the contacts in a contact list, and returns the list of contact IDs
366
	 *
367
	 * This is used to enable mailing lists as owner/participant
368
	 *
369
	 * @param string $id Mailing list participant ID, which is the mailing list
370
	 *	ID prefixed with 'l'
371
	 * @param boolean $ignore_acl = false Flag to skip ACL checks
372
	 * @param boolean $use_freebusy =true should freebusy rights are taken into account, default true, can be set to false eg. for a search
373
	 *
374
	 * @return array
375
	 */
376
	public function enum_mailing_list($id, $ignore_acl= false, $use_freebusy = true)
377
	{
378
		$contact_list = array();
379
		$contacts = new Api\Contacts();
380
		if($contacts->check_list((int)substr($id,1), ACL::READ) || (int)substr($id,1) < 0)
381
		{
382
			$options = array('list' => substr($id,1));
383
			$lists = $contacts->search('',true,'','','',false,'AND',false,$options);
384
			if(!$lists)
0 ignored issues
show
Bug Best Practice introduced by
The expression $lists of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

581
				$params = /** @scrutinizer ignore-deprecated */ ExecMethod($data['search_filter'],$params);

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

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

Loading history...
582
			}
583
		}
584
585
		if (empty($params['users']) ||
586
			is_array($params['users']) && count($params['users']) == 1 && empty($params['users'][0]))	// null or '' casted to an array
587
		{
588
			// for a search use all account you have read grants from
589
			$params['users'] = $params['query'] ? array_keys($this->grants) : $this->user;
590
		}
591
		// resolve users to add memberships for users and members for groups
592
		// for search, do NOT use freebusy rights, as it would allow to probe the content of event entries
593
		$users = $this->resolve_users($params['users'], $params['filter'] == 'no-enum-groups', $params['ignore_acl'], empty($params['query']));
594
		if($params['private_allowed'])
595
		{
596
			$params['private_allowed'] = $this->resolve_users($params['private_allowed'],$params['filter'] == 'no-enum-groups',$params['ignore_acl'], empty($params['query']));
597
		}
598
599
		// supply so with private_grants, to not query them again from the database
600
		if (!empty($params['query']))
601
		{
602
			$params['private_grants'] = array();
603
			foreach($this->grants as $user => $rights)
604
			{
605
				if ($rights & Acl::PRIVAT) $params['private_grants'][] = $user;
606
			}
607
		}
608
609
		// replace (by so not understood filter 'no-enum-groups' with 'default' filter
610
		if ($params['filter'] == 'no-enum-groups')
611
		{
612
			$params['filter'] = 'default';
613
		}
614
		// if we have no grants from the given user(s), we directly return no events / an empty array,
615
		// as calling the so-layer without users would give the events of all users (!)
616
		if (!count($users) && !$params['ignore_acl'])
617
		{
618
			return false;
619
		}
620
		if (isset($params['start'])) $start = $this->date2ts($params['start']);
621
622
		if (isset($params['end']))
623
		{
624
			$end = $this->date2ts($params['end']);
625
			$this->check_move_horizont($end);
626
		}
627
		$daywise = !isset($params['daywise']) ? False : !!$params['daywise'];
628
		$params['enum_recuring'] = $enum_recuring = $daywise || !isset($params['enum_recuring']) || !!$params['enum_recuring'];
0 ignored issues
show
Unused Code introduced by
The assignment to $enum_recuring is dead and can be removed.
Loading history...
629
		$cat_id = isset($params['cat_id']) ? $params['cat_id'] : 0;
630
		$filter = isset($params['filter']) ? $params['filter'] : 'all';
631
		$offset = isset($params['offset']) && $params['offset'] !== false ? (int) $params['offset'] : false;
632
		// socal::search() returns rejected group-invitations, as only the user not also the group is rejected
633
		// as we cant remove them efficiantly in SQL, we kick them out here, but only if just one user is displayed
634
		$users_in = (array)$params_in['users'];
635
		$remove_rejected_by_user = !in_array($filter,array('all','rejected','everything')) &&
636
			count($users_in) == 1 && $users_in[0] > 0 ? $users_in[0] : null;
637
		//error_log(__METHOD__.'('.array2string($params_in).", $sql_filter) params[users]=".array2string($params['users']).' --> remove_rejected_by_user='.array2string($remove_rejected_by_user));
638
639
		if ($this->debug && ($this->debug > 1 || $this->debug == 'search'))
640
		{
641
			$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)',
642
				True,$params,$start,$end,$daywise,$cat_id,$filter,$params['query'],$offset,(int)$params['num_rows'],$params['order'],$params['sql_filter']);
643
		}
644
		// date2ts(,true) converts to server time, db2data converts again to user-time
645
		$events =& $this->so->search(isset($start) ? $this->date2ts($start,true) : null,isset($end) ? $this->date2ts($end,true) : null,
646
			$users,$cat_id,$filter,$offset,(int)$params['num_rows'],$params,$remove_rejected_by_user);
647
648
		if (isset($params['cols']))
649
		{
650
			return $events;
651
		}
652
		$this->total = $this->so->total;
0 ignored issues
show
Bug Best Practice introduced by
The property total does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
653
		$this->db2data($events,isset($params['date_format']) ? $params['date_format'] : 'ts');
654
655
		//echo "<p align=right>remove_rejected_by_user=$remove_rejected_by_user, filter=$filter, params[users]=".print_r($param['users'])."</p>\n";
656
		foreach($events as $id => $event)
657
		{
658
			if ($params['enum_groups'] && $this->enum_groups($event))
659
			{
660
				$events[$id] = $event;
661
			}
662
			$matches = null;
663
			if (!(int)$event['id'] && preg_match('/^([a-z_]+)([0-9]+)$/',$event['id'],$matches))
664
			{
665
				$is_private = self::integration_get_private($matches[1],$matches[2],$event);
666
			}
667
			else
668
			{
669
				$is_private = !$this->check_perms(Acl::READ,$event);
670
			}
671
			if (!$params['ignore_acl'] && ($is_private || (!$event['public'] && $filter == 'hideprivate')))
672
			{
673
				$this->clear_private_infos($events[$id],$params['private_allowed'] ? $params['private_allowed'] : $users);
674
			}
675
		}
676
677
		if ($daywise)
678
		{
679
			if ($this->debug && ($this->debug > 2 || $this->debug == 'search'))
680
			{
681
				$this->debug_message('socalendar::search daywise sorting from %1 to %2 of %3',False,$start,$end,$events);
682
			}
683
			// create empty entries for each day in the reported time
684
			for($ts = $start; $ts <= $end; $ts += DAY_s) // good enough for array creation, but see while loop below.
685
			{
686
				$daysEvents[$this->date2string($ts)] = array();
687
			}
688
			foreach($events as $k => $event)
689
			{
690
				$e_start = max($this->date2ts($event['start']),$start);
691
				// $event['end']['raw']-1 to allow events to end on a full hour/day without the need to enter it as minute=59
692
				$e_end   = min($this->date2ts($event['end'])-1,$end);
693
694
				// add event to each day in the reported time
695
				$ts = $e_start;
696
				//  $ts += DAY_s in a 'for' loop does not work for daylight savings in week view
697
				// because the day is longer than DAY_s: Fullday events will be added twice.
698
				$ymd = null;
699
				while ($ts <= $e_end)
700
				{
701
					$daysEvents[$ymd = $this->date2string($ts)][] =& $events[$k];
702
					$ts = strtotime("+1 day",$ts);
703
				}
704
				if ($ymd != ($last = $this->date2string($e_end)))
705
				{
706
					$daysEvents[$last][] =& $events[$k];
707
				}
708
			}
709
			$events =& $daysEvents;
710
			if ($this->debug && ($this->debug > 2 || $this->debug == 'search'))
711
			{
712
				$this->debug_message('socalendar::search daywise events=%1',False,$events);
713
			}
714
		}
715
		if ($this->debug && ($this->debug > 0 || $this->debug == 'search'))
716
		{
717
			$this->debug_message('calendar_bo::search(%1)=%2',True,$params,$events);
718
		}
719
		//error_log(__METHOD__."() returning ".count($events)." entries, total=$this->total ".function_backtrace());
720
		return $events;
721
	}
722
723
	/**
724
	 * Get integration data for a given app of a part (value for a certain key) of it
725
	 *
726
	 * @param string $app
727
	 * @param string $part
728
	 * @return array
729
	 */
730
	static function integration_get_data($app,$part=null)
731
	{
732
		static $integration_data=null;
733
734
		if (!isset($integration_data))
735
		{
736
			$integration_data = calendar_so::get_integration_data();
737
		}
738
739
		if (!isset($integration_data[$app])) return null;
740
741
		return $part ? $integration_data[$app][$part] : $integration_data[$app];
742
	}
743
744
	/**
745
	 * Get private attribute for an integration event
746
	 *
747
	 * Attribute 'is_private' is either a boolean value, eg. false to make all events of $app public
748
	 * or an ExecMethod callback with parameters $id,$event
749
	 *
750
	 * @param string $app
751
	 * @param int|string $id
752
	 * @return string
753
	 */
754
	static function integration_get_private($app,$id,$event)
755
	{
756
		$app_data = self::integration_get_data($app,'is_private');
757
758
		// no method, fall back to link title
759
		if (is_null($app_data))
0 ignored issues
show
introduced by
The condition is_null($app_data) is always false.
Loading history...
760
		{
761
			$is_private = !Link::title($app,$id);
762
		}
763
		// boolean value to make all events of $app public (false) or private (true)
764
		elseif (is_bool($app_data))
0 ignored issues
show
introduced by
The condition is_bool($app_data) is always false.
Loading history...
765
		{
766
			$is_private = $app_data;
767
		}
768
		else
769
		{
770
			$is_private = (bool)ExecMethod2($app_data,$id,$event);
0 ignored issues
show
Deprecated Code introduced by
The function ExecMethod2() has been deprecated: use autoloadable class-names, instanciate and call method or use static methods ( Ignorable by Annotation )

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

770
			$is_private = (bool)/** @scrutinizer ignore-deprecated */ ExecMethod2($app_data,$id,$event);

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

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

Loading history...
771
		}
772
		//echo '<p>'.__METHOD__."($app,$id,) app_data=".array2string($app_data).' returning '.array2string($is_private)."</p>\n";
773
		return $is_private;
774
	}
775
776
	/**
777
	 * Clears all non-private info from a privat event
778
	 *
779
	 * That function only returns the infos allowed to be viewed by people without Acl::PRIVAT grants
780
	 *
781
	 * @param array &$event
782
	 * @param array $allowed_participants ids of the allowed participants, eg. the ones the search is over or eg. the owner of the calendar
783
	 */
784
	function clear_private_infos(&$event,$allowed_participants = array())
785
	{
786
		if ($event == false) return;
787
		if (!is_array($event['participants'])) error_log(__METHOD__.'('.array2string($event).', '.array2string($allowed_participants).') NO PARTICIPANTS '.function_backtrace());
788
789
		$event = array(
790
			'id'    => $event['id'],
791
			'start' => $event['start'],
792
			'end'   => $event['end'],
793
			'whole_day' => $event['whole_day'],
794
			'tzid'  => $event['tzid'],
795
			'title' => lang('private'),
796
			'modified'	=> $event['modified'],
797
			'owner'		=> $event['owner'],
798
			'uid'	=> $event['uid'],
799
			'etag'	=> $event['etag'],
800
			'participants' => array_intersect_key($event['participants'],array_flip($allowed_participants)),
801
			'public'=> 0,
802
			'category' => $event['category'],	// category is visible anyway, eg. by using planner by cat
803
			'non_blocking' => $event['non_blocking'],
804
			'caldav_name' => $event['caldav_name'],
805
		// we need full recurrence information, as they are relevant free/busy information
806
		)+($event['recur_type'] ? array(
807
			'recur_type'     => $event['recur_type'],
808
			'recur_interval' => $event['recur_interval'],
809
			'recur_data'     => $event['recur_data'],
810
			'recur_enddate'  => $event['recur_enddate'],
811
			'recur_exception'=> $event['recur_exception'],
812
		):array(
813
			'reference'      => $event['reference'],
814
			'recurrence'     => $event['recurrence'],
815
		));
816
	}
817
818
	/**
819
	 * check and evtl. move the horizont (maximum date for unlimited recuring events) to a new date
820
	 *
821
	 * @internal automaticaly called by search
822
	 * @param mixed $_new_horizont time to set the horizont to (user-time)
823
	 */
824
	function check_move_horizont($_new_horizont)
825
	{
826
		if ((int) $this->debug >= 2 || $this->debug == 'check_move_horizont')
827
		{
828
			$this->debug_message('calendar_bo::check_move_horizont(%1) horizont=%2',true,$_new_horizont,(int)$this->config['horizont']);
829
		}
830
		$new_horizont = $this->date2ts($_new_horizont,true);	// now we are in server-time, where this function operates
831
832
		if ($new_horizont <= $this->config['horizont'])	// no move necessary
833
		{
834
			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']);
835
			return;
836
		}
837
		if (!empty($GLOBALS['egw_info']['server']['calendar_horizont']))
838
		{
839
			$maxdays = abs($GLOBALS['egw_info']['server']['calendar_horizont']);
840
		}
841
		if (empty($maxdays)) $maxdays = 1000; // old default
842
		if ($new_horizont > time()+$maxdays*DAY_s)		// some user tries to "look" more then the maximum number of days in the future
843
		{
844
			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);
845
			$this->warnings['horizont'] = lang('Requested date %1 outside allowed range of %2 days: recurring events obmitted!', Api\DateTime::to($new_horizont,true), $maxdays);
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with EGroupware\Api\DateTime::to($new_horizont, true). ( Ignorable by Annotation )

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

845
			$this->warnings['horizont'] = /** @scrutinizer ignore-call */ lang('Requested date %1 outside allowed range of %2 days: recurring events obmitted!', Api\DateTime::to($new_horizont,true), $maxdays);

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

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

Loading history...
846
			return;
847
		}
848
		if ($new_horizont < time()+31*DAY_s)
849
		{
850
			$new_horizont = time()+31*DAY_s;
851
		}
852
		$old_horizont = $this->config['horizont'];
853
		$this->config['horizont'] = $new_horizont;
854
855
		// create further recurrences for all recurring and not yet (at the old horizont) ended events
856
		if (($recuring = $this->so->unfinished_recuring($old_horizont)))
857
		{
858
			@set_time_limit(0);	// disable time-limit, in case it takes longer to calculate the recurrences
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for set_time_limit(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

858
			/** @scrutinizer ignore-unhandled */ @set_time_limit(0);	// disable time-limit, in case it takes longer to calculate the recurrences

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
859
			foreach($this->read(array_keys($recuring)) as $cal_id => $event)
860
			{
861
				if ($this->debug == 'check_move_horizont')
862
				{
863
					$this->debug_message('calendar_bo::check_move_horizont(%1): calling set_recurrences(%2,%3)',true,$new_horizont,$event,$old_horizont);
864
				}
865
				// insert everything behind max(cal_start), which can be less then $old_horizont because of bugs in the past
866
				$this->set_recurrences($event,Api\DateTime::server2user($recuring[$cal_id]+1));	// set_recurences operates in user-time!
867
			}
868
		}
869
		// update the horizont
870
		Api\Config::save_value('horizont',$this->config['horizont'],'calendar');
871
872
		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']);
873
	}
874
875
	/**
876
	 * set all recurrences for an event until the defined horizont $this->config['horizont']
877
	 *
878
	 * This methods operates in usertime, while $this->config['horizont'] is in servertime!
879
	 *
880
	 * @param array $event
881
	 * @param mixed $start =0 minimum start-time for new recurrences or !$start = since the start of the event
882
	 */
883
	function set_recurrences($event,$start=0)
884
	{
885
		if ($this->debug && ((int) $this->debug >= 2 || $this->debug == 'set_recurrences' || $this->debug == 'check_move_horizont'))
886
		{
887
			$this->debug_message('calendar_bo::set_recurrences(%1,%2)',true,$event,$start);
888
		}
889
		// check if the caller gave us enough information and if not read it from the DB
890
		if (!isset($event['participants']) || !isset($event['start']) || !isset($event['end']))
891
		{
892
			$event_read = current($this->so->read($event['id']));
893
			if (!isset($event['participants']))
894
			{
895
				$event['participants'] = $event_read['participants'];
896
			}
897
			if (!isset($event['start']) || !isset($event['end']))
898
			{
899
				$event['start'] = $this->date2usertime($event_read['start']);
900
				$event['end'] = $this->date2usertime($event_read['end']);
901
			}
902
		}
903
		if (!$start) $start = $event['start'];
904
905
		$events = array();
906
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)
0 ignored issues
show
introduced by
$event is overwriting one of the parameters of this function.
Loading history...
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
0 ignored issues
show
Documentation Bug introduced by
The doc comment ='ts' at position 0 could not be parsed: Unknown type name '='ts'' at position 0 in ='ts'.
Loading history...
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
			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
			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
			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
	 * @param boolean $read_recurrence =false true: read the exception, not the series master (only for recur_date && $ids='<uid>'!)
1046
	 * @return boolean|array event or array of id => event pairs, false if the acl-check went wrong, null if $ids not found
1047
	 */
1048
	function read($ids,$date=null, $ignore_acl=False, $date_format='ts', $clear_private_infos_users=null, $read_recurrence=false)
1049
	{
1050
		if (!$ids) return false;
1051
1052
		if ($date) $date = $this->date2ts($date);
1053
1054
		$return = null;
1055
1056
		$check = $clear_private_infos_users ? self::ACL_FREEBUSY : Acl::READ;
1057
		if ($ignore_acl || is_array($ids) || ($return = $this->check_perms($check,$ids,0,$date_format,$date)))
1058
		{
1059
			if (is_array($ids) || !isset(self::$cached_event['id']) || self::$cached_event['id'] != $ids ||
1060
				self::$cached_event_date_format != $date_format || $read_recurrence ||
1061
				self::$cached_event['recur_type'] != MCAL_RECUR_NONE && self::$cached_event_date != $date)
1062
			{
1063
				$events = $this->so->read($ids,$date ? $this->date2ts($date,true) : 0, $read_recurrence);
1064
1065
				if ($events)
0 ignored issues
show
introduced by
$events is a non-empty array, thus is always true.
Loading history...
Bug Best Practice introduced by
The expression $events of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

1244
				list($info) = $this->resources[$uid[0]]['info'] ? /** @scrutinizer ignore-deprecated */ ExecMethod($this->resources[$uid[0]]['info'],substr($uid,1)) : false;

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

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

Loading history...
1245
				if ($info)
1246
				{
1247
					$info['type'] = $uid[0];
1248
					if (!$info['email'] && $info['responsible'])
1249
					{
1250
						$info['email'] = $GLOBALS['egw']->accounts->id2name($info['responsible'],'account_email');
1251
					}
1252
					$info['app'] = $this->resources[$uid[0]]['app'];
1253
				}
1254
			}
1255
			$res_info_cache[$uid] = $info;
1256
		}
1257
		if ($this->debug && ($this->debug > 2 || $this->debug == 'resource_info'))
1258
		{
1259
			$this->debug_message('calendar_bo::resource_info(%1) = %2',True,$uid,$res_info_cache[$uid]);
1260
		}
1261
		return $res_info_cache[$uid];
1262
	}
1263
1264
	/**
1265
	 * Checks if the current user has the necessary ACL rights
1266
	 *
1267
	 * The check is performed on an event or generally on the cal of an other user
1268
	 *
1269
	 * Note: Participating in an event is considered as haveing read-access on that event,
1270
	 *	even if you have no general read-grant from that user.
1271
	 *
1272
	 * @param int $needed necessary ACL right: Acl::{READ|EDIT|DELETE}
1273
	 * @param mixed $event event as array or the event-id or 0 for a general check
1274
	 * @param int $other uid to check (if event==0) or 0 to check against $this->user
1275
	 * @param string $date_format ='ts' date-format used for reading: 'ts'=timestamp, 'array'=array, 'string'=iso8601 string for xmlrpc
1276
	 * @param mixed $date_to_read =null date used for reading, internal param for the caching
1277
	 * @param int $user =null for which user to check, default current user
1278
	 * @return boolean true permission granted, false for permission denied or null if event not found
1279
	 */
1280
	function check_perms($needed,$event=0,$other=0,$date_format='ts',$date_to_read=null,$user=null)
1281
	{
1282
		if (!$user) $user = $this->user;
0 ignored issues
show
Bug Best Practice introduced by
The expression $user of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1283
		if ($user == $this->user)
1284
		{
1285
			$grants = $this->grants;
1286
		}
1287
		else
1288
		{
1289
			$grants = $GLOBALS['egw']->acl->get_grants('calendar',true,$user);
1290
		}
1291
1292
		if ($other && !is_numeric($other))
0 ignored issues
show
introduced by
The condition is_numeric($other) is always true.
Loading history...
1293
		{
1294
			$resource = $this->resource_info($other);
1295
			return $needed & $resource['rights'];
1296
		}
1297
		if (is_int($event) && $event == 0)
1298
		{
1299
			$owner = $other ? $other : $user;
1300
		}
1301
		else
1302
		{
1303
			if (!is_array($event))
1304
			{
1305
				$event = $this->read($event,$date_to_read,true,$date_format);	// = no ACL check !!!
1306
			}
1307
			if (!is_array($event))
1308
			{
1309
				if ($this->xmlrpc)
0 ignored issues
show
Bug Best Practice introduced by
The property xmlrpc does not exist on calendar_bo. Did you maybe forget to declare it?
Loading history...
1310
				{
1311
					$GLOBALS['server']->xmlrpc_error($GLOBALS['xmlrpcerr']['not_exist'],$GLOBALS['xmlrpcstr']['not_exist']);
1312
				}
1313
				return null;	// event not found
1314
			}
1315
			$owner = $event['owner'];
1316
			$private = !$event['public'];
1317
		}
1318
		$grant = $grants[$owner];
1319
1320
		// now any ACL rights (but invite rights!) implicate FREEBUSY rights (at least READ has to include FREEBUSY)
1321
		if ($grant & ~self::ACL_INVITE) $grant |= self::ACL_FREEBUSY;
1322
1323
		if (is_array($event) && ($needed == Acl::READ || $needed == self::ACL_FREEBUSY))
1324
		{
1325
			// Check if the $user is one of the participants or has a read-grant from one of them
1326
			// in that case he has an implicite READ grant for that event
1327
			//
1328
			if ($event['participants'] && is_array($event['participants']))
1329
			{
1330
				foreach(array_keys($event['participants']) as $uid)
1331
				{
1332
					if ($uid == $user || $uid < 0 && in_array($user, (array)$GLOBALS['egw']->accounts->members($uid,true)))
1333
					{
1334
						// if we are a participant, we have an implicite FREEBUSY, READ and PRIVAT grant
1335
						$grant |= self::ACL_FREEBUSY | Acl::READ | Acl::PRIVAT;
1336
						break;
1337
					}
1338
					elseif ($grants[$uid] & Acl::READ)
1339
					{
1340
						// if we have a READ grant from a participant, we dont give an implicit privat grant too
1341
						$grant |= self::ACL_FREEBUSY | Acl::READ;
1342
						// we cant break here, as we might be a participant too, and would miss the privat grant
1343
					}
1344
					elseif (!is_numeric($uid))
1345
					{
1346
						// if the owner only grants self::ACL_FREEBUSY we are not interested in the recources explicit rights
1347
						if ($grant == self::ACL_FREEBUSY) continue;
1348
						// if we have a resource as participant
1349
						$resource = $this->resource_info($uid);
1350
						$grant |= $resource['rights'];
1351
					}
1352
				}
1353
			}
1354
		}
1355
		if ($GLOBALS['egw']->accounts->get_type($owner) == 'g' && $needed == Acl::ADD)
1356
		{
1357
			$access = False;	// a group can't be the owner of an event
1358
		}
1359
		else
1360
		{
1361
			$access = $user == $owner || $grant & $needed
1362
				&& ($needed == self::ACL_FREEBUSY || !$private || $grant & Acl::PRIVAT);
1363
		}
1364
		// do NOT allow users to purge deleted events, if we dont have 'userpurge' enabled
1365
		if ($access && $needed == Acl::DELETE && $event['deleted'] &&
1366
			!$GLOBALS['egw_info']['user']['apps']['admin'] &&
1367
			$GLOBALS['egw_info']['server']['calendar_delete_history'] != 'userpurge')
1368
		{
1369
			$access = false;
1370
		}
1371
		if ($this->debug && ($this->debug > 2 || $this->debug == 'check_perms'))
1372
		{
1373
			$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);
1374
		}
1375
		//error_log(__METHOD__."($needed,".array2string($event).",$other,...,$user) returning ".array2string($access));
1376
		return $access;
1377
	}
1378
1379
	/**
1380
	 * Converts several date-types to a timestamp and optionally converts user- to server-time
1381
	 *
1382
	 * @param mixed $date date to convert, should be one of the following types
1383
	 *	string (!) in form YYYYMMDD or iso8601 YYYY-MM-DDThh:mm:ss or YYYYMMDDThhmmss
1384
	 *	int already a timestamp
1385
	 *	array with keys 'second', 'minute', 'hour', 'day' or 'mday' (depricated !), 'month' and 'year'
1386
	 * @param boolean $user2server =False conversion between user- and server-time; default False == Off
1387
	 */
1388
	static function date2ts($date,$user2server=False)
1389
	{
1390
		return $user2server ? Api\DateTime::user2server($date,'ts') : Api\DateTime::to($date,'ts');
1391
	}
1392
1393
	/**
1394
	 * Converts a date to an array and optionally converts server- to user-time
1395
	 *
1396
	 * @param mixed $date date to convert
1397
	 * @param boolean $server2user conversation between user- and server-time default False == Off
1398
	 * @return array with keys 'second', 'minute', 'hour', 'day', 'month', 'year', 'raw' (timestamp) and 'full' (Ymd-string)
1399
	 */
1400
	static function date2array($date,$server2user=False)
1401
	{
1402
		return $server2user ? Api\DateTime::server2user($date,'array') : Api\DateTime::to($date,'array');
1403
	}
1404
1405
	/**
1406
	 * Converts a date as timestamp or array to a date-string and optionaly converts server- to user-time
1407
	 *
1408
	 * @param mixed $date integer timestamp or array with ('year','month',..,'second') to convert
1409
	 * @param boolean $server2user conversation between user- and server-time default False == Off, not used if $format ends with \Z
1410
	 * @param string $format ='Ymd' format of the date to return, eg. 'Y-m-d\TH:i:sO' (2005-11-01T15:30:00+0100)
1411
	 * @return string date formatted according to $format
1412
	 */
1413
	static function date2string($date,$server2user=False,$format='Ymd')
1414
	{
1415
		return $server2user ? Api\DateTime::server2user($date,$format) : Api\DateTime::to($date,$format);
1416
	}
1417
1418
	/**
1419
	 * Formats a date given as timestamp or array
1420
	 *
1421
	 * @param mixed $date integer timestamp or array with ('year','month',..,'second') to convert
1422
	 * @param string|boolean $format ='' default common_prefs[dateformat], common_prefs[timeformat], false=time only, true=date only
1423
	 * @return string the formated date (incl. time)
1424
	 */
1425
	static function format_date($date,$format='')
1426
	{
1427
		return Api\DateTime::to($date,$format);
1428
	}
1429
1430
	/**
1431
	 * Gives out a debug-message with certain parameters
1432
	 *
1433
	 * All permanent debug-messages in the calendar should be done by this function !!!
1434
	 *	(In future they may be logged or sent as xmlrpc-faults back.)
1435
	 *
1436
	 * Permanent debug-message need to make sure NOT to give secret information like passwords !!!
1437
	 *
1438
	 * This function do NOT honor the setting of the debug variable, you may use it like
1439
	 * if ($this->debug > N) $this->debug_message('Error ;-)');
1440
	 *
1441
	 * The parameters get formated depending on their type. ACL-values need a ACL_TYPE_IDENTIFER prefix.
1442
	 *
1443
	 * @param string $msg message with parameters/variables like lang(), eg. '%1'
1444
	 * @param boolean $backtrace =True include a function-backtrace, default True=On
1445
	 *	should only be set to False=Off, if your code ensures a call with backtrace=On was made before !!!
1446
	 * @param mixed $param a variable number of parameters, to be inserted in $msg
1447
	 *	arrays get serialized with print_r() !
1448
	 */
1449
	static function debug_message($msg,$backtrace=True)
1450
	{
1451
		static $acl2string = array(
1452
			0               => 'ACL-UNKNOWN',
1453
			Acl::READ    => 'ACL_READ',
1454
			Acl::ADD     => 'ACL_ADD',
1455
			Acl::EDIT    => 'ACL_EDIT',
1456
			Acl::DELETE  => 'ACL_DELETE',
1457
			Acl::PRIVAT => 'ACL_PRIVATE',
1458
			self::ACL_FREEBUSY => 'ACL_FREEBUSY',
1459
		);
1460
		for($i = 2; $i < func_num_args(); ++$i)
1461
		{
1462
			$param = func_get_arg($i);
1463
1464
			if (is_null($param))
1465
			{
1466
				$param='NULL';
1467
			}
1468
			else
1469
			{
1470
				switch(gettype($param))
1471
				{
1472
					case 'string':
1473
						if (substr($param,0,strlen(ACL_TYPE_IDENTIFER))== ACL_TYPE_IDENTIFER)
1474
						{
1475
							$param = (int) substr($param,strlen(ACL_TYPE_IDENTIFER));
1476
							$param = (isset($acl2string[$param]) ? $acl2string[$param] : $acl2string[0])." ($param)";
1477
						}
1478
						else
1479
						{
1480
							$param = "'$param'";
1481
						}
1482
						break;
1483
					case 'EGroupware\\Api\\DateTime':
1484
					case 'egw_time':
1485
					case 'datetime':
1486
						$p = $param;
1487
						unset($param);
1488
						$param = $p->format('l, Y-m-d H:i:s').' ('.$p->getTimeZone()->getName().')';
1489
						break;
1490
					case 'array':
1491
					case 'object':
1492
						$param = array2string($param);
1493
						break;
1494
					case 'boolean':
1495
						$param = $param ? 'True' : 'False';
1496
						break;
1497
					case 'integer':
1498
						if ($param >= mktime(0,0,0,1,1,2000)) $param = adodb_date('Y-m-d H:i:s',$param)." ($param)";
1499
						break;
1500
				}
1501
			}
1502
			$msg = str_replace('%'.($i-1),$param,$msg);
1503
		}
1504
		error_log($msg);
1505
		if ($backtrace) error_log(function_backtrace(1));
1506
	}
1507
1508
	/**
1509
	 * Formats one or two dates (range) as long date (full monthname), optionaly with a time
1510
	 *
1511
	 * @param mixed $_first first date
1512
	 * @param mixed $last =0 last date if != 0 (default)
1513
	 * @param boolean $display_time =false should a time be displayed too
1514
	 * @param boolean $display_day =false should a day-name prefix the date, eg. monday June 20, 2006
1515
	 * @return string with formated date
1516
	 */
1517
	function long_date($_first,$last=0,$display_time=false,$display_day=false)
1518
	{
1519
		$first = $this->date2array($_first);
1520
		if ($last)
1521
		{
1522
			$last = $this->date2array($last);
1523
		}
1524
		$datefmt = $this->common_prefs['dateformat'];
1525
		$timefmt = $this->common_prefs['timeformat'] == 12 ? 'h:i a' : 'H:i';
1526
1527
		$month_before_day = strtolower($datefmt[0]) == 'm' ||
1528
			strtolower($datefmt[2]) == 'm' && $datefmt[4] == 'd';
1529
1530
		if ($display_day)
1531
		{
1532
			$range = lang(adodb_date('l',$first['raw'])).($this->common_prefs['dateformat'][0] != 'd' ? ' ' : ', ');
1533
		}
1534
		for ($i = 0; $i < 5; $i += 2)
1535
		{
1536
			switch($datefmt[$i])
1537
			{
1538
				case 'd':
1539
					$range .= $first['day'] . ($datefmt[1] == '.' ? '.' : '');
1540
					if ($first['month'] != $last['month'] || $first['year'] != $last['year'])
1541
					{
1542
						if (!$month_before_day)
1543
						{
1544
							$range .= ' '.lang(strftime('%B',$first['raw']));
1545
						}
1546
						if ($first['year'] != $last['year'] && $datefmt[0] != 'Y')
1547
						{
1548
							$range .= ($datefmt[0] != 'd' ? ', ' : ' ') . $first['year'];
1549
						}
1550
						if ($display_time)
1551
						{
1552
							$range .= ' '.adodb_date($timefmt,$first['raw']);
1553
						}
1554
						if (!$last)
1555
						{
1556
							return $range;
1557
						}
1558
						$range .= ' - ';
1559
1560
						if ($first['year'] != $last['year'] && $datefmt[0] == 'Y')
1561
						{
1562
							$range .= $last['year'] . ', ';
1563
						}
1564
1565
						if ($month_before_day)
1566
						{
1567
							$range .= lang(strftime('%B',$last['raw']));
1568
						}
1569
					}
1570
					else
1571
					{
1572
						if ($display_time)
1573
						{
1574
							$range .= ' '.adodb_date($timefmt,$first['raw']);
1575
						}
1576
						$range .= ' - ';
1577
					}
1578
					$range .= ' ' . $last['day'] . ($datefmt[1] == '.' ? '.' : '');
1579
					break;
1580
				case 'm':
1581
				case 'M':
1582
					$range .= ' '.lang(strftime('%B',$month_before_day ? $first['raw'] : $last['raw'])) . ' ';
1583
					break;
1584
				case 'Y':
1585
					if ($datefmt[0] != 'm')
1586
					{
1587
						$range .= ' ' . ($datefmt[0] == 'Y' ? $first['year'].($datefmt[2] == 'd' ? ', ' : ' ') : $last['year'].' ');
1588
					}
1589
					break;
1590
			}
1591
		}
1592
		if ($display_time && $last)
1593
		{
1594
			$range .= ' '.adodb_date($timefmt,$last['raw']);
1595
		}
1596
		if ($datefmt[4] == 'Y' && $datefmt[0] == 'm')
1597
		{
1598
			$range .= ', ' . $last['year'];
1599
		}
1600
		return $range;
1601
	}
1602
1603
	/**
1604
	 * Displays a timespan, eg. $both ? "10:00 - 13:00: 3h" (10:00 am - 1 pm: 3h) : "10:00 3h" (10:00 am 3h)
1605
	 *
1606
	 * @param int $start_m start time in minutes since 0h
1607
	 * @param int $end_m end time in minutes since 0h
1608
	 * @param boolean $both =false display the end-time too, duration is always displayed
1609
	 */
1610
	function timespan($start_m,$end_m,$both=false)
1611
	{
1612
		$duration = $end_m - $start_m;
1613
		if ($end_m == 24*60-1) ++$duration;
1614
		$duration = floor($duration/60).lang('h').($duration%60 ? $duration%60 : '');
1615
1616
		$timespan = $t = Api\DateTime::to('20000101T'.sprintf('%02d',$start_m/60).sprintf('%02d',$start_m%60).'00', false);
1617
1618
		if ($both)	// end-time too
1619
		{
1620
			$timespan .= ' - '.Api\DateTime::to('20000101T'.sprintf('%02d',$end_m/60).sprintf('%02d',$end_m%60).'00', false);
1621
			// dont double am/pm if they are the same in both times
1622
			if ($this->common_prefs['timeformat'] == 12 && substr($timespan,-2) == substr($t,-2))
1623
			{
1624
				$timespan = str_replace($t,substr($t,0,-3),$timespan);
1625
			}
1626
			$timespan .= ':';
1627
		}
1628
		return $timespan . ' ' . $duration;
1629
	}
1630
1631
	/**
1632
	* Converts a participant into a (readable) user- or resource-name
1633
	*
1634
	* @param string|int $id id of user or resource
1635
	* @param string|boolean $use_type =false type-letter or false
1636
	* @param boolean $append_email =false append email (Name <email>)
1637
	* @return string with name
1638
	*/
1639
	function participant_name($id,$use_type=false, $append_email=false)
1640
	{
1641
		static $id2lid = array();
1642
		static $id2email = array();
1643
1644
		if ($use_type && $use_type != 'u') $id = $use_type.$id;
0 ignored issues
show
Bug introduced by
Are you sure $use_type of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

1644
		if ($use_type && $use_type != 'u') $id = /** @scrutinizer ignore-type */ $use_type.$id;
Loading history...
1645
1646
		if (!isset($id2lid[$id]))
1647
		{
1648
			if (!is_numeric($id))
1649
			{
1650
				$id2lid[$id] = '#'.$id;
1651
				if (($info = $this->resource_info($id)))
1652
				{
1653
					$id2lid[$id] = $info['name'] ? $info['name'] : $info['email'];
1654
					if ($info['name']) $id2email[$id] = $info['email'];
1655
				}
1656
			}
1657
			else
1658
			{
1659
				$id2lid[$id] = Api\Accounts::username($id);
1660
				$id2email[$id] = $GLOBALS['egw']->accounts->id2name($id,'account_email');
1661
			}
1662
		}
1663
		return $id2lid[$id].(($append_email || $id[0] == 'e') && $id2email[$id] ? ' <'.$id2email[$id].'>' : '');
1664
	}
1665
1666
	/**
1667
	* Converts participants array of an event into array of (readable) participant-names with status
1668
	*
1669
	* @param array $event event-data
1670
	* @param boolean $long_status =false should the long/verbose status or an icon be use
1671
	* @param boolean $show_group_invitation =false show group-invitations (status == 'G') or not (default)
1672
	* @return array with id / names with status pairs
1673
	*/
1674
	function participants($event,$long_status=false,$show_group_invitation=false)
1675
	{
1676
		//error_log(__METHOD__.__LINE__.array2string($event['participants']));
1677
		$names = array();
1678
		foreach((array)$event['participants'] as $id => $status)
1679
		{
1680
			if (!is_string($status)) continue;
1681
			$quantity = $role = null;
1682
			calendar_so::split_status($status,$quantity,$role);
1683
1684
			if ($status == 'G' && !$show_group_invitation) continue;	// dont show group-invitation
1685
1686
			$lang_status = lang($this->verbose_status[$status]);
1687
			if (!$long_status)
1688
			{
1689
				switch($status[0])
1690
				{
1691
					case 'A':	// accepted
1692
						$status = Api\Html::image('calendar','accepted',$lang_status);
1693
						break;
1694
					case 'R':	// rejected
1695
						$status = Api\Html::image('calendar','rejected',$lang_status);
1696
						break;
1697
					case 'T':	// tentative
1698
						$status = Api\Html::image('calendar','tentative',$lang_status);
1699
						break;
1700
					case 'U':	// no response = unknown
1701
						$status = Api\Html::image('calendar','needs-action',$lang_status);
1702
						break;
1703
					case 'D':	// delegated
1704
						$status = Api\Html::image('calendar','forward',$lang_status);
1705
						break;
1706
					case 'G':	// group invitation
1707
						// Todo: Image, seems not to be used
1708
						$status = '('.$lang_status.')';
1709
						break;
1710
				}
1711
			}
1712
			else
1713
			{
1714
				$status = '('.$lang_status.')';
1715
			}
1716
			$names[$id] = Api\Html::htmlspecialchars($this->participant_name($id)).($quantity > 1 ? ' ('.$quantity.')' : '').' '.$status;
1717
1718
			// add role, if not a regular participant
1719
			if ($role != 'REQ-PARTICIPANT')
1720
			{
1721
				if (isset($this->roles[$role]))
1722
				{
1723
					$role = lang($this->roles[$role]);
1724
				}
1725
				// allow to use cats as roles (beside regular iCal ones)
1726
				elseif (substr($role,0,6) == 'X-CAT-' && ($cat_id = (int)substr($role,6)) > 0)
1727
				{
1728
					$role = $GLOBALS['egw']->categories->id2name($cat_id);
1729
				}
1730
				else
1731
				{
1732
					$role = lang(str_replace('X-','',$role));
1733
				}
1734
				$names[$id] .= ' '.$role;
1735
			}
1736
		}
1737
		natcasesort($names);
1738
1739
		return $names;
1740
	}
1741
1742
	/**
1743
	* Converts category string of an event into array of (readable) category-names
1744
	*
1745
	* @param string $category cat-id (multiple id's commaseparated)
1746
	* @param int $color color of the category, if multiple cats, the color of the last one with color is returned
1747
	* @return array with id / names
1748
	*/
1749
	function categories($category,&$color)
1750
	{
1751
		static $id2cat = array();
1752
		$cats = array();
1753
		$color = 0;
1754
1755
		foreach(explode(',',$category) as $cat_id)
1756
		{
1757
			if (!$cat_id) continue;
1758
1759
			if (!isset($id2cat[$cat_id]))
1760
			{
1761
				$id2cat[$cat_id] = Api\Categories::read($cat_id);
1762
			}
1763
			$cat = $id2cat[$cat_id];
1764
1765
			$parts = null;
1766
			if (is_array($cat['data']) && !empty($cat['data']['color']))
1767
			{
1768
				$color = $cat['data']['color'];
1769
			}
1770
			elseif(preg_match('/(#[0-9A-Fa-f]{6})/', $cat['description'], $parts))
1771
			{
1772
				$color = $parts[1];
1773
			}
1774
			$cats[$cat_id] = stripslashes($cat['name']);
1775
		}
1776
		return $cats;
1777
	}
1778
1779
	/**
1780
	 *  This is called only by list_cals().  It was moved here to remove fatal error in php5 beta4
1781
	 */
1782
	private static function _list_cals_add($id,&$users,&$groups)
1783
	{
1784
		$name = Api\Accounts::username($id);
1785
		if (!($egw_name = $GLOBALS['egw']->accounts->id2name($id)))
1786
		{
1787
			return;	// do not return no longer existing accounts which eg. still mentioned in acl
1788
		}
1789
		if (($type = $GLOBALS['egw']->accounts->get_type($id)) == 'g')
1790
		{
1791
			$arr = &$groups;
1792
		}
1793
		else
1794
		{
1795
			$arr = &$users;
1796
		}
1797
		$arr[$id] = array(
1798
			'grantor' => $id,
1799
			'value'   => ($type == 'g' ? 'g_' : '') . $id,
1800
			'name'    => $name,
1801
			'sname'	  => $egw_name
1802
		);
1803
	}
1804
1805
	/**
1806
	 * generate list of user- / group-calendars for the selectbox in the header
1807
	 *
1808
	 * @return array alphabeticaly sorted array with users first and then groups: array('grantor'=>$id,'value'=>['g_'.]$id,'name'=>$name)
1809
	 */
1810
	function list_cals()
1811
	{
1812
		return self::list_calendars($GLOBALS['egw_info']['user']['account_id'], $this->grants);
1813
	}
1814
1815
	/**
1816
	 * generate list of user- / group-calendars or a given user
1817
	 *
1818
	 * @param int $user account_id of user to generate list for
1819
	 * @param array $grants =null calendar grants from user, or null to query them from acl class
1820
	 */
1821
	public static function list_calendars($user, array $grants=null)
1822
	{
1823
		if (is_null($grants)) $grants = $GLOBALS['egw']->acl->get_grants('calendar', true, $user);
1824
1825
		$users = $groups = array();
1826
		foreach(array_keys($grants) as $id)
1827
		{
1828
			self::_list_cals_add($id,$users,$groups);
1829
		}
1830
		if (($memberships = $GLOBALS['egw']->accounts->memberships($user, true)))
1831
		{
1832
			foreach($memberships as $group)
1833
			{
1834
				self::_list_cals_add($group,$users,$groups);
1835
1836
				if (($account_perms = $GLOBALS['egw']->acl->get_ids_for_location($group,Acl::READ,'calendar')))
1837
				{
1838
					foreach($account_perms as $id)
1839
					{
1840
						self::_list_cals_add($id,$users,$groups);
1841
					}
1842
				}
1843
			}
1844
		}
1845
		usort($users, array(__CLASS__, 'name_cmp'));
1846
		usort($groups, array(__CLASS__, 'name_cmp'));
1847
1848
		return array_merge($users, $groups);	// users first and then groups, both alphabeticaly
1849
	}
1850
1851
	/**
1852
	 * Compare function for sort by value of key 'name'
1853
	 *
1854
	 * @param array $a
1855
	 * @param array $b
1856
	 * @return int
1857
	 */
1858
	public static function name_cmp(array $a, array $b)
1859
	{
1860
		return strnatcasecmp($a['name'], $b['name']);
1861
	}
1862
1863
	/**
1864
	 * Convert the recurrence-information of an event, into a human readable string
1865
	 *
1866
	 * @param array $event
1867
	 * @return string
1868
	 */
1869
	function recure2string($event)
1870
	{
1871
		if (!is_array($event)) return false;
0 ignored issues
show
introduced by
The condition is_array($event) is always true.
Loading history...
1872
		return (string)calendar_rrule::event2rrule($event);
1873
	}
1874
1875
	/**
1876
	 * Read the holidays for a given $year
1877
	 *
1878
	 * The holidays get cached in the session (performance), so changes in holidays or birthdays do NOT affect a current session!!!
1879
	 *
1880
	 * @param int $year =0 year, defaults to 0 = current year
1881
	 * @return array indexed with Ymd of array of holidays. A holiday is an array with the following fields:
1882
	 *	name: string
1883
	 *  title: optional string with description
1884
	 *	day: numerical day in month
1885
	 *	month: numerical month
1886
	 *	occurence: numerical year or 0 for every year
1887
	 */
1888
	function read_holidays($year=0)
1889
	{
1890
		if (!$year) $year = (int) date('Y',$this->now_su);
1891
1892
		$holidays = calendar_holidays::read(
1893
				!empty($GLOBALS['egw_info']['server']['ical_holiday_url']) ?
1894
				$GLOBALS['egw_info']['server']['ical_holiday_url'] :
1895
				$GLOBALS['egw_info']['user']['preferences']['common']['country'], $year);
1896
1897
		// search for birthdays
1898
		if ($GLOBALS['egw_info']['server']['hide_birthdays'] != 'yes')
1899
		{
1900
			$contacts = new Api\Contacts();
1901
			foreach($contacts->get_addressbooks() as $owner => $name)
1902
			{
1903
				$birthdays = $contacts->read_birthdays($owner, $year);
1904
1905
				// Add them in, being careful not to override any existing
1906
				foreach($birthdays as $date => $bdays)
1907
				{
1908
					if(!array_key_exists($date, $holidays))
1909
					{
1910
						$holidays[$date] = array();
1911
					}
1912
					foreach($bdays as $birthday)
1913
					{
1914
						// Skip if name / date are already there - duplicate contacts
1915
						if(in_array($birthday['name'], array_column($holidays[$date], 'name'))) continue;
1916
						$holidays[$date][] = $birthday;
1917
					}
1918
				}
1919
			}
1920
		}
1921
1922
		if ((int) $this->debug >= 2 || $this->debug == 'read_holidays')
1923
		{
1924
			$this->debug_message('calendar_bo::read_holidays(%1)=%2',true,$year,$holidays);
1925
		}
1926
		return $holidays;
1927
	}
1928
1929
	/**
1930
	 * Get translated calendar event fields, presenting as link title options
1931
	 *
1932
	 * @param type $event
1933
	 * @return array array of selected calendar fields
1934
	 */
1935
	public static function get_link_options ($event = array())
1936
	{
1937
		unset($event);	// not used, but required by function signature
1938
		$options = array (
1939
			'end' => lang('End date'),
1940
			'id' => lang('ID'),
1941
			'owner' => lang('Owner'),
1942
			'category' => lang('Category'),
1943
			'location' => lang('Location'),
1944
			'creator' => lang('Creator'),
1945
			'participants' => lang('Participants')
1946
		);
1947
		return $options;
1948
	}
1949
1950
	/**
1951
	 * get title for an event identified by $event
1952
	 *
1953
	 * Is called as hook to participate in the linking
1954
	 *
1955
	 * @param int|array $entry int cal_id or array with event
1956
	 * @param string|boolean string with title, null if not found or false if not read perms
1957
	 */
1958
	function link_title($event)
1959
	{
1960
		if (!is_array($event) && strpos($event, '-') !== false)
1961
		{
1962
			list($id, $recur) = explode('-', $event, 2);
1963
			$event = $this->read($id, $recur);
1964
		}
1965
		else if (!is_array($event) && (int) $event > 0)
1966
		{
1967
			$event = $this->read($event);
1968
		}
1969
		if (!is_array($event))
1970
		{
1971
			return $event;
1972
		}
1973
		$type = explode(',',$this->cal_prefs['link_title']);
1974
		if (is_array($type))
0 ignored issues
show
introduced by
The condition is_array($type) is always true.
Loading history...
1975
		{
1976
			foreach ($type as &$val)
1977
			{
1978
				switch ($val)
1979
				{
1980
					case 'end':
1981
					case 'modified':
1982
						$extra_fields [$val] = $this->format_date($event[$val]);
1983
						break;
1984
					case 'participants':
1985
						foreach (array_keys($event[$val]) as $key)
1986
						{
1987
							$extra_fields [$val] = Api\Accounts::id2name($key, 'account_fullname');
1988
						}
1989
						break;
1990
					case 'modifier':
1991
					case 'creator':
1992
					case 'owner':
1993
						$extra_fields [$val] = Api\Accounts::id2name($event[$val], 'account_fullname');
1994
						break;
1995
					case 'category':
1996
						$extra_fields [$val] = Api\Categories::id2name($event[$val]);
1997
						break;
1998
					default:
1999
						$extra_fields [] = $event[$val];
2000
				}
2001
			}
2002
			$str_fields = implode(', ',$extra_fields);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $extra_fields seems to be defined by a foreach iteration on line 1976. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
2003
			if (is_array($extra_fields)) return $this->format_date($event['start']) . ': ' . $event['title'] . ($str_fields? ', ' . $str_fields:'');
2004
		}
2005
		return $this->format_date($event['start']) . ': ' . $event['title'];
2006
	}
2007
2008
	/**
2009
	 * query calendar for events matching $pattern
2010
	 *
2011
	 * Is called as hook to participate in the linking
2012
	 *
2013
	 * @param string $pattern pattern to search
2014
	 * @return array with cal_id - title pairs of the matching entries
2015
	 */
2016
	function link_query($pattern, Array &$options = array())
2017
	{
2018
		$result = array();
2019
		$query = array(
2020
			'query'	=>	$pattern,
2021
			'offset' =>	$options['start'],
2022
			'order' => 'cal_start DESC',
2023
		);
2024
		if($options['num_rows']) {
2025
			$query['num_rows'] = $options['num_rows'];
2026
		}
2027
		foreach((array) $this->search($query) as $event)
2028
		{
2029
			$result[$event['id']] = $this->link_title($event);
2030
		}
2031
		$options['total'] = $this->total;
2032
		return $result;
2033
	}
2034
2035
	/**
2036
	 * Check access to the file store
2037
	 *
2038
	 * @param int $id id of entry
2039
	 * @param int $check Acl::READ for read and Acl::EDIT for write or delete access
2040
	 * @param string $rel_path =null currently not used in calendar
2041
	 * @param int $user =null for which user to check, default current user
2042
	 * @return boolean true if access is granted or false otherwise
2043
	 */
2044
	function file_access($id,$check,$rel_path,$user=null)
2045
	{
2046
		unset($rel_path);	// not used, but required by function signature
2047
2048
		return $this->check_perms($check,$id,0,'ts',null,$user);
2049
	}
2050
2051
	/**
2052
	 * sets the default prefs, if they are not already set (on a per pref. basis)
2053
	 *
2054
	 * It sets a flag in the app-session-data to be called only once per session
2055
	 */
2056
	function check_set_default_prefs()
2057
	{
2058
		if ($this->cal_prefs['interval'] && ($set = Api\Cache::getSession('calendar', 'default_prefs_set')))
0 ignored issues
show
Unused Code introduced by
The assignment to $set is dead and can be removed.
Loading history...
2059
		{
2060
			return;
2061
		}
2062
		Api\Cache::setSession('calendar', 'default_prefs_set', 'set');
2063
2064
		$default_prefs =& $GLOBALS['egw']->preferences->default['calendar'];
2065
		$forced_prefs  =& $GLOBALS['egw']->preferences->forced['calendar'];
2066
2067
		$subject = lang('Calendar Event') . ' - $$action$$: $$startdate$$ $$title$$'."\n";
2068
		$values = array(
2069
			'notifyAdded'     => $subject . lang ('You have a meeting scheduled for %1','$$startdate$$'),
0 ignored issues
show
Unused Code introduced by
The call to lang() has too many arguments starting with '$$startdate$$'. ( Ignorable by Annotation )

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

2069
			'notifyAdded'     => $subject . /** @scrutinizer ignore-call */ lang ('You have a meeting scheduled for %1','$$startdate$$'),

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

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

Loading history...
2070
			'notifyCanceled'  => $subject . lang ('Your meeting scheduled for %1 has been canceled','$$startdate$$'),
2071
			'notifyModified'  => $subject . lang ('Your meeting that had been scheduled for %1 has been rescheduled to %2','$$olddate$$','$$startdate$$'),
2072
			'notifyDisinvited'=> $subject . lang ('You have been uninvited from the meeting at %1','$$startdate$$'),
2073
			'notifyResponse'  => $subject . lang ('On %1 %2 %3 your meeting request for %4','$$date$$','$$fullname$$','$$action$$','$$startdate$$'),
2074
			'notifyAlarm'     => lang('Alarm for %1 at %2 in %3','$$title$$','$$startdate$$','$$location$$')."\n".lang ('Here is your requested alarm.'),
2075
			'interval'        => 30,
2076
		);
2077
		foreach($values as $var => $default)
2078
		{
2079
			$type = substr($var,0,6) == 'notify' ? 'forced' : 'default';
2080
2081
			// only set, if neither default nor forced pref exists
2082
			if ((!isset($default_prefs[$var]) || (string)$default_prefs[$var] === '') && (!isset($forced_prefs[$var]) || (string)$forced_prefs[$var] === ''))
2083
			{
2084
				$GLOBALS['egw']->preferences->add('calendar',$var,$default,'default');	// always store default, even if we have a forced too
2085
				if ($type == 'forced') $GLOBALS['egw']->preferences->add('calendar',$var,$default,'forced');
2086
				$this->cal_prefs[$var] = $default;
2087
				$need_save = True;
2088
			}
2089
		}
2090
		if ($need_save)
2091
		{
2092
			$GLOBALS['egw']->preferences->save_repository(False,'default');
2093
			$GLOBALS['egw']->preferences->save_repository(False,'forced');
2094
		}
2095
	}
2096
2097
	/**
2098
	 * Get the freebusy URL of a user
2099
	 *
2100
	 * @param int|string $user account_id or account_lid
2101
	 * @param string $pw =null password
2102
	 */
2103
	static function freebusy_url($user='',$pw=null)
2104
	{
2105
		if (is_numeric($user)) $user = $GLOBALS['egw']->accounts->id2name($user);
2106
2107
		$credentials = '';
2108
2109
		if ($pw)
2110
		{
2111
			$credentials = '&password='.urlencode($pw);
2112
		}
2113
		elseif ($GLOBALS['egw_info']['user']['preferences']['calendar']['freebusy'] == 2)
2114
		{
2115
			$credentials = $GLOBALS['egw_info']['user']['account_lid']
2116
				. ':' . $GLOBALS['egw_info']['user']['passwd'];
2117
			$credentials = '&cred=' . base64_encode($credentials);
2118
		}
2119
		return Api\Framework::getUrl($GLOBALS['egw_info']['server']['webserver_url']).
2120
			'/calendar/freebusy.php/?user='.urlencode($user).$credentials;
2121
	}
2122
2123
	/**
2124
	 * Check if the event is the whole day
2125
	 *
2126
	 * @param array $event event
2127
	 * @return boolean true if whole day event, false othwerwise
2128
	 */
2129
	public static function isWholeDay($event)
2130
	{
2131
		// check if the event is the whole day
2132
		$start = self::date2array($event['start']);
2133
		$end = self::date2array($event['end']);
2134
2135
		return !$start['hour'] && !$start['minute'] && $end['hour'] == 23 && $end['minute'] == 59;
2136
	}
2137
2138
	/**
2139
	 * Get the etag for an entry
2140
	 *
2141
	 * As all update routines (incl. set_status and add/delete alarms) update (series master) modified timestamp,
2142
	 * we do NOT need any special handling for series master anymore
2143
	 *
2144
	 * @param array|int|string $entry array with event or cal_id, or cal_id:recur_date for virtual exceptions
2145
	 * @param string &$schedule_tag=null on return schedule-tag (egw_cal.cal_id:egw_cal.cal_etag, no participant modifications!)
2146
	 * @return string|boolean string with etag or false
2147
	 */
2148
	function get_etag($entry, &$schedule_tag=null)
2149
	{
2150
		if (!is_array($entry))
2151
		{
2152
			list($id,$recur_date) = explode(':',$entry);
2153
			$entry = $this->read($id, $recur_date, true, 'server');
2154
		}
2155
		$etag = $schedule_tag = $entry['id'].':'.$entry['etag'];
2156
		$etag .= ':'.$entry['modified'];
2157
2158
		//error_log(__METHOD__ . "($entry[id],$client_share_uid_excpetions) entry=".array2string($entry)." --> etag=$etag");
2159
		return $etag;
2160
	}
2161
2162
	/**
2163
	 * Query ctag for calendar
2164
	 *
2165
	 * @param int|string|array $user integer user-id or array of user-id's to use, defaults to the current user
2166
	 * @param string $filter ='owner' all (not rejected), accepted, unknown, tentative, rejected or hideprivate
2167
	 * @param boolean $master_only =false only check recurance master (egw_cal_user.recur_date=0)
2168
	 * @return integer
2169
	 */
2170
	public function get_ctag($user, $filter='owner', $master_only=false)
2171
	{
2172
		if ($this->debug > 1) $startime = microtime(true);
2173
2174
		// resolve users to add memberships for users and members for groups
2175
		$users = $this->resolve_users($user);
2176
		$ctag = $users ? $this->so->get_ctag($users, $filter == 'owner', $master_only) : 0;	// no rights, return 0 as ctag (otherwise we get SQL error!)
2177
2178
		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");
2179
		return $ctag;
2180
	}
2181
2182
	/**
2183
	 * Hook for infolog  to set some extra data and links
2184
	 *
2185
	 * @param array $data event-array preset by infolog plus
2186
	 * @param int $data[id] cal_id
2187
	 * @return array with key => value pairs to set in new event and link_app/link_id arrays
2188
	 */
2189
	function infolog_set($data)
2190
	{
2191
		if (!($calendar = $this->read($data['id'])))
2192
		{
2193
			return array();
2194
		}
2195
2196
		$content = array(
2197
			'info_cat'       => $GLOBALS['egw']->categories->check_list(Acl::READ, $calendar['category']),
2198
			'info_priority'  => $calendar['priority'] ,
2199
			'info_public'    => $calendar['public'] != 'private',
2200
			'info_subject'   => $calendar['title'],
2201
			'info_des'       => $calendar['description'],
2202
			'info_location'  => $calendar['location'],
2203
			'info_startdate' => $calendar['range_start'],
2204
			//'info_enddate' => $calendar['range_end'] ? $calendar['range_end'] : $calendar['uid']
2205
			'info_contact'   => 'calendar:'.$data['id'],
2206
		);
2207
2208
		unset($content['id']);
2209
		// Add calendar link to infolog entry
2210
		$content['link_app'][] = $calendar['info_link']['app'];
2211
		$content['link_id'][]  = $calendar['info_link']['id'];
2212
		// Copy claendar's links
2213
		foreach(Link::get_links('calendar',$calendar['id'],'','link_lastmod DESC',true) as $link)
2214
		{
2215
			if ($link['app'] != Link::VFS_APPNAME)
2216
			{
2217
				$content['link_app'][] = $link['app'];
2218
				$content['link_id'][]  = $link['id'];
2219
			}
2220
			if ($link['app'] == 'addressbook')	// prefering contact as primary contact over calendar entry set above
2221
			{
2222
				$content['info_contact'] = 'addressbook:'.$link['id'];
2223
			}
2224
		}
2225
		// Copy same custom fields
2226
		foreach(array_keys(Api\Storage\Customfields::get('infolog')) as $name)
2227
		{
2228
			if ($this->customfields[$name]) $content['#'.$name] = $calendar['#'.$name];
2229
		}
2230
		//error_log(__METHOD__.'('.array2string($data).') calendar='.array2string($calendar).' returning '.array2string($content));
2231
		return $content;
2232
	}
2233
2234
	/**
2235
	 * Hook for timesheet to set some extra data and links
2236
	 *
2237
	 * @param array $data
2238
	 * @param int $data[id] cal_id:recurrence
2239
	 * @return array with key => value pairs to set in new timesheet and link_app/link_id arrays
2240
	 */
2241
	function timesheet_set($data)
2242
	{
2243
		$set = array();
2244
		list($id,$recurrence) = explode(':',$data['id']);
2245
		if ((int)$id && ($event = $this->read($id,$recurrence)))
2246
		{
2247
			$set['ts_start'] = $event['start'];
2248
			$set['ts_title'] = $this->link_title($event);
2249
			$set['start_time'] = Api\DateTime::to($event['start'],'H:i');
2250
			$set['ts_description'] = $event['description'];
2251
			if ($this->isWholeDay($event)) $event['end']++;	// whole day events are 1sec short
2252
			$set['ts_duration']	= ($event['end'] - $event['start']) / 60;
2253
			$set['ts_quantity'] = ($event['end'] - $event['start']) / 3600;
2254
			$set['end_time'] = null;	// unset end-time
2255
			$set['cat_id'] = (int)$event['category'];
2256
2257
			foreach(Link::get_links('calendar',$id,'','link_lastmod DESC',true) as $link)
2258
			{
2259
				if ($link['app'] != 'timesheet' && $link['app'] != Link::VFS_APPNAME)
2260
				{
2261
					$set['link_app'][] = $link['app'];
2262
					$set['link_id'][]  = $link['id'];
2263
				}
2264
			}
2265
		}
2266
		return $set;
2267
	}
2268
}
2269