Completed
Push — 16.1 ( 2d8fa6...ca3a66 )
by Nathan
28:18 queued 12:38
created

calendar_bo::name_cmp()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 4
rs 10
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');
231
232
		$this->user = $GLOBALS['egw_info']['user']['account_id'];
233
234
		$this->grants = $GLOBALS['egw']->acl->get_grants('calendar');
235
236
		if (!is_array($this->resources = Api\Cache::getSession('calendar', 'resources')))
237
		{
238
			$this->resources = array();
239
			foreach(Api\Hooks::process('calendar_resources') as $app => $data)
240
			{
241
				if ($data && $data['type'])
242
				{
243
					$this->resources[$data['type']] = $data + array('app' => $app);
244
				}
245
			}
246
			$this->resources['e'] = array(
247
				'type' => 'e',
248
				'info' => __CLASS__.'::email_info',
249
				'app'  => 'email',
250
			);
251
			$this->resources['l'] = array(
252
				'type' => 'l',// one char type-identifier for this resources
253
				'info' => __CLASS__ .'::mailing_lists',// info method, returns array with id, type & name for a given id
254
				'app' => 'Distribution list'
255
			);
256
			$this->resources[''] = array(
257
				'type' => '',
258
				'app' => 'api-accounts',
259
			);
260
			$this->resources['l'] = array(
261
				'type' => 'l',// one char type-identifier for this resources
262
				'info' => __CLASS__ .'::mailing_lists',// info method, returns array with id, type & name for a given id
263
				'app' => 'Distribution list'
264
			);
265
			Api\Cache::setSession('calendar', 'resources', $this->resources);
266
		}
267
		//error_log(__METHOD__ . " registered resources=". array2string($this->resources));
268
269
		$this->config = Api\Config::read('calendar');	// only used for horizont, regular calendar config is under phpgwapi
270
		$this->require_acl_invite = $GLOBALS['egw_info']['server']['require_acl_invite'];
271
272
		$this->categories = new Api\Categories($this->user,'calendar');
273
274
		$this->customfields = Api\Storage\Customfields::get('calendar');
275
276
		foreach($this->alarms as $secs => &$label)
277
		{
278
			$label = self::secs2label($secs);
279
		}
280
	}
281
282
	/**
283
	 * Generate translated label for a given number of seconds
284
	 *
285
	 * @param int $secs
286
	 * @return string
287
	 */
288
	static public function secs2label($secs)
289
	{
290
		if ($secs <= 3600)
291
		{
292
			$label = lang('%1 minutes', $secs/60);
293
		}
294
		elseif($secs <= 86400)
295
		{
296
			$label = lang('%1 hours', $secs/3600);
297
		}
298
		else
299
		{
300
			$label = lang('%1 days', $secs/86400);
301
		}
302
		return $label;
303
	}
304
305
	/**
306
	 * returns info about email addresses as participants
307
	 *
308
	 * @param int|array $ids single contact-id or array of id's
309
	 * @return array
310
	 */
311
	static function email_info($ids)
312
	{
313
		if (!$ids) return null;
314
315
		$data = array();
316
		foreach((array)$ids as $id)
317
		{
318
			$email = $id;
319
			$name = '';
320
			$matches = null;
321 View Code Duplication
			if (preg_match('/^(.*) *<([a-z0-9_.@-]{8,})>$/iU',$email,$matches))
322
			{
323
				$name = $matches[1];
324
				$email = $matches[2];
325
			}
326
			$data[] = array(
327
				'res_id' => $id,
328
				'email' => $email,
329
				'rights' => self::ACL_READ_FOR_PARTICIPANTS,
330
				'name' => $name,
331
			);
332
		}
333
		//error_log(__METHOD__.'('.array2string($ids).')='.array2string($data).' '.function_backtrace());
334
		return $data;
335
	}
336
337
	/**
338
	 * returns info about mailing lists as participants
339
	 *
340
	 * @param int|array $ids single mailing list ID or array of id's
341
	 * @return array
342
	 */
343
	static function mailing_lists($ids)
344
	{
345
		if(!is_array($ids))
346
		{
347
			$ids = array($ids);
348
		}
349
		$data = array();
350
351
		// Email list
352
		$contacts_obj = new Api\Contacts();
353
		$bo = new calendar_bo();
354
		foreach($ids as $id)
355
		{
356
			$list = $contacts_obj->read_list((int)$id);
357
358
			$data[] = array(
359
				'res_id' => $id,
360
				'rights' => self::ACL_READ_FOR_PARTICIPANTS,
361
				'name' => $list['list_name'],
362
				'resources' => $bo->enum_mailing_list('l'.$id, false, false)
363
			);
364
		}
365
366
		return $data;
367
	}
368
369
	/**
370
	 * Enumerates the contacts in a contact list, and returns the list of contact IDs
371
	 *
372
	 * This is used to enable mailing lists as owner/participant
373
	 *
374
	 * @param string $id Mailing list participant ID, which is the mailing list
375
	 *	ID prefixed with 'l'
376
	 * @param boolean $ignore_acl = false Flag to skip ACL checks
377
	 * @param boolean $use_freebusy =true should freebusy rights are taken into account, default true, can be set to false eg. for a search
378
	 *
379
	 * @return array
380
	 */
381
	public function enum_mailing_list($id, $ignore_acl= false, $use_freebusy = true)
382
	{
383
		$contact_list = array();
384
		$contacts = new Api\Contacts();
385
		if($contacts->check_list((int)substr($id,1), ACL::READ) || (int)substr($id,1) < 0)
386
		{
387
			$options = array('list' => substr($id,1));
388
			$lists = $contacts->search('',true,'','','',false,'AND',false,$options);
389
			if(!$lists)
390
			{
391
				return $contact_list;
392
			}
393
			foreach($lists as &$contact)
0 ignored issues
show
Bug introduced by
The expression $lists of type boolean|object<EGroupwar...\Db2DataIterator>|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

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

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

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

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

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

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

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

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

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

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

There are different options of fixing this problem.

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

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

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

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

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

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

$answer = 42;

$correct = false;

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

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

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

Loading history...
1241
				if ($info)
1242
				{
1243
					$info['type'] = $uid[0];
1244 View Code Duplication
					if (!$info['email'] && $info['responsible'])
1245
					{
1246
						$info['email'] = $GLOBALS['egw']->accounts->id2name($info['responsible'],'account_email');
1247
					}
1248
					$info['app'] = $this->resources[$uid[0]]['app'];
1249
				}
1250
			}
1251
			$res_info_cache[$uid] = $info;
1252
		}
1253
		if ($this->debug && ($this->debug > 2 || $this->debug == 'resource_info'))
1254
		{
1255
			$this->debug_message('calendar_bo::resource_info(%1) = %2',True,$uid,$res_info_cache[$uid]);
1256
		}
1257
		return $res_info_cache[$uid];
1258
	}
1259
1260
	/**
1261
	 * Checks if the current user has the necessary ACL rights
1262
	 *
1263
	 * The check is performed on an event or generally on the cal of an other user
1264
	 *
1265
	 * Note: Participating in an event is considered as haveing read-access on that event,
1266
	 *	even if you have no general read-grant from that user.
1267
	 *
1268
	 * @param int $needed necessary ACL right: Acl::{READ|EDIT|DELETE}
1269
	 * @param mixed $event event as array or the event-id or 0 for a general check
1270
	 * @param int $other uid to check (if event==0) or 0 to check against $this->user
1271
	 * @param string $date_format ='ts' date-format used for reading: 'ts'=timestamp, 'array'=array, 'string'=iso8601 string for xmlrpc
1272
	 * @param mixed $date_to_read =null date used for reading, internal param for the caching
1273
	 * @param int $user =null for which user to check, default current user
1274
	 * @return boolean true permission granted, false for permission denied or null if event not found
1275
	 */
1276
	function check_perms($needed,$event=0,$other=0,$date_format='ts',$date_to_read=null,$user=null)
1277
	{
1278
		if (!$user) $user = $this->user;
1279 View Code Duplication
		if ($user == $this->user)
1280
		{
1281
			$grants = $this->grants;
1282
		}
1283
		else
1284
		{
1285
			$grants = $GLOBALS['egw']->acl->get_grants('calendar',true,$user);
1286
		}
1287
1288
		if ($other && !is_numeric($other))
1289
		{
1290
			$resource = $this->resource_info($other);
1291
			return $needed & $resource['rights'];
1292
		}
1293
		if (is_int($event) && $event == 0)
1294
		{
1295
			$owner = $other ? $other : $user;
1296
		}
1297
		else
1298
		{
1299
			if (!is_array($event))
1300
			{
1301
				$event = $this->read($event,$date_to_read,true,$date_format);	// = no ACL check !!!
1302
			}
1303
			if (!is_array($event))
1304
			{
1305
				if ($this->xmlrpc)
1306
				{
1307
					$GLOBALS['server']->xmlrpc_error($GLOBALS['xmlrpcerr']['not_exist'],$GLOBALS['xmlrpcstr']['not_exist']);
1308
				}
1309
				return null;	// event not found
1310
			}
1311
			$owner = $event['owner'];
1312
			$private = !$event['public'];
1313
		}
1314
		$grant = $grants[$owner];
1315
1316
		// now any ACL rights (but invite rights!) implicate FREEBUSY rights (at least READ has to include FREEBUSY)
1317
		if ($grant & ~self::ACL_INVITE) $grant |= self::ACL_FREEBUSY;
1318
1319
		if (is_array($event) && ($needed == Acl::READ || $needed == self::ACL_FREEBUSY))
1320
		{
1321
			// Check if the $user is one of the participants or has a read-grant from one of them
1322
			// in that case he has an implicite READ grant for that event
1323
			//
1324
			if ($event['participants'] && is_array($event['participants']))
1325
			{
1326
				foreach(array_keys($event['participants']) as $uid)
1327
				{
1328
					if ($uid == $user || $uid < 0 && in_array($user, (array)$GLOBALS['egw']->accounts->members($uid,true)))
1329
					{
1330
						// if we are a participant, we have an implicite FREEBUSY, READ and PRIVAT grant
1331
						$grant |= self::ACL_FREEBUSY | Acl::READ | Acl::PRIVAT;
1332
						break;
1333
					}
1334
					elseif ($grants[$uid] & Acl::READ)
1335
					{
1336
						// if we have a READ grant from a participant, we dont give an implicit privat grant too
1337
						$grant |= Acl::READ;
1338
						// we cant break here, as we might be a participant too, and would miss the privat grant
1339
					}
1340
					elseif (!is_numeric($uid))
1341
					{
1342
						// if the owner only grants self::ACL_FREEBUSY we are not interested in the recources explicit rights
1343
						if ($grant == self::ACL_FREEBUSY) continue;
1344
						// if we have a resource as participant
1345
						$resource = $this->resource_info($uid);
1346
						$grant |= $resource['rights'];
1347
					}
1348
				}
1349
			}
1350
		}
1351
		if ($GLOBALS['egw']->accounts->get_type($owner) == 'g' && $needed == Acl::ADD)
1352
		{
1353
			$access = False;	// a group can't be the owner of an event
1354
		}
1355
		else
1356
		{
1357
			$access = $user == $owner || $grant & $needed
1358
				&& ($needed == self::ACL_FREEBUSY || !$private || $grant & Acl::PRIVAT);
1359
		}
1360
		// do NOT allow users to purge deleted events, if we dont have 'userpurge' enabled
1361
		if ($access && $needed == Acl::DELETE && $event['deleted'] &&
1362
			!$GLOBALS['egw_info']['user']['apps']['admin'] &&
1363
			$GLOBALS['egw_info']['server']['calendar_delete_history'] != 'userpurge')
1364
		{
1365
			$access = false;
1366
		}
1367
		if ($this->debug && ($this->debug > 2 || $this->debug == 'check_perms'))
1368
		{
1369
			$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);
1370
		}
1371
		//error_log(__METHOD__."($needed,".array2string($event).",$other,...,$user) returning ".array2string($access));
1372
		return $access;
1373
	}
1374
1375
	/**
1376
	 * Converts several date-types to a timestamp and optionally converts user- to server-time
1377
	 *
1378
	 * @param mixed $date date to convert, should be one of the following types
1379
	 *	string (!) in form YYYYMMDD or iso8601 YYYY-MM-DDThh:mm:ss or YYYYMMDDThhmmss
1380
	 *	int already a timestamp
1381
	 *	array with keys 'second', 'minute', 'hour', 'day' or 'mday' (depricated !), 'month' and 'year'
1382
	 * @param boolean $user2server =False conversion between user- and server-time; default False == Off
1383
	 */
1384
	static function date2ts($date,$user2server=False)
1385
	{
1386
		return $user2server ? Api\DateTime::user2server($date,'ts') : Api\DateTime::to($date,'ts');
1387
	}
1388
1389
	/**
1390
	 * Converts a date to an array and optionally converts server- to user-time
1391
	 *
1392
	 * @param mixed $date date to convert
1393
	 * @param boolean $server2user conversation between user- and server-time default False == Off
1394
	 * @return array with keys 'second', 'minute', 'hour', 'day', 'month', 'year', 'raw' (timestamp) and 'full' (Ymd-string)
1395
	 */
1396
	static function date2array($date,$server2user=False)
1397
	{
1398
		return $server2user ? Api\DateTime::server2user($date,'array') : Api\DateTime::to($date,'array');
1399
	}
1400
1401
	/**
1402
	 * Converts a date as timestamp or array to a date-string and optionaly converts server- to user-time
1403
	 *
1404
	 * @param mixed $date integer timestamp or array with ('year','month',..,'second') to convert
1405
	 * @param boolean $server2user conversation between user- and server-time default False == Off, not used if $format ends with \Z
1406
	 * @param string $format ='Ymd' format of the date to return, eg. 'Y-m-d\TH:i:sO' (2005-11-01T15:30:00+0100)
1407
	 * @return string date formatted according to $format
1408
	 */
1409
	static function date2string($date,$server2user=False,$format='Ymd')
1410
	{
1411
		return $server2user ? Api\DateTime::server2user($date,$format) : Api\DateTime::to($date,$format);
1412
	}
1413
1414
	/**
1415
	 * Formats a date given as timestamp or array
1416
	 *
1417
	 * @param mixed $date integer timestamp or array with ('year','month',..,'second') to convert
1418
	 * @param string|boolean $format ='' default common_prefs[dateformat], common_prefs[timeformat], false=time only, true=date only
1419
	 * @return string the formated date (incl. time)
1420
	 */
1421
	static function format_date($date,$format='')
1422
	{
1423
		return Api\DateTime::to($date,$format);
0 ignored issues
show
Bug introduced by
It seems like $format defined by parameter $format on line 1421 can also be of type boolean; however, EGroupware\Api\DateTime::to() does only seem to accept string, maybe add an additional type check?

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

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

An additional type check may prevent trouble.

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