calendar_so::events()   F
last analyzed

Complexity

Conditions 22
Paths 769

Size

Total Lines 67
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 22
eloc 40
nc 769
nop 9
dl 0
loc 67
rs 0.3207
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
/**
3
 * EGroupware - Calendar's storage-object
4
 *
5
 * @link http://www.egroupware.org
6
 * @package calendar
7
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
8
 * @author Christian Binder <christian-AT-jaytraxx.de>
9
 * @author Joerg Lehrke <[email protected]>
10
 * @copyright (c) 2005-16 by RalfBecker-At-outdoor-training.de
11
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
12
 * @version $Id$
13
 */
14
15
use EGroupware\Api;
16
use EGroupware\Api\Link;
17
use EGroupware\Api\Acl;
18
19
/**
20
 * some necessary defines used by the calendar
21
 */
22
if(!extension_loaded('mcal'))
23
{
24
	define('MCAL_RECUR_NONE',0);
25
	define('MCAL_RECUR_DAILY',1);
26
	define('MCAL_RECUR_WEEKLY',2);
27
	define('MCAL_RECUR_MONTHLY_MDAY',3);
28
	define('MCAL_RECUR_MONTHLY_WDAY',4);
29
	define('MCAL_RECUR_YEARLY',5);
30
	define('MCAL_RECUR_SECONDLY',6);
31
	define('MCAL_RECUR_MINUTELY',7);
32
	define('MCAL_RECUR_HOURLY',8);
33
34
	define('MCAL_M_SUNDAY',1);
35
	define('MCAL_M_MONDAY',2);
36
	define('MCAL_M_TUESDAY',4);
37
	define('MCAL_M_WEDNESDAY',8);
38
	define('MCAL_M_THURSDAY',16);
39
	define('MCAL_M_FRIDAY',32);
40
	define('MCAL_M_SATURDAY',64);
41
42
	define('MCAL_M_WEEKDAYS',62);
43
	define('MCAL_M_WEEKEND',65);
44
	define('MCAL_M_ALLDAYS',127);
45
}
46
47
define('REJECTED',0);
48
define('NO_RESPONSE',1);
49
define('TENTATIVE',2);
50
define('ACCEPTED',3);
51
define('DELEGATED',4);
52
53
define('HOUR_s',60*60);
0 ignored issues
show
Coding Style introduced by
This constant is not in uppercase (expected 'HOUR_S').
Loading history...
54
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...
55
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...
56
57
/**
58
 * Class to store all calendar data (storage object)
59
 *
60
 * Tables used by calendar_so:
61
 *	- egw_cal: general calendar data: cal_id, title, describtion, locations, range-start and -end dates
62
 *	- egw_cal_dates: start- and enddates (multiple entry per cal_id for recuring events!), recur_exception flag
63
 *	- egw_cal_user: participant info including status (multiple entries per cal_id AND startdate for recuring events)
64
 * 	- egw_cal_repeats: recur-data: type, interval, days etc.
65
 *  - egw_cal_extra: custom fields (multiple entries per cal_id possible)
66
 *
67
 * The new UI, BO and SO classes have a strict definition, in which timezone they operate:
68
 *  UI only operates in user-time, so there have to be no conversation at all !!!
69
 *  BO's functions take and return user-time only (!), they convert internaly everything to servertime, because
70
 *  SO operates only on server-time
71
 *
72
 * DB-model uses egw_cal_user.cal_status='X' for participants who got deleted. They never get returned by
73
 * read or search methods, but influence the ctag of the deleted users calendar!
74
 *
75
 * DB-model uses egw_cal_user.cal_status='E' for participants only participating in exceptions of recurring
76
 * events, so whole recurring event get found for these participants too!
77
 *
78
 * All update methods now take care to update modification time of (evtl. existing) series master too,
79
 * to force an etag, ctag and sync-token change! Methods not doing that are private to this class.
80
 *
81
 * range_start/_end in main-table contains start and end of whole event series (range_end is NULL for unlimited recuring events),
82
 * saving the need to always join dates table, to query non-enumerating recuring events (like CalDAV or ActiveSync does).
83
 * This effectivly stores MIN(cal_start) and MAX(cal_end) permanently as column in main-table and improves speed tremendiously
84
 * (few milisecs instead of more then 2 minutes on huge installations)!
85
 * It's set in calendar_so::save from start and end or recur_enddate, so nothing changes for higher level classes.
86
 *
87
 * egw_cal_user.cal_user_id contains since 14.3.001 only an md5-hash of a lowercased raw email address (not rfc822 address!).
88
 * Real email address and other possible attendee information for iCal or CalDAV are stored in cal_user_attendee.
89
 * This allows a short 32byte ascii cal_user_id and also storing attendee information for accounts and contacts.
90
 * Outside of this class uid for email address is still "e$cn <$email>" or "e$email".
91
 * We use calendar_so::split_user($uid, &$user_type, &$user_id, $md5_email=false) with last param true to generate
92
 * egw_cal_user.cal_user_id for DB and calendar_so::combine_user($user_type, $user_id, $user_attendee) to generate
93
 * uid used outside of this class. Both methods are unchanged when using with their default parameters.
94
 *
95
 * @ToDo drop egw_cal_repeats table in favor of a rrule colum in main table (saves always used left join and allows to store all sorts of rrules)
96
 */
97
class calendar_so
98
{
99
	/**
100
	 * name of the main calendar table and prefix for all other calendar tables
101
	 */
102
	var $cal_table = 'egw_cal';
103
	var $extra_table,$repeats_table,$user_table,$dates_table,$all_tables;
104
105
	/**
106
	 * reference to global db-object
107
	 *
108
	 * @var Api\Db
109
	 */
110
	var $db;
111
	/**
112
	 * instance of the async object
113
	 *
114
	 * @var Api\Asyncservice
115
	 */
116
	var $async;
117
	/**
118
	 * SQL to sort by status U, T, A, R
119
	 *
120
	 */
121
	const STATUS_SORT = "CASE cal_status WHEN 'U' THEN 1 WHEN 'T' THEN 2 WHEN 'A' THEN 3 WHEN 'R' THEN 4 ELSE 0 END ASC";
122
123
	/**
124
	 * Time to keep alarms in async table to allow eg. alarm snozzing
125
	 */
126
	const ALARM_KEEP_TIME = 86400;
127
128
	/**
129
	 * Cached timezone data
130
	 *
131
	 * @var array id => data
132
	 */
133
	protected static $tz_cache = array();
134
135
	/**
136
	 * Constructor of the socal class
137
	 */
138
	function __construct()
139
	{
140
		$this->async = $GLOBALS['egw']->asyncservice;
141
		$this->db = $GLOBALS['egw']->db;
142
143
		$this->all_tables = array($this->cal_table);
144
		foreach(array('extra','repeats','user','dates') as $name)
145
		{
146
			$vname = $name.'_table';
147
			$this->all_tables[] = $this->$vname = $this->cal_table.'_'.$name;
148
		}
149
	}
150
151
	/**
152
	 * Return sql to fetch all events in a given timerange, to be used instead of full table in further sql queries
153
	 *
154
	 * @param int $start
155
	 * @param int $end
156
	 * @param array $_where =null
157
	 * @param boolean $deleted =false
158
	 * @return string
159
	 */
160
	protected function cal_range_view($start, $end, array $_where=null, $deleted=false)
161
	{
162
		if ($GLOBALS['egw_info']['server']['no_timerange_views'] || !$start)	// using view without start-date is slower!
163
		{
164
			return $this->cal_table;	// no need / use for a view
165
		}
166
167
		$where = array();
168
		if (isset($deleted)) $where[] = "cal_deleted IS ".($deleted ? '' : 'NOT').' NULL';
169
		if ($end) $where[] = "range_start<".(int)$end;
170
		if ($start) $where[] = "(range_end IS NULL OR range_end>".(int)$start.")";
171
		if ($_where) $where = array_merge($where, $_where);
172
173
		$sql = "(SELECT * FROM $this->cal_table WHERE ".$this->db->expression($this->cal_table, $where).") $this->cal_table";
174
175
		return $sql;
176
	}
177
178
	/**
179
	 * Return sql to fetch all dates in a given timerange, to be used instead of full dates table in further sql queries
180
	 *
181
	 * Currently NOT used, as using two views joined together appears slower in my tests (probably because no index) then
182
	 * joining cal_range_view with real dates table (with index).
183
	 *
184
	 * @param int $start
185
	 * @param int $end
186
	 * @param array $_where =null
187
	 * @param boolean $deleted =false
188
	 * @return string
189
	 */
190
	protected function dates_range_view($start, $end, array $_where=null, $deleted=false)
191
	{
192
		if ($GLOBALS['egw_info']['server']['no_timerange_views'] || !$start || !$end)	// using view without start- AND end-date is slower!
193
		{
194
			return $this->dates_table;	// no need / use for a view
195
		}
196
197
		$where = array();
198
		if (isset($deleted)) $where['recur_exception'] = $deleted;
199
		if ($end) $where[] = "cal_start<".(int)$end;
200
		if ($start) $where[] = "cal_end>".(int)$start;
201
		if ($_where) $where = array_merge($where, $_where);
202
203
		// Api\Db::union uses Api\Db::select which check if join contains "WHERE"
204
		// to support old join syntax like ", other_table WHERE ...",
205
		// therefore we have to use eg. "WHERe" instead!
206
		$sql = "(SELECT * FROM $this->dates_table WHERe ".$this->db->expression($this->dates_table, $where).") $this->dates_table";
207
208
		return $sql;
209
	}
210
211
	/**
212
	 * Return events in a given timespan containing given participants (similar to search but quicker)
213
	 *
214
	 * Not all search parameters are currently supported!!!
215
	 *
216
	 * @param int $start startdate of the search/list (servertime)
217
	 * @param int $end enddate of the search/list (servertime)
218
	 * @param int|array $users user-id or array of user-id's, !$users means all entries regardless of users
219
	 * @param int|array $cat_id =0 mixed category-id or array of cat-id's (incl. all sub-categories), default 0 = all
220
	 * @param string $filter ='default' string filter-name: all (not rejected), accepted, unknown, tentative, rejected or everything (incl. rejected, deleted)
221
	 * @param int|boolean $offset =False offset for a limited query or False (default)
222
	 * @param int $num_rows =0 number of rows to return if offset set, default 0 = use default in user prefs
223
	 * @param array $params =array()
224
	 * @param string|array $params['query'] string: pattern so search for, if unset or empty all matching entries are returned (no search)
225
	 *		Please Note: a search never returns repeating events more then once AND does not honor start+end date !!!
226
	 *      array: everything is directly used as $where
227
	 * @param string $params['order'] ='cal_start' column-names plus optional DESC|ASC separted by comma
228
	 * @param string $params['sql_filter'] sql to be and'ed into query (fully quoted)
229
	 * @param string|array $params['cols'] what to select, default "$this->repeats_table.*,$this->cal_table.*,cal_start,cal_end,cal_recur_date",
230
	 * 						if specified and not false an iterator for the rows is returned
231
	 * @param string $params['append'] SQL to append to the query before $order, eg. for a GROUP BY clause
232
	 * @param array $params['cfs'] custom fields to query, null = none, array() = all, or array with cfs names
233
	 * @param array $params['users'] raw parameter as passed to calendar_bo::search() no memberships resolved!
234
	 * @param boolean $params['master_only'] =false, true only take into account participants/status from master (for AS)
235
	 * @param boolean $params['enum_recuring'] =true enumerate recuring events
236
	 * @param int $remove_rejected_by_user =null add join to remove entry, if given user has rejected it
237
	 * @return array of events
238
	 */
239
	function &events($start,$end,$users,$cat_id=0,$filter='all',$offset=False,$num_rows=0,array $params=array(),$remove_rejected_by_user=null)
240
	{
241
		error_log(__METHOD__.'('.($start ? date('Y-m-d H:i',$start) : '').','.($end ? date('Y-m-d H:i',$end) : '').','.array2string($users).','.array2string($cat_id).",'$filter',".array2string($offset).",$num_rows,".array2string($params).') '.function_backtrace());
242
		$start_time = microtime(true);
243
		// not everything is supported by now
244
		if (!$start || !$end || is_string($params['query']) ||
245
			//in_array($filter,array('owner','deleted')) ||
246
			$params['enum_recuring']===false)
247
		{
248
			throw new Api\Exception\AssertionFailed("Unsupported value for parameters!");
249
		}
250
		$where = is_array($params['query']) ? $params['query'] : array();
251
		if ($cat_id) $where[] = $this->cat_filter($cat_id);
252
		$egw_cal = $this->cal_range_view($start, $end, $where, $filter == 'everything' ? null : $filter != 'deleted');
253
254
		$status_filter = $this->status_filter($filter, $params['enum_recuring']);
255
256
		$sql = "SELECT DISTINCT {$this->cal_table}_repeats.*,$this->cal_table.*,\n".
257
			"	CASE WHEN recur_type IS NULL THEN egw_cal.range_start ELSE cal_start END AS cal_start,\n".
258
			"	CASE WHEN recur_type IS NULL THEN egw_cal.range_end ELSE cal_end END AS cal_end\n".
259
			// using time-limited range view, instead of complete table, give a big performance plus
260
			"FROM $egw_cal\n".
261
			"JOIN egw_cal_user ON egw_cal_user.cal_id=egw_cal.cal_id\n".
262
			// need to left join dates, as egw_cal_user.recur_date is null for non-recuring event
263
			"LEFT JOIN egw_cal_dates ON egw_cal_user.cal_id=egw_cal_dates.cal_id AND egw_cal_dates.cal_start=egw_cal_user.cal_recur_date\n".
264
			"LEFT JOIN egw_cal_repeats ON egw_cal_user.cal_id=egw_cal_repeats.cal_id\n".
265
			"WHERE ".($status_filter ? $this->db->expression($this->table, $status_filter, " AND \n") : '').
0 ignored issues
show
Bug Best Practice introduced by
The property table does not exist on calendar_so. Did you maybe forget to declare it?
Loading history...
266
			"	CASE WHEN recur_type IS NULL THEN egw_cal.range_start ELSE cal_start END<".(int)$end." AND\n".
267
			"	CASE WHEN recur_type IS NULL THEN egw_cal.range_end ELSE cal_end END>".(int)$start;
268
269
		if ($users)
270
		{
271
			// fix $users to also prefix system users and groups (with 'u')
272
			if (!is_array($users)) $users = $users ? (array)$users : array();
273
			foreach($users as &$uid)
274
			{
275
				$user_type = $user_id = null;
276
				self::split_user($uid, $user_type, $user_id, true);
277
				$uid = $user_type.$user_id;
278
			}
279
			$sql .= " AND\n	CONCAT(cal_user_type,cal_user_id) IN (".implode(',', array_map(array($this->db, 'quote'), $users)).")";
280
		}
281
282
		if ($remove_rejected_by_user && !in_array($filter, array('everything', 'deleted')))
0 ignored issues
show
Bug Best Practice introduced by
The expression $remove_rejected_by_user of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
283
		{
284
			$sql .= " AND\n	(cal_user_type!='u' OR cal_user_id!=".(int)$remove_rejected_by_user." OR cal_status!='R')";
285
		}
286
287
		if (!empty($params['sql_filter']) && is_string($params['sql_filter']))
288
		{
289
			$sql .= " AND\n	".$params['sql_filter'];
290
		}
291
292
		if ($params['order'])	// only order if requested
293
		{
294
			if (!preg_match('/^[a-z_ ,c]+$/i',$params['order'])) $params['order'] = 'cal_start';		// gard against SQL injection
295
			$sql .= "\nORDER BY ".$params['order'];
296
		}
297
298
		if ($offset === false)	// return all rows --> Api\Db::query wants offset=0, num_rows=-1
299
		{
300
			$offset = 0;
301
			$num_rows = -1;
302
		}
303
		$events =& $this->get_events($this->db->query($sql, __LINE__, __FILE__, $offset, $num_rows));
304
		error_log(__METHOD__."(...) $sql --> ".number_format(microtime(true)-$start_time, 3));
305
		return $events;
306
	}
307
308
	/**
309
	 * reads one or more calendar entries
310
	 *
311
	 * All times (start, end and modified) are returned as timesstamps in servertime!
312
	 *
313
	 * @param int|array|string $ids id or array of id's of the entries to read, or string with a single uid
314
	 * @param int $recur_date =0 if set read the next recurrence at or after the timestamp, default 0 = read the initital one
315
	 * @param boolean $read_recurrence =false true: read the exception, not the series master (only for recur_date && $ids='<uid>'!)
316
	 * @return array|boolean array with cal_id => event array pairs or false if entry not found
317
	 */
318
	function read($ids, $recur_date=0, $read_recurrence=false)
319
	{
320
		//error_log(__METHOD__.'('.array2string($ids).",$recur_date) ".function_backtrace());
321
		$cols = self::get_columns('calendar', $this->cal_table);
322
		$cols[0] = $this->db->to_varchar($this->cal_table.'.cal_id');
323
		$cols = "$this->repeats_table.recur_type,$this->repeats_table.recur_interval,$this->repeats_table.recur_data,".implode(',',$cols);
324
		$join = "LEFT JOIN $this->repeats_table ON $this->cal_table.cal_id=$this->repeats_table.cal_id";
325
326
		$where = array();
327
		if (is_scalar($ids) && !is_numeric($ids))	// a single uid
328
		{
329
			// We want only the parents to match
330
			$where['cal_uid'] = $ids;
331
			if ($read_recurrence)
332
			{
333
				$where['cal_recurrence'] = $recur_date;
334
			}
335
			else
336
			{
337
				$where['cal_reference'] = 0;
338
			}
339
		}
340
		elseif(is_array($ids) && isset($ids[count($ids)-1]) || is_scalar($ids))	// one or more cal_id's
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (is_array($ids) && IssetNode) || is_scalar($ids), Probably Intended Meaning: is_array($ids) && (IssetNode || is_scalar($ids))
Loading history...
341
		{
342
			$where['cal_id'] = $ids;
343
		}
344
		else	// array with column => value pairs
345
		{
346
			$where = $ids;
347
			unset($ids);	// otherwise users get not read!
348
		}
349
		if (isset($where['cal_id']))	// prevent non-unique column-name cal_id
350
		{
351
			$where[] = $this->db->expression($this->cal_table, $this->cal_table.'.',array(
352
				'cal_id' => $where['cal_id'],
353
			));
354
			unset($where['cal_id']);
355
		}
356
		if ((int) $recur_date && !$read_recurrence)
357
		{
358
			$where[] = 'cal_start >= '.(int)$recur_date;
359
			$group_by = 'GROUP BY '.$cols;
360
			$cols .= ',MIN(cal_start) AS cal_start,MIN(cal_end) AS cal_end';
361
			$join = "JOIN $this->dates_table ON $this->cal_table.cal_id=$this->dates_table.cal_id $join";
362
		}
363
		else
364
		{
365
			$cols .= ',range_start AS cal_start,(SELECT MIN(cal_end) FROM egw_cal_dates WHERE egw_cal.cal_id=egw_cal_dates.cal_id) AS cal_end';
366
		}
367
		$cols .= ',range_end-1 AS recur_enddate';
368
369
		// sort deleted to the end, to prefer non-deleted events over deleted ones when querying by uid
370
		$group_by .= ' ORDER BY cal_deleted IS NOT NULL,' . $this->db->to_varchar($this->cal_table.'.cal_id') . ' DESC';
371
372
		$events =& $this->get_events($this->db->select($this->cal_table, $cols, $where, __LINE__, __FILE__, false, $group_by, 'calendar', 0, $join), $recur_date);
373
374
		// if we wanted to read the real recurrence, but we have eg. only a virtual one, we need to try again without $read_recurrence
375
		if ((!$events || ($e = current($events)) && $e['deleted']) && $recur_date && $read_recurrence)
376
		{
377
			return $this->read($ids, $recur_date);
378
		}
379
380
		return $events ? $events : false;
381
	}
382
383
	/**
384
	 * Get full event information from an iterator of a select on egw_cal
385
	 *
386
	 * @param array|Iterator $rs
387
	 * @param int $recur_date =0
388
	 * @return array
389
	 */
390
	protected function &get_events($rs, $recur_date=0)
391
	{
392
		if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length']))
393
		{
394
			$minimum_uid_length = $GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'];
395
		}
396
		else
397
		{
398
			$minimum_uid_length = 8;
399
		}
400
401
		$events = array();
402
		foreach($rs as $row)
403
		{
404
			if (!$row['recur_type'])
405
			{
406
				$row['recur_type'] = MCAL_RECUR_NONE;
407
				unset($row['recur_enddate']);
408
			}
409
			$row['recur_exception'] = $row['alarm'] = array();
410
			$events[$row['cal_id']] = Api\Db::strip_array_keys($row,'cal_');
411
		}
412
		if (!$events) return $events;
413
414
		$ids = array_keys($events);
415
		if (count($ids) == 1) $ids = $ids[0];
416
417
		foreach ($events as &$event)
418
		{
419
			if (!isset($event['uid']) || strlen($event['uid']) < $minimum_uid_length)
420
			{
421
				// event (without uid), not strong enough uid => create new uid
422
				$event['uid'] = Api\CalDAV::generate_uid('calendar',$event['id']);
423
				$this->db->update($this->cal_table, array('cal_uid' => $event['uid']),
424
					array('cal_id' => $event['id']),__LINE__,__FILE__,'calendar');
425
			}
426
			if (!(int)$recur_date && $event['recur_type'] != MCAL_RECUR_NONE)
427
			{
428
				foreach($this->db->select($this->dates_table, 'cal_id,cal_start', array(
429
					'cal_id' => $ids,
430
					'recur_exception' => true,
431
				), __LINE__, __FILE__, false, 'ORDER BY cal_id,cal_start', 'calendar') as $row)
432
				{
433
					$events[$row['cal_id']]['recur_exception'][] = $row['cal_start'];
434
				}
435
				break;	// as above select read all exceptions (and I dont think too short uid problem still exists)
436
			}
437
			// make sure we fetch only real exceptions (deleted occurrences of a series should not show up)
438
			if (($recur_date &&	$event['recur_type'] != MCAL_RECUR_NONE))
439
			{
440
				//_debug_array(__METHOD__.__LINE__.' recur_date:'.$recur_date.' check cal_start:'.$event['start']);
441
				foreach($this->db->select($this->dates_table, 'cal_id,cal_start', array(
442
					'cal_id' => $event['id'],
443
					'cal_start' => $event['start'],
444
					'recur_exception' => true,
445
				), __LINE__, __FILE__, false, '', 'calendar') as $row)
446
				{
447
					$isException[$row['cal_id']] = true;
448
				}
449
				if ($isException[$event['id']])
450
				{
451
					if (!$this->db->select($this->cal_table, 'COUNT(*)', array(
452
						'cal_uid' => $event['uid'],
453
						'cal_recurrence' => $event['start'],
454
						'cal_deleted' => NULL
455
					), __LINE__, __FILE__, false, '', 'calendar')->fetchColumn())
456
					{
457
						$e = $this->read($event['id'],$event['start']+1);
458
						$event = $e[$event['id']];
459
						break;
460
					}
461
					else
462
					{
463
						//real exception -> should we return it? probably not, so we live with the result of the next occurrence of the series
464
					}
465
				}
466
			}
467
		}
468
469
		// check if we have a real recurance, if not set $recur_date=0
470
		if (is_array($ids) || $events[(int)$ids]['recur_type'] == MCAL_RECUR_NONE)
471
		{
472
			$recur_date = 0;
473
		}
474
		else	// adjust the given recurance to the real time, it can be a date without time(!)
475
		{
476
			if ($recur_date)
477
			{
478
				// also remember recur_date, maybe we need it later, duno now
479
				$recur_date = array(0,$events[$ids]['recur_date'] = $events[$ids]['start']);
480
			}
481
		}
482
483
		// participants, if a recur_date give, we read that recurance, plus the one users from the default entry with recur_date=0
484
		// sorting by cal_recur_date ASC makes sure recurence status always overwrites series status
485
		foreach($this->db->select($this->user_table,'*',array(
486
			'cal_id'      => $ids,
487
			'cal_recur_date' => $recur_date,
488
			"cal_status NOT IN ('X','E')",
489
		),__LINE__,__FILE__,false,'ORDER BY cal_user_type DESC,cal_recur_date ASC,'.self::STATUS_SORT,'calendar') as $row)	// DESC puts users before resources and contacts
490
		{
491
			// combine all participant data in uid and status values
492
			$uid    = self::combine_user($row['cal_user_type'], $row['cal_user_id'], $row['cal_user_attendee']);
493
			$status = self::combine_status($row['cal_status'],$row['cal_quantity'],$row['cal_role']);
494
495
			$events[$row['cal_id']]['participants'][$uid] = $status;
496
			$events[$row['cal_id']]['participant_types'][$row['cal_user_type']][is_numeric($uid) ? $uid : substr($uid, 1)] = $status;
497
			// make extra attendee information available eg. for iCal export (attendee used eg. in response to organizer for an account)
498
			$events[$row['cal_id']]['attendee'][$uid] = $row['cal_user_attendee'];
499
		}
500
501
		// custom fields
502
		foreach($this->db->select($this->extra_table,'*',array('cal_id'=>$ids),__LINE__,__FILE__,false,'','calendar') as $row)
503
		{
504
			$events[$row['cal_id']]['#'.$row['cal_extra_name']] = $row['cal_extra_value'];
505
		}
506
507
		// alarms
508
		if (is_array($ids))
509
		{
510
			foreach($this->read_alarms((array)$ids) as $cal_id => $alarms)
511
			{
512
				$events[$cal_id]['alarm'] = $alarms;
513
			}
514
		}
515
		else
516
		{
517
			$events[$ids]['alarm'] = $this->read_alarms($ids);
518
		}
519
520
		//echo "<p>socal::read(".print_r($ids,true).")=<pre>".print_r($events,true)."</pre>\n";
521
		return $events;
522
	}
523
524
	/**
525
	 * Maximum time a ctag get cached, as ActiveSync ping requests can run for a long time
526
	 */
527
	const MAX_CTAG_CACHE_TIME = 29;
528
529
	/**
530
	 * Get maximum modification time of events for given participants and optional owned by them
531
	 *
532
	 * This includes ALL recurences of an event series
533
	 *
534
	 * @param int|string|array $users one or mulitple calendar users
535
	 * @param booelan $owner_too =false if true return also events owned by given users
0 ignored issues
show
Bug introduced by
The type booelan was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
536
	 * @param boolean $master_only =false only check recurance master (egw_cal_user.recur_date=0)
537
	 * @return int maximum modification timestamp
538
	 */
539
	function get_ctag($users, $owner_too=false,$master_only=false)
540
	{
541
		static $ctags = array();	// some per-request caching
542
		static $last_request = null;
543
		if (!isset($last_request) || time()-$last_request > self::MAX_CTAG_CACHE_TIME)
544
		{
545
			$ctags = array();
546
			$last_request = time();
547
		}
548
		$signature = serialize(func_get_args());
549
		if (isset($ctags[$signature])) return $ctags[$signature];
550
551
		$types = array();
552
		foreach((array)$users as $uid)
553
		{
554
			$type = $id = null;
555
			self::split_user($uid, $type, $id, true);
556
			$types[$type][] = $id;
557
		}
558
		foreach($types as $type => $ids)
559
		{
560
			$where = array(
561
				'cal_user_type' => $type,
562
				'cal_user_id' => $ids,
563
			);
564
			if (count($types) > 1)
565
			{
566
				$types[$type] = $this->db->expression($this->user_table, $where);
567
			}
568
		}
569
		if (count($types) > 1)
570
		{
571
			$where[] = '('.explode(' OR ', $types).')';
0 ignored issues
show
Bug introduced by
Are you sure explode(' OR ', $types) of type string[] can be used in concatenation? ( Ignorable by Annotation )

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

571
			$where[] = '('./** @scrutinizer ignore-type */ explode(' OR ', $types).')';
Loading history...
572
		}
573
		if ($master_only)
574
		{
575
			$where['cal_recur_date'] = 0;
576
		}
577
		if ($owner_too)
578
		{
579
			// owner can only by users, no groups or resources
580
			foreach($users as $key => $user)
581
			{
582
				if (!($user > 0)) unset($users[$key]);
583
			}
584
			$where = $this->db->expression($this->user_table, '(', $where, ' OR ').
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $where seems to be defined by a foreach iteration on line 558. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
585
				$this->db->expression($this->cal_table, array(
586
					'cal_owner' => $users,
587
				),')');
588
		}
589
		return $ctags[$signature] = $this->db->select($this->user_table,'MAX(cal_modified)',
590
			$where,__LINE__,__FILE__,false,'','calendar',0,'JOIN egw_cal ON egw_cal.cal_id=egw_cal_user.cal_id')->fetchColumn();
591
	}
592
593
	/**
594
	 * Query calendar main table and return iterator of query
595
	 *
596
	 * Use as: foreach(get_cal_data() as $data) { $data = Api\Db::strip_array_keys($data, 'cal_'); // do something with $data
597
	 *
598
	 * @param array $query filter, keys have to use 'cal_' prefix
599
	 * @param string|array $cols ='cal_id,cal_reference,cal_etag,cal_modified,cal_user_modified' cols to query
600
	 * @return Iterator as Api\Db::select
601
	 */
602
	function get_cal_data(array $query, $cols='cal_id,cal_reference,cal_etag,cal_modified,cal_user_modified')
603
	{
604
		if (!is_array($cols)) $cols = explode(',', $cols);
605
606
		// special handling of cal_user_modified "pseudo" column
607
		if (($key = array_search('cal_user_modified', $cols)) !== false)
608
		{
609
			$cols[$key] = $this->db->unix_timestamp('(SELECT MAX(cal_user_modified) FROM '.
610
				$this->user_table.' WHERE '.$this->cal_table.'.cal_id='.$this->user_table.'.cal_id)').
611
				' AS cal_user_modified';
612
		}
613
		return $this->db->select($this->cal_table, $cols, $query, __LINE__, __FILE__);
614
	}
615
616
	/**
617
	 * generate SQL to filter after a given category (incl. subcategories)
618
	 *
619
	 * @param array|int $cat_id cat-id or array of cat-ids, or !$cat_id for none
620
	 * @return string SQL to include in the query
621
	 */
622
	function cat_filter($cat_id)
623
	{
624
		$sql = '';
625
		if ($cat_id)
626
		{
627
			$cats = $GLOBALS['egw']->categories->return_all_children($cat_id);
628
			array_walk($cats, function(&$val, $key)
629
			{
630
				unset($key);	// not used, but required by function signature
631
				$val = (int) $val;
632
			});
633
			if (is_array($cat_id) && count($cat_id)==1) $cat_id = $cat_id[0];
634
			$sql = '(cal_category'.(count($cats) > 1 ? " IN ('".implode("','",$cats)."')" : '='.$this->db->quote((int)$cat_id));
635
			foreach($cats as $cat)
636
			{
637
				$sql .= ' OR '.$this->db->concat("','",'cal_category',"','").' LIKE '.$this->db->quote('%,'.$cat.',%');
638
			}
639
			$sql .= ') ';
640
		}
641
		return $sql;
642
	}
643
644
	/**
645
	 * Return filters to filter by given status
646
	 *
647
	 * @param string $filter "default", "all", ...
648
	 * @param boolean $enum_recuring are recuring events enumerated or not
649
	 * @param array $where =array() array to add filters too
650
	 * @return array
651
	 */
652
	protected function status_filter($filter, $enum_recuring=true, array $where=array())
653
	{
654
		if($filter != 'deleted' && $filter != 'everything')
655
		{
656
			$where[] = 'cal_deleted IS NULL';
657
		}
658
		switch($filter)
659
		{
660
			case 'everything':	// no filter at all
661
				break;
662
			case 'showonlypublic':
663
				$where['cal_public'] = 1;
664
				$where[] = "$this->user_table.cal_status NOT IN ('R','X','E')";
665
				break;
666
			case 'deleted':
667
				$where[] = 'cal_deleted IS NOT NULL';
668
				break;
669
			case 'unknown':
670
				$where[] = "$this->user_table.cal_status='U'";
671
				break;
672
			case 'not-unknown':
673
				$where[] = "$this->user_table.cal_status NOT IN ('U','X','E')";
674
				break;
675
			case 'accepted':
676
				$where[] = "$this->user_table.cal_status='A'";
677
				break;
678
			case 'tentative':
679
				$where[] = "$this->user_table.cal_status='T'";
680
				break;
681
			case 'rejected':
682
				$where[] = "$this->user_table.cal_status='R'";
683
				break;
684
			case 'delegated':
685
				$where[] = "$this->user_table.cal_status='D'";
686
				break;
687
			case 'all':
688
			case 'owner':
689
				$where[] = "$this->user_table.cal_status NOT IN ('X','E')";
690
				break;
691
			default:
692
				if ($enum_recuring)	// regular UI
693
				{
694
					$where[] = "$this->user_table.cal_status NOT IN ('R','X','E')";
695
				}
696
				else	// CalDAV / eSync / iCal need to include 'E' = exceptions
697
				{
698
					$where[] = "$this->user_table.cal_status NOT IN ('R','X')";
699
				}
700
				break;
701
		}
702
		return $where;
703
	}
704
705
	/**
706
	 * Searches / lists calendar entries, including repeating ones
707
	 *
708
	 * @param int $start startdate of the search/list (servertime)
709
	 * @param int $end enddate of the search/list (servertime)
710
	 * @param int|array $users user-id or array of user-id's, !$users means all entries regardless of users
711
	 * @param int|array $cat_id =0 mixed category-id or array of cat-id's (incl. all sub-categories), default 0 = all
712
	 * @param string $filter ='all' string filter-name: all (not rejected), accepted, unknown, tentative, rejected or everything (incl. rejected, deleted)
713
	 * @param int|boolean $offset =False offset for a limited query or False (default)
714
	 * @param int $num_rows =0 number of rows to return if offset set, default 0 = use default in user prefs
715
	 * @param array $params =array()
716
	 * @param string|array $params['query'] string: pattern so search for, if unset or empty all matching entries are returned (no search)
717
	 *		Please Note: a search never returns repeating events more then once AND does not honor start+end date !!!
718
	 *      array: everything is directly used as $where
719
	 * @param string $params['order'] ='cal_start' column-names plus optional DESC|ASC separted by comma
720
	 * @param string|array $params['sql_filter'] sql to be and'ed into query (fully quoted), or usual filter array
721
	 * @param string|array $params['cols'] what to select, default "$this->repeats_table.*,$this->cal_table.*,cal_start,cal_end,cal_recur_date",
722
	 * 						if specified and not false an iterator for the rows is returned
723
	 * @param string $params['append'] SQL to append to the query before $order, eg. for a GROUP BY clause
724
	 * @param array $params['cfs'] custom fields to query, null = none, array() = all, or array with cfs names
725
	 * @param array $params['users'] raw parameter as passed to calendar_bo::search() no memberships resolved!
726
	 * @param boolean $params['master_only'] =false, true only take into account participants/status from master (for AS)
727
	 * @param boolean $params['enum_recuring'] =true enumerate recuring events
728
	 * @param boolean $params['use_so_events'] =false, true return result of new $this->events()
729
	 * @param int $remove_rejected_by_user =null add join to remove entry, if given user has rejected it
730
	 * @return Iterator|array of events
731
	 */
732
	function &search($start,$end,$users,$cat_id=0,$filter='all',$offset=False,$num_rows=0,array $params=array(),$remove_rejected_by_user=null)
733
	{
734
		//error_log(__METHOD__.'('.($start ? date('Y-m-d H:i',$start) : '').','.($end ? date('Y-m-d H:i',$end) : '').','.array2string($users).','.array2string($cat_id).",'$filter',".array2string($offset).",$num_rows,".array2string($params).') '.function_backtrace());
735
736
		/* not using new events method currently, as it not yet fully working and
737
		   using time-range views in old code gives simmilar improvments
738
		// uncomment to use new events method for supported parameters
739
		//if (!isset($params['use_so_events'])) $params['use_so_events'] = $params['use_so_events'] || $start && $end && !in_array($filter, array('owner', 'deleted')) && $params['enum_recuring']!==false;
740
741
		// use new events method only if explicit requested
742
		if ($params['use_so_events'])
743
		{
744
			return call_user_func_array(array($this,'events'), func_get_args());
745
		}
746
		*/
747
		if (isset($params['cols']))
748
		{
749
			$cols = $params['cols'];
750
		}
751
		else
752
		{
753
			$all_cols = self::get_columns('calendar', $this->cal_table);
754
			$all_cols[0] = $this->db->to_varchar($this->cal_table.'.cal_id');
755
			$cols = "$this->repeats_table.recur_type,$this->repeats_table.recur_interval,$this->repeats_table.recur_data,range_end - 1 AS recur_enddate,".implode(',',$all_cols).",cal_start,cal_end,$this->user_table.cal_recur_date";
756
		}
757
		$where = array();
758
		$join = '';
759
		if (is_array($params['query']))
760
		{
761
			$where = $params['query'];
762
		}
763
		elseif ($params['query'])
764
		{
765
			$columns = array('cal_title','cal_description','cal_location');
766
767
			$wildcard = '%'; $op = null;
768
			$so_sql = new Api\Storage('calendar', $this->cal_table, $this->extra_table, '', 'cal_extra_name', 'cal_extra_value', 'cal_id', $this->db);
769
			$where = $so_sql->search2criteria($params['query'], $wildcard, $op, null, $columns);
770
771
			// Searching - restrict private to own or private grant
772
			if (!isset($params['private_grants']))
773
			{
774
				$params['private_grants'] = $GLOBALS['egw']->acl->get_ids_for_location($GLOBALS['egw_info']['user']['account_id'], Acl::PRIVAT, 'calendar');
775
				$params['private_grants'][] = $GLOBALS['egw_info']['user']['account_id'];	// db query does NOT return current user
776
			}
777
			$private_filter = '(cal_public=1 OR cal_public=0 AND '.$this->db->expression($this->cal_table, array('cal_owner' => $params['private_grants'])) . ')';
778
			$where[] = $private_filter;
779
		}
780
		if (!empty($params['sql_filter']))
781
		{
782
			if (is_string($params['sql_filter']))
783
			{
784
				$where[] = $params['sql_filter'];
785
			}
786
			elseif(is_array($params['sql_filter']))
787
			{
788
				$where = array_merge($where, $params['sql_filter']);
789
			}
790
		}
791
		$useUnionQuery = $this->db->capabilities['distinct_on_text'] && $this->db->capabilities['union'];
792
		if ($users)
793
		{
794
			$users_by_type = array();
795
			foreach((array)$users as $user)
796
			{
797
				if (is_numeric($user))
798
				{
799
					$users_by_type['u'][] = (int) $user;
800
				}
801
				else
802
				{
803
					$user_type = $user_id = null;
804
					self::split_user($user, $user_type, $user_id, true);
805
					$users_by_type[$user_type][] = $user_id;
806
				}
807
			}
808
			$to_or = $user_or = array();
809
			$owner_or = null;
810
			$table_def = $this->db->get_table_definitions('calendar',$this->user_table);
811
			foreach($users_by_type as $type => $ids)
812
			{
813
				// when we are able to use Union Querys, we do not OR our query, we save the needed parts for later construction of the union
814
				if ($useUnionQuery)
815
				{
816
					$user_or[] = $this->db->expression($table_def,$this->user_table.'.',array(
817
						'cal_user_type' => $type,
818
					),' AND '.$this->user_table.'.',array(
819
						'cal_user_id'   => $ids,
820
					));
821
					if ($type == 'u' && $filter == 'owner')
822
					{
823
						$cal_table_def = $this->db->get_table_definitions('calendar',$this->cal_table);
824
						// only users can be owners, no need to add groups
825
						$user_ids = array();
826
						foreach($ids as $user_id)
827
						{
828
							if ($GLOBALS['egw']->accounts->get_type($user_id) === 'u') $user_ids[] = $user_id;
829
						}
830
						$owner_or = $this->db->expression($cal_table_def,array('cal_owner' => $user_ids));
831
					}
832
				}
833
				else
834
				{
835
					$to_or[] = $this->db->expression($table_def,$this->user_table.'.',array(
836
						'cal_user_type' => $type,
837
					),' AND '.$this->user_table.'.',array(
838
						'cal_user_id'   => $ids,
839
					));
840
					if ($type == 'u' && $filter == 'owner')
841
					{
842
						$cal_table_def = $this->db->get_table_definitions('calendar',$this->cal_table);
843
						$to_or[] = $this->db->expression($cal_table_def,array('cal_owner' => $ids));
844
					}
845
				}
846
			}
847
			// this is only used, when we cannot use UNIONS
848
			if (!$useUnionQuery) $where[] = '('.implode(' OR ',$to_or).')';
849
850
			$where = $this->status_filter($filter, $params['enum_recuring'], $where);
851
		}
852
		if ($cat_id)
853
		{
854
			$where[] = $this->cat_filter($cat_id);
855
		}
856
		if ($start)
857
		{
858
			if ($params['enum_recuring'])
859
			{
860
				$where[] = (int)$start.' < cal_end';
861
			}
862
			else
863
			{
864
				$where[] = '('.((int)$start).' < range_end OR range_end IS NULL)';
865
			}
866
		}
867
		if (!preg_match('/^[a-z_ ,c]+$/i',$params['order'])) $params['order'] = 'cal_start';		// gard against SQL injection
868
869
		// if not enum recuring events, we have to use minimum start- AND end-dates, otherwise we get more then one event per cal_id!
870
		if (!$params['enum_recuring'])
871
		{
872
			$where[] = "$this->user_table.cal_recur_date=0";
873
			$cols = str_replace(array('cal_start','cal_end'),array('range_start AS cal_start','(SELECT MIN(cal_end) FROM egw_cal_dates WHERE egw_cal.cal_id=egw_cal_dates.cal_id) AS cal_end'),$cols);
874
			// in case cal_start is used in a query, eg. calendar_ical::find_event
875
			$where = str_replace(array('cal_start','cal_end'), array('range_start','(SELECT MIN(cal_end) FROM egw_cal_dates WHERE egw_cal.cal_id=egw_cal_dates.cal_id)'), $where);
876
			$params['order'] = str_replace('cal_start', 'range_start', $params['order']);
877
			if ($end) $where[] = (int)$end.' > range_start';
878
  		}
879
		elseif ($end) $where[] = (int)$end.' > cal_start';
880
881
		if ($remove_rejected_by_user && $filter != 'everything')
0 ignored issues
show
Bug Best Practice introduced by
The expression $remove_rejected_by_user of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
882
		{
883
			$rejected_by_user_join = "LEFT JOIN $this->user_table rejected_by_user".
884
				" ON $this->cal_table.cal_id=rejected_by_user.cal_id".
885
				" AND rejected_by_user.cal_user_type='u'".
886
				" AND rejected_by_user.cal_user_id=".$this->db->quote($remove_rejected_by_user).
887
				" AND ".(!$params['enum_recuring'] ? 'rejected_by_user.cal_recur_date=0' :
888
					'(recur_type IS NULL AND rejected_by_user.cal_recur_date=0 OR cal_start=rejected_by_user.cal_recur_date)');
889
			$or_required = array(
890
				'rejected_by_user.cal_status IS NULL',
891
				"rejected_by_user.cal_status NOT IN ('R','X')",
892
			);
893
			if ($filter == 'owner') $or_required[] = 'cal_owner='.(int)$remove_rejected_by_user;
894
			$where[] = '('.implode(' OR ',$or_required).')';
895
		}
896
		// using a time-range and deleted attribute limited view instead of full table
897
		$cal_table = $this->cal_range_view($start, $end, null, $filter == 'everything' ? null : $filter != 'deleted');
898
		$cal_table_def = $this->db->get_table_definitions('calendar', $this->cal_table);
899
900
		$u_join = "JOIN $this->user_table ON $this->cal_table.cal_id=$this->user_table.cal_id ".
901
			"LEFT JOIN $this->repeats_table ON $this->cal_table.cal_id=$this->repeats_table.cal_id ".
902
			$rejected_by_user_join;
903
		// dates table join only needed to enum recuring events, we use a time-range limited view here too
904
		if ($params['enum_recuring'])
905
		{
906
			$join .= "JOIN ".$this->dates_table.	// using dates_table direct seems quicker then an other view
907
				//$this->dates_range_view($start, $end, null, $filter == 'everything' ? null : $filter == 'deleted').
908
				" ON $this->cal_table.cal_id=$this->dates_table.cal_id ".$u_join;
909
		}
910
		else
911
		{
912
			$join .= $u_join;
913
		}
914
915
		// Check for some special sorting, used by planner views
916
		if($params['order'] == 'participants , cal_non_blocking DESC')
917
		{
918
			$order = ($GLOBALS['egw_info']['user']['preferences']['common']['account_display'] == 'lastname' ? 'n_family' : 'n_fileas');
919
			$cols .= ",egw_addressbook.{$order}";
920
			$join .= "LEFT JOIN egw_addressbook ON ".
921
					($this->db->Type == 'pgsql'? "egw_addressbook.account_id::varchar = ":"egw_addressbook.account_id = ").
922
					"{$this->user_table}.cal_user_id";
923
			$params['order'] = "$order, cal_non_blocking DESC";
924
		}
925
		else if ($params['order'] == 'categories , cal_non_blocking DESC')
926
		{
927
			$params['order'] = 'cat_name, cal_non_blocking DESC';
928
			$cols .= ',egw_categories.cat_name';
929
			$join .= "LEFT JOIN egw_categories ON egw_categories.cat_id = {$this->cal_table}.cal_category";
930
		}
931
932
		//$starttime = microtime(true);
933
		if ($useUnionQuery)
934
		{
935
			// allow apps to supply participants and/or icons
936
			if (!isset($params['cols'])) $cols .= ',NULL AS participants,NULL AS icons';
937
938
			// changed the original OR in the query into a union, to speed up the query execution under MySQL 5
939
			// with time-range views benefit is now at best slim for huge tables or none at all!
940
			$select = array(
941
				'table' => $cal_table,
942
				'join'  => $join,
943
				'cols'  => $cols,
944
				'where' => $where,
945
				'app'   => 'calendar',
946
				'append'=> $params['append'],
947
				'table_def' => $cal_table_def,
948
			);
949
			$selects = array();
950
			// we check if there are parts to use for the construction of our UNION query,
951
			// as replace the OR by construction of a suitable UNION for performance reasons
952
			if ($owner_or || $user_or)
953
			{
954
				foreach($user_or as $user_sql)
955
				{
956
					$selects[] = $select;
957
					$selects[count($selects)-1]['where'][] = $user_sql;
958
					if ($params['enum_recuring'])
959
					{
960
						$selects[count($selects)-1]['where'][] = "recur_type IS NULL AND $this->user_table.cal_recur_date=0";
961
						$selects[] = $select;
962
						$selects[count($selects)-1]['where'][] = $user_sql;
963
						$selects[count($selects)-1]['where'][] = "$this->user_table.cal_recur_date=cal_start";
964
					}
965
				}
966
				// if the query is to be filtered by owner we need to add more selects for the union
967
				if ($owner_or)
968
				{
969
					$selects[] = $select;
970
					$selects[count($selects)-1]['where'][] = $owner_or;
971
					if ($params['enum_recuring'])
972
					{
973
						$selects[count($selects)-1]['where'][] = "recur_type IS NULL AND $this->user_table.cal_recur_date=0";
974
						$selects[] = $select;
975
						$selects[count($selects)-1]['where'][] = $owner_or;
976
						$selects[count($selects)-1]['where'][] = "$this->user_table.cal_recur_date=cal_start";
977
					}
978
				}
979
			}
980
			else
981
			{
982
				// if the query is to be filtered by neither by user nor owner (should not happen?) we need 2 selects for the union
983
				$selects[] = $select;
984
				if ($params['enum_recuring'])
985
				{
986
					$selects[count($selects)-1]['where'][] = "recur_type IS NULL AND $this->user_table.cal_recur_date=0";
987
					$selects[] = $select;
988
					$selects[count($selects)-1]['where'][] = "$this->user_table.cal_recur_date=cal_start";
989
				}
990
			}
991
			if (is_numeric($offset) && !$params['no_total'])	// get the total too
992
			{
993
				$save_selects = $selects;
994
				// we only select cal_table.cal_id (and not cal_table.*) to be able to use DISTINCT (eg. MsSQL does not allow it for text-columns)
995
				foreach(array_keys($selects) as $key)
996
				{
997
					$selects[$key]['cols'] = "$this->repeats_table.recur_type,range_end AS recur_enddate,$this->repeats_table.recur_interval,$this->repeats_table.recur_data,".$this->db->to_varchar($this->cal_table.'.cal_id').",cal_start,cal_end,$this->user_table.cal_recur_date";
998
					if (!$params['enum_recuring'])
999
					{
1000
						$selects[$key]['cols'] = str_replace(array('cal_start','cal_end'),
1001
							array('range_start AS cal_start','range_end AS cal_end'), $selects[$key]['cols']);
1002
					}
1003
				}
1004
				if (!isset($params['cols']) && !$params['no_integration']) self::get_union_selects($selects,$start,$end,$users,$cat_id,$filter,$params['query'],$params['users']);
1005
1006
				$this->total = $this->db->union($selects,__LINE__,__FILE__)->NumRows();
0 ignored issues
show
Bug Best Practice introduced by
The property total does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
1007
1008
				// restore original cols / selects
1009
				$selects = $save_selects; unset($save_selects);
1010
			}
1011
			if (!isset($params['cols']) && !$params['no_integration']) self::get_union_selects($selects,$start,$end,$users,$cat_id,$filter,$params['query'],$params['users']);
1012
1013
			$rs = $this->db->union($selects,__LINE__,__FILE__,$params['order'],$offset,$num_rows);
1014
		}
1015
		else	// MsSQL oder MySQL 3.23
1016
		{
1017
			$where[] = "(recur_type IS NULL AND $this->user_table.cal_recur_date=0 OR $this->user_table.cal_recur_date=cal_start)";
1018
1019
			$selects = array(array(
1020
				'table' => $cal_table,
1021
				'join'  => $join,
1022
				'cols'  => $cols,
1023
				'where' => $where,
1024
				'app'   => 'calendar',
1025
				'append'=> $params['append'],
1026
				'table_def' => $cal_table_def,
1027
			));
1028
1029
			if (is_numeric($offset) && !$params['no_total'])	// get the total too
1030
			{
1031
				$save_selects = $selects;
1032
				// we only select cal_table.cal_id (and not cal_table.*) to be able to use DISTINCT (eg. MsSQL does not allow it for text-columns)
1033
				$selects[0]['cols'] = "$this->cal_table.cal_id,cal_start";
1034
				if (!isset($params['cols']) && !$params['no_integration'] && $this->db->capabilities['union'])
1035
				{
1036
					self::get_union_selects($selects,$start,$end,$users,$cat_id,$filter,$params['query'],$params['users']);
1037
				}
1038
				$this->total = $this->db->union($selects, __LINE__, __FILE__)->NumRows();
1039
				$selects = $save_selects;
1040
			}
1041
			if (!isset($params['cols']) && !$params['no_integration'] && $this->db->capabilities['union'])
1042
			{
1043
				self::get_union_selects($selects,$start,$end,$users,$cat_id,$filter,$params['query'],$params['users']);
1044
			}
1045
			$rs = $this->db->union($selects,__LINE__,__FILE__,$params['order'],$offset,$num_rows);
1046
		}
1047
		//error_log(__METHOD__."() useUnionQuery=$useUnionQuery --> query took ".(microtime(true)-$starttime).'s '.$rs->sql);
1048
1049
		if (isset($params['cols']))
1050
		{
1051
			return $rs;	// if colums are specified we return the recordset / iterator
1052
		}
1053
		// Todo: return $this->get_events($rs);
1054
1055
		$events = $ids = $recur_dates = $recur_ids = array();
1056
		foreach($rs as $row)
1057
		{
1058
			$id = $row['cal_id'];
1059
			if (is_numeric($id)) $ids[] = $id;
1060
1061
			if ($row['cal_recur_date'])
1062
			{
1063
				$id .= '-'.$row['cal_recur_date'];
1064
				$recur_dates[] = $row['cal_recur_date'];
1065
			}
1066
			if ($row['participants'])
1067
			{
1068
				$row['participants'] = explode(',',$row['participants']);
1069
				$row['participants'] = array_combine($row['participants'],
1070
					array_fill(0,count($row['participants']),''));
1071
			}
1072
			else
1073
			{
1074
				$row['participants'] = array();
1075
			}
1076
			$row['recur_exception'] = $row['alarm'] = array();
1077
1078
			// compile a list of recurrences per cal_id
1079
			if (!in_array($id,(array)$recur_ids[$row['cal_id']])) $recur_ids[$row['cal_id']][] = $id;
1080
1081
			$events[$id] = Api\Db::strip_array_keys($row,'cal_');
1082
		}
1083
		//_debug_array($events);
1084
		if (count($ids))
1085
		{
1086
			$ids = array_unique($ids);
1087
1088
			// now ready all users with the given cal_id AND (cal_recur_date=0 or the fitting recur-date)
1089
			// This will always read the first entry of each recuring event too, we eliminate it later
1090
			$recur_dates[] = 0;
1091
			$utcal_id_view = " (SELECT * FROM ".$this->user_table." WHERE cal_id IN (".implode(',',$ids).")".
1092
				($filter != 'everything' ? " AND cal_status NOT IN ('X','E')" : '').") utcalid ";
1093
			//$utrecurdate_view = " (select * from ".$this->user_table." where cal_recur_date in (".implode(',',array_unique($recur_dates)).")) utrecurdates ";
1094
			foreach($this->db->select($utcal_id_view,'*',array(
1095
					//'cal_id' => array_unique($ids),
1096
					'cal_recur_date' => $recur_dates,
1097
				),__LINE__,__FILE__,false,'ORDER BY cal_id,cal_user_type DESC,'.self::STATUS_SORT,'calendar',-1,$join='',
1098
				$this->db->get_table_definitions('calendar',$this->user_table)) as $row)	// DESC puts users before resources and contacts
1099
			{
1100
				$id = $row['cal_id'];
1101
				if ($row['cal_recur_date']) $id .= '-'.$row['cal_recur_date'];
1102
1103
				// combine all participant data in uid and status values
1104
				$uid = self::combine_user($row['cal_user_type'], $row['cal_user_id'], $row['cal_user_attendee']);
1105
				$status = self::combine_status($row['cal_status'],$row['cal_quantity'],$row['cal_role']);
1106
1107
				// set accept/reject/tentative of series for all recurrences
1108
				if (!$row['cal_recur_date'])
1109
				{
1110
					foreach((array)$recur_ids[$row['cal_id']] as $i)
1111
					{
1112
						if (isset($events[$i]) && !isset($events[$i]['participants'][$uid]))
1113
						{
1114
							$events[$i]['participants'][$uid] = $status;
1115
						}
1116
					}
1117
				}
1118
1119
				// set data, if recurrence is requested
1120
				if (isset($events[$id])) $events[$id]['participants'][$uid] = $status;
1121
			}
1122
			// query recurrance exceptions, if needed: enum_recuring && !daywise is used in calendar_groupdav::get_series($uid,...)
1123
			if (!$params['enum_recuring'] || !$params['daywise'])
1124
			{
1125
				foreach($this->db->select($this->dates_table, 'cal_id,cal_start', array(
1126
					'cal_id' => $ids,
1127
					'recur_exception' => true,
1128
				), __LINE__, __FILE__, false, 'ORDER BY cal_id,cal_start', 'calendar') as $row)
1129
				{
1130
					// for enum_recurring events are not indexed by cal_id, but $cal_id.'-'.$cal_start
1131
					// find master, which is first recurrence
1132
					if (!isset($events[$id=$row['cal_id']]))
1133
					{
1134
						foreach($events as $id => $event)
1135
						{
1136
							if ($event['id'] == $row['cal_id']) break;
1137
						}
1138
					}
1139
					$events[$id]['recur_exception'][] = $row['cal_start'];
1140
				}
1141
			}
1142
			//custom fields are not shown in the regular views, so we only query them, if explicitly required
1143
			if (!is_null($params['cfs']))
1144
			{
1145
				$where = array('cal_id' => $ids);
1146
				if ($params['cfs']) $where['cal_extra_name'] = $params['cfs'];
1147
				foreach($this->db->select($this->extra_table,'*',$where,
1148
					__LINE__,__FILE__,false,'','calendar') as $row)
1149
				{
1150
					foreach((array)$recur_ids[$row['cal_id']] as $id)
1151
					{
1152
						if (isset($events[$id]))
1153
						{
1154
							$events[$id]['#'.$row['cal_extra_name']] = $row['cal_extra_value'];
1155
						}
1156
					}
1157
				}
1158
			}
1159
			// alarms
1160
			foreach($this->read_alarms($ids) as $cal_id => $alarms)
1161
			{
1162
				foreach($alarms as $id => $alarm)
1163
				{
1164
					$event_start = $alarm['time'] + $alarm['offset'];
1165
1166
					if (isset($events[$cal_id]))	// none recuring event
1167
					{
1168
						$events[$cal_id]['alarm'][$id] = $alarm;
1169
					}
1170
					elseif (isset($events[$cal_id.'-'.$event_start]))	// recuring event
1171
					{
1172
						$events[$cal_id.'-'.$event_start]['alarm'][$id] = $alarm;
1173
					}
1174
				}
1175
			}
1176
		}
1177
		//echo "<p>socal::search\n"; _debug_array($events);
1178
		//error_log(__METHOD__."(,filter=".array2string($params['query']).",offset=$offset, num_rows=$num_rows) returning ".count($events)." entries".($offset!==false?" total=$this->total":'').' '.function_backtrace());
1179
		return $events;
1180
	}
1181
1182
	/**
1183
	 * Data returned by calendar_search_union hook
1184
	 */
1185
	private static $integration_data;
1186
1187
	/**
1188
	 * Ask other apps if they want to participate in calendar search / display
1189
	 *
1190
	 * @param &$selects parts of union query
0 ignored issues
show
Bug introduced by
The type parts was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1191
	 * @param $start see search()
1192
	 * @param $end
1193
	 * @param $users as used in calendar_so ($users_raw plus all members and memberships added by calendar_bo)
1194
	 * @param $cat_id
1195
	 * @param $filter
1196
	 * @param $query
1197
	 * @param $users_raw as passed to calendar_bo::search (no members and memberships added)
1198
	 */
1199
	private static function get_union_selects(array &$selects,$start,$end,$users,$cat_id,$filter,$query,$users_raw)
1200
	{
1201
		if (in_array(basename($_SERVER['SCRIPT_FILENAME']),array('groupdav.php','rpc.php','xmlrpc.php','/activesync/index.php')) ||
1202
			!in_array($GLOBALS['egw_info']['flags']['currentapp'],array('calendar','home')))
1203
		{
1204
			return;    // disable integration for GroupDAV, SyncML, ...
1205
		}
1206
		self::$integration_data = Api\Hooks::process(array(
1207
			'location' => 'calendar_search_union',
1208
			'cols'  => $selects[0]['cols'],    // cols to return
1209
			'start' => $start,
1210
			'end'   => $end,
1211
			'users' => $users,
1212
			'users_raw' => $users_raw,
1213
			'cat_id'=> $cat_id,
1214
			'filter'=> $filter,
1215
			'query' => $query,
1216
		));
1217
		foreach(self::$integration_data as $data)
1218
		{
1219
			if (is_array($data['selects']))
1220
			{
1221
				//echo $app; _debug_array($data);
1222
				$selects = array_merge($selects,$data['selects']);
1223
			}
1224
		}
1225
	}
1226
1227
	/**
1228
	 * Get data from last 'calendar_search_union' hook call
1229
	 *
1230
	 * @return array
1231
	 */
1232
	public static function get_integration_data()
1233
	{
1234
		return self::$integration_data;
1235
	}
1236
1237
	/**
1238
	 * Return union cols constructed from application cols and required cols
1239
	 *
1240
	 * Every col not supplied in $app_cols get returned as NULL.
1241
	 *
1242
	 * @param array $app_cols required name => own name pairs
1243
	 * @param string|array $required array or comma separated column names or table.*
1244
	 * @param string $required_app ='calendar'
1245
	 * @return string cols for union query to match ones supplied in $required
1246
	 */
1247
	public static function union_cols(array $app_cols,$required,$required_app='calendar')
1248
	{
1249
		// remove evtl. used DISTINCT, we currently dont need it
1250
		if (($distinct = substr($required,0,9) == 'DISTINCT '))
0 ignored issues
show
Unused Code introduced by
The assignment to $distinct is dead and can be removed.
Loading history...
1251
		{
1252
			$required = substr($required,9);
1253
		}
1254
		$return_cols = array();
1255
		foreach(is_array($required) ? $required : explode(',',$required) as $cols)
1256
		{
1257
			$matches = null;
1258
			if (substr($cols,-2) == '.*')
1259
			{
1260
				$cols = self::get_columns($required_app,substr($cols,0,-2));
1261
			}
1262
			// remove CAST added for PostgreSQL from eg. "CAST(egw_cal.cal_id AS varchar)"
1263
			elseif (preg_match('/CAST\(([a-z0-9_.]+) AS [a-z0-9_]+\)/i', $cols, $matches))
1264
			{
1265
				$cols = $matches[1];
1266
			}
1267
			elseif (strpos($cols,' AS ') !== false)
1268
			{
1269
				list(,$cols) = explode(' AS ',$cols);
1270
			}
1271
			foreach((array)$cols as $col)
1272
			{
1273
				if (substr($col,0,7) == 'egw_cal')	// remove table name
1274
				{
1275
					$col = preg_replace('/^egw_cal[a-z_]*\./','',$col);
1276
				}
1277
				if (isset($app_cols[$col]))
1278
				{
1279
					$return_cols[] = $app_cols[$col];
1280
				}
1281
				else
1282
				{
1283
					$return_cols[] = 'NULL';
1284
				}
1285
			}
1286
		}
1287
		//error_log(__METHOD__."(".array2string($app_cols).", ".array2string($required).", '$required_app') returning ".array2string(implode(',',$return_cols)));
1288
		return implode(',',$return_cols);
1289
	}
1290
1291
	/**
1292
	 * Get columns of given table, taking into account historically different column order of egw_cal table
1293
	 *
1294
	 * @param string $app
1295
	 * @param string $table
1296
	 * @return array of column names
1297
	 */
1298
	static private function get_columns($app,$table)
1299
	{
1300
		if ($table != 'egw_cal')
1301
		{
1302
			$table_def = $GLOBALS['egw']->db->get_table_definitions($app,$table);
1303
			$cols = array_keys($table_def['fd']);
1304
		}
1305
		else
1306
		{
1307
			// special handling for egw_cal, as old databases have a different column order!!!
1308
			$cols =& Api\Cache::getSession(__CLASS__,$table);
1309
1310
			if (is_null($cols))
1311
			{
1312
				$meta = $GLOBALS['egw']->db->metadata($table,true);
1313
				$cols = array_keys($meta['meta']);
1314
			}
1315
		}
1316
		return $cols;
1317
	}
1318
1319
	/**
1320
	 * Checks for conflicts
1321
	 */
1322
1323
/* folowing SQL checks for conflicts completly on DB level
1324
1325
SELECT cal_user_type, cal_user_id, SUM( cal_quantity )
1326
FROM egw_cal, egw_cal_dates, egw_cal_user
1327
LEFT JOIN egw_cal_repeats ON egw_cal.cal_id = egw_cal_repeats.cal_id
1328
WHERE egw_cal.cal_id = egw_cal_dates.cal_id
1329
AND egw_cal.cal_id = egw_cal_user.cal_id
1330
AND (
1331
recur_type IS NULL
1332
AND cal_recur_date =0
1333
OR cal_recur_date = cal_start
1334
)
1335
AND (
1336
(
1337
cal_user_type = 'u'			# user of the checked event
1338
AND cal_user_id
1339
IN ( 7, 5 )
1340
)
1341
AND 1118822400 < cal_end	# start- and end-time of the checked event
1342
AND cal_start <1118833200
1343
)
1344
AND egw_cal.cal_id !=26		# id of the checked event
1345
AND cal_non_blocking !=1
1346
AND cal_status != 'R'
1347
GROUP BY cal_user_type, cal_user_id
1348
ORDER BY cal_user_type, cal_usre_id
1349
1350
*/
1351
1352
	/**
1353
	 * Saves or creates an event
1354
	 *
1355
	 * We always set cal_modified and cal_modifier and for new events cal_uid.
1356
	 * All other column are only written if they are set in the $event parameter!
1357
	 *
1358
	 * @param array $event
1359
	 * @param boolean &$set_recurrences on return: true if the recurrences need to be written, false otherwise
1360
	 * @param int &$set_recurrences_start=0 on return: time from which on the recurrences should be rebuilt, default 0=all
1361
	 * @param int $change_since =0 time from which on the repetitions should be changed, default 0=all
1362
	 * @param int &$etag etag=null etag to check or null, on return new etag
1363
	 * @return boolean|int false on error, 0 if etag does not match, cal_id otherwise
1364
	 */
1365
	function save(&$event,&$set_recurrences,&$set_recurrences_start=0,$change_since=0,&$etag=null)
1366
	{
1367
		if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length']))
1368
		{
1369
			$minimum_uid_length = $GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'];
1370
			if (empty($minimum_uid_length) || $minimum_uid_length<=1) $minimum_uid_length = 8; // we just do not accept no uid, or uid way to short!
1371
		}
1372
		else
1373
		{
1374
			$minimum_uid_length = 8;
1375
		}
1376
1377
		$old_min = $old_duration = 0;
1378
1379
		//error_log(__METHOD__.'('.array2string($event).",$set_recurrences,$change_since,$etag) ".function_backtrace());
1380
1381
		$cal_id = (int) $event['id'];
1382
		unset($event['id']);
1383
		$set_recurrences = $set_recurrences || !$cal_id && $event['recur_type'] != MCAL_RECUR_NONE;
1384
1385
		if ($event['recur_type'] != MCAL_RECUR_NONE &&
1386
			!(int)$event['recur_interval'])
1387
		{
1388
			$event['recur_interval'] = 1;
1389
		}
1390
1391
		// add colum prefix 'cal_' if there's not already a 'recur_' prefix
1392
		foreach(array_keys($event) as $col)
1393
		{
1394
			if ($col[0] != '#' && substr($col,0,6) != 'recur_' && substr($col,0,6) != 'range_' && $col != 'alarm' && $col != 'tz_id' && $col != 'caldav_name')
1395
			{
1396
				$event['cal_'.$col] = $event[$col];
1397
				unset($event[$col]);
1398
			}
1399
		}
1400
		// set range_start/_end, but only if we have cal_start/_end, as otherwise we destroy present values!
1401
		if (isset($event['cal_start'])) $event['range_start'] = $event['cal_start'];
1402
		if (isset($event['cal_end']))
1403
		{
1404
			$event['range_end'] = $event['recur_type'] == MCAL_RECUR_NONE ? $event['cal_end'] :
1405
				($event['recur_enddate'] ? $event['recur_enddate'] : null);
1406
		}
1407
		// ensure that we find mathing entries later on
1408
		if (!is_array($event['cal_category']))
1409
		{
1410
			$categories = array_unique(explode(',',$event['cal_category']));
1411
			sort($categories);
1412
		}
1413
		else
1414
		{
1415
			$categories = array_unique($event['cal_category']);
1416
		}
1417
		sort($categories, SORT_NUMERIC);
1418
1419
		$event['cal_category'] = implode(',',$categories);
1420
1421
		// make sure recurring events never reference to an other recurrent event
1422
		if ($event['recur_type'] != MCAL_RECUR_NONE) $event['cal_reference'] = 0;
1423
1424
		if ($cal_id)
1425
		{
1426
			// query old recurrance information, before updating main table, where recur_endate is now stored
1427
			if ($event['recur_type'] != MCAL_RECUR_NONE)
1428
			{
1429
				$old_repeats = $this->db->select($this->repeats_table, "$this->repeats_table.*,range_end AS recur_enddate",
1430
					"$this->repeats_table.cal_id=".(int)$cal_id, __LINE__, __FILE__,
1431
					false, '', 'calendar', 0, "JOIN $this->cal_table ON $this->repeats_table.cal_id=$this->cal_table.cal_id")->fetch();
1432
			}
1433
			$where = array('cal_id' => $cal_id);
1434
			// read only timezone id, to check if it is changed
1435
			if ($event['recur_type'] != MCAL_RECUR_NONE)
1436
			{
1437
				$old_tz_id = $this->db->select($this->cal_table,'tz_id',$where,__LINE__,__FILE__,'calendar')->fetchColumn();
1438
			}
1439
			if (!is_null($etag)) $where['cal_etag'] = $etag;
1440
1441
			unset($event['cal_etag']);
1442
			$event[] = 'cal_etag=COALESCE(cal_etag,0)+1';	// always update the etag, even if none given to check
1443
1444
			$this->db->update($this->cal_table,$event,$where,__LINE__,__FILE__,'calendar');
1445
1446
			if (!is_null($etag) && $this->db->affected_rows() < 1)
1447
			{
1448
				return 0;	// wrong etag, someone else updated the entry
1449
			}
1450
			if (!is_null($etag)) ++$etag;
1451
		}
1452
		else
1453
		{
1454
			// new event
1455
			if (!$event['cal_owner']) $event['cal_owner'] = $GLOBALS['egw_info']['user']['account_id'];
1456
1457
			if (!$event['cal_id'] && !isset($event['cal_uid'])) $event['cal_uid'] = '';	// uid is NOT NULL!
1458
1459
			$event['cal_etag'] = $etag = 0;
1460
			$this->db->insert($this->cal_table,$event,false,__LINE__,__FILE__,'calendar');
1461
			if (!($cal_id = $this->db->get_last_insert_id($this->cal_table,'cal_id')))
1462
			{
1463
				return false;
1464
			}
1465
		}
1466
		$update = array();
1467
		// event without uid or not strong enough uid
1468
		if (!isset($event['cal_uid']) || strlen($event['cal_uid']) < $minimum_uid_length)
1469
		{
1470
			$update['cal_uid'] = $event['cal_uid'] = Api\CalDAV::generate_uid('calendar',$cal_id);
1471
		}
1472
		// set caldav_name, if not given by caller
1473
		if (empty($event['caldav_name']) && version_compare($GLOBALS['egw_info']['apps']['calendar']['version'], '1.9.003', '>='))
1474
		{
1475
			$update['caldav_name'] = $event['caldav_name'] = $cal_id.'.ics';
1476
		}
1477
		if ($update)
1478
		{
1479
			$this->db->update($this->cal_table, $update, array('cal_id' => $cal_id),__LINE__,__FILE__,'calendar');
1480
		}
1481
1482
		if ($event['recur_type'] == MCAL_RECUR_NONE)
1483
		{
1484
			$this->db->delete($this->dates_table,array(
1485
				'cal_id' => $cal_id),
1486
				__LINE__,__FILE__,'calendar');
1487
1488
			// delete all user-records, with recur-date != 0
1489
			$this->db->delete($this->user_table,array(
1490
				'cal_id' => $cal_id, 'cal_recur_date != 0'),
1491
				__LINE__,__FILE__,'calendar');
1492
1493
			$this->db->delete($this->repeats_table,array(
1494
				'cal_id' => $cal_id),
1495
				__LINE__,__FILE__,'calendar');
1496
1497
			// add exception marker to master, so participants added to exceptions *only* get found
1498
			if ($event['cal_reference'])
1499
			{
1500
				$master_participants = array();
1501
				foreach($this->db->select($this->user_table, 'cal_user_type,cal_user_id,cal_user_attendee', array(
1502
					'cal_id' => $event['cal_reference'],
1503
					'cal_recur_date' => 0,
1504
					"cal_status != 'X'",	// deleted need to be replaced with exception marker too
1505
				), __LINE__, __FILE__, 'calendar') as $row)
1506
				{
1507
					$master_participants[] = self::combine_user($row['cal_user_type'], $row['cal_user_id'], $row['cal_user_attendee']);
1508
				}
1509
				foreach(array_diff(array_keys((array)$event['cal_participants']), $master_participants) as $uid)
1510
				{
1511
					$user_type = $user_id = null;
1512
					self::split_user($uid, $user_type, $user_id, true);
1513
					$this->db->insert($this->user_table, array(
1514
						'cal_status' => 'E',
1515
						'cal_user_attendee' => $user_type == 'e' ? substr($uid, 1) : null,
1516
					), array(
1517
						'cal_id' => $event['cal_reference'],
1518
						'cal_recur_date' => 0,
1519
						'cal_user_type' => $user_type,
1520
						'cal_user_id' => $user_id,
1521
					), __LINE__, __FILE__, 'calendar');
1522
				}
1523
			}
1524
		}
1525
		else // write information about recuring event, if recur_type is present in the array
1526
		{
1527
			// fetch information about the currently saved (old) event
1528
			$old_min = (int) $this->db->select($this->dates_table,'MIN(cal_start)',array('cal_id'=>$cal_id),__LINE__,__FILE__,false,'','calendar')->fetchColumn();
1529
			$old_duration = (int) $this->db->select($this->dates_table,'MIN(cal_end)',array('cal_id'=>$cal_id),__LINE__,__FILE__,false,'','calendar')->fetchColumn() - $old_min;
1530
			$old_exceptions = array();
1531
			foreach($this->db->select($this->dates_table, 'cal_start', array(
1532
				'cal_id' => $cal_id,
1533
				'recur_exception' => true
1534
			), __LINE__, __FILE__, false, 'ORDER BY cal_start', 'calendar') as $row)
1535
			{
1536
				$old_exceptions[] = $row['cal_start'];
1537
			}
1538
1539
			$event['recur_exception'] = is_array($event['recur_exception']) ? $event['recur_exception'] : array();
1540
			if (!empty($event['recur_exception']))
1541
			{
1542
				sort($event['recur_exception']);
1543
			}
1544
1545
			$where = array(
1546
				'cal_id' => $cal_id,
1547
				'cal_recur_date' => 0,
1548
			);
1549
			$old_participants = array();
1550
			foreach ($this->db->select($this->user_table,'cal_user_type,cal_user_id,cal_user_attendee,cal_status,cal_quantity,cal_role', $where,
1551
				__LINE__,__FILE__,false,'','calendar') as $row)
1552
			{
1553
				$uid = self::combine_user($row['cal_user_type'], $row['cal_user_id'], $row['cal_user_attendee']);
1554
				$status = self::combine_status($row['cal_status'], $row['cal_quantity'], $row['cal_role']);
1555
				$old_participants[$uid] = $status;
1556
			}
1557
1558
			// re-check: did so much recurrence data change that we have to rebuild it from scratch?
1559
			if (!$set_recurrences)
1560
			{
1561
				$set_recurrences = (isset($event['cal_start']) && (int)$old_min != (int) $event['cal_start']) ||
1562
				    $event['recur_type'] != $old_repeats['recur_type'] || $event['recur_data'] != $old_repeats['recur_data'] ||
1563
					(int)$event['recur_interval'] != (int)$old_repeats['recur_interval'] || $event['tz_id'] != $old_tz_id;
1564
			}
1565
1566
			if ($set_recurrences)
1567
			{
1568
				// too much recurrence data has changed, we have to do a rebuild from scratch
1569
				// delete all, but the lowest dates record
1570
				$this->db->delete($this->dates_table,array(
1571
					'cal_id' => $cal_id,
1572
					'cal_start > '.(int)$old_min,
1573
				),__LINE__,__FILE__,'calendar');
1574
1575
				// delete all user-records, with recur-date != 0
1576
				$this->db->delete($this->user_table,array(
1577
					'cal_id' => $cal_id,
1578
					'cal_recur_date != 0',
1579
				),__LINE__,__FILE__,'calendar');
1580
			}
1581
			else
1582
			{
1583
				// we adjust some possibly changed recurrences manually
1584
				// deleted exceptions: re-insert recurrences into the user and dates table
1585
				if (count($deleted_exceptions = array_diff($old_exceptions,$event['recur_exception'])))
1586
				{
1587
					if (isset($event['cal_participants']))
1588
					{
1589
						$participants = $event['cal_participants'];
1590
					}
1591
					else
1592
					{
1593
						// use old default
1594
						$participants = $old_participants;
1595
					}
1596
					foreach($deleted_exceptions as $id => $deleted_exception)
1597
					{
1598
						// rebuild participants for the re-inserted recurrence
1599
						$this->recurrence($cal_id, $deleted_exception, $deleted_exception + $old_duration, $participants);
1600
					}
1601
				}
1602
1603
				// check if recurrence enddate was adjusted
1604
				if(isset($event['recur_enddate']))
1605
				{
1606
					// recurrences need to be truncated
1607
					if((int)$event['recur_enddate'] > 0 &&
1608
						((int)$old_repeats['recur_enddate'] == 0 || (int)$old_repeats['recur_enddate'] > (int)$event['recur_enddate'])
1609
					)
1610
					{
1611
						$this->db->delete($this->user_table,array('cal_id' => $cal_id,'cal_recur_date >= '.($event['recur_enddate'] + 1*DAY_s)),__LINE__,__FILE__,'calendar');
1612
						$this->db->delete($this->dates_table,array('cal_id' => $cal_id,'cal_start >= '.($event['recur_enddate'] + 1*DAY_s)),__LINE__,__FILE__,'calendar');
1613
					}
1614
1615
					// recurrences need to be expanded
1616
					if(((int)$event['recur_enddate'] == 0 && (int)$old_repeats['recur_enddate'] > 0)
1617
						|| ((int)$event['recur_enddate'] > 0 && (int)$old_repeats['recur_enddate'] > 0 && (int)$old_repeats['recur_enddate'] < (int)$event['recur_enddate'])
1618
					)
1619
					{
1620
						$set_recurrences = true;
1621
						$set_recurrences_start = ($old_repeats['recur_enddate'] + 1*DAY_s);
1622
					}
1623
					//error_log(__METHOD__."() event[recur_enddate]=$event[recur_enddate], old_repeats[recur_enddate]=$old_repeats[recur_enddate] --> set_recurrences=".array2string($set_recurrences).", set_recurrences_start=$set_recurrences_start");
1624
				}
1625
1626
				// truncate recurrences by given exceptions
1627
				if (count($event['recur_exception']))
1628
				{
1629
					// added and existing exceptions: delete the execeptions from the user table, it could be the first time
1630
					$this->db->delete($this->user_table,array('cal_id' => $cal_id,'cal_recur_date' => $event['recur_exception']),__LINE__,__FILE__,'calendar');
1631
					// update recur_exception flag based on current exceptions
1632
					$this->db->update($this->dates_table, 'recur_exception='.$this->db->expression($this->dates_table,array(
1633
						'cal_start' => $event['recur_exception'],
1634
					)), array(
1635
						'cal_id' => $cal_id,
1636
					), __LINE__, __FILE__, 'calendar');
1637
				}
1638
			}
1639
1640
			// write the repeats table
1641
			unset($event[0]);	// unset the 'etag=etag+1', as it's not in the repeats table
1642
			$this->db->insert($this->repeats_table,$event,array('cal_id' => $cal_id),__LINE__,__FILE__,'calendar');
1643
		}
1644
		// update start- and endtime if present in the event-array, evtl. we need to move all recurrences
1645
		if (isset($event['cal_start']) && isset($event['cal_end']))
1646
		{
1647
			$this->move($cal_id,$event['cal_start'],$event['cal_end'],!$cal_id ? false : $change_since, $old_min, $old_min +  $old_duration);
1648
		}
1649
		// update participants if present in the event-array
1650
		if (isset($event['cal_participants']))
1651
		{
1652
			$this->participants($cal_id,$event['cal_participants'],!$cal_id ? false : $change_since);
1653
		}
1654
		// Custom fields
1655
		Api\Storage\Customfields::handle_files('calendar', $cal_id, $event);
1656
1657
		foreach($event as $name => $value)
1658
		{
1659
			if ($name[0] == '#')
1660
			{
1661
				if (is_array($value) && array_key_exists('id',$value))
1662
				{
1663
					//error_log(__METHOD__.__LINE__."$name => ".array2string($value).function_backtrace());
1664
					$value = $value['id'];
1665
					//error_log(__METHOD__.__LINE__."$name => ".array2string($value));
1666
				}
1667
				if ($value)
1668
				{
1669
					$this->db->insert($this->extra_table,array(
1670
						'cal_extra_value'	=> is_array($value) ? implode(',',$value) : $value,
1671
					),array(
1672
						'cal_id'			=> $cal_id,
1673
						'cal_extra_name'	=> substr($name,1),
1674
					),__LINE__,__FILE__,'calendar');
1675
				}
1676
				else
1677
				{
1678
					$this->db->delete($this->extra_table,array(
1679
						'cal_id'			=> $cal_id,
1680
						'cal_extra_name'	=> substr($name,1),
1681
					),__LINE__,__FILE__,'calendar');
1682
				}
1683
			}
1684
		}
1685
		// updating or saving the alarms; new alarms have a temporary numeric id!
1686
		if (is_array($event['alarm']))
1687
		{
1688
			foreach ($event['alarm'] as $id => $alarm)
1689
			{
1690
				if ($alarm['id'] && strpos($alarm['id'], 'cal:'.$cal_id.':') !== 0)
1691
				{
1692
					unset($alarm['id']);	// unset the temporary id to add the alarm
1693
				}
1694
				if(!isset($alarm['offset']))
1695
				{
1696
					$alarm['offset'] = $event['cal_start'] - $alarm['time'];
1697
				}
1698
				elseif (!isset($alarm['time']))
1699
				{
1700
					$alarm['time'] = $event['cal_start'] - $alarm['offset'];
1701
				}
1702
1703
				if ($alarm['time'] < time() && !self::shift_alarm($event, $alarm))
1704
				{
1705
					continue;	// pgoerzen: don't add alarm in the past
1706
				}
1707
				$this->save_alarm($cal_id, $alarm, false);	// false: not update modified, we do it anyway
1708
			}
1709
		}
1710
		if (is_null($etag))
1711
		{
1712
			$etag = $this->db->select($this->cal_table,'cal_etag',array('cal_id' => $cal_id),__LINE__,__FILE__,false,'','calendar')->fetchColumn();
1713
		}
1714
1715
		// if event is an exception: update modified of master, to force etag, ctag and sync-token change
1716
		if ($event['cal_reference'])
1717
		{
1718
			$this->updateModified($event['cal_reference']);
1719
		}
1720
		return $cal_id;
1721
	}
1722
1723
	/**
1724
	 * Shift alarm on recurring events to next future recurrence
1725
	 *
1726
	 * @param array $_event event with optional 'cal_' prefix in keys
1727
	 * @param array &$alarm
1728
	 * @param int $timestamp For recurring events, this is the date we
1729
	 *	are dealing with, default is now.
1730
	 * @return boolean true if alarm could be shifted, false if not
1731
	 */
1732
	public static function shift_alarm(array $_event, array &$alarm, $timestamp=null)
1733
	{
1734
		if ($_event['recur_type'] == MCAL_RECUR_NONE)
1735
		{
1736
			return false;
1737
		}
1738
		$start = $timestamp ? $timestamp : (int)time() + $alarm['offset'];
1739
		$event = Api\Db::strip_array_keys($_event, 'cal_');
1740
		$rrule = calendar_rrule::event2rrule($event, false);
1741
		foreach ($rrule as $time)
1742
		{
1743
			if ($start < ($ts = Api\DateTime::to($time,'server')))
1744
			{
1745
				$alarm['time'] = $ts - $alarm['offset'];
1746
				return true;
1747
			}
1748
		}
1749
		return false;
1750
	}
1751
1752
	/**
1753
	 * moves an event to an other start- and end-time taken into account the evtl. recurrences of the event(!)
1754
	 *
1755
	 * @param int $cal_id
1756
	 * @param int $start new starttime
1757
	 * @param int $end new endtime
1758
	 * @param int|boolean $change_since =0 false=new entry, > 0 time from which on the repetitions should be changed, default 0=all
1759
	 * @param int $old_start =0 old starttime or (default) 0, to query it from the db
1760
	 * @param int $old_end =0 old starttime or (default) 0
1761
	 * @todo Recalculate recurrences, if timezone changes
1762
	 * @return int|boolean number of moved recurrences or false on error
1763
	 */
1764
	function move($cal_id,$start,$end,$change_since=0,$old_start=0,$old_end=0)
1765
	{
1766
		//echo "<p>socal::move($cal_id,$start,$end,$change_since,$old_start,$old_end)</p>\n";
1767
1768
		if (!(int) $cal_id) return false;
1769
1770
		if (!$old_start)
1771
		{
1772
			if ($change_since !== false) $row = $this->db->select($this->dates_table,'MIN(cal_start) AS cal_start,MIN(cal_end) AS cal_end',
1773
				array('cal_id'=>$cal_id),__LINE__,__FILE__,false,'','calendar')->fetch();
1774
			// if no recurrence found, create one with the new dates
1775
			if ($change_since === false || !$row || !$row['cal_start'] || !$row['cal_end'])
1776
			{
1777
				$this->db->insert($this->dates_table,array(
1778
					'cal_id'    => $cal_id,
1779
					'cal_start' => $start,
1780
					'cal_end'   => $end,
1781
				),false,__LINE__,__FILE__,'calendar');
1782
1783
				return 1;
1784
			}
1785
			$move_start = (int) ($start-$row['cal_start']);
1786
			$move_end   = (int) ($end-$row['cal_end']);
1787
		}
1788
		else
1789
		{
1790
			$move_start = (int) ($start-$old_start);
1791
			$move_end   = (int) ($end-$old_end);
1792
		}
1793
		$where = 'cal_id='.(int)$cal_id;
1794
1795
		if ($move_start)
1796
		{
1797
			// move the recur-date of the participants
1798
			$this->db->query("UPDATE $this->user_table SET cal_recur_date=cal_recur_date+$move_start WHERE $where AND cal_recur_date ".
1799
				((int)$change_since ? '>= '.(int)$change_since : '!= 0'),__LINE__,__FILE__);
1800
		}
1801
		if ($move_start || $move_end)
1802
		{
1803
			// move the event and it's recurrences
1804
			$this->db->query("UPDATE $this->dates_table SET cal_start=cal_start+$move_start,cal_end=cal_end+$move_end WHERE $where".
1805
				((int) $change_since ? ' AND cal_start >= '.(int) $change_since : ''),__LINE__,__FILE__);
1806
		}
1807
		return $this->db->affected_rows();
1808
	}
1809
1810
	/**
1811
	 * Format attendee as email
1812
	 *
1813
	 * @param string|array $attendee attendee information: email, json or array with attr cn and url
1814
	 * @return type
1815
	 */
1816
	static function attendee2email($attendee)
1817
	{
1818
		if (is_string($attendee) && $attendee[0] == '{' && substr($attendee, -1) == '}')
1819
		{
1820
			$user_attendee = json_decode($user_attendee, true);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $user_attendee seems to be never defined.
Loading history...
1821
		}
1822
		if (is_array($attendee))
1823
		{
1824
			$email = !empty($attendee['email']) ? $user_attendee['email'] :
1825
				(strtolower(substr($attendee['url'], 0, 7)) == 'mailto:' ? substr($user_attendee['url'], 7) : $attendee['url']);
1826
			$attendee = !empty($attendee['cn']) ? $attendee['cn'].' <'.$email.'>' : $email;
1827
		}
1828
		return $attendee;
1829
	}
1830
	/**
1831
	 * combines user_type and user_id into a single string or integer (for users)
1832
	 *
1833
	 * @param string $user_type 1-char type: 'u' = user, ...
1834
	 * @param string|int $user_id id
1835
	 * @param string|array $attendee attendee information: email, json or array with attr cn and url
1836
	 * @return string|int combined id
1837
	 */
1838
	static function combine_user($user_type, $user_id, $attendee=null)
1839
	{
1840
		if (!$user_type || $user_type == 'u')
1841
		{
1842
			return (int) $user_id;
1843
		}
1844
		if ($user_type == 'e' && $attendee)
1845
		{
1846
			$user_id = self::attendee2email($attendee);
1847
		}
1848
		return $user_type.$user_id;
1849
	}
1850
1851
	/**
1852
	 * splits the combined user_type and user_id into a single values
1853
	 *
1854
	 * This is the only method building (normalized) md5 hashes for user_type="e",
1855
	 * if called with $md5_email=true parameter!
1856
	 *
1857
	 * @param string|int $uid
1858
	 * @param string &$user_type 1-char type: 'u' = user, ...
1859
	 * @param string|int &$user_id id
1860
	 * @param boolean $md5_email =false md5 hash user_id for email / user_type=="e"
1861
	 */
1862
	static function split_user($uid, &$user_type, &$user_id, $md5_email=false)
1863
	{
1864
		if (is_numeric($uid))
1865
		{
1866
			$user_type = 'u';
1867
			$user_id = (int) $uid;
1868
		}
1869
		// create md5 hash from lowercased and trimed raw email ("[email protected]", not "Ralf Becker <[email protected]>")
1870
		elseif ($md5_email && $uid[0] == 'e')
1871
		{
1872
			$user_type = $uid[0];
1873
			$email = substr($uid, 1);
1874
			$matches = null;
1875
			if (preg_match('/<([^<>]+)>$/', $email, $matches)) $email = $matches[1];
1876
			$user_id = md5(trim(strtolower($email)));
1877
		}
1878
		else
1879
		{
1880
			$user_type = $uid[0];
1881
			$user_id = substr($uid,1);
1882
		}
1883
	}
1884
1885
	/**
1886
	 * Combine status, quantity and role into one value
1887
	 *
1888
	 * @param string $status status letter: U, T, A, R
1889
	 * @param int $quantity =1
1890
	 * @param string $role ='REQ-PARTICIPANT'
1891
	 * @return string
1892
	 */
1893
	static function combine_status($status,$quantity=1,$role='REQ-PARTICIPANT')
1894
	{
1895
		if ((int)$quantity > 1) $status .= (int)$quantity;
1896
		if ($role != 'REQ-PARTICIPANT') $status .= $role;
1897
1898
		return $status;
1899
	}
1900
1901
	/**
1902
	 * splits the combined status, quantity and role
1903
	 *
1904
	 * @param string &$status I: combined value, O: status letter: U, T, A, R
1905
	 * @param int &$quantity=null only O: quantity
1906
	 * @param string &$role=null only O: role
1907
	 * @return string status U, T, A or R, same as $status parameter on return
1908
	 */
1909
	static function split_status(&$status,&$quantity=null,&$role=null)
1910
	{
1911
		$quantity = 1;
1912
		$role = 'REQ-PARTICIPANT';
1913
		//error_log(__METHOD__.__LINE__.array2string($status));
1914
		$matches = null;
1915
		if (is_string($status) && strlen($status) > 1 && preg_match('/^.([0-9]*)(.*)$/',$status,$matches))
1916
		{
1917
			if ((int)$matches[1] > 0) $quantity = (int)$matches[1];
1918
			if ($matches[2]) $role = $matches[2];
1919
			$status = $status[0];
1920
		}
1921
		elseif ($status === true)
1922
		{
1923
			$status = 'U';
1924
		}
1925
		return $status;
1926
	}
1927
1928
	/**
1929
	 * updates the participants of an event, taken into account the evtl. recurrences of the event(!)
1930
	 * this method just adds new participants or removes not longer set participants
1931
	 * this method does never overwrite existing entries (except the 0-recurrence and for delete)
1932
	 *
1933
	 * @param int $cal_id
1934
	 * @param array $participants uid => status pairs
1935
	 * @param int|boolean $change_since =0, false=new event,
1936
	 * 		0=all, > 0 time from which on the repetitions should be changed
1937
	 * @param boolean $add_only =false
1938
	 *		false = add AND delete participants if needed (full list of participants required in $participants)
1939
	 *		true = only add participants if needed, no participant will be deleted (participants to check/add required in $participants)
1940
	 * @return int|boolean number of updated recurrences or false on error
1941
	 */
1942
	function participants($cal_id,$participants,$change_since=0,$add_only=false)
1943
	{
1944
		//error_log(__METHOD__."($cal_id,".array2string($participants).",$change_since,$add_only");
1945
1946
		$recurrences = array();
1947
1948
		// remove group-invitations, they are NOT stored in the db
1949
		foreach($participants as $uid => $status)
1950
		{
1951
			if ($status[0] == 'G')
1952
			{
1953
				unset($participants[$uid]);
1954
			}
1955
		}
1956
		$where = array('cal_id' => $cal_id);
1957
1958
		if ((int) $change_since)
1959
		{
1960
			$where[] = '(cal_recur_date=0 OR cal_recur_date >= '.(int)$change_since.')';
1961
		}
1962
1963
		if ($change_since !== false)
1964
		{
1965
			// find all existing recurrences
1966
			foreach($this->db->select($this->user_table,'DISTINCT cal_recur_date',$where,__LINE__,__FILE__,false,'','calendar') as $row)
1967
			{
1968
				$recurrences[] = $row['cal_recur_date'];
1969
			}
1970
1971
			// update existing entries
1972
			$existing_entries = $this->db->select($this->user_table,'*',$where,__LINE__,__FILE__,false,'ORDER BY cal_recur_date DESC','calendar');
1973
1974
			// create a full list of participants which already exist in the db
1975
			// with status, quantity and role of the earliest recurence
1976
			$old_participants = array();
1977
			foreach($existing_entries as $row)
1978
			{
1979
				$uid = self::combine_user($row['cal_user_type'], $row['cal_user_id'], $row['cal_user_attendee']);
1980
				if ($row['cal_recur_date'] || !isset($old_participants[$uid]))
1981
				{
1982
					$old_participants[$uid] = self::combine_status($row['cal_status'],$row['cal_quantity'],$row['cal_role']);
1983
				}
1984
			}
1985
1986
			// tag participants which should be deleted
1987
			if($add_only === false)
1988
			{
1989
				$deleted = array();
1990
				foreach($existing_entries as $row)
1991
				{
1992
					$uid = self::combine_user($row['cal_user_type'], $row['cal_user_id'], $row['cal_user_attendee']);
1993
					// delete not longer set participants
1994
					if (!isset($participants[$uid]))
1995
					{
1996
						$deleted[$row['cal_user_type']][] = $row['cal_user_id'];
1997
					}
1998
				}
1999
			}
2000
2001
			// only keep added OR status (incl. quantity!) changed participants for further steps
2002
			// we do not touch unchanged (!) existing ones
2003
			foreach($participants as $uid => $status)
2004
			{
2005
				if ($old_participants[$uid] === $status)
2006
				{
2007
					unset($participants[$uid]);
2008
				}
2009
			}
2010
2011
			// delete participants tagged for delete
2012
			if ($add_only === false && count($deleted))
2013
			{
2014
				$to_or = array();
2015
				$table_def = $this->db->get_table_definitions('calendar',$this->user_table);
2016
				foreach($deleted as $type => $ids)
2017
				{
2018
					$to_or[] = $this->db->expression($table_def,array(
2019
						'cal_user_type' => $type,
2020
						'cal_user_id'   => $ids,
2021
					));
2022
				}
2023
				$where[] = '('.implode(' OR ',$to_or).')';
2024
				$where[] = "cal_status!='E'";	// do NOT delete exception marker
2025
				$this->db->update($this->user_table,array('cal_status'=>'X'),$where,__LINE__,__FILE__,'calendar');
2026
			}
2027
		}
2028
2029
		if (count($participants))	// participants which need to be added
2030
		{
2031
			if (!count($recurrences)) $recurrences[] = 0;   // insert the default recurrence
2032
2033
			$delete_deleted = array();
2034
2035
			// update participants
2036
			foreach($participants as $uid => $status)
2037
			{
2038
				$type = $id = $quantity = $role = null;
2039
				self::split_user($uid, $type, $id, true);
2040
				self::split_status($status,$quantity,$role);
2041
				$set = array(
2042
					'cal_status'	  => $status,
2043
					'cal_quantity'	  => $quantity,
2044
					'cal_role'        => $role,
2045
					'cal_user_attendee' => $type == 'e' ? substr($uid, 1) : null,
2046
				);
2047
				foreach($recurrences as $recur_date)
2048
				{
2049
					$this->db->insert($this->user_table,$set,array(
2050
						'cal_id'	      => $cal_id,
2051
						'cal_recur_date'  => $recur_date,
2052
						'cal_user_type'   => $type,
2053
						'cal_user_id' 	  => $id,
2054
					),__LINE__,__FILE__,'calendar');
2055
				}
2056
				// for new or changed group-invitations, remove previously deleted members, so they show up again
2057
				if ($uid < 0)
2058
				{
2059
					$delete_deleted = array_merge($delete_deleted, $GLOBALS['egw']->accounts->members($uid, true));
2060
				}
2061
			}
2062
			if ($delete_deleted)
2063
			{
2064
				$this->db->delete($this->user_table, $where=array(
2065
					'cal_id' => $cal_id,
2066
					'cal_recur_date' => $recurrences,
2067
					'cal_user_type' => 'u',
2068
					'cal_user_id' => array_unique($delete_deleted),
2069
					'cal_status' => 'X',
2070
				),__LINE__,__FILE__,'calendar');
2071
				//error_log(__METHOD__."($cal_id, ".array2string($participants).", since=$change_since, add_only=$add_only) db->delete('$this->user_table', ".array2string($where).") affected ".$this->db->affected_rows().' rows');
2072
			}
2073
		}
2074
		return true;
2075
	}
2076
2077
	/**
2078
	 * set the status of one participant for a given recurrence or for all recurrences since now (includes recur_date=0)
2079
	 *
2080
	 * @param int $cal_id
2081
	 * @param char $user_type 'u' regular user, 'r' resource, 'c' contact
0 ignored issues
show
Bug introduced by
The type char was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
2082
	 * @param int|string $user_id
2083
	 * @param int|char $status numeric status (defines) or 1-char code: 'R', 'U', 'T' or 'A'
2084
	 * @param int $recur_date =0 date to change, or 0 = all since now
2085
	 * @param string $role =null role to set if !is_null($role)
2086
	 * @param string $attendee =null extra attendee information to set for all types (incl. accounts!)
2087
	 * @return int number of changed recurrences
2088
	 */
2089
	function set_status($cal_id,$user_type,$user_id,$status,$recur_date=0,$role=null,$attendee=null)
2090
	{
2091
		static $status_code_short = array(
2092
			REJECTED 	=> 'R',
2093
			NO_RESPONSE	=> 'U',
2094
			TENTATIVE	=> 'T',
2095
			ACCEPTED	=> 'A',
2096
			DELEGATED	=> 'D'
2097
		);
2098
		if (!(int)$cal_id || !(int)$user_id && $user_type != 'e')
2099
		{
2100
			return false;
2101
		}
2102
2103
		if (is_numeric($status)) $status = $status_code_short[$status];
2104
2105
		$uid = self::combine_user($user_type, $user_id);
2106
		$user_id_md5 = null;
2107
		self::split_user($uid, $user_type, $user_id_md5, true);
2108
2109
		$where = array(
2110
			'cal_id'		=> $cal_id,
2111
			'cal_user_type'	=> $user_type,
2112
			'cal_user_id'   => $user_id_md5,
2113
		);
2114
		if ((int) $recur_date)
2115
		{
2116
			$where['cal_recur_date'] = $recur_date;
2117
		}
2118
		else
2119
		{
2120
			$where[] = '(cal_recur_date=0 OR cal_recur_date >= '.time().')';
2121
		}
2122
2123
		if ($status == 'G')		// remove group invitations, as we dont store them in the db
2124
		{
2125
			$this->db->delete($this->user_table,$where,__LINE__,__FILE__,'calendar');
2126
			$ret = $this->db->affected_rows();
2127
		}
2128
		else
2129
		{
2130
			$set = array('cal_status' => $status);
2131
			if ($user_type == 'e' || $attendee) $set['cal_user_attendee'] = $attendee ? $attendee : $user_id;
2132
			if (!is_null($role) && $role != 'REQ-PARTICIPANT') $set['cal_role'] = $role;
2133
			$this->db->insert($this->user_table,$set,$where,__LINE__,__FILE__,'calendar');
2134
			// for new or changed group-invitations, remove previously deleted members, so they show up again
2135
			if (($ret = $this->db->affected_rows()) && $user_type == 'u' && $user_id < 0)
2136
			{
2137
				$where['cal_user_id'] = $GLOBALS['egw']->accounts->members($user_id, true);
2138
				$where['cal_status'] = 'X';
2139
				$this->db->delete($this->user_table, $where, __LINE__, __FILE__, 'calendar');
2140
				//error_log(__METHOD__."($cal_id,$user_type,$user_id,$status,$recur_date) = $ret, db->delete('$this->user_table', ".array2string($where).") affected ".$this->db->affected_rows().' rows');
2141
			}
2142
		}
2143
		// update modified and modifier in main table
2144
		if ($ret)
2145
		{
2146
			$this->updateModified($cal_id, true);	// true = update series master too
2147
		}
2148
		//error_log(__METHOD__."($cal_id,$user_type,$user_id,$status,$recur_date) = $ret");
2149
		return $ret;
2150
	}
2151
2152
	/**
2153
	 * creates or update a recurrence in the dates and users table
2154
	 *
2155
	 * @param int $cal_id
2156
	 * @param int $start
2157
	 * @param int $end
2158
	 * @param array $participants uid => status pairs
2159
	 * @param boolean $exception =null true or false to set recure_exception flag, null leave it unchanged (new are by default no exception)
2160
	 */
2161
	function recurrence($cal_id,$start,$end,$participants,$exception=null)
2162
	{
2163
		//error_log(__METHOD__."($cal_id, $start, $end, ".array2string($participants).", ".array2string($exception));
2164
		$update = array('cal_end' => $end);
2165
		if (isset($exception)) $update['recur_exception'] = $exception;
2166
2167
		$this->db->insert($this->dates_table, $update, array(
2168
			'cal_id' => $cal_id,
2169
			'cal_start'  => $start,
2170
		),__LINE__,__FILE__,'calendar');
2171
2172
		if (!is_array($participants))
0 ignored issues
show
introduced by
The condition is_array($participants) is always true.
Loading history...
2173
		{
2174
			error_log(__METHOD__."($cal_id, $start, $end, ".array2string($participants).") participants is NO array! ".function_backtrace());
2175
		}
2176
		if ($exception !== true)
2177
		{
2178
			foreach($participants as $uid => $status)
2179
			{
2180
				if ($status == 'G') continue;	// dont save group-invitations
2181
2182
				$type = '';
2183
				$id = null;
2184
				self::split_user($uid, $type, $id, true);
2185
				$quantity = $role = null;
2186
				self::split_status($status,$quantity,$role);
2187
				$this->db->insert($this->user_table,array(
2188
					'cal_status'	=> $status,
2189
					'cal_quantity'	=> $quantity,
2190
					'cal_role'		=> $role,
2191
					'cal_user_attendee' => $type == 'e' ? substr($uid, 1) : null,
2192
				),array(
2193
					'cal_id'		 => $cal_id,
2194
					'cal_recur_date' => $start,
2195
					'cal_user_type'  => $type,
2196
					'cal_user_id' 	 => $id,
2197
				),__LINE__,__FILE__,'calendar');
2198
			}
2199
		}
2200
	}
2201
2202
	/**
2203
	 * Get all unfinished recuring events (or all users) after a given time
2204
	 *
2205
	 * @param int $time
2206
	 * @return array with cal_id => max(cal_start) pairs
2207
	 */
2208
	function unfinished_recuring($time)
2209
	{
2210
		$ids = array();
2211
		foreach($rs=$this->db->select($this->repeats_table, "$this->repeats_table.cal_id,MAX(cal_start) AS cal_start",
0 ignored issues
show
Unused Code introduced by
The assignment to $rs is dead and can be removed.
Loading history...
2212
			'(range_end IS NULL OR range_end > '.(int)$time.')',
2213
			__LINE__, __FILE__, false, "GROUP BY $this->repeats_table.cal_id,range_end", 'calendar', 0,
2214
			" JOIN $this->cal_table ON $this->repeats_table.cal_id=$this->cal_table.cal_id".
2215
			" JOIN $this->dates_table ON $this->repeats_table.cal_id=$this->dates_table.cal_id") as $row)
2216
		{
2217
			$ids[$row['cal_id']] = $row['cal_start'];
2218
		}
2219
		//error_log(__METHOD__."($time) query='$rs->sql' --> ids=".array2string($ids));
2220
		return $ids;
2221
	}
2222
2223
	/**
2224
	 * deletes an event incl. all recurrences, participants and alarms
2225
	 *
2226
	 * @param int $cal_id
2227
	 */
2228
	function delete($cal_id)
2229
	{
2230
		//echo "<p>socal::delete($cal_id)</p>\n";
2231
2232
		$this->delete_alarms($cal_id);
2233
2234
		// update timestamp of series master, updates own timestamp too, which does not hurt ;-)
2235
		$this->updateModified($cal_id, true);
2236
2237
		foreach($this->all_tables as $table)
2238
		{
2239
			$this->db->delete($table,array('cal_id'=>$cal_id),__LINE__,__FILE__,'calendar');
2240
		}
2241
	}
2242
2243
	/**
2244
	 * Delete all events that were before the given date.
2245
	 *
2246
	 * Recurring events that finished before the date will be deleted.
2247
	 * Recurring events that span the date will be ignored.  Non-recurring
2248
	 * events before the date will be deleted.
2249
	 *
2250
	 * @param int $date
2251
	 */
2252
	function purge($date)
2253
	{
2254
		// with new range_end we simple delete all with range_end < $date (range_end NULL is never returned)
2255
		foreach($this->db->select($this->cal_table, 'cal_id', 'range_end < '.(int)$date, __LINE__, __FILE__, false, '', 'calendar') as $row)
2256
		{
2257
			//echo __METHOD__." About to delete".$row['cal_id']."\r\n";
2258
			foreach($this->all_tables as $table)
2259
			{
2260
				$this->db->delete($table, array('cal_id'=>$row['cal_id']), __LINE__, __FILE__, 'calendar');
2261
			}
2262
			// handle links
2263
			Link::unlink('', 'calendar', $row['cal_id']);
2264
		}
2265
	}
2266
2267
	/**
2268
	 * Caches all alarms read from async table to not re-read them in same request
2269
	 *
2270
	 * @var array cal_id => array(async_id => data)
2271
	 */
2272
	static $alarm_cache;
2273
2274
	/**
2275
	 * read the alarms of one or more calendar-event(s) specified by $cal_id
2276
	 *
2277
	 * alarm-id is a string of 'cal:'.$cal_id.':'.$alarm_nr, it is used as the job-id too
2278
	 *
2279
	 * @param int|array $cal_id
2280
	 * @param boolean $update_cache =null true: re-read given $cal_id, false: delete given $cal_id
2281
	 * @return array of (cal_id => array of) alarms with alarm-id as key
2282
	 */
2283
	function read_alarms($cal_id, $update_cache=null)
2284
	{
2285
		if (!isset(self::$alarm_cache) && is_array($cal_id))
2286
		{
2287
			self::$alarm_cache = array();
2288
			if (($jobs = $this->async->read('cal:%')))
2289
			{
2290
				foreach($jobs as $id => $job)
2291
				{
2292
					$alarm         = $job['data'];	// text, enabled
2293
					$alarm['id']   = $id;
2294
					$alarm['time'] = $job['next'];
2295
2296
					self::$alarm_cache[$alarm['cal_id']][$id] = $alarm;
2297
				}
2298
			}
2299
			unset($update_cache);	// just done
2300
		}
2301
		$alarms = array();
2302
2303
		if (isset(self::$alarm_cache))
2304
		{
2305
			if (isset($update_cache))
2306
			{
2307
				foreach((array)$cal_id as $id)
2308
				{
2309
					if ($update_cache === false)
2310
					{
2311
						unset(self::$alarm_cache[$cal_id]);
2312
					}
2313
					elseif($update_cache === true)
2314
					{
2315
						self::$alarm_cache[$cal_id] = $this->read_alarms_nocache($cal_id);
2316
					}
2317
				}
2318
			}
2319
			if (!is_array($cal_id))
2320
			{
2321
				$alarms = (array)self::$alarm_cache[$cal_id];
2322
			}
2323
			else
2324
			{
2325
				foreach($cal_id as $id)
2326
				{
2327
					$alarms[$id] = (array)self::$alarm_cache[$id];
2328
				}
2329
			}
2330
			//error_log(__METHOD__."(".array2string($cal_id).", ".array2string($update_cache).") returning from cache ".array2string($alarms));
2331
			return $alarms;
2332
		}
2333
		return $this->read_alarms_nocache($cal_id);
2334
	}
2335
2336
	private function read_alarms_nocache($cal_id)
2337
	{
2338
		if (($jobs = $this->async->read('cal:'.(int)$cal_id.':%')))
2339
		{
2340
			foreach($jobs as $id => $job)
2341
			{
2342
				$alarm         = $job['data'];	// text, enabled
2343
				$alarm['id']   = $id;
2344
				$alarm['time'] = $job['next'];
2345
2346
				$alarms[$id] = $alarm;
2347
			}
2348
		}
2349
		//error_log(__METHOD__."(".array2string($cal_id).") returning ".array2string($alarms));
2350
		return $alarms ? $alarms : array();
2351
	}
2352
2353
	/**
2354
	 * read a single alarm specified by it's $id
2355
	 *
2356
	 * @param string $id alarm-id is a string of 'cal:'.$cal_id.':'.$alarm_nr, it is used as the job-id too
2357
	 * @return array with data of the alarm
2358
	 */
2359
	function read_alarm($id)
2360
	{
2361
		if (!($jobs = $this->async->read($id)))
2362
		{
2363
			return False;
2364
		}
2365
		$alarm_id = key($jobs);
2366
		$job = current($jobs);
2367
		$alarm         = $job['data'];	// text, enabled
2368
		$alarm['id']   = $alarm_id;
2369
		$alarm['time'] = $job['next'];
2370
2371
		//echo "<p>read_alarm('$id')="; print_r($alarm); echo "</p>\n";
2372
		return $alarm;
2373
	}
2374
2375
	/**
2376
	 * saves a new or updated alarm
2377
	 *
2378
	 * @param int $cal_id Id of the calendar-entry
2379
	 * @param array $alarm array with fields: text, owner, enabled, ..
2380
	 * @param boolean $update_modified =true call update modified, default true
2381
	 * @return string id of the alarm
2382
	 */
2383
	function save_alarm($cal_id, $alarm, $update_modified=true)
2384
	{
2385
		//error_log(__METHOD__."($cal_id, ".array2string($alarm).', '.array2string($update_modified).') '.function_backtrace());
2386
		if (!($id = $alarm['id']))
2387
		{
2388
			$alarms = $this->read_alarms($cal_id);	// find a free alarm#
2389
			$n = count($alarms);
2390
			do
2391
			{
2392
				$id = 'cal:'.(int)$cal_id.':'.$n;
2393
				++$n;
2394
			}
2395
			while (@isset($alarms[$id]));
2396
		}
2397
		else
2398
		{
2399
			$this->async->cancel_timer($id);
2400
		}
2401
		$alarm['cal_id'] = $cal_id;		// we need the back-reference
2402
		// do not deleted async-job, as we need it for alarm snozzing
2403
		$alarm['keep'] = self::ALARM_KEEP_TIME;
2404
		// past alarms need NOT to be triggered, but kept around for a while to allow alarm snozzing
2405
		if ($alarm['time'] < time())
2406
		{
2407
			$alarm['time'] = $alarm['keep_time'] = time()+self::ALARM_KEEP_TIME;
2408
		}
2409
		// add an alarm uid, if none is given
2410
		if (empty($alarm['uid']) && class_exists('Horde_Support_Uuid')) $alarm['uid'] = (string)new Horde_Support_Uuid;
2411
		//error_log(__METHOD__.__LINE__.' Save Alarm for CalID:'.$cal_id.'->'.array2string($alarm).'-->'.$id.'#'.function_backtrace());
2412
		// allways store job with the alarm owner as job-owner to get eg. the correct from address
2413
		if (!$this->async->set_timer($alarm['time'], $id, 'calendar.calendar_boupdate.send_alarm', $alarm, $alarm['owner'], false, true))
2414
		{
2415
			return False;
2416
		}
2417
2418
		// update the modification information of the related event
2419
		if ($update_modified) $this->updateModified($cal_id, true);
2420
2421
		// update cache, if used
2422
		if (isset(self::$alarm_cache)) $this->read_alarms($cal_id, true);
2423
2424
		return $id;
2425
	}
2426
2427
	/**
2428
	 * Delete all alarms of a calendar-entry
2429
	 *
2430
	 * Does not update timestamps of series master, therefore private!
2431
	 *
2432
	 * @param int $cal_id Id of the calendar-entry
2433
	 * @return int number of alarms deleted
2434
	 */
2435
	private function delete_alarms($cal_id)
2436
	{
2437
		//error_log(__METHOD__."($cal_id) ".function_backtrace());
2438
		if (($alarms = $this->read_alarms($cal_id)))
2439
		{
2440
			foreach(array_keys($alarms) as $id)
2441
			{
2442
				$this->async->cancel_timer($id);
2443
			}
2444
			// update cache, if used
2445
			if (isset(self::$alarm_cache)) $this->read_alarms($cal_id, false);
2446
		}
2447
		return count($alarms);
2448
	}
2449
2450
	/**
2451
	 * delete one alarms identified by its id
2452
	 *
2453
	 * @param string $id alarm-id is a string of 'cal:'.$cal_id.':'.$alarm_nr, it is used as the job-id too
2454
	 * @return int number of alarms deleted
2455
	 */
2456
	function delete_alarm($id)
2457
	{
2458
		//error_log(__METHOD__."('$id') ".function_backtrace());
2459
		// update the modification information of the related event
2460
		list(,$cal_id) = explode(':',$id);
2461
		if ($cal_id)
2462
		{
2463
			$this->updateModified($cal_id, true);
2464
		}
2465
		$ret = $this->async->cancel_timer($id);
2466
2467
		// update cache, if used
2468
		if (isset(self::$alarm_cache)) $this->read_alarms($cal_id, true);
2469
2470
		return $ret;
2471
	}
2472
2473
	/**
2474
	 * Delete account hook
2475
	 *
2476
	 * @param array|int $old_user integer old user or array with keys 'account_id' and 'new_owner' as the deleteaccount hook uses it
2477
	 * @param int $new_user =null
2478
	 */
2479
	function deleteaccount($old_user, $new_user=null)
2480
	{
2481
		if (is_array($old_user))
2482
		{
2483
			$new_user = $old_user['new_owner'];
2484
			$old_user = $old_user['account_id'];
2485
		}
2486
		if (!(int)$new_user)
2487
		{
2488
			$user_type = '';
2489
			$user_id = null;
2490
			self::split_user($old_user,$user_type,$user_id);
2491
2492
			if ($user_type == 'u')	// only accounts can be owners of events
2493
			{
2494
				foreach($this->db->select($this->cal_table,'cal_id',array('cal_owner' => $old_user),__LINE__,__FILE__,false,'','calendar') as $row)
2495
				{
2496
					$this->delete($row['cal_id']);
2497
				}
2498
			}
2499
			$this->db->delete($this->user_table,array(
2500
				'cal_user_type' => $user_type,
2501
				'cal_user_id'   => $user_id,
2502
			),__LINE__,__FILE__,'calendar');
2503
2504
			// delete calendar entries without participants (can happen if the deleted user is the only participants, but not the owner)
2505
			foreach($this->db->select($this->cal_table,"DISTINCT $this->cal_table.cal_id",'cal_user_id IS NULL',__LINE__,__FILE__,
2506
				False,'','calendar',0,"LEFT JOIN $this->user_table ON $this->cal_table.cal_id=$this->user_table.cal_id") as $row)
2507
			{
2508
				$this->delete($row['cal_id']);
2509
			}
2510
		}
2511
		else
2512
		{
2513
			$this->db->update($this->cal_table,array('cal_owner' => $new_user),array('cal_owner' => $old_user),__LINE__,__FILE__,'calendar');
2514
			// delete participation of old user, if new user is already a participant
2515
			$ids = array();
2516
			foreach($this->db->select($this->user_table,'cal_id',array(		// MySQL does NOT allow to run this as delete!
2517
				'cal_user_type' => 'u',
2518
				'cal_user_id' => $old_user,
2519
				"cal_id IN (SELECT cal_id FROM $this->user_table other WHERE other.cal_id=cal_id AND other.cal_user_id=".$this->db->quote($new_user)." AND cal_user_type='u')",
2520
			),__LINE__,__FILE__,false,'','calendar') as $row)
2521
			{
2522
				$ids[] = $row['cal_id'];
2523
			}
2524
			if ($ids) $this->db->delete($this->user_table,array(
2525
				'cal_user_type' => 'u',
2526
				'cal_user_id' => $old_user,
2527
				'cal_id' => $ids,
2528
			),__LINE__,__FILE__,'calendar');
2529
			// now change participant in the rest to contain new user instead of old user
2530
			$this->db->update($this->user_table,array(
2531
				'cal_user_id' => $new_user,
2532
			),array(
2533
				'cal_user_type' => 'u',
2534
				'cal_user_id' => $old_user,
2535
			),__LINE__,__FILE__,'calendar');
2536
		}
2537
	}
2538
2539
	/**
2540
	 * get stati of all recurrences of an event for a specific participant
2541
	 *
2542
	 * @param int $cal_id
2543
	 * @param int $uid =null  participant uid; if == null return only the recur dates
2544
	 * @param int $start =0  if != 0: startdate of the search/list (servertime)
2545
	 * @param int $end =0  if != 0: enddate of the search/list (servertime)
2546
	 *
2547
	 * @return array recur_date => status pairs (index 0 => main status)
2548
	 */
2549
	function get_recurrences($cal_id, $uid=null, $start=0, $end=0)
2550
	{
2551
		$participant_status = array();
2552
		$where = array('cal_id' => $cal_id);
2553
		if ($start != 0 && $end == 0) $where[] = '(cal_recur_date = 0 OR cal_recur_date >= ' . (int)$start . ')';
2554
		if ($start == 0 && $end != 0) $where[] = '(cal_recur_date = 0 OR cal_recur_date <= ' . (int)$end . ')';
2555
		if ($start != 0 && $end != 0)
2556
		{
2557
			$where[] = '(cal_recur_date = 0 OR (cal_recur_date >= ' . (int)$start .
2558
						' AND cal_recur_date <= ' . (int)$end . '))';
2559
		}
2560
		foreach($this->db->select($this->user_table,'DISTINCT cal_recur_date',$where,__LINE__,__FILE__,false,'','calendar') as $row)
2561
		{
2562
			// inititalize the array
2563
			$participant_status[$row['cal_recur_date']] = null;
2564
		}
2565
		if (is_null($uid)) return $participant_status;
2566
		$user_type = $user_id = null;
2567
		self::split_user($uid, $user_type, $user_id, true);
2568
2569
		$where2 = array(
2570
			'cal_id'		=> $cal_id,
2571
			'cal_user_type'	=> $user_type ? $user_type : 'u',
2572
			'cal_user_id'   => $user_id,
2573
		);
2574
		if ($start != 0 && $end == 0) $where2[] = '(cal_recur_date = 0 OR cal_recur_date >= ' . (int)$start . ')';
2575
		if ($start == 0 && $end != 0) $where2[] = '(cal_recur_date = 0 OR cal_recur_date <= ' . (int)$end . ')';
2576
		if ($start != 0 && $end != 0)
2577
		{
2578
			$where2[] = '(cal_recur_date = 0 OR (cal_recur_date >= ' . (int)$start .
2579
						' AND cal_recur_date <= ' . (int)$end . '))';
2580
		}
2581
		foreach ($this->db->select($this->user_table,'cal_recur_date,cal_status,cal_quantity,cal_role',$where2,
2582
				__LINE__,__FILE__,false,'','calendar') as $row)
2583
		{
2584
			$status = self::combine_status($row['cal_status'],$row['cal_quantity'],$row['cal_role']);
2585
			$participant_status[$row['cal_recur_date']] = $status;
2586
		}
2587
		return $participant_status;
2588
	}
2589
2590
	/**
2591
	 * get all participants of an event
2592
	 *
2593
	 * @param int $cal_id
2594
	 * @param int $recur_date =0 gives participants of this recurrence, default 0=all
2595
	 *
2596
	 * @return array participants
2597
	 */
2598
	/* seems NOT to be used anywhere, NOT ported to new md5-email schema!
2599
	function get_participants($cal_id, $recur_date=0)
2600
	{
2601
		$participants = array();
2602
		$where = array('cal_id' => $cal_id);
2603
		if ($recur_date)
2604
		{
2605
			$where['cal_recur_date'] = $recur_date;
2606
		}
2607
2608
		foreach ($this->db->select($this->user_table,'DISTINCT cal_user_type,cal_user_id', $where,
2609
				__LINE__,__FILE__,false,'','calendar') as $row)
2610
		{
2611
			$uid = self::combine_user($row['cal_user_type'], $row['cal_user_id']);
2612
			$id = $row['cal_user_type'] . $row['cal_user_id'];
2613
			$participants[$id]['type'] = $row['cal_user_type'];
2614
			$participants[$id]['id'] = $row['cal_user_id'];
2615
			$participants[$id]['uid'] = $uid;
2616
		}
2617
		return $participants;
2618
	}*/
2619
2620
	/**
2621
	 * get all releated events
2622
	 *
2623
	 * @param int $uid					UID of the series
2624
	 *
2625
	 * @return array of event exception ids for all events which share $uid
2626
	 */
2627
	function get_related($uid)
2628
	{
2629
		$where = array(
2630
			'cal_uid'		=> $uid,
2631
		);
2632
		$related = array();
2633
		foreach ($this->db->select($this->cal_table,'cal_id,cal_reference',$where,
2634
				__LINE__,__FILE__,false,'','calendar') as $row)
2635
		{
2636
			if ($row['cal_reference'] != 0)
2637
			{
2638
				// not the series master
2639
				$related[] = $row['cal_id'];
2640
			}
2641
		}
2642
		return $related;
2643
	}
2644
2645
	/**
2646
	 * Gets the exception days of a given recurring event caused by
2647
	 * irregular participant stati or timezone transitions
2648
	 *
2649
	 * @param array $event			Recurring Event.
2650
	 * @param string tz_id=null		timezone for exports (null for event's timezone)
0 ignored issues
show
Documentation Bug introduced by
The doc comment tz_id=null at position 0 could not be parsed: Unknown type name 'tz_id=null' at position 0 in tz_id=null.
Loading history...
2651
	 * @param int $start =0  if != 0: startdate of the search/list (servertime)
2652
	 * @param int $end =0  if != 0:	enddate of the search/list (servertime)
2653
	 * @param string $filter ='all'	string filter-name: all (not rejected),
2654
	 * 		accepted, unknown, tentative, rejected, delegated
2655
	 *      rrule					return array of remote exceptions in servertime
2656
	 * 		tz_rrule/tz_only,		return (only by) timezone transition affected entries
2657
	 * 		map						return array of dates with no pseudo exception
2658
	 * 									key remote occurrence date
2659
	 * 		tz_map					return array of all dates with no tz pseudo exception
2660
	 *
2661
	 * @return array		Array of exception days (false for non-recurring events).
2662
	 */
2663
	function get_recurrence_exceptions($event, $tz_id=null, $start=0, $end=0, $filter='all')
2664
	{
2665
		if (!is_array($event)) return false;
2666
		$cal_id = (int) $event['id'];
2667
		//error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2668
		//		"($cal_id, $tz_id, $filter): " . $event['tzid']);
2669
		if (!$cal_id || $event['recur_type'] == MCAL_RECUR_NONE) return false;
2670
2671
		$days = array();
2672
2673
		$expand_all = (!$this->isWholeDay($event) && $tz_id && $tz_id != $event['tzid']);
2674
2675
		if ($filter == 'tz_only' && !$expand_all) return $days;
2676
2677
		$remote = in_array($filter, array('tz_rrule', 'rrule'));
2678
2679
		$egw_rrule = calendar_rrule::event2rrule($event, false);
2680
		$egw_rrule->current = clone $egw_rrule->time;
2681
		if ($expand_all)
2682
		{
2683
			unset($event['recur_exception']);
2684
			$remote_rrule = calendar_rrule::event2rrule($event, false, $tz_id);
2685
			$remote_rrule->current = clone $remote_rrule->time;
2686
		}
2687
		while ($egw_rrule->valid())
2688
		{
2689
			while ($egw_rrule->exceptions &&
0 ignored issues
show
Bug Best Practice introduced by
The expression $egw_rrule->exceptions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
2690
				in_array($egw_rrule->current->format('Ymd'),$egw_rrule->exceptions))
2691
			{
2692
				if (in_array($filter, array('map','tz_map','rrule','tz_rrule')))
2693
				{
2694
					 // real exception
2695
					$locts = (int)Api\DateTime::to($egw_rrule->current(),'server');
2696
					if ($expand_all)
2697
					{
2698
						$remts = (int)Api\DateTime::to($remote_rrule->current(),'server');
2699
						if ($remote)
2700
						{
2701
							$days[$locts]= $remts;
2702
						}
2703
						else
2704
						{
2705
							$days[$remts]= $locts;
2706
						}
2707
					}
2708
					else
2709
					{
2710
						$days[$locts]= $locts;
2711
					}
2712
				}
2713
				if ($expand_all)
2714
				{
2715
					$remote_rrule->next_no_exception();
2716
				}
2717
				$egw_rrule->next_no_exception();
2718
				if (!$egw_rrule->valid()) return $days;
2719
			}
2720
			$day = $egw_rrule->current();
2721
			$locts = (int)Api\DateTime::to($day,'server');
2722
			$tz_exception = ($filter == 'tz_rrule');
2723
			//error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2724
			//	'()[EVENT Server]: ' . $day->format('Ymd\THis') . " ($locts)");
2725
			if ($expand_all)
2726
			{
2727
				$remote_day = $remote_rrule->current();
2728
				$remts = (int)Api\DateTime::to($remote_day,'server');
2729
			//	error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2730
			//	'()[EVENT Device]: ' . $remote_day->format('Ymd\THis') . " ($remts)");
2731
			}
2732
2733
2734
			if (!($end && $end < $locts) && $start <= $locts)
2735
			{
2736
				// we are within the relevant time period
2737
				if ($expand_all && $day->format('U') != $remote_day->format('U'))
2738
				{
2739
					$tz_exception = true;
2740
					if ($filter != 'map' && $filter != 'tz_map')
2741
					{
2742
						// timezone pseudo exception
2743
						//error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2744
						//	'() tz exception: ' . $day->format('Ymd\THis'));
2745
						if ($remote)
2746
						{
2747
							$days[$locts]= $remts;
2748
						}
2749
						else
2750
						{
2751
							$days[$remts]= $locts;
2752
						}
2753
					}
2754
				}
2755
				if ($filter != 'tz_map' && (!$tz_exception || $filter == 'tz_only') &&
2756
					$this->status_pseudo_exception($event['id'], $locts, $filter))
2757
				{
2758
					// status pseudo exception
2759
					//error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2760
					//	'() status exception: ' . $day->format('Ymd\THis'));
2761
					if ($expand_all)
2762
					{
2763
						if ($filter == 'tz_only')
2764
						{
2765
								unset($days[$remts]);
2766
						}
2767
						else
2768
						{
2769
							if ($filter != 'map')
2770
							{
2771
								if ($remote)
2772
								{
2773
									$days[$locts]= $remts;
2774
								}
2775
								else
2776
								{
2777
									$days[$remts]= $locts;
2778
								}
2779
							}
2780
						}
2781
					}
2782
					elseif ($filter != 'map')
2783
					{
2784
						$days[$locts]= $locts;
2785
					}
2786
				}
2787
				elseif (($filter == 'map' || filter == 'tz_map') &&
0 ignored issues
show
Bug introduced by
The constant filter was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
2788
						!$tz_exception)
2789
				{
2790
					// no pseudo exception date
2791
					if ($expand_all)
2792
					{
2793
2794
						$days[$remts]= $locts;
2795
					}
2796
					else
2797
					{
2798
						$days[$locts]= $locts;
2799
					}
2800
				}
2801
			}
2802
			if ($expand_all)
2803
			{
2804
				$remote_rrule->next_no_exception();
2805
			}
2806
			$egw_rrule->next_no_exception();
2807
		}
2808
		return $days;
2809
	}
2810
2811
	/**
2812
	 * Checks for status only pseudo exceptions
2813
	 *
2814
	 * @param int $cal_id		event id
2815
	 * @param int $recur_date	occurrence to check
2816
	 * @param string $filter	status filter criteria for user
2817
	 *
2818
	 * @return boolean			true, if stati don't match with defaults
2819
	 */
2820
	function status_pseudo_exception($cal_id, $recur_date, $filter)
2821
	{
2822
		static $recurrence_zero=null;
2823
		static $cached_id=null;
2824
		static $user=null;
2825
2826
		if (!isset($cached_id) || $cached_id != $cal_id)
2827
		{
2828
			// get default stati
2829
			$recurrence_zero = array();
2830
			$user = $GLOBALS['egw_info']['user']['account_id'];
2831
			$where = array(
2832
				'cal_id' => $cal_id,
2833
				'cal_recur_date' => 0,
2834
			);
2835
			foreach ($this->db->select($this->user_table,'cal_user_type,cal_user_id,cal_user_attendee,cal_status',$where,
2836
				__LINE__,__FILE__,false,'','calendar') as $row)
2837
			{
2838
				switch ($row['cal_user_type'])
2839
				{
2840
					case 'u':	// account
2841
					case 'c':	// contact
2842
					case 'e':	// email address
2843
						$uid = self::combine_user($row['cal_user_type'], $row['cal_user_id'], $row['cal_user_attendee']);
2844
						$recurrence_zero[$uid] = $row['cal_status'];
2845
				}
2846
			}
2847
			$cached_id = $cal_id;
2848
		}
2849
2850
		//error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2851
		//	"($cal_id, $recur_date, $filter)[DEFAULTS]: " .
2852
		//	array2string($recurrence_zero));
2853
2854
		$participants = array();
2855
		$where = array(
2856
			'cal_id' => $cal_id,
2857
			'cal_recur_date' => $recur_date,
2858
		);
2859
		foreach ($this->db->select($this->user_table,'cal_user_type,cal_user_id,cal_user_attendee,cal_status',$where,
2860
			__LINE__,__FILE__,false,'','calendar') as $row)
2861
		{
2862
			switch ($row['cal_user_type'])
2863
			{
2864
				case 'u':	// account
2865
				case 'c':	// contact
2866
				case 'e':	// email address
2867
					$uid = self::combine_user($row['cal_user_type'], $row['cal_user_id'], $row['cal_user_attendee']);
2868
					$participants[$uid] = $row['cal_status'];
2869
			}
2870
		}
2871
2872
		if (empty($participants)) return false; // occurrence does not exist at all yet
2873
2874
		foreach ($recurrence_zero as $uid => $status)
2875
		{
2876
			if ($uid == $user)
2877
			{
2878
				// handle filter for current user
2879
				switch ($filter)
2880
				{
2881
					case 'unknown':
2882
						if ($status != 'U')
2883
						{
2884
							unset($participants[$uid]);
2885
							continue 2;	// +1 for switch
2886
						}
2887
						break;
2888
					case 'accepted':
2889
						if ($status != 'A')
2890
						{
2891
							unset($participants[$uid]);
2892
							continue 2;	// +1 for switch
2893
						}
2894
						break;
2895
					case 'tentative':
2896
						if ($status != 'T')
2897
						{
2898
							unset($participants[$uid]);
2899
							continue 2;	// +1 for switch
2900
						}
2901
						break;
2902
					case 'rejected':
2903
						if ($status != 'R')
2904
						{
2905
							unset($participants[$uid]);
2906
							continue 2;	// +1 for switch
2907
						}
2908
						break;
2909
					case 'delegated':
2910
						if ($status != 'D')
2911
						{
2912
							unset($participants[$uid]);
2913
							continue 2;	// +1 for switch
2914
						}
2915
						break;
2916
					case 'default':
2917
						if ($status == 'R')
2918
						{
2919
							unset($participants[$uid]);
2920
							continue 2;	// +1 for switch
2921
						}
2922
						break;
2923
					default:
2924
						// All entries
2925
				}
2926
			}
2927
			if (!isset($participants[$uid])
2928
				|| $participants[$uid] != $status)
2929
				return true;
2930
			unset($participants[$uid]);
2931
		}
2932
		return (!empty($participants));
2933
	}
2934
2935
	/**
2936
	 * Check if the event is the whole day
2937
	 *
2938
	 * @param array $event event (all timestamps in servertime)
2939
	 * @return boolean true if whole day event within its timezone, false othwerwise
2940
	 */
2941
	function isWholeDay($event)
2942
	{
2943
		if (!isset($event['start']) || !isset($event['end'])) return false;
2944
2945
		if (empty($event['tzid']))
2946
		{
2947
			$timezone = Api\DateTime::$server_timezone;
2948
		}
2949
		else
2950
		{
2951
			if (!isset(self::$tz_cache[$event['tzid']]))
2952
			{
2953
				self::$tz_cache[$event['tzid']] = calendar_timezones::DateTimeZone($event['tzid']);
2954
			}
2955
			$timezone = self::$tz_cache[$event['tzid']];
2956
		}
2957
		$start_time = new Api\DateTime($event['start'],Api\DateTime::$server_timezone);
2958
		$start_time->setTimezone($timezone);
2959
		$end_time = new Api\DateTime($event['end'],Api\DateTime::$server_timezone);
2960
		$end_time->setTimezone($timezone);
2961
		//error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2962
		//	'(): ' . $start . '-' . $end);
2963
		$start = Api\DateTime::to($start_time,'array');
2964
		$end = Api\DateTime::to($end_time,'array');
2965
2966
2967
		return !$start['hour'] && !$start['minute'] && $end['hour'] == 23 && $end['minute'] == 59;
2968
	}
2969
2970
	/**
2971
	 * Moves a datetime to the beginning of the day within timezone
2972
	 *
2973
	 * @param Api\DateTime	$time	the datetime entry
2974
	 * @param string tz_id		timezone
0 ignored issues
show
Bug introduced by
The type tz_id was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
2975
	 *
2976
	 * @return DateTime
2977
	 */
2978
	function &startOfDay(Api\DateTime $time, $tz_id=null)
2979
	{
2980
		if (empty($tz_id))
2981
		{
2982
			$timezone = Api\DateTime::$server_timezone;
2983
		}
2984
		else
2985
		{
2986
			if (!isset(self::$tz_cache[$tz_id]))
2987
			{
2988
				self::$tz_cache[$tz_id] = calendar_timezones::DateTimeZone($tz_id);
2989
			}
2990
			$timezone = self::$tz_cache[$tz_id];
2991
		}
2992
		return new Api\DateTime($time->format('Y-m-d 00:00:00'), $timezone);
2993
	}
2994
2995
	/**
2996
	 * Updates the modification timestamp to force an etag, ctag and sync-token change
2997
	 *
2998
	 * @param int $id event id
2999
	 * @param int|boolean $update_master =false id of series master or true, to update series master too
3000
	 * @param int $time =null new timestamp, default current (server-)time
3001
	 * @param int $modifier =null uid of the modifier, default current user
3002
	 */
3003
	function updateModified($id, $update_master=false, $time=null, $modifier=null)
3004
	{
3005
		if (is_null($time) || !$time) $time = time();
3006
		if (is_null($modifier)) $modifier = $GLOBALS['egw_info']['user']['account_id'];
3007
3008
		$this->db->update($this->cal_table,
3009
			array('cal_modified' => $time, 'cal_modifier' => $modifier),
3010
			array('cal_id' => $id), __LINE__,__FILE__, 'calendar');
3011
3012
		// if event is an exception: update modified of master, to force etag, ctag and sync-token change
3013
		if ($update_master)
3014
		{
3015
			if ($update_master !== true || ($update_master = $this->db->select($this->cal_table, 'cal_reference', array('cal_id' => $id), __LINE__, __FILE__)->fetchColumn()))
3016
			{
3017
				$this->updateModified($update_master, false, $time, $modifier);
3018
			}
3019
		}
3020
	}
3021
}
3022