Completed
Push — 16.1 ( 74433c...7a42e7 )
by Nathan
14:21
created

calendar_bo::__construct()   C

Complexity

Conditions 9
Paths 24

Size

Total Lines 78
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 46
nc 24
nop 0
dl 0
loc 78
rs 5.7191
c 0
b 0
f 0

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * EGroupware - Calendar's buisness-object - access only
4
 *
5
 * @link http://www.egroupware.org
6
 * @package calendar
7
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
8
 * @author Joerg Lehrke <[email protected]>
9
 * @copyright (c) 2004-16 by RalfBecker-At-outdoor-training.de
10
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
11
 * @version $Id$
12
 */
13
14
use EGroupware\Api;
15
use EGroupware\Api\Link;
16
use EGroupware\Api\Acl;
17
18
if (!defined('ACL_TYPE_IDENTIFER'))	// used to mark ACL-values for the debug_message methode
19
{
20
	define('ACL_TYPE_IDENTIFER','***ACL***');
21
}
22
23
define('HOUR_s',60*60);
0 ignored issues
show
Coding Style introduced by
This constant is not in uppercase (expected 'HOUR_S').
Loading history...
24
define('DAY_s',24*HOUR_s);
0 ignored issues
show
Coding Style introduced by
This constant is not in uppercase (expected 'DAY_S').
Loading history...
25
define('WEEK_s',7*DAY_s);
0 ignored issues
show
Coding Style introduced by
This constant is not in uppercase (expected 'WEEK_S').
Loading history...
26
27
/**
28
 * Required (!) include, as we use the MCAL_* constants, BEFORE instanciating (and therefore autoloading) the class
29
 */
30
require_once(EGW_INCLUDE_ROOT.'/calendar/inc/class.calendar_so.inc.php');
31
32
/**
33
 * Class to access all calendar data
34
 *
35
 * For updating calendar data look at the bocalupdate class, which extends this class.
36
 *
37
 * The new UI, BO and SO classes have a strikt definition, in which time-zone they operate:
38
 *  UI only operates in user-time, so there have to be no conversation at all !!!
39
 *  BO's functions take and return user-time only (!), they convert internaly everything to servertime, because
40
 *  SO operates only in server-time
41
 *
42
 * As this BO class deals with dates/times of several types and timezone, each variable should have a postfix
43
 * appended, telling with type it is: _s = seconds, _su = secs in user-time, _ss = secs in server-time, _h = hours
44
 *
45
 * All new BO code (should be true for eGW in general) NEVER use any $_REQUEST ($_POST or $_GET) vars itself.
46
 * Nor does it store the state of any UI-elements (eg. cat-id selectbox). All this is the task of the UI class(es) !!!
47
 *
48
 * All permanent debug messages of the calendar-code should done via the debug-message method of this class !!!
49
 */
50
class calendar_bo
51
{
52
	/**
53
	 * Gives read access to the calendar, but all events the user is not participating are private!
54
	 * Used by addressbook.
55
	 */
56
	const ACL_READ_FOR_PARTICIPANTS = Acl::CUSTOM1;
57
	/**
58
	 * Right to see free/busy data only
59
	 */
60
	const ACL_FREEBUSY = Acl::CUSTOM2;
61
	/**
62
	 * Allows to invite an other user (if configured to be used!)
63
	 */
64
	const ACL_INVITE = Acl::CUSTOM3;
65
66
	/**
67
	 * @var int $debug name of method to debug or level of debug-messages:
68
	 *	False=Off as higher as more messages you get ;-)
69
	 *	1 = function-calls incl. parameters to general functions like search, read, write, delete
70
	 *	2 = function-calls to exported helper-functions like check_perms
71
	 *	4 = function-calls to exported conversation-functions like date2ts, date2array, ...
72
	 *	5 = function-calls to private functions
73
	 */
74
	var $debug=false;
75
76
	/**
77
	 * @var int $now timestamp in server-time
78
	 */
79
	var $now;
80
81
	/**
82
	 * @var int $now_su timestamp of actual user-time
83
	 */
84
	var $now_su;
85
86
	/**
87
	 * @var array $cal_prefs calendar-specific prefs
88
	 */
89
	var $cal_prefs;
90
91
	/**
92
	 * @var array $common_prefs common preferences
93
	 */
94
	var $common_prefs;
95
	/**
96
	 * Custom fields read from the calendar config
97
	 *
98
	 * @var array
99
	 */
100
	var $customfields = array();
101
	/**
102
	 * @var int $user nummerical id of the current user-id
103
	 */
104
	var $user=0;
105
106
	/**
107
	 * @var array $grants grants of the current user, array with user-id / ored-ACL-rights pairs
108
	 */
109
	var $grants=array();
110
111
	/**
112
	 * @var array $verbose_status translated 1-char status values to a verbose name, run through lang() by the constructor
113
	 */
114
	var $verbose_status = array(
115
		'A' => 'Accepted',
116
		'R' => 'Rejected',
117
		'T' => 'Tentative',
118
		'U' => 'No Response',
119
		'D' => 'Delegated',
120
		'G' => 'Group invitation',
121
	);
122
	/**
123
	 * @var array recur_types translates MCAL recur-types to verbose labels
124
	 */
125
	var $recur_types = Array(
126
		MCAL_RECUR_NONE         => 'No recurrence',
127
		MCAL_RECUR_DAILY        => 'Daily',
128
		MCAL_RECUR_WEEKLY       => 'Weekly',
129
		MCAL_RECUR_MONTHLY_WDAY => 'Monthly (by day)',
130
		MCAL_RECUR_MONTHLY_MDAY => 'Monthly (by date)',
131
		MCAL_RECUR_YEARLY       => 'Yearly'
132
	);
133
	/**
134
	 * @var array recur_days translates MCAL recur-days to verbose labels
135
	 */
136
	var $recur_days = array(
137
		MCAL_M_MONDAY    => 'Monday',
138
		MCAL_M_TUESDAY   => 'Tuesday',
139
		MCAL_M_WEDNESDAY => 'Wednesday',
140
		MCAL_M_THURSDAY  => 'Thursday',
141
		MCAL_M_FRIDAY    => 'Friday',
142
		MCAL_M_SATURDAY  => 'Saturday',
143
		MCAL_M_SUNDAY    => 'Sunday',
144
	);
145
	/**
146
	 * Standard iCal attendee roles
147
	 *
148
	 * @var array
149
	 */
150
	var $roles = array(
151
		'REQ-PARTICIPANT' => 'Requested',
152
		'CHAIR'           => 'Chair',
153
		'OPT-PARTICIPANT' => 'Optional',
154
		'NON-PARTICIPANT' => 'None',
155
	);
156
	/**
157
	 * Alarm times
158
	 *
159
	 * @var array
160
	 */
161
	var $alarms = array(
162
		300 => '5 Minutes',
163
		600 => '10 Minutes',
164
		900 => '15 Minutes',
165
		1800 => '30 Minutes',
166
		3600 => '1 Hour',
167
		7200 => '2 Hours',
168
		43200 => '12 Hours',
169
		86400 => '1 Day',
170
		172800 => '2 Days',
171
		604800 => '1 Week',
172
	);
173
	/**
174
	 * @var array $resources registered scheduling resources of the calendar (gets cached in the session for performance reasons)
175
	 */
176
	var $resources;
177
	/**
178
	 * @var array $cached_event here we do some caching to read single events only once
179
	 */
180
	protected static $cached_event = array();
181
	protected static $cached_event_date_format = false;
182
	protected static $cached_event_date = 0;
183
184
	/**
185
	 * Instance of the socal class
186
	 *
187
	 * @var calendar_so
188
	 */
189
	var $so;
190
	/**
191
	 * Instance of the categories class
192
	 *
193
	 * @var Api\Categories
194
	 */
195
	var $categories;
196
	/**
197
	 * Config values for "calendar", only used for horizont, regular calendar config is under phpgwapi
198
	 *
199
	 * @var array
200
	 */
201
	var $config;
202
203
	/**
204
	 * Does a user require an extra invite grant, to be able to invite an other user, default no
205
	 *
206
	 * @var string 'all', 'groups' or null
207
	 */
208
	public $require_acl_invite = null;
209
210
	/**
211
	 * Warnings to show in regular UI
212
	 *
213
	 * @var array
214
	 */
215
	var $warnings = array();
216
217
	/**
218
	 * Constructor
219
	 */
220
	function __construct()
221
	{
222
		if ($this->debug > 0) $this->debug_message('calendar_bo::bocal() started',True);
223
224
		$this->so = new calendar_so();
225
226
		// run 16.1.002 --> .003 update automatic, to not show user time-grid in month-view, if admin did not run the update
227
		if ($GLOBALS['egw_info']['apps']['calendar']['version'] == '16.1.002')
228
		{
229
			include_once(EGW_SERVER_ROOT.'/calendar/setup/tables_update.inc.php');
230
			if (function_exists('calendar_upgrade16_1_002'))
231
			{
232
233
				$GLOBALS['egw']->db->update('egw_applications', array(
234
					'app_version' => calendar_upgrade16_1_002(),
235
				),
236
				array(
237
					'app_name' => 'calendar',
238
					'app_version' => '16.1.002',
239
				), __LINE__, __FILE__);
240
			}
241
			$GLOBALS['egw_info']['user']['preferences'] = $GLOBALS['egw']->preferences->read_repository();
242
		}
243
		$this->common_prefs =& $GLOBALS['egw_info']['user']['preferences']['common'];
244
		$this->cal_prefs =& $GLOBALS['egw_info']['user']['preferences']['calendar'];
245
246
		$this->now = time();
247
		$this->now_su = Api\DateTime::server2user($this->now,'ts');
248
249
		$this->user = $GLOBALS['egw_info']['user']['account_id'];
250
251
		$this->grants = $GLOBALS['egw']->acl->get_grants('calendar');
252
253
		if (!is_array($this->resources = Api\Cache::getSession('calendar', 'resources')))
254
		{
255
			$this->resources = array();
256
			foreach(Api\Hooks::process('calendar_resources') as $app => $data)
257
			{
258
				if ($data && $data['type'])
259
				{
260
					$this->resources[$data['type']] = $data + array('app' => $app);
261
				}
262
			}
263
			$this->resources['e'] = array(
264
				'type' => 'e',
265
				'info' => __CLASS__.'::email_info',
266
				'app'  => 'email',
267
			);
268
			$this->resources['l'] = array(
269
				'type' => 'l',// one char type-identifier for this resources
270
				'info' => __CLASS__ .'::mailing_lists',// info method, returns array with id, type & name for a given id
271
				'app' => 'Distribution list'
272
			);
273
			$this->resources[''] = array(
274
				'type' => '',
275
				'app' => 'api-accounts',
276
			);
277
			$this->resources['l'] = array(
278
				'type' => 'l',// one char type-identifier for this resources
279
				'info' => __CLASS__ .'::mailing_lists',// info method, returns array with id, type & name for a given id
280
				'app' => 'Distribution list'
281
			);
282
			Api\Cache::setSession('calendar', 'resources', $this->resources);
283
		}
284
		//error_log(__METHOD__ . " registered resources=". array2string($this->resources));
285
286
		$this->config = Api\Config::read('calendar');	// only used for horizont, regular calendar config is under phpgwapi
287
		$this->require_acl_invite = $GLOBALS['egw_info']['server']['require_acl_invite'];
288
289
		$this->categories = new Api\Categories($this->user,'calendar');
290
291
		$this->customfields = Api\Storage\Customfields::get('calendar');
292
293
		foreach($this->alarms as $secs => &$label)
294
		{
295
			$label = self::secs2label($secs);
296
		}
297
	}
298
299
	/**
300
	 * Generate translated label for a given number of seconds
301
	 *
302
	 * @param int $secs
303
	 * @return string
304
	 */
305
	static public function secs2label($secs)
306
	{
307
		if ($secs <= 3600)
308
		{
309
			$label = lang('%1 minutes', $secs/60);
310
		}
311
		elseif($secs <= 86400)
312
		{
313
			$label = lang('%1 hours', $secs/3600);
314
		}
315
		else
316
		{
317
			$label = lang('%1 days', $secs/86400);
318
		}
319
		return $label;
320
	}
321
322
	/**
323
	 * returns info about email addresses as participants
324
	 *
325
	 * @param int|array $ids single contact-id or array of id's
326
	 * @return array
327
	 */
328
	static function email_info($ids)
329
	{
330
		if (!$ids) return null;
331
332
		$data = array();
333
		foreach((array)$ids as $id)
334
		{
335
			$email = $id;
336
			$name = '';
337
			$matches = null;
338 View Code Duplication
			if (preg_match('/^(.*) *<([a-z0-9_.@-]{8,})>$/iU',$email,$matches))
339
			{
340
				$name = $matches[1];
341
				$email = $matches[2];
342
			}
343
			$data[] = array(
344
				'res_id' => $id,
345
				'email' => $email,
346
				'rights' => self::ACL_READ_FOR_PARTICIPANTS,
347
				'name' => $name,
348
			);
349
		}
350
		//error_log(__METHOD__.'('.array2string($ids).')='.array2string($data).' '.function_backtrace());
351
		return $data;
352
	}
353
354
	/**
355
	 * returns info about mailing lists as participants
356
	 *
357
	 * @param int|array $ids single mailing list ID or array of id's
358
	 * @return array
359
	 */
360
	static function mailing_lists($ids)
361
	{
362
		if(!is_array($ids))
363
		{
364
			$ids = array($ids);
365
		}
366
		$data = array();
367
368
		// Email list
369
		$contacts_obj = new Api\Contacts();
370
		$bo = new calendar_bo();
371
		foreach($ids as $id)
372
		{
373
			$list = $contacts_obj->read_list((int)$id);
374
375
			$data[] = array(
376
				'res_id' => $id,
377
				'rights' => self::ACL_READ_FOR_PARTICIPANTS,
378
				'name' => $list['list_name'],
379
				'resources' => $bo->enum_mailing_list('l'.$id, false, false)
380
			);
381
		}
382
383
		return $data;
384
	}
385
386
	/**
387
	 * Enumerates the contacts in a contact list, and returns the list of contact IDs
388
	 *
389
	 * This is used to enable mailing lists as owner/participant
390
	 *
391
	 * @param string $id Mailing list participant ID, which is the mailing list
392
	 *	ID prefixed with 'l'
393
	 * @param boolean $ignore_acl = false Flag to skip ACL checks
394
	 * @param boolean $use_freebusy =true should freebusy rights are taken into account, default true, can be set to false eg. for a search
395
	 *
396
	 * @return array
397
	 */
398
	public function enum_mailing_list($id, $ignore_acl= false, $use_freebusy = true)
399
	{
400
		$contact_list = array();
401
		$contacts = new Api\Contacts();
402
		if($contacts->check_list((int)substr($id,1), ACL::READ) || (int)substr($id,1) < 0)
403
		{
404
			$options = array('list' => substr($id,1));
405
			$lists = $contacts->search('',true,'','','',false,'AND',false,$options);
406
			if(!$lists)
407
			{
408
				return $contact_list;
409
			}
410
			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...
411
			{
412
				// Check for user account
413
				if (($account_id = $GLOBALS['egw']->accounts->name2id($contact['id'],'person_id')))
414
				{
415
					$contact = ''.$account_id;
416
				}
417
				else
418
				{
419
					$contact = 'c'.$contact['id'];
420
				}
421 View Code Duplication
				if ($ignore_acl || $this->check_perms(ACL::READ|self::ACL_READ_FOR_PARTICIPANTS|($use_freebusy?self::ACL_FREEBUSY:0),0,$contact))
422
				{
423
					if ($contact && !in_array($contact,$contact_list))	// already added?
424
					{
425
						$contact_list[] = $contact;
426
					}
427
				}
428
			}
429
		}
430
		return $contact_list;
431
	}
432
433
	/**
434
	 * Add group-members as participants with status 'G'
435
	 *
436
	 * @param array $event event-array
437
	 * @return int number of added participants
438
	 */
439
	function enum_groups(&$event)
440
	{
441
		$added = 0;
442
		foreach(array_keys($event['participants']) as $uid)
443
		{
444
			if (is_numeric($uid) && $GLOBALS['egw']->accounts->get_type($uid) == 'g' &&
445
				($members = $GLOBALS['egw']->accounts->members($uid, true)))
446
			{
447
				foreach($members as $member)
448
				{
449
					if (!isset($event['participants'][$member]))
450
					{
451
						$event['participants'][$member] = 'G';
452
						++$added;
453
					}
454
				}
455
			}
456
		}
457
		return $added;
458
	}
459
460
	/**
461
	 * Resolve users to add memberships for users and members for groups
462
	 *
463
	 * @param int|array $_users
464
	 * @param boolean $no_enum_groups =true
465
	 * @param boolean $ignore_acl =false
466
	 * @param boolean $use_freebusy =true should freebusy rights are taken into account, default true, can be set to false eg. for a search
467
	 * @return array of user-ids
468
	 */
469
	private function resolve_users($_users, $no_enum_groups=true, $ignore_acl=false, $use_freebusy=true)
470
	{
471
		if (!is_array($_users))
472
		{
473
			$_users = $_users ? array($_users) : array();
474
		}
475
		// only query calendars of users, we have READ-grants from
476
		$users = array();
477
		foreach($_users as $user)
478
		{
479
			$user = trim($user);
480
			
481
			// Handle email lists
482
			if(!is_numeric($user) && $user[0] == 'l')
483
			{
484
				foreach($this->enum_mailing_list($user, $ignore_acl, $use_freebusy) as $contact)
485
				{
486
					if ($contact && !in_array($contact,$users))	// already added?
487
					{
488
						$users[] = $contact;
489
					}
490
				}
491
				continue;
492
			}
493
			if ($ignore_acl || $this->check_perms(ACL::READ|self::ACL_READ_FOR_PARTICIPANTS|($use_freebusy?self::ACL_FREEBUSY:0),0,$user))
494
			{
495
				if ($user && !in_array($user,$users))	// already added?
496
				{
497
					// General expansion check
498
					if (!is_numeric($user) && $this->resources[$user[0]]['info'])
499
					{
500
						$info = $this->resource_info($user);
501
						if($info && $info['resources'])
502
						{
503
							foreach($info['resources'] as $_user)
504
							{
505
								if($_user && !in_array($_user, $users))
506
								{
507
									$users[] = $_user;
508
								}
509
							}
510
							continue;
511
						}
512
					}
513
					$users[] = $user;
514
				}
515
			}
516
			elseif ($GLOBALS['egw']->accounts->get_type($user) != 'g')
517
			{
518
				continue;	// for non-groups (eg. users), we stop here if we have no read-rights
519
			}
520
			// the further code is only for real users
521
			if (!is_numeric($user)) continue;
522
523
			// for groups we have to include the members
524
			if ($GLOBALS['egw']->accounts->get_type($user) == 'g')
525
			{
526
				if ($no_enum_groups) continue;
527
528
				$members = $GLOBALS['egw']->accounts->members($user, true);
529 View Code Duplication
				if (is_array($members))
530
				{
531
					foreach($members as $member)
532
					{
533
						// use only members which gave the user a read-grant
534
						if (!in_array($member, $users) &&
535
							($ignore_acl || $this->check_perms(Acl::READ|($use_freebusy?self::ACL_FREEBUSY:0),0,$member)))
536
						{
537
							$users[] = $member;
538
						}
539
					}
540
				}
541
			}
542 View Code Duplication
			else	// for users we have to include all the memberships, to get the group-events
543
			{
544
				$memberships = $GLOBALS['egw']->accounts->memberships($user, true);
545
				if (is_array($memberships))
546
				{
547
					foreach($memberships as $group)
548
					{
549
						if (!in_array($group,$users))
550
						{
551
							$users[] = $group;
552
						}
553
					}
554
				}
555
			}
556
		}
557
		return $users;
558
	}
559
560
	/**
561
	 * Searches / lists calendar entries, including repeating ones
562
	 *
563
	 * @param array $params array with the following keys
564
	 *	start date startdate of the search/list, defaults to today
565
	 *	end   date enddate of the search/list, defaults to start + one day
566
	 *	users  int|array integer user-id or array of user-id's to use, defaults to the current user
567
	 *  cat_id int|array category-id or array of cat-id's (incl. all sub-categories), default 0 = all
568
	 *	filter string all (not rejected), accepted, unknown, tentative, rejected, hideprivate or everything (incl. rejected, deleted)
569
	 *	query string pattern so search for, if unset or empty all matching entries are returned (no search)
570
	 *		Please Note: a search never returns repeating events more then once AND does not honor start+end date !!!
571
	 *	daywise boolean on True it returns an array with YYYYMMDD strings as keys and an array with events
572
	 *		(events spanning multiple days are returned each day again (!)) otherwise it returns one array with
573
	 *		the events (default), not honored in a search ==> always returns an array of events!
574
	 *	date_format string date-formats: 'ts'=timestamp (default), 'array'=array, or string with format for date
575
	 *  offset boolean|int false (default) to return all entries or integer offset to return only a limited result
576
	 *  enum_recuring boolean if true or not set (default) or daywise is set, each recurence of a recuring events is returned,
577
	 *		otherwise the original recuring event (with the first start- + enddate) is returned
578
	 *  num_rows int number of entries to return, default or if 0, max_entries from the prefs
579
	 *  order column-names plus optional DESC|ASC separted by comma
580
	 *  ignore_acl if set and true no check_perms for a general Acl::READ grants is performed
581
	 *  enum_groups boolean if set and true, group-members will be added as participants with status 'G'
582
	 *  cols string|array columns to select, if set an iterator will be returned
583
	 *  append string to append to the query, eg. GROUP BY
584
	 *  cfs array if set, query given custom fields or all for empty array, none are returned, if not set (default)
585
	 *  master_only boolean default false, true only take into account participants/status from master (for AS)
586
	 * @param string $sql_filter =null sql to be and'ed into query (fully quoted), default none
587
	 * @return iterator|array|boolean array of events or array with YYYYMMDD strings / array of events pairs (depending on $daywise param)
588
	 *	or false if there are no read-grants from _any_ of the requested users or iterator/recordset if cols are given
589
	 */
590
	function &search($params,$sql_filter=null)
591
	{
592
		$params_in = $params;
593
594
		$params['sql_filter'] = $sql_filter;	// dont allow to set it via UI or xmlrpc
595
596
		// check if any resource wants to hook into
597
		foreach($this->resources as $data)
598
		{
599
			if (isset($data['search_filter']))
600
			{
601
				$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...
602
			}
603
		}
604
605
		if (!isset($params['users']) || !$params['users'] ||
606
			count($params['users']) == 1 && isset($params['users'][0]) && !$params['users'][0])	// null or '' casted to an array
607
		{
608
			// for a search use all account you have read grants from
609
			$params['users'] = $params['query'] ? array_keys($this->grants) : $this->user;
610
		}
611
		// resolve users to add memberships for users and members for groups
612
		// for search, do NOT use freebusy rights, as it would allow to probe the content of event entries
613
		$users = $this->resolve_users($params['users'], $params['filter'] == 'no-enum-groups', $params['ignore_acl'], empty($params['query']));
614
615
		// supply so with private_grants, to not query them again from the database
616
		if (!empty($params['query']))
617
		{
618
			$params['private_grants'] = array();
619
			foreach($this->grants as $user => $rights)
620
			{
621
				if ($rights & Acl::PRIVAT) $params['private_grants'][] = $user;
622
			}
623
		}
624
625
		// replace (by so not understood filter 'no-enum-groups' with 'default' filter
626
		if ($params['filter'] == 'no-enum-groups')
627
		{
628
			$params['filter'] = 'default';
629
		}
630
		// if we have no grants from the given user(s), we directly return no events / an empty array,
631
		// as calling the so-layer without users would give the events of all users (!)
632
		if (!count($users) && !$params['ignore_acl'])
633
		{
634
			return false;
635
		}
636
		if (isset($params['start'])) $start = $this->date2ts($params['start']);
637
638
		if (isset($params['end']))
639
		{
640
			$end = $this->date2ts($params['end']);
641
			$this->check_move_horizont($end);
642
		}
643
		$daywise = !isset($params['daywise']) ? False : !!$params['daywise'];
644
		$params['enum_recuring'] = $enum_recuring = $daywise || !isset($params['enum_recuring']) || !!$params['enum_recuring'];
645
		$cat_id = isset($params['cat_id']) ? $params['cat_id'] : 0;
646
		$filter = isset($params['filter']) ? $params['filter'] : 'all';
647
		$offset = isset($params['offset']) && $params['offset'] !== false ? (int) $params['offset'] : false;
648
		// socal::search() returns rejected group-invitations, as only the user not also the group is rejected
649
		// as we cant remove them efficiantly in SQL, we kick them out here, but only if just one user is displayed
650
		$users_in = (array)$params_in['users'];
651
		$remove_rejected_by_user = !in_array($filter,array('all','rejected','everything')) &&
652
			count($users_in) == 1 && $users_in[0] > 0 ? $users_in[0] : null;
653
		//error_log(__METHOD__.'('.array2string($params_in).", $sql_filter) params[users]=".array2string($params['users']).' --> remove_rejected_by_user='.array2string($remove_rejected_by_user));
654
655
		if ($this->debug && ($this->debug > 1 || $this->debug == 'search'))
656
		{
657
			$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)',
658
				True,$params,$start,$end,$daywise,$cat_id,$filter,$params['query'],$offset,(int)$params['num_rows'],$params['order'],$params['sql_filter']);
659
		}
660
		// date2ts(,true) converts to server time, db2data converts again to user-time
661
		$events =& $this->so->search(isset($start) ? $this->date2ts($start,true) : null,isset($end) ? $this->date2ts($end,true) : null,
662
			$users,$cat_id,$filter,$offset,(int)$params['num_rows'],$params,$remove_rejected_by_user);
663
664
		if (isset($params['cols']))
665
		{
666
			return $events;
667
		}
668
		$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...
669
		$this->db2data($events,isset($params['date_format']) ? $params['date_format'] : 'ts');
670
671
		//echo "<p align=right>remove_rejected_by_user=$remove_rejected_by_user, filter=$filter, params[users]=".print_r($param['users'])."</p>\n";
672
		foreach($events as $id => $event)
673
		{
674
			if ($params['enum_groups'] && $this->enum_groups($event))
675
			{
676
				$events[$id] = $event;
677
			}
678
			$matches = null;
679
			if (!(int)$event['id'] && preg_match('/^([a-z_]+)([0-9]+)$/',$event['id'],$matches))
680
			{
681
				$is_private = self::integration_get_private($matches[1],$matches[2],$event);
682
			}
683
			else
684
			{
685
				$is_private = !$this->check_perms(Acl::READ,$event);
686
			}
687
			if (!$params['ignore_acl'] && ($is_private || (!$event['public'] && $filter == 'hideprivate')))
688
			{
689
				$this->clear_private_infos($events[$id],$users);
690
			}
691
		}
692
693
		if ($daywise)
694
		{
695
			if ($this->debug && ($this->debug > 2 || $this->debug == 'search'))
696
			{
697
				$this->debug_message('socalendar::search daywise sorting from %1 to %2 of %3',False,$start,$end,$events);
698
			}
699
			// create empty entries for each day in the reported time
700
			for($ts = $start; $ts <= $end; $ts += DAY_s) // good enough for array creation, but see while loop below.
701
			{
702
				$daysEvents[$this->date2string($ts)] = array();
703
			}
704
			foreach($events as $k => $event)
705
			{
706
				$e_start = max($this->date2ts($event['start']),$start);
707
				// $event['end']['raw']-1 to allow events to end on a full hour/day without the need to enter it as minute=59
708
				$e_end   = min($this->date2ts($event['end'])-1,$end);
709
710
				// add event to each day in the reported time
711
				$ts = $e_start;
712
				//  $ts += DAY_s in a 'for' loop does not work for daylight savings in week view
713
				// because the day is longer than DAY_s: Fullday events will be added twice.
714
				$ymd = null;
715
				while ($ts <= $e_end)
716
				{
717
					$daysEvents[$ymd = $this->date2string($ts)][] =& $events[$k];
718
					$ts = strtotime("+1 day",$ts);
719
				}
720
				if ($ymd != ($last = $this->date2string($e_end)))
721
				{
722
					$daysEvents[$last][] =& $events[$k];
723
				}
724
			}
725
			$events =& $daysEvents;
726
			if ($this->debug && ($this->debug > 2 || $this->debug == 'search'))
727
			{
728
				$this->debug_message('socalendar::search daywise events=%1',False,$events);
729
			}
730
		}
731
		if ($this->debug && ($this->debug > 0 || $this->debug == 'search'))
732
		{
733
			$this->debug_message('calendar_bo::search(%1)=%2',True,$params,$events);
734
		}
735
		//error_log(__METHOD__."() returning ".count($events)." entries, total=$this->total ".function_backtrace());
736
		return $events;
737
	}
738
739
	/**
740
	 * Get integration data for a given app of a part (value for a certain key) of it
741
	 *
742
	 * @param string $app
743
	 * @param string $part
744
	 * @return array
745
	 */
746
	static function integration_get_data($app,$part=null)
747
	{
748
		static $integration_data=null;
749
750
		if (!isset($integration_data))
751
		{
752
			$integration_data = calendar_so::get_integration_data();
753
		}
754
755
		if (!isset($integration_data[$app])) return null;
756
757
		return $part ? $integration_data[$app][$part] : $integration_data[$app];
758
	}
759
760
	/**
761
	 * Get private attribute for an integration event
762
	 *
763
	 * Attribute 'is_private' is either a boolean value, eg. false to make all events of $app public
764
	 * or an ExecMethod callback with parameters $id,$event
765
	 *
766
	 * @param string $app
767
	 * @param int|string $id
768
	 * @return string
769
	 */
770
	static function integration_get_private($app,$id,$event)
771
	{
772
		$app_data = self::integration_get_data($app,'is_private');
773
774
		// no method, fall back to link title
775
		if (is_null($app_data))
776
		{
777
			$is_private = !Link::title($app,$id);
778
		}
779
		// boolean value to make all events of $app public (false) or private (true)
780
		elseif (is_bool($app_data))
781
		{
782
			$is_private = $app_data;
783
		}
784
		else
785
		{
786
			$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...
787
		}
788
		//echo '<p>'.__METHOD__."($app,$id,) app_data=".array2string($app_data).' returning '.array2string($is_private)."</p>\n";
789
		return $is_private;
790
	}
791
792
	/**
793
	 * Clears all non-private info from a privat event
794
	 *
795
	 * That function only returns the infos allowed to be viewed by people without Acl::PRIVAT grants
796
	 *
797
	 * @param array &$event
798
	 * @param array $allowed_participants ids of the allowed participants, eg. the ones the search is over or eg. the owner of the calendar
799
	 */
800
	function clear_private_infos(&$event,$allowed_participants = array())
801
	{
802
		if ($event == false) return;
803
		if (!is_array($event['participants'])) error_log(__METHOD__.'('.array2string($event).', '.array2string($allowed_participants).') NO PARTICIPANTS '.function_backtrace());
804
805
		$event = array(
806
			'id'    => $event['id'],
807
			'start' => $event['start'],
808
			'end'   => $event['end'],
809
			'whole_day' => $event['whole_day'],
810
			'tzid'  => $event['tzid'],
811
			'title' => lang('private'),
812
			'modified'	=> $event['modified'],
813
			'owner'		=> $event['owner'],
814
			'uid'	=> $event['uid'],
815
			'etag'	=> $event['etag'],
816
			'participants' => array_intersect_key($event['participants'],array_flip($allowed_participants)),
817
			'public'=> 0,
818
			'category' => $event['category'],	// category is visible anyway, eg. by using planner by cat
819
			'non_blocking' => $event['non_blocking'],
820
			'caldav_name' => $event['caldav_name'],
821
		// we need full recurrence information, as they are relevant free/busy information
822
		)+($event['recur_type'] ? array(
823
			'recur_type'     => $event['recur_type'],
824
			'recur_interval' => $event['recur_interval'],
825
			'recur_data'     => $event['recur_data'],
826
			'recur_enddate'  => $event['recur_enddate'],
827
			'recur_exception'=> $event['recur_exception'],
828
		):array(
829
			'reference'      => $event['reference'],
830
			'recurrence'     => $event['recurrence'],
831
		));
832
	}
833
834
	/**
835
	 * check and evtl. move the horizont (maximum date for unlimited recuring events) to a new date
836
	 *
837
	 * @internal automaticaly called by search
838
	 * @param mixed $_new_horizont time to set the horizont to (user-time)
839
	 */
840
	function check_move_horizont($_new_horizont)
841
	{
842
		if ((int) $this->debug >= 2 || $this->debug == 'check_move_horizont')
843
		{
844
			$this->debug_message('calendar_bo::check_move_horizont(%1) horizont=%2',true,$_new_horizont,(int)$this->config['horizont']);
845
		}
846
		$new_horizont = $this->date2ts($_new_horizont,true);	// now we are in server-time, where this function operates
847
848
		if ($new_horizont <= $this->config['horizont'])	// no move necessary
849
		{
850
			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']);
851
			return;
852
		}
853
		if (!empty($GLOBALS['egw_info']['server']['calendar_horizont']))
854
		{
855
			$maxdays = abs($GLOBALS['egw_info']['server']['calendar_horizont']);
856
		}
857
		if (empty($maxdays)) $maxdays = 1000; // old default
858
		if ($new_horizont > time()+$maxdays*DAY_s)		// some user tries to "look" more then the maximum number of days in the future
859
		{
860
			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);
861
			$this->warnings['horizont'] = lang('Requested date %1 outside allowed range of %2 days: recurring events obmitted!', Api\DateTime::to($new_horizont,true), $maxdays);
862
			return;
863
		}
864
		if ($new_horizont < time()+31*DAY_s)
865
		{
866
			$new_horizont = time()+31*DAY_s;
867
		}
868
		$old_horizont = $this->config['horizont'];
869
		$this->config['horizont'] = $new_horizont;
870
871
		// create further recurrences for all recurring and not yet (at the old horizont) ended events
872
		if (($recuring = $this->so->unfinished_recuring($old_horizont)))
873
		{
874
			@set_time_limit(0);	// disable time-limit, in case it takes longer to calculate the recurrences
875
			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...
876
			{
877
				if ($this->debug == 'check_move_horizont')
878
				{
879
					$this->debug_message('calendar_bo::check_move_horizont(%1): calling set_recurrences(%2,%3)',true,$new_horizont,$event,$old_horizont);
880
				}
881
				// insert everything behind max(cal_start), which can be less then $old_horizont because of bugs in the past
882
				$this->set_recurrences($event,Api\DateTime::server2user($recuring[$cal_id]+1));	// set_recurences operates in user-time!
883
			}
884
		}
885
		// update the horizont
886
		Api\Config::save_value('horizont',$this->config['horizont'],'calendar');
887
888
		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']);
889
	}
890
891
	/**
892
	 * set all recurrences for an event until the defined horizont $this->config['horizont']
893
	 *
894
	 * This methods operates in usertime, while $this->config['horizont'] is in servertime!
895
	 *
896
	 * @param array $event
897
	 * @param mixed $start =0 minimum start-time for new recurrences or !$start = since the start of the event
898
	 */
899
	function set_recurrences($event,$start=0)
900
	{
901 View Code Duplication
		if ($this->debug && ((int) $this->debug >= 2 || $this->debug == 'set_recurrences' || $this->debug == 'check_move_horizont'))
902
		{
903
			$this->debug_message('calendar_bo::set_recurrences(%1,%2)',true,$event,$start);
904
		}
905
		// check if the caller gave us enough information and if not read it from the DB
906
		if (!isset($event['participants']) || !isset($event['start']) || !isset($event['end']))
907
		{
908
			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...
909
			if (!isset($event['participants']))
910
			{
911
				$event['participants'] = $event_read['participants'];
912
			}
913
			if (!isset($event['start']) || !isset($event['end']))
914
			{
915
				$event['start'] = $this->date2usertime($event_read['start']);
916
				$event['end'] = $this->date2usertime($event_read['end']);
917
			}
918
		}
919
		if (!$start) $start = $event['start'];
920
		$start_obj = new Api\DateTime($start);
921
		$read_start = new Api\DateTime($event_read['start']);
922
923
		$events = array();
924
		$this->insert_all_recurrences($event,$start,$this->date2usertime($this->config['horizont']),$events);
925
926
		$exceptions = array();
927
		foreach((array)$event['recur_exception'] as $exception)
928
		{
929
			$exceptions[] = Api\DateTime::to($exception, true);	// true = date
930
		}
931
		foreach($events as $event)
932
		{
933
			$is_exception = in_array(Api\DateTime::to($event['start'], true), $exceptions);
934
			$start = $this->date2ts($event['start'],true);
935
			if ($event['whole_day'])
936
			{
937
				$start = new Api\DateTime($event['start'], Api\DateTime::$server_timezone);
938
				$start->setTime(0,0,0);
939
				$start = $start->format('ts');
940
				$time = $this->so->startOfDay(new Api\DateTime($event['end'], Api\DateTime::$user_timezone));
941
				$time->setTime(23, 59, 59);
942
				$end = $this->date2ts($time,true);
943
			}
944
			else
945
			{
946
				$end = $this->date2ts($event['end'],true);
947
			}
948
			//error_log(__METHOD__."() start=".Api\DateTime::to($start).", is_exception=".array2string($is_exception));
949
			$this->so->recurrence($event['id'], $start, $end, $event['participants'], $is_exception);
950
		}
951
	}
952
953
	/**
954
	 * Convert data read from the db, eg. convert server to user-time
955
	 *
956
	 * Also make sure all timestamps comming from DB as string are converted to integer,
957
	 * to avoid misinterpretation by Api\DateTime as Ymd string.
958
	 *
959
	 * @param array &$events array of event-arrays (reference)
960
	 * @param $date_format ='ts' date-formats: 'ts'=timestamp, 'server'=timestamp in server-time, 'array'=array or string with date-format
961
	 */
962
	function db2data(&$events,$date_format='ts')
963
	{
964
		if (!is_array($events)) echo "<p>calendar_bo::db2data(\$events,$date_format) \$events is no array<br />\n".function_backtrace()."</p>\n";
965
		foreach ($events as &$event)
966
		{
967
			// convert timezone id of event to tzid (iCal id like 'Europe/Berlin')
968 View Code Duplication
			if (empty($event['tzid']) && (!$event['tz_id'] || !($event['tzid'] = calendar_timezones::id2tz($event['tz_id']))))
969
			{
970
				$event['tzid'] = Api\DateTime::$server_timezone->getName();
971
			}
972
			// database returns timestamps as string, convert them to integer
973
			// to avoid misinterpretation by Api\DateTime as Ymd string
974
			// (this will fail on 32bit systems for times > 2038!)
975
			$event['start'] = (int)$event['start'];	// this is for isWholeDay(), which also calls Api\DateTime
976
			$event['end'] = (int)$event['end'];
977
			$event['whole_day'] = self::isWholeDay($event);
978
			if ($event['whole_day'] && $date_format != 'server')
979
			{
980
				// Adjust dates to user TZ
981
				$stime =& $this->so->startOfDay(new Api\DateTime((int)$event['start'], Api\DateTime::$server_timezone), $event['tzid']);
982
				$event['start'] = Api\DateTime::to($stime, $date_format);
983
				$time =& $this->so->startOfDay(new Api\DateTime((int)$event['end'], Api\DateTime::$server_timezone), $event['tzid']);
984
				$time->setTime(23, 59, 59);
985
				$event['end'] = Api\DateTime::to($time, $date_format);
986
				if (!empty($event['recurrence']))
987
				{
988
					$time =& $this->so->startOfDay(new Api\DateTime((int)$event['recurrence'], Api\DateTime::$server_timezone), $event['tzid']);
989
					$event['recurrence'] = Api\DateTime::to($time, $date_format);
990
				}
991
				if (!empty($event['recur_enddate']))
992
				{
993
					$time =& $this->so->startOfDay(new Api\DateTime((int)$event['recur_enddate'], Api\DateTime::$server_timezone), $event['tzid']);
994
					$time->setTime(23, 59, 59);
995
					$event['recur_enddate'] = Api\DateTime::to($time, $date_format);
996
				}
997
				$timestamps = array('modified','created','deleted');
998
			}
999 View Code Duplication
			else
1000
			{
1001
				$timestamps = array('start','end','modified','created','recur_enddate','recurrence','recur_date','deleted');
1002
			}
1003
			// we convert here from the server-time timestamps to user-time and (optional) to a different date-format!
1004
			foreach ($timestamps as $ts)
1005
			{
1006
				if (!empty($event[$ts]))
1007
				{
1008
					$event[$ts] = $this->date2usertime((int)$event[$ts],$date_format);
1009
				}
1010
			}
1011
			// same with the recur exceptions
1012
			if (isset($event['recur_exception']) && is_array($event['recur_exception']))
1013
			{
1014
				foreach($event['recur_exception'] as &$date)
1015
				{
1016
					if ($event['whole_day'] && $date_format != 'server')
1017
					{
1018
						// Adjust dates to user TZ
1019
						$time =& $this->so->startOfDay(new Api\DateTime((int)$date, Api\DateTime::$server_timezone), $event['tzid']);
1020
						$date = Api\DateTime::to($time, $date_format);
1021
					}
1022
					else
1023
					{
1024
						$date = $this->date2usertime((int)$date,$date_format);
1025
					}
1026
				}
1027
			}
1028
			// same with the alarms
1029 View Code Duplication
			if (isset($event['alarm']) && is_array($event['alarm']))
1030
			{
1031
				foreach($event['alarm'] as &$alarm)
1032
				{
1033
					$alarm['time'] = $this->date2usertime((int)$alarm['time'],$date_format);
1034
				}
1035
			}
1036
		}
1037
	}
1038
1039
	/**
1040
	 * convert a date from server to user-time
1041
	 *
1042
	 * @param int $ts timestamp in server-time
1043
	 * @param string $date_format ='ts' date-formats: 'ts'=timestamp, 'server'=timestamp in server-time, 'array'=array or string with date-format
1044
	 * @return mixed depending of $date_format
1045
	 */
1046
	function date2usertime($ts,$date_format='ts')
1047
	{
1048
		if (empty($ts) || $date_format == 'server') return $ts;
1049
1050
		return Api\DateTime::server2user($ts,$date_format);
1051
	}
1052
1053
	/**
1054
	 * Reads a calendar-entry
1055
	 *
1056
	 * @param int|array|string $ids id or array of id's of the entries to read, or string with a single uid
1057
	 * @param mixed $date =null date to specify a single event of a series
1058
	 * @param boolean $ignore_acl should we ignore the acl, default False for a single id, true for multiple id's
1059
	 * @param string $date_format ='ts' date-formats: 'ts'=timestamp, 'server'=timestamp in servertime, 'array'=array, or string with date-format
1060
	 * @param array|int $clear_private_infos_users =null if not null, return events with self::ACL_FREEBUSY too,
1061
	 * 	but call clear_private_infos() with the given users
1062
	 * @return boolean|array event or array of id => event pairs, false if the acl-check went wrong, null if $ids not found
1063
	 */
1064
	function read($ids,$date=null,$ignore_acl=False,$date_format='ts',$clear_private_infos_users=null)
1065
	{
1066
		if (!$ids) return false;
1067
1068
		if ($date) $date = $this->date2ts($date);
1069
1070
		$return = null;
1071
1072
		$check = $clear_private_infos_users ? self::ACL_FREEBUSY : Acl::READ;
1073
		if ($ignore_acl || is_array($ids) || ($return = $this->check_perms($check,$ids,0,$date_format,$date)))
1074
		{
1075
			if (is_array($ids) || !isset(self::$cached_event['id']) || self::$cached_event['id'] != $ids ||
1076
				self::$cached_event_date_format != $date_format ||
1077
				self::$cached_event['recur_type'] != MCAL_RECUR_NONE && self::$cached_event_date != $date)
1078
			{
1079
				$events = $this->so->read($ids,$date ? $this->date2ts($date,true) : 0);
1080
1081
				if ($events)
1082
				{
1083
					$this->db2data($events,$date_format);
1084
1085
					if (is_array($ids))
1086
					{
1087
						$return =& $events;
1088
					}
1089
					else
1090
					{
1091
						self::$cached_event = array_shift($events);
1092
						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...
1093
						self::$cached_event_date = $date;
1094
						$return = self::$cached_event;
1095
					}
1096
				}
1097
			}
1098
			else
1099
			{
1100
				$return = self::$cached_event;
1101
			}
1102
		}
1103
		if ($clear_private_infos_users && !is_array($ids) && !$this->check_perms(Acl::READ,$return))
1104
		{
1105
			$this->clear_private_infos($return, (array)$clear_private_infos_users);
1106
		}
1107
		if ($this->debug && ($this->debug > 1 || $this->debug == 'read'))
1108
		{
1109
			$this->debug_message('calendar_bo::read(%1,%2,%3,%4,%5)=%6',True,$ids,$date,$ignore_acl,$date_format,$clear_private_infos_users,$return);
1110
		}
1111
		return $return;
1112
	}
1113
1114
	/**
1115
	 * Inserts all repetions of $event in the timespan between $start and $end into $events
1116
	 *
1117
	 * The new entries are just appended to $events, so $events is no longer sorted by startdate !!!
1118
	 *
1119
	 * Recurrences get calculated by rrule iterator implemented in calendar_rrule class.
1120
	 *
1121
	 * @param array $event repeating event whos repetions should be inserted
1122
	 * @param mixed $start start-date
1123
	 * @param mixed $end end-date
1124
	 * @param array $events where the repetions get inserted
1125
	 * @param array $recur_exceptions with date (in Ymd) as key (and True as values), seems not to be used anymore
1126
	 */
1127
	function insert_all_recurrences($event,$_start,$end,&$events)
1128
	{
1129 View Code Duplication
		if ((int) $this->debug >= 3 || $this->debug == 'set_recurrences' || $this->debug == 'check_move_horizont' || $this->debug == 'insert_all_recurrences')
1130
		{
1131
			$this->debug_message(__METHOD__.'(%1,%2,%3,&$events)',true,$event,$_start,$end);
1132
		}
1133
		$end_in = $end;
1134
1135
		$start = $this->date2ts($_start);
1136
		$event_start_ts = $this->date2ts($event['start']);
1137
		$event_length = $this->date2ts($event['end']) - $event_start_ts;	// we use a constant event-length, NOT a constant end-time!
1138
1139
		// if $end is before recur_enddate, use it instead
1140
		if (!$event['recur_enddate'] || $this->date2ts($event['recur_enddate']) > $this->date2ts($end))
1141
		{
1142
			//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";
1143
			// insert at least the event itself, if it's behind the horizont
1144
			$event['recur_enddate'] = $this->date2ts($end) < $this->date2ts($event['end']) ? $event['end'] : $end;
1145
		}
1146
		$event['recur_enddate'] = is_a($event['recur_enddate'],'DateTime') ?
1147
				$event['recur_enddate'] :
1148
				new Api\DateTime($event['recur_enddate'], calendar_timezones::DateTimeZone($event['tzid']));
1149
		// unset exceptions, as we need to add them as recurrence too, but marked as exception
1150
		unset($event['recur_exception']);
1151
		// loop over all recurrences and insert them, if they are after $start
1152
 		$rrule = calendar_rrule::event2rrule($event, !$event['whole_day'], Api\DateTime::$user_timezone->getName());	// true = we operate in usertime, like the rest of calendar_bo
1153
		foreach($rrule as $time)
1154
		{
1155
			$time->setUser();	// $time is in timezone of event, convert it to usertime used here
1156
			if($event['whole_day'])
1157
			{
1158
				// All day events are processed in server timezone
1159
				$time->setServer();
1160
				$time->setTime(0,0,0);
1161
			}
1162
			if (($ts = $this->date2ts($time)) < $start-$event_length)
1163
			{
1164
				//echo "<p>".$time." --> ignored as $ts < $start-$event_length</p>\n";
1165
				continue;	// to early or original event (returned by interator too)
1166
			}
1167
1168
			$ts_end = $ts + $event_length;
1169
			// adjust ts_end for whole day events in case it does not fit due to
1170
			// spans over summer/wintertime adjusted days
1171
			if($event['whole_day'] && ($arr_end = $this->date2array($ts_end)) &&
1172
				!($arr_end['hour'] == 23 && $arr_end['minute'] == 59 && $arr_end['second'] == 59))
1173
			{
1174
				$arr_end['hour'] = 23;
1175
				$arr_end['minute'] = 59;
1176
				$arr_end['second'] = 59;
1177
				$ts_end_guess = $this->date2ts($arr_end);
1178
				if($ts_end_guess - $ts_end > DAY_s/2)
1179
				{
1180
					$ts_end = $ts_end_guess - DAY_s; // $ts_end_guess was one day too far in the future
1181
				}
1182
				else
1183
				{
1184
					$ts_end = $ts_end_guess; // $ts_end_guess was ok
1185
				}
1186
			}
1187
1188
			$event['start'] = $ts;
1189
			$event['end'] = $ts_end;
1190
			$events[] = $event;
1191
		}
1192
		if ($this->debug && ((int) $this->debug > 2 || $this->debug == 'set_recurrences' || $this->debug == 'check_move_horizont' || $this->debug == 'insert_all_recurrences'))
1193
		{
1194
			$event['start'] = $event_start_ts;
1195
			$event['end'] = $event_start_ts + $event_length;
1196
			$this->debug_message(__METHOD__.'(%1,start=%2,end=%3,events) events=%5',True,$event,$_start,$end_in,$events);
1197
		}
1198
	}
1199
1200
	/**
1201
	 * Adds one repetion of $event for $date_ymd to the $events array, after adjusting its start- and end-time
1202
	 *
1203
	 * @param array $events array in which the event gets inserted
1204
	 * @param array $event event to insert, it has start- and end-date of the first recurrence, not of $date_ymd
1205
	 * @param int|string $date_ymd of the date of the event
1206
	 */
1207
	function add_adjusted_event(&$events,$event,$date_ymd)
1208
	{
1209
		$event_in = $event;
1210
		// calculate the new start- and end-time
1211
		$length_s = $this->date2ts($event['end']) - $this->date2ts($event['start']);
1212
		$event_start_arr = $this->date2array($event['start']);
1213
1214
		$date_arr = $this->date2array((string) $date_ymd);
1215
		$date_arr['hour'] = $event_start_arr['hour'];
1216
		$date_arr['minute'] = $event_start_arr['minute'];
1217
		$date_arr['second'] = $event_start_arr['second'];
1218
		unset($date_arr['raw']);	// else date2ts would use it
1219
		$event['start'] = $this->date2ts($date_arr);
1220
		$event['end'] = $event['start'] + $length_s;
1221
1222
		$events[] = $event;
1223
1224
		if ($this->debug && ($this->debug > 2 || $this->debug == 'add_adjust_event'))
1225
		{
1226
			$this->debug_message('calendar_bo::add_adjust_event(,%1,%2) as %3',True,$event_in,$date_ymd,$event);
1227
		}
1228
	}
1229
1230
	/**
1231
	 * Fetch information about a resource
1232
	 *
1233
	 * We do some caching here, as the resource itself might not do it.
1234
	 *
1235
	 * @param string $uid string with one-letter resource-type and numerical resource-id, eg. "r19"
1236
	 * @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
1237
	 */
1238
	function resource_info($uid)
1239
	{
1240
		static $res_info_cache = array();
1241
1242
		if (!is_scalar($uid)) throw new Api\Exception\WrongParameter(__METHOD__.'('.array2string($uid).') parameter must be scalar');
1243
1244
		if (!isset($res_info_cache[$uid]))
1245
		{
1246
			if (is_numeric($uid))
1247
			{
1248
				$info = array(
1249
					'res_id'    => $uid,
1250
					'email' => $GLOBALS['egw']->accounts->id2name($uid,'account_email'),
1251
					'name'  => trim($GLOBALS['egw']->accounts->id2name($uid,'account_firstname'). ' ' .
1252
					$GLOBALS['egw']->accounts->id2name($uid,'account_lastname')),
1253
					'type'  => $GLOBALS['egw']->accounts->get_type($uid),
1254
					'app'   => 'accounts',
1255
				);
1256
			}
1257
			else
1258
			{
1259
				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...
1260
				if ($info)
1261
				{
1262
					$info['type'] = $uid[0];
1263 View Code Duplication
					if (!$info['email'] && $info['responsible'])
1264
					{
1265
						$info['email'] = $GLOBALS['egw']->accounts->id2name($info['responsible'],'account_email');
1266
					}
1267
					$info['app'] = $this->resources[$uid[0]]['app'];
1268
				}
1269
			}
1270
			$res_info_cache[$uid] = $info;
1271
		}
1272
		if ($this->debug && ($this->debug > 2 || $this->debug == 'resource_info'))
1273
		{
1274
			$this->debug_message('calendar_bo::resource_info(%1) = %2',True,$uid,$res_info_cache[$uid]);
1275
		}
1276
		return $res_info_cache[$uid];
1277
	}
1278
1279
	/**
1280
	 * Checks if the current user has the necessary ACL rights
1281
	 *
1282
	 * The check is performed on an event or generally on the cal of an other user
1283
	 *
1284
	 * Note: Participating in an event is considered as haveing read-access on that event,
1285
	 *	even if you have no general read-grant from that user.
1286
	 *
1287
	 * @param int $needed necessary ACL right: Acl::{READ|EDIT|DELETE}
1288
	 * @param mixed $event event as array or the event-id or 0 for a general check
1289
	 * @param int $other uid to check (if event==0) or 0 to check against $this->user
1290
	 * @param string $date_format ='ts' date-format used for reading: 'ts'=timestamp, 'array'=array, 'string'=iso8601 string for xmlrpc
1291
	 * @param mixed $date_to_read =null date used for reading, internal param for the caching
1292
	 * @param int $user =null for which user to check, default current user
1293
	 * @return boolean true permission granted, false for permission denied or null if event not found
1294
	 */
1295
	function check_perms($needed,$event=0,$other=0,$date_format='ts',$date_to_read=null,$user=null)
1296
	{
1297
		if (!$user) $user = $this->user;
1298 View Code Duplication
		if ($user == $this->user)
1299
		{
1300
			$grants = $this->grants;
1301
		}
1302
		else
1303
		{
1304
			$grants = $GLOBALS['egw']->acl->get_grants('calendar',true,$user);
1305
		}
1306
1307
		if ($other && !is_numeric($other))
1308
		{
1309
			$resource = $this->resource_info($other);
1310
			return $needed & $resource['rights'];
1311
		}
1312
		if (is_int($event) && $event == 0)
1313
		{
1314
			$owner = $other ? $other : $user;
1315
		}
1316
		else
1317
		{
1318
			if (!is_array($event))
1319
			{
1320
				$event = $this->read($event,$date_to_read,true,$date_format);	// = no ACL check !!!
1321
			}
1322
			if (!is_array($event))
1323
			{
1324
				if ($this->xmlrpc)
1325
				{
1326
					$GLOBALS['server']->xmlrpc_error($GLOBALS['xmlrpcerr']['not_exist'],$GLOBALS['xmlrpcstr']['not_exist']);
1327
				}
1328
				return null;	// event not found
1329
			}
1330
			$owner = $event['owner'];
1331
			$private = !$event['public'];
1332
		}
1333
		$grant = $grants[$owner];
1334
1335
		// now any ACL rights (but invite rights!) implicate FREEBUSY rights (at least READ has to include FREEBUSY)
1336
		if ($grant & ~self::ACL_INVITE) $grant |= self::ACL_FREEBUSY;
1337
1338
		if (is_array($event) && ($needed == Acl::READ || $needed == self::ACL_FREEBUSY))
1339
		{
1340
			// Check if the $user is one of the participants or has a read-grant from one of them
1341
			// in that case he has an implicite READ grant for that event
1342
			//
1343
			if ($event['participants'] && is_array($event['participants']))
1344
			{
1345
				foreach(array_keys($event['participants']) as $uid)
1346
				{
1347
					if ($uid == $user || $uid < 0 && in_array($user, (array)$GLOBALS['egw']->accounts->members($uid,true)))
1348
					{
1349
						// if we are a participant, we have an implicite FREEBUSY, READ and PRIVAT grant
1350
						$grant |= self::ACL_FREEBUSY | Acl::READ | Acl::PRIVAT;
1351
						break;
1352
					}
1353
					elseif ($grants[$uid] & Acl::READ)
1354
					{
1355
						// if we have a READ grant from a participant, we dont give an implicit privat grant too
1356
						$grant |= Acl::READ;
1357
						// we cant break here, as we might be a participant too, and would miss the privat grant
1358
					}
1359
					elseif (!is_numeric($uid))
1360
					{
1361
						// if the owner only grants self::ACL_FREEBUSY we are not interested in the recources explicit rights
1362
						if ($grant == self::ACL_FREEBUSY) continue;
1363
						// if we have a resource as participant
1364
						$resource = $this->resource_info($uid);
1365
						$grant |= $resource['rights'];
1366
					}
1367
				}
1368
			}
1369
		}
1370
		if ($GLOBALS['egw']->accounts->get_type($owner) == 'g' && $needed == Acl::ADD)
1371
		{
1372
			$access = False;	// a group can't be the owner of an event
1373
		}
1374
		else
1375
		{
1376
			$access = $user == $owner || $grant & $needed
1377
				&& ($needed == self::ACL_FREEBUSY || !$private || $grant & Acl::PRIVAT);
1378
		}
1379
		// do NOT allow users to purge deleted events, if we dont have 'userpurge' enabled
1380
		if ($access && $needed == Acl::DELETE && $event['deleted'] &&
1381
			!$GLOBALS['egw_info']['user']['apps']['admin'] &&
1382
			$GLOBALS['egw_info']['server']['calendar_delete_history'] != 'userpurge')
1383
		{
1384
			$access = false;
1385
		}
1386
		if ($this->debug && ($this->debug > 2 || $this->debug == 'check_perms'))
1387
		{
1388
			$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);
1389
		}
1390
		//error_log(__METHOD__."($needed,".array2string($event).",$other,...,$user) returning ".array2string($access));
1391
		return $access;
1392
	}
1393
1394
	/**
1395
	 * Converts several date-types to a timestamp and optionally converts user- to server-time
1396
	 *
1397
	 * @param mixed $date date to convert, should be one of the following types
1398
	 *	string (!) in form YYYYMMDD or iso8601 YYYY-MM-DDThh:mm:ss or YYYYMMDDThhmmss
1399
	 *	int already a timestamp
1400
	 *	array with keys 'second', 'minute', 'hour', 'day' or 'mday' (depricated !), 'month' and 'year'
1401
	 * @param boolean $user2server =False conversion between user- and server-time; default False == Off
1402
	 */
1403
	static function date2ts($date,$user2server=False)
1404
	{
1405
		return $user2server ? Api\DateTime::user2server($date,'ts') : Api\DateTime::to($date,'ts');
1406
	}
1407
1408
	/**
1409
	 * Converts a date to an array and optionally converts server- to user-time
1410
	 *
1411
	 * @param mixed $date date to convert
1412
	 * @param boolean $server2user conversation between user- and server-time default False == Off
1413
	 * @return array with keys 'second', 'minute', 'hour', 'day', 'month', 'year', 'raw' (timestamp) and 'full' (Ymd-string)
1414
	 */
1415
	static function date2array($date,$server2user=False)
1416
	{
1417
		return $server2user ? Api\DateTime::server2user($date,'array') : Api\DateTime::to($date,'array');
1418
	}
1419
1420
	/**
1421
	 * Converts a date as timestamp or array to a date-string and optionaly converts server- to user-time
1422
	 *
1423
	 * @param mixed $date integer timestamp or array with ('year','month',..,'second') to convert
1424
	 * @param boolean $server2user conversation between user- and server-time default False == Off, not used if $format ends with \Z
1425
	 * @param string $format ='Ymd' format of the date to return, eg. 'Y-m-d\TH:i:sO' (2005-11-01T15:30:00+0100)
1426
	 * @return string date formatted according to $format
1427
	 */
1428
	static function date2string($date,$server2user=False,$format='Ymd')
1429
	{
1430
		return $server2user ? Api\DateTime::server2user($date,$format) : Api\DateTime::to($date,$format);
1431
	}
1432
1433
	/**
1434
	 * Formats a date given as timestamp or array
1435
	 *
1436
	 * @param mixed $date integer timestamp or array with ('year','month',..,'second') to convert
1437
	 * @param string|boolean $format ='' default common_prefs[dateformat], common_prefs[timeformat], false=time only, true=date only
1438
	 * @return string the formated date (incl. time)
1439
	 */
1440
	static function format_date($date,$format='')
1441
	{
1442
		return Api\DateTime::to($date,$format);
0 ignored issues
show
Bug introduced by
It seems like $format defined by parameter $format on line 1440 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...
1443
	}
1444
1445
	/**
1446
	 * Gives out a debug-message with certain parameters
1447
	 *
1448
	 * All permanent debug-messages in the calendar should be done by this function !!!
1449
	 *	(In future they may be logged or sent as xmlrpc-faults back.)
1450
	 *
1451
	 * Permanent debug-message need to make sure NOT to give secret information like passwords !!!
1452
	 *
1453
	 * This function do NOT honor the setting of the debug variable, you may use it like
1454
	 * if ($this->debug > N) $this->debug_message('Error ;-)');
1455
	 *
1456
	 * The parameters get formated depending on their type. ACL-values need a ACL_TYPE_IDENTIFER prefix.
1457
	 *
1458
	 * @param string $msg message with parameters/variables like lang(), eg. '%1'
1459
	 * @param boolean $backtrace =True include a function-backtrace, default True=On
1460
	 *	should only be set to False=Off, if your code ensures a call with backtrace=On was made before !!!
1461
	 * @param mixed $param a variable number of parameters, to be inserted in $msg
1462
	 *	arrays get serialized with print_r() !
1463
	 */
1464
	static function debug_message($msg,$backtrace=True)
1465
	{
1466
		static $acl2string = array(
1467
			0               => 'ACL-UNKNOWN',
1468
			Acl::READ    => 'ACL_READ',
1469
			Acl::ADD     => 'ACL_ADD',
1470
			Acl::EDIT    => 'ACL_EDIT',
1471
			Acl::DELETE  => 'ACL_DELETE',
1472
			Acl::PRIVAT => 'ACL_PRIVATE',
1473
			self::ACL_FREEBUSY => 'ACL_FREEBUSY',
1474
		);
1475
		for($i = 2; $i < func_num_args(); ++$i)
1476
		{
1477
			$param = func_get_arg($i);
1478
1479
			if (is_null($param))
1480
			{
1481
				$param='NULL';
1482
			}
1483
			else
1484
			{
1485
				switch(gettype($param))
1486
				{
1487
					case 'string':
1488
						if (substr($param,0,strlen(ACL_TYPE_IDENTIFER))== ACL_TYPE_IDENTIFER)
1489
						{
1490
							$param = (int) substr($param,strlen(ACL_TYPE_IDENTIFER));
1491
							$param = (isset($acl2string[$param]) ? $acl2string[$param] : $acl2string[0])." ($param)";
1492
						}
1493
						else
1494
						{
1495
							$param = "'$param'";
1496
						}
1497
						break;
1498
					case 'EGroupware\\Api\\DateTime':
1499
					case 'egw_time':
1500
					case 'datetime':
1501
						$p = $param;
1502
						unset($param);
1503
						$param = $p->format('l, Y-m-d H:i:s').' ('.$p->getTimeZone()->getName().')';
1504
						break;
1505
					case 'array':
1506
					case 'object':
1507
						$param = array2string($param);
1508
						break;
1509
					case 'boolean':
1510
						$param = $param ? 'True' : 'False';
1511
						break;
1512
					case 'integer':
1513
						if ($param >= mktime(0,0,0,1,1,2000)) $param = adodb_date('Y-m-d H:i:s',$param)." ($param)";
1514
						break;
1515
				}
1516
			}
1517
			$msg = str_replace('%'.($i-1),$param,$msg);
1518
		}
1519
		error_log($msg);
1520
		if ($backtrace) error_log(function_backtrace(1));
1521
	}
1522
1523
	/**
1524
	 * Formats one or two dates (range) as long date (full monthname), optionaly with a time
1525
	 *
1526
	 * @param mixed $_first first date
1527
	 * @param mixed $last =0 last date if != 0 (default)
1528
	 * @param boolean $display_time =false should a time be displayed too
1529
	 * @param boolean $display_day =false should a day-name prefix the date, eg. monday June 20, 2006
1530
	 * @return string with formated date
1531
	 */
1532
	function long_date($_first,$last=0,$display_time=false,$display_day=false)
1533
	{
1534
		$first = $this->date2array($_first);
1535
		if ($last)
1536
		{
1537
			$last = $this->date2array($last);
1538
		}
1539
		$datefmt = $this->common_prefs['dateformat'];
1540
		$timefmt = $this->common_prefs['timeformat'] == 12 ? 'h:i a' : 'H:i';
1541
1542
		$month_before_day = strtolower($datefmt[0]) == 'm' ||
1543
			strtolower($datefmt[2]) == 'm' && $datefmt[4] == 'd';
1544
1545
		if ($display_day)
1546
		{
1547
			$range = lang(adodb_date('l',$first['raw'])).($this->common_prefs['dateformat'][0] != 'd' ? ' ' : ', ');
1548
		}
1549
		for ($i = 0; $i < 5; $i += 2)
1550
		{
1551
			switch($datefmt[$i])
1552
			{
1553
				case 'd':
1554
					$range .= $first['day'] . ($datefmt[1] == '.' ? '.' : '');
1555
					if ($first['month'] != $last['month'] || $first['year'] != $last['year'])
1556
					{
1557
						if (!$month_before_day)
1558
						{
1559
							$range .= ' '.lang(strftime('%B',$first['raw']));
1560
						}
1561
						if ($first['year'] != $last['year'] && $datefmt[0] != 'Y')
1562
						{
1563
							$range .= ($datefmt[0] != 'd' ? ', ' : ' ') . $first['year'];
1564
						}
1565
						if ($display_time)
1566
						{
1567
							$range .= ' '.adodb_date($timefmt,$first['raw']);
1568
						}
1569
						if (!$last)
1570
						{
1571
							return $range;
1572
						}
1573
						$range .= ' - ';
1574
1575
						if ($first['year'] != $last['year'] && $datefmt[0] == 'Y')
1576
						{
1577
							$range .= $last['year'] . ', ';
1578
						}
1579
1580
						if ($month_before_day)
1581
						{
1582
							$range .= lang(strftime('%B',$last['raw']));
1583
						}
1584
					}
1585
					else
1586
					{
1587
						if ($display_time)
1588
						{
1589
							$range .= ' '.adodb_date($timefmt,$first['raw']);
1590
						}
1591
						$range .= ' - ';
1592
					}
1593
					$range .= ' ' . $last['day'] . ($datefmt[1] == '.' ? '.' : '');
1594
					break;
1595
				case 'm':
1596
				case 'M':
1597
					$range .= ' '.lang(strftime('%B',$month_before_day ? $first['raw'] : $last['raw'])) . ' ';
1598
					break;
1599
				case 'Y':
1600
					if ($datefmt[0] != 'm')
1601
					{
1602
						$range .= ' ' . ($datefmt[0] == 'Y' ? $first['year'].($datefmt[2] == 'd' ? ', ' : ' ') : $last['year'].' ');
1603
					}
1604
					break;
1605
			}
1606
		}
1607
		if ($display_time && $last)
1608
		{
1609
			$range .= ' '.adodb_date($timefmt,$last['raw']);
1610
		}
1611
		if ($datefmt[4] == 'Y' && $datefmt[0] == 'm')
1612
		{
1613
			$range .= ', ' . $last['year'];
1614
		}
1615
		return $range;
1616
	}
1617
1618
	/**
1619
	 * Displays a timespan, eg. $both ? "10:00 - 13:00: 3h" (10:00 am - 1 pm: 3h) : "10:00 3h" (10:00 am 3h)
1620
	 *
1621
	 * @param int $start_m start time in minutes since 0h
1622
	 * @param int $end_m end time in minutes since 0h
1623
	 * @param boolean $both =false display the end-time too, duration is always displayed
1624
	 */
1625
	function timespan($start_m,$end_m,$both=false)
1626
	{
1627
		$duration = $end_m - $start_m;
1628
		if ($end_m == 24*60-1) ++$duration;
1629
		$duration = floor($duration/60).lang('h').($duration%60 ? $duration%60 : '');
1630
1631
		$timespan = $t = Api\DateTime::to('20000101T'.sprintf('%02d',$start_m/60).sprintf('%02d',$start_m%60).'00', false);
1632
1633
		if ($both)	// end-time too
1634
		{
1635
			$timespan .= ' - '.Api\DateTime::to('20000101T'.sprintf('%02d',$end_m/60).sprintf('%02d',$end_m%60).'00', false);
1636
			// dont double am/pm if they are the same in both times
1637
			if ($this->common_prefs['timeformat'] == 12 && substr($timespan,-2) == substr($t,-2))
1638
			{
1639
				$timespan = str_replace($t,substr($t,0,-3),$timespan);
1640
			}
1641
			$timespan .= ':';
1642
		}
1643
		return $timespan . ' ' . $duration;
1644
	}
1645
1646
	/**
1647
	* Converts a participant into a (readable) user- or resource-name
1648
	*
1649
	* @param string|int $id id of user or resource
1650
	* @param string|boolean $use_type =false type-letter or false
1651
	* @param boolean $append_email =false append email (Name <email>)
1652
	* @return string with name
1653
	*/
1654
	function participant_name($id,$use_type=false, $append_email=false)
1655
	{
1656
		static $id2lid = array();
1657
		static $id2email = array();
1658
1659
		if ($use_type && $use_type != 'u') $id = $use_type.$id;
1660
1661
		if (!isset($id2lid[$id]))
1662
		{
1663
			if (!is_numeric($id))
1664
			{
1665
				$id2lid[$id] = '#'.$id;
1666
				if (($info = $this->resource_info($id)))
1667
				{
1668
					$id2lid[$id] = $info['name'] ? $info['name'] : $info['email'];
1669
					if ($info['name']) $id2email[$id] = $info['email'];
1670
				}
1671
			}
1672
			else
1673
			{
1674
				$id2lid[$id] = Api\Accounts::username($id);
1675
				$id2email[$id] = $GLOBALS['egw']->accounts->id2name($id,'account_email');
1676
			}
1677
		}
1678
		return $id2lid[$id].(($append_email || $id[0] == 'e') && $id2email[$id] ? ' <'.$id2email[$id].'>' : '');
1679
	}
1680
1681
	/**
1682
	* Converts participants array of an event into array of (readable) participant-names with status
1683
	*
1684
	* @param array $event event-data
1685
	* @param boolean $long_status =false should the long/verbose status or an icon be use
1686
	* @param boolean $show_group_invitation =false show group-invitations (status == 'G') or not (default)
1687
	* @return array with id / names with status pairs
1688
	*/
1689
	function participants($event,$long_status=false,$show_group_invitation=false)
1690
	{
1691
		//error_log(__METHOD__.__LINE__.array2string($event['participants']));
1692
		$names = array();
1693
		foreach((array)$event['participants'] as $id => $status)
1694
		{
1695
			if (!is_string($status)) continue;
1696
			$quantity = $role = null;
1697
			calendar_so::split_status($status,$quantity,$role);
1698
1699
			if ($status == 'G' && !$show_group_invitation) continue;	// dont show group-invitation
1700
1701
			$lang_status = lang($this->verbose_status[$status]);
1702
			if (!$long_status)
1703
			{
1704
				switch($status[0])
1705
				{
1706
					case 'A':	// accepted
1707
						$status = Api\Html::image('calendar','accepted',$lang_status);
1708
						break;
1709
					case 'R':	// rejected
1710
						$status = Api\Html::image('calendar','rejected',$lang_status);
1711
						break;
1712
					case 'T':	// tentative
1713
						$status = Api\Html::image('calendar','tentative',$lang_status);
1714
						break;
1715
					case 'U':	// no response = unknown
1716
						$status = Api\Html::image('calendar','needs-action',$lang_status);
1717
						break;
1718
					case 'D':	// delegated
1719
						$status = Api\Html::image('calendar','forward',$lang_status);
1720
						break;
1721
					case 'G':	// group invitation
1722
						// Todo: Image, seems not to be used
1723
						$status = '('.$lang_status.')';
1724
						break;
1725
				}
1726
			}
1727
			else
1728
			{
1729
				$status = '('.$lang_status.')';
1730
			}
1731
			$names[$id] = Api\Html::htmlspecialchars($this->participant_name($id)).($quantity > 1 ? ' ('.$quantity.')' : '').' '.$status;
1732
1733
			// add role, if not a regular participant
1734
			if ($role != 'REQ-PARTICIPANT')
1735
			{
1736
				if (isset($this->roles[$role]))
1737
				{
1738
					$role = lang($this->roles[$role]);
1739
				}
1740
				// allow to use cats as roles (beside regular iCal ones)
1741
				elseif (substr($role,0,6) == 'X-CAT-' && ($cat_id = (int)substr($role,6)) > 0)
1742
				{
1743
					$role = $GLOBALS['egw']->categories->id2name($cat_id);
1744
				}
1745
				else
1746
				{
1747
					$role = lang(str_replace('X-','',$role));
1748
				}
1749
				$names[$id] .= ' '.$role;
1750
			}
1751
		}
1752
		natcasesort($names);
1753
1754
		return $names;
1755
	}
1756
1757
	/**
1758
	* Converts category string of an event into array of (readable) category-names
1759
	*
1760
	* @param string $category cat-id (multiple id's commaseparated)
1761
	* @param int $color color of the category, if multiple cats, the color of the last one with color is returned
1762
	* @return array with id / names
1763
	*/
1764
	function categories($category,&$color)
1765
	{
1766
		static $id2cat = array();
1767
		$cats = array();
1768
		$color = 0;
1769
1770
		foreach(explode(',',$category) as $cat_id)
1771
		{
1772
			if (!$cat_id) continue;
1773
1774
			if (!isset($id2cat[$cat_id]))
1775
			{
1776
				$id2cat[$cat_id] = Api\Categories::read($cat_id);
1777
			}
1778
			$cat = $id2cat[$cat_id];
1779
1780
			$parts = null;
1781
			if (is_array($cat['data']) && !empty($cat['data']['color']))
1782
			{
1783
				$color = $cat['data']['color'];
1784
			}
1785
			elseif(preg_match('/(#[0-9A-Fa-f]{6})/', $cat['description'], $parts))
1786
			{
1787
				$color = $parts[1];
1788
			}
1789
			$cats[$cat_id] = stripslashes($cat['name']);
1790
		}
1791
		return $cats;
1792
	}
1793
1794
	/**
1795
	 *  This is called only by list_cals().  It was moved here to remove fatal error in php5 beta4
1796
	 */
1797
	private static function _list_cals_add($id,&$users,&$groups)
1798
	{
1799
		$name = Api\Accounts::username($id);
1800
		if (!($egw_name = $GLOBALS['egw']->accounts->id2name($id)))
1801
		{
1802
			return;	// do not return no longer existing accounts which eg. still mentioned in acl
1803
		}
1804
		if (($type = $GLOBALS['egw']->accounts->get_type($id)) == 'g')
1805
		{
1806
			$arr = &$groups;
1807
		}
1808
		else
1809
		{
1810
			$arr = &$users;
1811
		}
1812
		$arr[$id] = array(
1813
			'grantor' => $id,
1814
			'value'   => ($type == 'g' ? 'g_' : '') . $id,
1815
			'name'    => $name,
1816
			'sname'	  => $egw_name
1817
		);
1818
	}
1819
1820
	/**
1821
	 * generate list of user- / group-calendars for the selectbox in the header
1822
	 *
1823
	 * @return array alphabeticaly sorted array with users first and then groups: array('grantor'=>$id,'value'=>['g_'.]$id,'name'=>$name)
1824
	 */
1825
	function list_cals()
1826
	{
1827
		return self::list_calendars($GLOBALS['egw_info']['user']['account_id'], $this->grants);
1828
	}
1829
1830
	/**
1831
	 * generate list of user- / group-calendars or a given user
1832
	 *
1833
	 * @param int $user account_id of user to generate list for
1834
	 * @param array $grants =null calendar grants from user, or null to query them from acl class
1835
	 */
1836
	public static function list_calendars($user, array $grants=null)
1837
	{
1838
		if (is_null($grants)) $grants = $GLOBALS['egw']->acl->get_grants('calendar', true, $user);
1839
1840
		$users = $groups = array();
1841
		foreach(array_keys($grants) as $id)
1842
		{
1843
			self::_list_cals_add($id,$users,$groups);
1844
		}
1845
		if (($memberships = $GLOBALS['egw']->accounts->memberships($user, true)))
1846
		{
1847
			foreach($memberships as $group)
1848
			{
1849
				self::_list_cals_add($group,$users,$groups);
1850
1851
				if (($account_perms = $GLOBALS['egw']->acl->get_ids_for_location($group,Acl::READ,'calendar')))
1852
				{
1853
					foreach($account_perms as $id)
1854
					{
1855
						self::_list_cals_add($id,$users,$groups);
1856
					}
1857
				}
1858
			}
1859
		}
1860
		usort($users, array(__CLASS__, 'name_cmp'));
1861
		usort($groups, array(__CLASS__, 'name_cmp'));
1862
1863
		return array_merge($users, $groups);	// users first and then groups, both alphabeticaly
1864
	}
1865
1866
	/**
1867
	 * Compare function for sort by value of key 'name'
1868
	 *
1869
	 * @param array $a
1870
	 * @param array $b
1871
	 * @return int
1872
	 */
1873
	public static function name_cmp(array $a, array $b)
1874
	{
1875
		return strnatcasecmp($a['name'], $b['name']);
1876
	}
1877
1878
	/**
1879
	 * Convert the recurrence-information of an event, into a human readable string
1880
	 *
1881
	 * @param array $event
1882
	 * @return string
1883
	 */
1884
	function recure2string($event)
1885
	{
1886
		if (!is_array($event)) return false;
1887
		return (string)calendar_rrule::event2rrule($event);
1888
	}
1889
1890
	/**
1891
	 * Read the holidays for a given $year
1892
	 *
1893
	 * The holidays get cached in the session (performance), so changes in holidays or birthdays do NOT affect a current session!!!
1894
	 *
1895
	 * @param int $year =0 year, defaults to 0 = current year
1896
	 * @return array indexed with Ymd of array of holidays. A holiday is an array with the following fields:
1897
	 *	name: string
1898
	 *  title: optional string with description
1899
	 *	day: numerical day in month
1900
	 *	month: numerical month
1901
	 *	occurence: numerical year or 0 for every year
1902
	 */
1903
	function read_holidays($year=0)
1904
	{
1905
		if (!$year) $year = (int) date('Y',$this->now_su);
1906
1907
		$holidays = calendar_holidays::read(
1908
				!empty($GLOBALS['egw_info']['server']['ical_holiday_url']) ?
1909
				$GLOBALS['egw_info']['server']['ical_holiday_url'] :
1910
				$GLOBALS['egw_info']['user']['preferences']['common']['country'], $year);
1911
1912
		// search for birthdays
1913
		if ($GLOBALS['egw_info']['server']['hide_birthdays'] != 'yes')
1914
		{
1915
			$contacts = new Api\Contacts();
1916
			foreach($contacts->get_addressbooks() as $owner => $name)
1917
			{
1918
				$holidays += $contacts->read_birthdays($owner, $year);
1919
			}
1920
		}
1921
1922
		if ((int) $this->debug >= 2 || $this->debug == 'read_holidays')
1923
		{
1924
			$this->debug_message('calendar_bo::read_holidays(%1)=%2',true,$year,$holidays);
1925
		}
1926
		return $holidays;
1927
	}
1928
1929
	/**
1930
	 * Get translated calendar event fields, presenting as link title options
1931
	 *
1932
	 * @param type $event
1933
	 * @return array array of selected calendar fields
1934
	 */
1935
	public static function get_link_options ($event = array())
1936
	{
1937
		unset($event);	// not used, but required by function signature
1938
		$options = array (
1939
			'end' => lang('End date'),
1940
			'id' => lang('ID'),
1941
			'owner' => lang('Event owner'),
1942
			'category' => lang('Category'),
1943
			'location' => lang('Location'),
1944
			'creator' => lang('Creator'),
1945
			'participants' => lang('Participants')
1946
		);
1947
		return $options;
1948
	}
1949
1950
	/**
1951
	 * get title for an event identified by $event
1952
	 *
1953
	 * Is called as hook to participate in the linking
1954
	 *
1955
	 * @param int|array $entry int cal_id or array with event
1956
	 * @param string|boolean string with title, null if not found or false if not read perms
1957
	 */
1958
	function link_title($event)
1959
	{
1960
		if (!is_array($event) && strpos($event, '-') !== false)
1961
		{
1962
			list($id, $recur) = explode('-', $event, 2);
1963
			$event = $this->read($id, $recur);
1964
		}
1965
		else if (!is_array($event) && (int) $event > 0)
1966
		{
1967
			$event = $this->read($event);
1968
		}
1969
		if (!is_array($event))
1970
		{
1971
			return $event;
1972
		}
1973
		$type = explode(',',$this->cal_prefs['link_title']);
1974
		if (is_array($type))
1975
		{
1976
			foreach ($type as &$val)
1977
			{
1978
				switch ($val)
1979
				{
1980
					case 'end':
1981
					case 'modified':
1982
						$extra_fields [$val] = $this->format_date($event[$val]);
1983
						break;
1984
					case 'participants':
1985
						foreach (array_keys($event[$val]) as $key)
1986
						{
1987
							$extra_fields [$val] = Api\Accounts::id2name($key, 'account_fullname');
1988
						}
1989
						break;
1990
					case 'modifier':
1991
					case 'creator':
1992
					case 'owner':
1993
						$extra_fields [$val] = Api\Accounts::id2name($event[$val], 'account_fullname');
1994
						break;
1995
					default:
1996
						$extra_fields [] = $event[$val];
1997
				}
1998
			}
1999
			$str_fields = implode(', ',$extra_fields);
2000
			if (is_array($extra_fields)) return $this->format_date($event['start']) . ': ' . $event['title'] . ($str_fields? ', ' . $str_fields:'');
2001
		}
2002
		return $this->format_date($event['start']) . ': ' . $event['title'];
2003
	}
2004
2005
	/**
2006
	 * query calendar for events matching $pattern
2007
	 *
2008
	 * Is called as hook to participate in the linking
2009
	 *
2010
	 * @param string $pattern pattern to search
2011
	 * @return array with cal_id - title pairs of the matching entries
2012
	 */
2013
	function link_query($pattern, Array &$options = array())
2014
	{
2015
		$result = array();
2016
		$query = array(
2017
			'query'	=>	$pattern,
2018
			'offset' =>	$options['start'],
2019
			'order' => 'cal_start DESC',
2020
		);
2021
		if($options['num_rows']) {
2022
			$query['num_rows'] = $options['num_rows'];
2023
		}
2024
		foreach((array) $this->search($query) as $event)
2025
		{
2026
			$result[$event['id']] = $this->link_title($event);
2027
		}
2028
		$options['total'] = $this->total;
2029
		return $result;
2030
	}
2031
2032
	/**
2033
	 * Check access to the file store
2034
	 *
2035
	 * @param int $id id of entry
2036
	 * @param int $check Acl::READ for read and Acl::EDIT for write or delete access
2037
	 * @param string $rel_path =null currently not used in calendar
2038
	 * @param int $user =null for which user to check, default current user
2039
	 * @return boolean true if access is granted or false otherwise
2040
	 */
2041
	function file_access($id,$check,$rel_path,$user=null)
2042
	{
2043
		unset($rel_path);	// not used, but required by function signature
2044
2045
		return $this->check_perms($check,$id,0,'ts',null,$user);
2046
	}
2047
2048
	/**
2049
	 * sets the default prefs, if they are not already set (on a per pref. basis)
2050
	 *
2051
	 * It sets a flag in the app-session-data to be called only once per session
2052
	 */
2053
	function check_set_default_prefs()
2054
	{
2055
		if ($this->cal_prefs['interval'] && ($set = Api\Cache::getSession('calendar', 'default_prefs_set')))
2056
		{
2057
			return;
2058
		}
2059
		Api\Cache::setSession('calendar', 'default_prefs_set', 'set');
2060
2061
		$default_prefs =& $GLOBALS['egw']->preferences->default['calendar'];
2062
		$forced_prefs  =& $GLOBALS['egw']->preferences->forced['calendar'];
2063
2064
		$subject = lang('Calendar Event') . ' - $$action$$: $$startdate$$ $$title$$'."\n";
2065
		$values = array(
2066
			'notifyAdded'     => $subject . lang ('You have a meeting scheduled for %1','$$startdate$$'),
2067
			'notifyCanceled'  => $subject . lang ('Your meeting scheduled for %1 has been canceled','$$startdate$$'),
2068
			'notifyModified'  => $subject . lang ('Your meeting that had been scheduled for %1 has been rescheduled to %2','$$olddate$$','$$startdate$$'),
2069
			'notifyDisinvited'=> $subject . lang ('You have been disinvited from the meeting at %1','$$startdate$$'),
2070
			'notifyResponse'  => $subject . lang ('On %1 %2 %3 your meeting request for %4','$$date$$','$$fullname$$','$$action$$','$$startdate$$'),
2071
			'notifyAlarm'     => lang('Alarm for %1 at %2 in %3','$$title$$','$$startdate$$','$$location$$')."\n".lang ('Here is your requested alarm.'),
2072
			'interval'        => 30,
2073
		);
2074
		foreach($values as $var => $default)
2075
		{
2076
			$type = substr($var,0,6) == 'notify' ? 'forced' : 'default';
2077
2078
			// only set, if neither default nor forced pref exists
2079
			if ((!isset($default_prefs[$var]) || (string)$default_prefs[$var] === '') && (!isset($forced_prefs[$var]) || (string)$forced_prefs[$var] === ''))
2080
			{
2081
				$GLOBALS['egw']->preferences->add('calendar',$var,$default,'default');	// always store default, even if we have a forced too
2082
				if ($type == 'forced') $GLOBALS['egw']->preferences->add('calendar',$var,$default,'forced');
2083
				$this->cal_prefs[$var] = $default;
2084
				$need_save = True;
2085
			}
2086
		}
2087
		if ($need_save)
2088
		{
2089
			$GLOBALS['egw']->preferences->save_repository(False,'default');
2090
			$GLOBALS['egw']->preferences->save_repository(False,'forced');
2091
		}
2092
	}
2093
2094
	/**
2095
	 * Get the freebusy URL of a user
2096
	 *
2097
	 * @param int|string $user account_id or account_lid
2098
	 * @param string $pw =null password
2099
	 */
2100
	static function freebusy_url($user='',$pw=null)
2101
	{
2102
		if (is_numeric($user)) $user = $GLOBALS['egw']->accounts->id2name($user);
2103
2104
		$credentials = '';
2105
2106
		if ($pw)
2107
		{
2108
			$credentials = '&password='.urlencode($pw);
2109
		}
2110
		elseif ($GLOBALS['egw_info']['user']['preferences']['calendar']['freebusy'] == 2)
2111
		{
2112
			$credentials = $GLOBALS['egw_info']['user']['account_lid']
2113
				. ':' . $GLOBALS['egw_info']['user']['passwd'];
2114
			$credentials = '&cred=' . base64_encode($credentials);
2115
		}
2116
		return (!$GLOBALS['egw_info']['server']['webserver_url'] || $GLOBALS['egw_info']['server']['webserver_url'][0] == '/' ?
2117
			($_SERVER['HTTPS'] ? 'https://' : 'http://').$_SERVER['HTTP_HOST'] : '').
2118
			$GLOBALS['egw_info']['server']['webserver_url'].'/calendar/freebusy.php/?user='.urlencode($user).$credentials;
2119
	}
2120
2121
	/**
2122
	 * Check if the event is the whole day
2123
	 *
2124
	 * @param array $event event
2125
	 * @return boolean true if whole day event, false othwerwise
2126
	 */
2127
	public static function isWholeDay($event)
2128
	{
2129
		// check if the event is the whole day
2130
		$start = self::date2array($event['start']);
2131
		$end = self::date2array($event['end']);
2132
2133
		return !$start['hour'] && !$start['minute'] && $end['hour'] == 23 && $end['minute'] == 59;
2134
	}
2135
2136
	/**
2137
	 * Get the etag for an entry
2138
	 *
2139
	 * As all update routines (incl. set_status and add/delete alarms) update (series master) modified timestamp,
2140
	 * we do NOT need any special handling for series master anymore
2141
	 *
2142
	 * @param array|int|string $entry array with event or cal_id, or cal_id:recur_date for virtual exceptions
2143
	 * @param string &$schedule_tag=null on return schedule-tag (egw_cal.cal_id:egw_cal.cal_etag, no participant modifications!)
2144
	 * @return string|boolean string with etag or false
2145
	 */
2146
	function get_etag($entry, &$schedule_tag=null)
2147
	{
2148
		if (!is_array($entry))
2149
		{
2150
			list($id,$recur_date) = explode(':',$entry);
2151
			$entry = $this->read($id, $recur_date, true, 'server');
2152
		}
2153
		$etag = $schedule_tag = $entry['id'].':'.$entry['etag'];
2154
		$etag .= ':'.$entry['modified'];
2155
2156
		//error_log(__METHOD__ . "($entry[id],$client_share_uid_excpetions) entry=".array2string($entry)." --> etag=$etag");
2157
		return $etag;
2158
	}
2159
2160
	/**
2161
	 * Query ctag for calendar
2162
	 *
2163
	 * @param int|string|array $user integer user-id or array of user-id's to use, defaults to the current user
2164
	 * @param string $filter ='owner' all (not rejected), accepted, unknown, tentative, rejected or hideprivate
2165
	 * @param boolean $master_only =false only check recurance master (egw_cal_user.recur_date=0)
2166
	 * @return integer
2167
	 */
2168
	public function get_ctag($user, $filter='owner', $master_only=false)
2169
	{
2170
		if ($this->debug > 1) $startime = microtime(true);
2171
2172
		// resolve users to add memberships for users and members for groups
2173
		$users = $this->resolve_users($user);
2174
		$ctag = $users ? $this->so->get_ctag($users, $filter == 'owner', $master_only) : 0;	// no rights, return 0 as ctag (otherwise we get SQL error!)
2175
2176
		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");
2177
		return $ctag;
2178
	}
2179
2180
	/**
2181
	 * Hook for infolog  to set some extra data and links
2182
	 *
2183
	 * @param array $data event-array preset by infolog plus
2184
	 * @param int $data[id] cal_id
2185
	 * @return array with key => value pairs to set in new event and link_app/link_id arrays
2186
	 */
2187
	function infolog_set($data)
2188
	{
2189
		if (!($calendar = $this->read($data['id'])))
2190
		{
2191
			return array();
2192
		}
2193
2194
		$content = array(
2195
			'info_cat'       => $GLOBALS['egw']->categories->check_list(Acl::READ, $calendar['category']),
2196
			'info_priority'  => $calendar['priority'] ,
2197
			'info_public'    => $calendar['public'] != 'private',
2198
			'info_subject'   => $calendar['title'],
2199
			'info_des'       => $calendar['description'],
2200
			'info_location'  => $calendar['location'],
2201
			'info_startdate' => $calendar['range_start'],
2202
			//'info_enddate' => $calendar['range_end'] ? $calendar['range_end'] : $calendar['uid']
2203
			'info_contact'   => 'calendar:'.$data['id'],
2204
		);
2205
2206
		unset($content['id']);
2207
		// Add calendar link to infolog entry
2208
		$content['link_app'][] = $calendar['info_link']['app'];
2209
		$content['link_id'][]  = $calendar['info_link']['id'];
2210
		// Copy claendar's links
2211
		foreach(Link::get_links('calendar',$calendar['id'],'','link_lastmod DESC',true) as $link)
2212
		{
2213
			if ($link['app'] != Link::VFS_APPNAME)
2214
			{
2215
				$content['link_app'][] = $link['app'];
2216
				$content['link_id'][]  = $link['id'];
2217
			}
2218
			if ($link['app'] == 'addressbook')	// prefering contact as primary contact over calendar entry set above
2219
			{
2220
				$content['info_contact'] = 'addressbook:'.$link['id'];
2221
			}
2222
		}
2223
		// Copy same custom fields
2224
		foreach(array_keys(Api\Storage\Customfields::get('infolog')) as $name)
2225
		{
2226
			if ($this->customfields[$name]) $content['#'.$name] = $calendar['#'.$name];
2227
		}
2228
		//error_log(__METHOD__.'('.array2string($data).') calendar='.array2string($calendar).' returning '.array2string($content));
2229
		return $content;
2230
	}
2231
2232
	/**
2233
	 * Hook for timesheet to set some extra data and links
2234
	 *
2235
	 * @param array $data
2236
	 * @param int $data[id] cal_id:recurrence
2237
	 * @return array with key => value pairs to set in new timesheet and link_app/link_id arrays
2238
	 */
2239
	function timesheet_set($data)
2240
	{
2241
		$set = array();
2242
		list($id,$recurrence) = explode(':',$data['id']);
2243
		if ((int)$id && ($event = $this->read($id,$recurrence)))
2244
		{
2245
			$set['ts_start'] = $event['start'];
2246
			$set['ts_title'] = $this->link_title($event);
2247
			$set['start_time'] = Api\DateTime::to($event['start'],'H:i');
2248
			$set['ts_description'] = $event['description'];
2249
			if ($this->isWholeDay($event)) $event['end']++;	// whole day events are 1sec short
2250
			$set['ts_duration']	= ($event['end'] - $event['start']) / 60;
2251
			$set['ts_quantity'] = ($event['end'] - $event['start']) / 3600;
2252
			$set['end_time'] = null;	// unset end-time
2253
			$set['cat_id'] = (int)$event['category'];
2254
2255 View Code Duplication
			foreach(Link::get_links('calendar',$id,'','link_lastmod DESC',true) as $link)
2256
			{
2257
				if ($link['app'] != 'timesheet' && $link['app'] != Link::VFS_APPNAME)
2258
				{
2259
					$set['link_app'][] = $link['app'];
2260
					$set['link_id'][]  = $link['id'];
2261
				}
2262
			}
2263
		}
2264
		return $set;
2265
	}
2266
}
2267