calendar_so   F
last analyzed

Complexity

Total Complexity 564

Size/Duplication

Total Lines 2921
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 1251
dl 0
loc 2921
rs 0.8
c 0
b 0
f 0
wmc 564

43 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 2
F events() 0 67 22
B dates_range_view() 0 19 8
B cal_range_view() 0 16 8
B save_alarm() 0 42 9
A read_alarms_nocache() 0 15 4
A delete() 0 12 2
A get_integration_data() 0 3 1
A get_union_selects() 0 24 5
B union_cols() 0 42 10
C get_ctag() 0 52 12
A unfinished_recuring() 0 13 2
A get_columns() 0 19 3
A get_cal_data() 0 12 3
C move() 0 44 13
A startOfDay() 0 15 3
F search() 0 448 93
A shift_alarm() 0 18 5
D status_pseudo_exception() 0 113 28
B recurrence() 0 37 7
B split_status() 0 17 7
D participants() 0 133 24
B isWholeDay() 0 27 8
B updateModified() 0 15 7
C status_filter() 0 51 15
C get_recurrences() 0 39 17
A read_alarm() 0 14 2
A combine_user() 0 11 5
A delete_alarm() 0 15 3
F save() 0 356 87
A purge() 0 12 3
A split_user() 0 20 5
A get_related() 0 16 3
A delete_alarms() 0 13 4
A cat_filter() 0 20 6
C set_status() 0 61 16
A combine_status() 0 6 3
D get_recurrence_exceptions() 0 146 40
B attendee2email() 0 13 8
C read_alarms() 0 51 12
F get_events() 0 132 25
B deleteaccount() 0 57 8
C read() 0 63 16

How to fix   Complexity   

Complex Class

Complex classes like calendar_so often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use calendar_so, and based on these observations, apply Extract Interface, too.

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