Completed
Push — 16.1 ( 1e4888...9df24e )
by Ralf
12:06
created

calendar_so   D

Complexity

Total Complexity 558

Size/Duplication

Total Lines 2898
Duplicated Lines 5.73 %

Coupling/Cohesion

Components 1
Dependencies 10

Importance

Changes 0
Metric Value
wmc 558
lcom 1
cbo 10
dl 166
loc 2898
rs 4.4102
c 0
b 0
f 0

43 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 2
B cal_range_view() 0 17 8
B dates_range_view() 0 20 8
F events() 0 68 22
C read() 0 48 9
F get_events() 19 133 25
C get_ctag() 0 53 12
A get_cal_data() 0 13 3
B cat_filter() 0 17 6
C status_filter() 0 52 15
B shift_alarm() 0 19 5
C move() 12 45 13
B attendee2email() 0 14 8
B combine_user() 0 12 5
B split_user() 0 22 5
A combine_status() 0 7 3
B split_status() 0 18 7
D participants() 8 134 24
C set_status() 0 62 16
C recurrence() 0 40 7
A unfinished_recuring() 0 14 2
A delete() 0 14 2
A purge() 0 21 3
C read_alarms() 0 52 12
A read_alarm() 0 14 2
C save_alarm() 0 36 8
A delete_alarms() 14 14 4
A delete_alarm() 16 16 3
B deleteaccount() 0 59 8
F get_recurrences() 19 40 17
A get_related() 0 17 3
F get_recurrence_exceptions() 28 147 40
F status_pseudo_exception() 24 114 28
C isWholeDay() 4 28 8
A startOfDay() 0 16 3
B updateModified() 0 18 7
A read_alarms_nocache() 0 16 4
F search() 22 451 95
B get_union_selects() 0 27 5
A get_integration_data() 0 4 1
D union_cols() 0 43 10
A get_columns() 0 20 3
F save() 0 355 87

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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
	 * Cached timezone data
125
	 *
126
	 * @var array id => data
127
	 */
128
	protected static $tz_cache = array();
129
130
	/**
131
	 * Constructor of the socal class
132
	 */
133
	function __construct()
134
	{
135
		$this->async = $GLOBALS['egw']->asyncservice;
136
		$this->db = $GLOBALS['egw']->db;
137
138
		$this->all_tables = array($this->cal_table);
139
		foreach(array('extra','repeats','user','dates') as $name)
140
		{
141
			$vname = $name.'_table';
142
			$this->all_tables[] = $this->$vname = $this->cal_table.'_'.$name;
143
		}
144
	}
145
146
	/**
147
	 * Return sql to fetch all events in a given timerange, to be used instead of full table in further sql queries
148
	 *
149
	 * @param int $start
150
	 * @param int $end
151
	 * @param array $_where =null
152
	 * @param boolean $deleted =false
153
	 * @return string
154
	 */
155
	protected function cal_range_view($start, $end, array $_where=null, $deleted=false)
156
	{
157
		if ($GLOBALS['egw_info']['server']['no_timerange_views'] || !$start)	// using view without start-date is slower!
158
		{
159
			return $this->cal_table;	// no need / use for a view
160
		}
161
162
		$where = array();
163
		if (isset($deleted)) $where[] = "cal_deleted IS ".($deleted ? '' : 'NOT').' NULL';
164
		if ($end) $where[] = "range_start<".(int)$end;
165
		if ($start) $where[] = "(range_end IS NULL OR range_end>".(int)$start.")";
166
		if ($_where) $where = array_merge($where, $_where);
167
168
		$sql = "(SELECT * FROM $this->cal_table WHERE ".$this->db->expression($this->cal_table, $where).") $this->cal_table";
169
170
		return $sql;
171
	}
172
173
	/**
174
	 * Return sql to fetch all dates in a given timerange, to be used instead of full dates table in further sql queries
175
	 *
176
	 * Currently NOT used, as using two views joined together appears slower in my tests (probably because no index) then
177
	 * joining cal_range_view with real dates table (with index).
178
	 *
179
	 * @param int $start
180
	 * @param int $end
181
	 * @param array $_where =null
182
	 * @param boolean $deleted =false
183
	 * @return string
184
	 */
185
	protected function dates_range_view($start, $end, array $_where=null, $deleted=false)
186
	{
187
		if ($GLOBALS['egw_info']['server']['no_timerange_views'] || !$start || !$end)	// using view without start- AND end-date is slower!
188
		{
189
			return $this->dates_table;	// no need / use for a view
190
		}
191
192
		$where = array();
193
		if (isset($deleted)) $where['recur_exception'] = $deleted;
194
		if ($end) $where[] = "cal_start<".(int)$end;
195
		if ($start) $where[] = "cal_end>".(int)$start;
196
		if ($_where) $where = array_merge($where, $_where);
197
198
		// Api\Db::union uses Api\Db::select which check if join contains "WHERE"
199
		// to support old join syntax like ", other_table WHERE ...",
200
		// therefore we have to use eg. "WHERe" instead!
201
		$sql = "(SELECT * FROM $this->dates_table WHERe ".$this->db->expression($this->dates_table, $where).") $this->dates_table";
202
203
		return $sql;
204
	}
205
206
	/**
207
	 * Return events in a given timespan containing given participants (similar to search but quicker)
208
	 *
209
	 * Not all search parameters are currently supported!!!
210
	 *
211
	 * @param int $start startdate of the search/list (servertime)
212
	 * @param int $end enddate of the search/list (servertime)
213
	 * @param int|array $users user-id or array of user-id's, !$users means all entries regardless of users
214
	 * @param int|array $cat_id =0 mixed category-id or array of cat-id's (incl. all sub-categories), default 0 = all
215
	 * @param string $filter ='default' string filter-name: all (not rejected), accepted, unknown, tentative, rejected or everything (incl. rejected, deleted)
216
	 * @param int|boolean $offset =False offset for a limited query or False (default)
217
	 * @param int $num_rows =0 number of rows to return if offset set, default 0 = use default in user prefs
218
	 * @param array $params =array()
219
	 * @param string|array $params['query'] string: pattern so search for, if unset or empty all matching entries are returned (no search)
220
	 *		Please Note: a search never returns repeating events more then once AND does not honor start+end date !!!
221
	 *      array: everything is directly used as $where
222
	 * @param string $params['order'] ='cal_start' column-names plus optional DESC|ASC separted by comma
223
	 * @param string $params['sql_filter'] sql to be and'ed into query (fully quoted)
224
	 * @param string|array $params['cols'] what to select, default "$this->repeats_table.*,$this->cal_table.*,cal_start,cal_end,cal_recur_date",
225
	 * 						if specified and not false an iterator for the rows is returned
226
	 * @param string $params['append'] SQL to append to the query before $order, eg. for a GROUP BY clause
227
	 * @param array $params['cfs'] custom fields to query, null = none, array() = all, or array with cfs names
228
	 * @param array $params['users'] raw parameter as passed to calendar_bo::search() no memberships resolved!
229
	 * @param boolean $params['master_only'] =false, true only take into account participants/status from master (for AS)
230
	 * @param boolean $params['enum_recuring'] =true enumerate recuring events
231
	 * @param int $remove_rejected_by_user =null add join to remove entry, if given user has rejected it
232
	 * @return array of events
233
	 */
234
	function &events($start,$end,$users,$cat_id=0,$filter='all',$offset=False,$num_rows=0,array $params=array(),$remove_rejected_by_user=null)
235
	{
236
		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());
237
		$start_time = microtime(true);
238
		// not everything is supported by now
239
		if (!$start || !$end || is_string($params['query']) ||
240
			//in_array($filter,array('owner','deleted')) ||
241
			$params['enum_recuring']===false)
242
		{
243
			throw new Api\Exception\AssertionFailed("Unsupported value for parameters!");
244
		}
245
		$where = is_array($params['query']) ? $params['query'] : array();
246
		if ($cat_id) $where[] = $this->cat_filter($cat_id);
247
		$egw_cal = $this->cal_range_view($start, $end, $where, $filter == 'everything' ? null : $filter != 'deleted');
248
249
		$status_filter = $this->status_filter($filter, $params['enum_recuring']);
250
251
		$sql = "SELECT DISTINCT {$this->cal_table}_repeats.*,$this->cal_table.*,\n".
252
			"	CASE WHEN recur_type IS NULL THEN egw_cal.range_start ELSE cal_start END AS cal_start,\n".
253
			"	CASE WHEN recur_type IS NULL THEN egw_cal.range_end ELSE cal_end END AS cal_end\n".
254
			// using time-limited range view, instead of complete table, give a big performance plus
255
			"FROM $egw_cal\n".
256
			"JOIN egw_cal_user ON egw_cal_user.cal_id=egw_cal.cal_id\n".
257
			// need to left join dates, as egw_cal_user.recur_date is null for non-recuring event
258
			"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".
259
			"LEFT JOIN egw_cal_repeats ON egw_cal_user.cal_id=egw_cal_repeats.cal_id\n".
260
			"WHERE ".($status_filter ? $this->db->expression($this->table, $status_filter, " AND \n") : '').
261
			"	CASE WHEN recur_type IS NULL THEN egw_cal.range_start ELSE cal_start END<".(int)$end." AND\n".
262
			"	CASE WHEN recur_type IS NULL THEN egw_cal.range_end ELSE cal_end END>".(int)$start;
263
264
		if ($users)
265
		{
266
			// fix $users to also prefix system users and groups (with 'u')
267
			if (!is_array($users)) $users = $users ? (array)$users : array();
268
			foreach($users as &$uid)
269
			{
270
				$user_type = $user_id = null;
271
				self::split_user($uid, $user_type, $user_id, true);
272
				$uid = $user_type.$user_id;
273
			}
274
			$sql .= " AND\n	CONCAT(cal_user_type,cal_user_id) IN (".implode(',', array_map(array($this->db, 'quote'), $users)).")";
275
		}
276
277
		if ($remove_rejected_by_user && !in_array($filter, array('everything', 'deleted')))
278
		{
279
			$sql .= " AND\n	(cal_user_type!='u' OR cal_user_id!=".(int)$remove_rejected_by_user." OR cal_status!='R')";
280
		}
281
282
		if (!empty($params['sql_filter']) && is_string($params['sql_filter']))
283
		{
284
			$sql .= " AND\n	".$params['sql_filter'];
285
		}
286
287
		if ($params['order'])	// only order if requested
288
		{
289
			if (!preg_match('/^[a-z_ ,c]+$/i',$params['order'])) $params['order'] = 'cal_start';		// gard against SQL injection
290
			$sql .= "\nORDER BY ".$params['order'];
291
		}
292
293
		if ($offset === false)	// return all rows --> Api\Db::query wants offset=0, num_rows=-1
294
		{
295
			$offset = 0;
296
			$num_rows = -1;
297
		}
298
		$events =& $this->get_events($this->db->query($sql, __LINE__, __FILE__, $offset, $num_rows));
0 ignored issues
show
Bug introduced by
It seems like $offset defined by parameter $offset on line 234 can also be of type boolean; however, EGroupware\Api\Db::query() does only seem to accept integer, maybe add an additional type check?

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

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

An additional type check may prevent trouble.

Loading history...
299
		error_log(__METHOD__."(...) $sql --> ".number_format(microtime(true)-$start_time, 3));
300
		return $events;
301
	}
302
303
	/**
304
	 * reads one or more calendar entries
305
	 *
306
	 * All times (start, end and modified) are returned as timesstamps in servertime!
307
	 *
308
	 * @param int|array|string $ids id or array of id's of the entries to read, or string with a single uid
309
	 * @param int $recur_date =0 if set read the next recurrence at or after the timestamp, default 0 = read the initital one
310
	 * @return array|boolean array with cal_id => event array pairs or false if entry not found
311
	 */
312
	function read($ids,$recur_date=0)
313
	{
314
		//error_log(__METHOD__.'('.array2string($ids).",$recur_date) ".function_backtrace());
315
		$cols = self::get_columns('calendar', $this->cal_table);
316
		$cols[0] = $this->db->to_varchar($this->cal_table.'.cal_id');
317
		$cols = "$this->repeats_table.recur_type,$this->repeats_table.recur_interval,$this->repeats_table.recur_data,".implode(',',$cols);
318
		$join = "LEFT JOIN $this->repeats_table ON $this->cal_table.cal_id=$this->repeats_table.cal_id";
319
320
		$where = array();
321
		if (is_scalar($ids) && !is_numeric($ids))	// a single uid
322
		{
323
			// We want only the parents to match
324
			$where['cal_uid'] = $ids;
325
			$where['cal_reference'] = 0;
326
		}
327
		elseif(is_array($ids) && isset($ids[count($ids)-1]) || is_scalar($ids))	// one or more cal_id's
328
		{
329
			$where['cal_id'] = $ids;
330
		}
331
		else	// array with column => value pairs
332
		{
333
			$where = $ids;
334
			unset($ids);	// otherwise users get not read!
335
		}
336
		if (isset($where['cal_id']))	// prevent non-unique column-name cal_id
337
		{
338
			$where[] = $this->db->expression($this->cal_table, $this->cal_table.'.',array(
339
				'cal_id' => $where['cal_id'],
340
			));
341
			unset($where['cal_id']);
342
		}
343
		if ((int) $recur_date)
344
		{
345
			$where[] = 'cal_start >= '.(int)$recur_date;
346
			$group_by = 'GROUP BY '.$cols;
347
			$cols .= ',MIN(cal_start) AS cal_start,MIN(cal_end) AS cal_end';
348
			$join = "JOIN $this->dates_table ON $this->cal_table.cal_id=$this->dates_table.cal_id $join";
349
		}
350
		else
351
		{
352
			$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';
353
		}
354
		$cols .= ',range_end-1 AS recur_enddate';
355
356
		$events =& $this->get_events($this->db->select($this->cal_table, $cols, $where, __LINE__, __FILE__, false, $group_by, 'calendar', 0, $join), $recur_date);
357
358
		return $events ? $events : false;
359
	}
360
361
	/**
362
	 * Get full event information from an iterator of a select on egw_cal
363
	 *
364
	 * @param array|Iterator $rs
365
	 * @param int $recur_date =0
366
	 * @return array
367
	 */
368
	protected function &get_events($rs, $recur_date=0)
369
	{
370
		if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length']))
371
		{
372
			$minimum_uid_length = $GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'];
373
		}
374
		else
375
		{
376
			$minimum_uid_length = 8;
377
		}
378
379
		$events = array();
380
		foreach($rs as $row)
381
		{
382
			if (!$row['recur_type'])
383
			{
384
				$row['recur_type'] = MCAL_RECUR_NONE;
385
				unset($row['recur_enddate']);
386
			}
387
			$row['recur_exception'] = $row['alarm'] = array();
388
			$events[$row['cal_id']] = Api\Db::strip_array_keys($row,'cal_');
389
		}
390
		if (!$events) return $events;
391
392
		$ids = array_keys($events);
393
		if (count($ids) == 1) $ids = $ids[0];
394
395
		foreach ($events as &$event)
396
		{
397
			if (!isset($event['uid']) || strlen($event['uid']) < $minimum_uid_length)
398
			{
399
				// event (without uid), not strong enough uid => create new uid
400
				$event['uid'] = Api\CalDAV::generate_uid('calendar',$event['id']);
401
				$this->db->update($this->cal_table, array('cal_uid' => $event['uid']),
402
					array('cal_id' => $event['id']),__LINE__,__FILE__,'calendar');
403
			}
404
			if (!(int)$recur_date && $event['recur_type'] != MCAL_RECUR_NONE)
405
			{
406 View Code Duplication
				foreach($this->db->select($this->dates_table, 'cal_id,cal_start', array(
407
					'cal_id' => $ids,
408
					'recur_exception' => true,
409
				), __LINE__, __FILE__, false, 'ORDER BY cal_id,cal_start', 'calendar') as $row)
410
				{
411
					$events[$row['cal_id']]['recur_exception'][] = $row['cal_start'];
412
				}
413
				break;	// as above select read all exceptions (and I dont think too short uid problem still exists)
414
			}
415
			// make sure we fetch only real exceptions (deleted occurrences of a series should not show up)
416
			if (($recur_date &&	$event['recur_type'] != MCAL_RECUR_NONE))
417
			{
418
				//_debug_array(__METHOD__.__LINE__.' recur_date:'.$recur_date.' check cal_start:'.$event['start']);
419 View Code Duplication
				foreach($this->db->select($this->dates_table, 'cal_id,cal_start', array(
420
					'cal_id' => $event['id'],
421
					'cal_start' => $event['start'],
422
					'recur_exception' => true,
423
				), __LINE__, __FILE__, false, '', 'calendar') as $row)
424
				{
425
					$isException[$row['cal_id']] = true;
426
				}
427
				if ($isException[$event['id']])
428
				{
429
					if (!$this->db->select($this->cal_table, 'COUNT(*)', array(
430
						'cal_uid' => $event['uid'],
431
						'cal_recurrence' => $event['start'],
432
						'cal_deleted' => NULL
433
					), __LINE__, __FILE__, false, '', 'calendar')->fetchColumn())
434
					{
435
						$e = $this->read($event['id'],$event['start']+1);
436
						$event = $e[$event['id']];
437
						break;
438
					}
439
					else
0 ignored issues
show
Unused Code introduced by
This else statement is empty and can be removed.

This check looks for the else branches of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These else branches can be removed.

if (rand(1, 6) > 3) {
print "Check failed";
} else {
    //print "Check succeeded";
}

could be turned into

if (rand(1, 6) > 3) {
    print "Check failed";
}

This is much more concise to read.

Loading history...
440
					{
441
						//real exception -> should we return it? probably not, so we live with the result of the next occurrence of the series
442
					}
443
				}
444
			}
445
		}
446
447
		// check if we have a real recurance, if not set $recur_date=0
448
		if (is_array($ids) || $events[(int)$ids]['recur_type'] == MCAL_RECUR_NONE)
449
		{
450
			$recur_date = 0;
451
		}
452
		else	// adjust the given recurance to the real time, it can be a date without time(!)
453
		{
454
			if ($recur_date)
455
			{
456
				// also remember recur_date, maybe we need it later, duno now
457
				$recur_date = array(0,$events[$ids]['recur_date'] = $events[$ids]['start']);
458
			}
459
		}
460
461
		// participants, if a recur_date give, we read that recurance, plus the one users from the default entry with recur_date=0
462
		// sorting by cal_recur_date ASC makes sure recurence status always overwrites series status
463
		foreach($this->db->select($this->user_table,'*',array(
464
			'cal_id'      => $ids,
465
			'cal_recur_date' => $recur_date,
466
			"cal_status NOT IN ('X','E')",
467
		),__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
468
		{
469
			// combine all participant data in uid and status values
470
			$uid    = self::combine_user($row['cal_user_type'], $row['cal_user_id'], $row['cal_user_attendee']);
471
			$status = self::combine_status($row['cal_status'],$row['cal_quantity'],$row['cal_role']);
472
473
			$events[$row['cal_id']]['participants'][$uid] = $status;
474
			$events[$row['cal_id']]['participant_types'][$row['cal_user_type']][is_numeric($uid) ? $uid : substr($uid, 1)] = $status;
475
			// make extra attendee information available eg. for iCal export (attendee used eg. in response to organizer for an account)
476
			$events[$row['cal_id']]['attendee'][$uid] = $row['cal_user_attendee'];
477
		}
478
479
		// custom fields
480 View Code Duplication
		foreach($this->db->select($this->extra_table,'*',array('cal_id'=>$ids),__LINE__,__FILE__,false,'','calendar') as $row)
481
		{
482
			$events[$row['cal_id']]['#'.$row['cal_extra_name']] = $row['cal_extra_value'];
483
		}
484
485
		// alarms
486
		if (is_array($ids))
487
		{
488
			foreach($this->read_alarms((array)$ids) as $cal_id => $alarms)
489
			{
490
				$events[$cal_id]['alarm'] = $alarms;
491
			}
492
		}
493
		else
494
		{
495
			$events[$ids]['alarm'] = $this->read_alarms($ids);
496
		}
497
498
		//echo "<p>socal::read(".print_r($ids,true).")=<pre>".print_r($events,true)."</pre>\n";
499
		return $events;
500
	}
501
502
	/**
503
	 * Maximum time a ctag get cached, as ActiveSync ping requests can run for a long time
504
	 */
505
	const MAX_CTAG_CACHE_TIME = 29;
506
507
	/**
508
	 * Get maximum modification time of events for given participants and optional owned by them
509
	 *
510
	 * This includes ALL recurences of an event series
511
	 *
512
	 * @param int|string|array $users one or mulitple calendar users
513
	 * @param booelan $owner_too =false if true return also events owned by given users
514
	 * @param boolean $master_only =false only check recurance master (egw_cal_user.recur_date=0)
515
	 * @return int maximum modification timestamp
516
	 */
517
	function get_ctag($users, $owner_too=false,$master_only=false)
518
	{
519
		static $ctags = array();	// some per-request caching
520
		static $last_request = null;
521
		if (!isset($last_request) || time()-$last_request > self::MAX_CTAG_CACHE_TIME)
522
		{
523
			$ctags = array();
524
			$last_request = time();
525
		}
526
		$signature = serialize(func_get_args());
527
		if (isset($ctags[$signature])) return $ctags[$signature];
528
529
		$types = array();
530
		foreach((array)$users as $uid)
531
		{
532
			$type = $id = null;
533
			self::split_user($uid, $type, $id, true);
534
			$types[$type][] = $id;
535
		}
536
		foreach($types as $type => $ids)
537
		{
538
			$where = array(
539
				'cal_user_type' => $type,
540
				'cal_user_id' => $ids,
541
			);
542
			if (count($types) > 1)
543
			{
544
				$types[$type] = $this->db->expression($this->user_table, $where);
545
			}
546
		}
547
		if (count($types) > 1)
548
		{
549
			$where[] = '('.explode(' OR ', $types).')';
550
		}
551
		if ($master_only)
552
		{
553
			$where['cal_recur_date'] = 0;
554
		}
555
		if ($owner_too)
556
		{
557
			// owner can only by users, no groups or resources
558
			foreach($users as $key => $user)
0 ignored issues
show
Bug introduced by
The expression $users of type integer|string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

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

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
929
			{
930
				foreach($user_or as $user_sql)
931
				{
932
					$selects[] = $select;
933
					$selects[count($selects)-1]['where'][] = $user_sql;
934 View Code Duplication
					if ($params['enum_recuring'])
935
					{
936
						$selects[count($selects)-1]['where'][] = "recur_type IS NULL AND $this->user_table.cal_recur_date=0";
937
						$selects[] = $select;
938
						$selects[count($selects)-1]['where'][] = $user_sql;
939
						$selects[count($selects)-1]['where'][] = "$this->user_table.cal_recur_date=cal_start";
940
					}
941
				}
942
				// if the query is to be filtered by owner we need to add more selects for the union
943
				if ($owner_or)
0 ignored issues
show
Bug Best Practice introduced by
The expression $owner_or of type null|string is loosely compared to true; this is ambiguous if the string can be empty. 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 string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
944
				{
945
					$selects[] = $select;
946
					$selects[count($selects)-1]['where'][] = $owner_or;
947 View Code Duplication
					if ($params['enum_recuring'])
948
					{
949
						$selects[count($selects)-1]['where'][] = "recur_type IS NULL AND $this->user_table.cal_recur_date=0";
950
						$selects[] = $select;
951
						$selects[count($selects)-1]['where'][] = $owner_or;
952
						$selects[count($selects)-1]['where'][] = "$this->user_table.cal_recur_date=cal_start";
953
					}
954
				}
955
			}
956
			else
957
			{
958
				// if the query is to be filtered by neither by user nor owner (should not happen?) we need 2 selects for the union
959
				$selects[] = $select;
960
				if ($params['enum_recuring'])
961
				{
962
					$selects[count($selects)-1]['where'][] = "recur_type IS NULL AND $this->user_table.cal_recur_date=0";
963
					$selects[] = $select;
964
					$selects[count($selects)-1]['where'][] = "$this->user_table.cal_recur_date=cal_start";
965
				}
966
			}
967
			if (is_numeric($offset) && !$params['no_total'])	// get the total too
968
			{
969
				$save_selects = $selects;
970
				// 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)
971
				foreach(array_keys($selects) as $key)
972
				{
973
					$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";
974
					if (!$params['enum_recuring'])
975
					{
976
						$selects[$key]['cols'] = str_replace(array('cal_start','cal_end'),
977
							array('range_start AS cal_start','range_end AS cal_end'), $selects[$key]['cols']);
978
					}
979
				}
980
				if (!isset($params['cols']) && !$params['no_integration']) self::get_union_selects($selects,$start,$end,$users,$cat_id,$filter,$params['query'],$params['users']);
981
982
				$this->total = $this->db->union($selects,__LINE__,__FILE__)->NumRows();
983
984
				// restore original cols / selects
985
				$selects = $save_selects; unset($save_selects);
986
			}
987
			if (!isset($params['cols']) && !$params['no_integration']) self::get_union_selects($selects,$start,$end,$users,$cat_id,$filter,$params['query'],$params['users']);
988
989
			$rs = $this->db->union($selects,__LINE__,__FILE__,$params['order'],$offset,$num_rows);
990
		}
991
		else	// MsSQL oder MySQL 3.23
992
		{
993
			$where[] = "(recur_type IS NULL AND $this->user_table.cal_recur_date=0 OR $this->user_table.cal_recur_date=cal_start)";
994
995
			$selects = array(array(
996
				'table' => $cal_table,
997
				'join'  => $join,
998
				'cols'  => $cols,
999
				'where' => $where,
1000
				'app'   => 'calendar',
1001
				'append'=> $params['append'],
1002
				'table_def' => $cal_table_def,
1003
			));
1004
1005
			if (is_numeric($offset) && !$params['no_total'])	// get the total too
1006
			{
1007
				$save_selects = $selects;
1008
				// 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)
1009
				$selects[0]['cols'] = "$this->cal_table.cal_id,cal_start";
1010 View Code Duplication
				if (!isset($params['cols']) && !$params['no_integration'] && $this->db->capabilities['union'])
1011
				{
1012
					self::get_union_selects($selects,$start,$end,$users,$cat_id,$filter,$params['query'],$params['users']);
1013
				}
1014
				$this->total = $this->db->union($selects, __LINE__, __FILE__)->NumRows();
1015
				$selects = $save_selects;
1016
			}
1017 View Code Duplication
			if (!isset($params['cols']) && !$params['no_integration'] && $this->db->capabilities['union'])
1018
			{
1019
				self::get_union_selects($selects,$start,$end,$users,$cat_id,$filter,$params['query'],$params['users']);
1020
			}
1021
			$rs = $this->db->union($selects,__LINE__,__FILE__,$params['order'],$offset,$num_rows);
1022
		}
1023
		//error_log(__METHOD__."() useUnionQuery=$useUnionQuery --> query took ".(microtime(true)-$starttime).'s '.$rs->sql);
1024
1025
		if (isset($params['cols']))
1026
		{
1027
			return $rs;	// if colums are specified we return the recordset / iterator
1028
		}
1029
		// Todo: return $this->get_events($rs);
1030
1031
		$events = $ids = $recur_dates = $recur_ids = array();
1032
		foreach($rs as $row)
1033
		{
1034
			$id = $row['cal_id'];
1035
			if (is_numeric($id)) $ids[] = $id;
1036
1037
			if ($row['cal_recur_date'])
1038
			{
1039
				$id .= '-'.$row['cal_recur_date'];
1040
				$recur_dates[] = $row['cal_recur_date'];
1041
			}
1042
			if ($row['participants'])
1043
			{
1044
				$row['participants'] = explode(',',$row['participants']);
1045
				$row['participants'] = array_combine($row['participants'],
1046
					array_fill(0,count($row['participants']),''));
1047
			}
1048
			else
1049
			{
1050
				$row['participants'] = array();
1051
			}
1052
			$row['recur_exception'] = $row['alarm'] = array();
1053
1054
			// compile a list of recurrences per cal_id
1055
			if (!in_array($id,(array)$recur_ids[$row['cal_id']])) $recur_ids[$row['cal_id']][] = $id;
1056
1057
			$events[$id] = Api\Db::strip_array_keys($row,'cal_');
1058
		}
1059
		//_debug_array($events);
1060
		if (count($ids))
1061
		{
1062
			$ids = array_unique($ids);
1063
1064
			// now ready all users with the given cal_id AND (cal_recur_date=0 or the fitting recur-date)
1065
			// This will always read the first entry of each recuring event too, we eliminate it later
1066
			$recur_dates[] = 0;
1067
			$utcal_id_view = " (SELECT * FROM ".$this->user_table." WHERE cal_id IN (".implode(',',$ids).")".
1068
				($filter != 'everything' ? " AND cal_status NOT IN ('X','E')" : '').") utcalid ";
1069
			//$utrecurdate_view = " (select * from ".$this->user_table." where cal_recur_date in (".implode(',',array_unique($recur_dates)).")) utrecurdates ";
1070
			foreach($this->db->select($utcal_id_view,'*',array(
1071
					//'cal_id' => array_unique($ids),
1072
					'cal_recur_date' => $recur_dates,
1073
				),__LINE__,__FILE__,false,'ORDER BY cal_id,cal_user_type DESC,'.self::STATUS_SORT,'calendar',-1,$join='',
1074
				$this->db->get_table_definitions('calendar',$this->user_table)) as $row)	// DESC puts users before resources and contacts
1075
			{
1076
				$id = $row['cal_id'];
1077
				if ($row['cal_recur_date']) $id .= '-'.$row['cal_recur_date'];
1078
1079
				// combine all participant data in uid and status values
1080
				$uid = self::combine_user($row['cal_user_type'], $row['cal_user_id'], $row['cal_user_attendee']);
1081
				$status = self::combine_status($row['cal_status'],$row['cal_quantity'],$row['cal_role']);
1082
1083
				// set accept/reject/tentative of series for all recurrences
1084
				if (!$row['cal_recur_date'])
1085
				{
1086
					foreach((array)$recur_ids[$row['cal_id']] as $i)
1087
					{
1088
						if (isset($events[$i]) && !isset($events[$i]['participants'][$uid]))
1089
						{
1090
							$events[$i]['participants'][$uid] = $status;
1091
						}
1092
					}
1093
				}
1094
1095
				// set data, if recurrence is requested
1096
				if (isset($events[$id])) $events[$id]['participants'][$uid] = $status;
1097
			}
1098
			// query recurrance exceptions, if needed: enum_recuring && !daywise is used in calendar_groupdav::get_series($uid,...)
1099
			if (!$params['enum_recuring'] || !$params['daywise'])
1100
			{
1101
				foreach($this->db->select($this->dates_table, 'cal_id,cal_start', array(
1102
					'cal_id' => $ids,
1103
					'recur_exception' => true,
1104
				), __LINE__, __FILE__, false, 'ORDER BY cal_id,cal_start', 'calendar') as $row)
1105
				{
1106
					// for enum_recurring events are not indexed by cal_id, but $cal_id.'-'.$cal_start
1107
					// find master, which is first recurrence
1108
					if (!isset($events[$id=$row['cal_id']]))
1109
					{
1110
						foreach($events as $id => $event)
1111
						{
1112
							if ($event['id'] == $row['cal_id']) break;
1113
						}
1114
					}
1115
					$events[$id]['recur_exception'][] = $row['cal_start'];
1116
				}
1117
			}
1118
			//custom fields are not shown in the regular views, so we only query them, if explicitly required
1119
			if (!is_null($params['cfs']))
1120
			{
1121
				$where = array('cal_id' => $ids);
1122
				if ($params['cfs']) $where['cal_extra_name'] = $params['cfs'];
1123
				foreach($this->db->select($this->extra_table,'*',$where,
1124
					__LINE__,__FILE__,false,'','calendar') as $row)
1125
				{
1126
					foreach((array)$recur_ids[$row['cal_id']] as $id)
1127
					{
1128
						if (isset($events[$id]))
1129
						{
1130
							$events[$id]['#'.$row['cal_extra_name']] = $row['cal_extra_value'];
1131
						}
1132
					}
1133
				}
1134
			}
1135
			// alarms
1136
			foreach($this->read_alarms($ids) as $cal_id => $alarms)
1137
			{
1138
				foreach($alarms as $id => $alarm)
1139
				{
1140
					$event_start = $alarm['time'] + $alarm['offset'];
1141
1142
					if (isset($events[$cal_id]))	// none recuring event
1143
					{
1144
						$events[$cal_id]['alarm'][$id] = $alarm;
1145
					}
1146
					elseif (isset($events[$cal_id.'-'.$event_start]))	// recuring event
1147
					{
1148
						$events[$cal_id.'-'.$event_start]['alarm'][$id] = $alarm;
1149
					}
1150
				}
1151
			}
1152
		}
1153
		//echo "<p>socal::search\n"; _debug_array($events);
1154
		//error_log(__METHOD__."(,filter=".array2string($params['query']).",offset=$offset, num_rows=$num_rows) returning ".count($events)." entries".($offset!==false?" total=$this->total":'').' '.function_backtrace());
1155
		return $events;
1156
	}
1157
1158
	/**
1159
	 * Data returned by calendar_search_union hook
1160
	 */
1161
	private static $integration_data;
1162
1163
	/**
1164
	 * Ask other apps if they want to participate in calendar search / display
1165
	 *
1166
	 * @param &$selects parts of union query
1167
	 * @param $start see search()
1168
	 * @param $end
1169
	 * @param $users as used in calendar_so ($users_raw plus all members and memberships added by calendar_bo)
1170
	 * @param $cat_id
1171
	 * @param $filter
1172
	 * @param $query
1173
	 * @param $users_raw as passed to calendar_bo::search (no members and memberships added)
1174
	 */
1175
	private static function get_union_selects(array &$selects,$start,$end,$users,$cat_id,$filter,$query,$users_raw)
1176
	{
1177
		if (in_array(basename($_SERVER['SCRIPT_FILENAME']),array('groupdav.php','rpc.php','xmlrpc.php','/activesync/index.php')) ||
1178
			!in_array($GLOBALS['egw_info']['flags']['currentapp'],array('calendar','home')))
1179
		{
1180
			return;    // disable integration for GroupDAV, SyncML, ...
1181
		}
1182
		self::$integration_data = Api\Hooks::process(array(
1183
			'location' => 'calendar_search_union',
1184
			'cols'  => $selects[0]['cols'],    // cols to return
1185
			'start' => $start,
1186
			'end'   => $end,
1187
			'users' => $users,
1188
			'users_raw' => $users_raw,
1189
			'cat_id'=> $cat_id,
1190
			'filter'=> $filter,
1191
			'query' => $query,
1192
		));
1193
		foreach(self::$integration_data as $data)
1194
		{
1195
			if (is_array($data['selects']))
1196
			{
1197
				//echo $app; _debug_array($data);
1198
				$selects = array_merge($selects,$data['selects']);
1199
			}
1200
		}
1201
	}
1202
1203
	/**
1204
	 * Get data from last 'calendar_search_union' hook call
1205
	 *
1206
	 * @return array
1207
	 */
1208
	public static function get_integration_data()
1209
	{
1210
		return self::$integration_data;
1211
	}
1212
1213
	/**
1214
	 * Return union cols constructed from application cols and required cols
1215
	 *
1216
	 * Every col not supplied in $app_cols get returned as NULL.
1217
	 *
1218
	 * @param array $app_cols required name => own name pairs
1219
	 * @param string|array $required array or comma separated column names or table.*
1220
	 * @param string $required_app ='calendar'
1221
	 * @return string cols for union query to match ones supplied in $required
1222
	 */
1223
	public static function union_cols(array $app_cols,$required,$required_app='calendar')
1224
	{
1225
		// remove evtl. used DISTINCT, we currently dont need it
1226
		if (($distinct = substr($required,0,9) == 'DISTINCT '))
1227
		{
1228
			$required = substr($required,9);
1229
		}
1230
		$return_cols = array();
1231
		foreach(is_array($required) ? $required : explode(',',$required) as $cols)
1232
		{
1233
			$matches = null;
1234
			if (substr($cols,-2) == '.*')
1235
			{
1236
				$cols = self::get_columns($required_app,substr($cols,0,-2));
1237
			}
1238
			// remove CAST added for PostgreSQL from eg. "CAST(egw_cal.cal_id AS varchar)"
1239
			elseif (preg_match('/CAST\(([a-z0-9_.]+) AS [a-z0-9_]+\)/i', $cols, $matches))
1240
			{
1241
				$cols = $matches[1];
1242
			}
1243
			elseif (strpos($cols,' AS ') !== false)
1244
			{
1245
				list(,$cols) = explode(' AS ',$cols);
1246
			}
1247
			foreach((array)$cols as $col)
1248
			{
1249
				if (substr($col,0,7) == 'egw_cal')	// remove table name
1250
				{
1251
					$col = preg_replace('/^egw_cal[a-z_]*\./','',$col);
1252
				}
1253
				if (isset($app_cols[$col]))
1254
				{
1255
					$return_cols[] = $app_cols[$col];
1256
				}
1257
				else
1258
				{
1259
					$return_cols[] = 'NULL';
1260
				}
1261
			}
1262
		}
1263
		//error_log(__METHOD__."(".array2string($app_cols).", ".array2string($required).", '$required_app') returning ".array2string(implode(',',$return_cols)));
1264
		return implode(',',$return_cols);
1265
	}
1266
1267
	/**
1268
	 * Get columns of given table, taking into account historically different column order of egw_cal table
1269
	 *
1270
	 * @param string $app
1271
	 * @param string $table
1272
	 * @return array of column names
1273
	 */
1274
	static private function get_columns($app,$table)
1275
	{
1276
		if ($table != 'egw_cal')
1277
		{
1278
			$table_def = $GLOBALS['egw']->db->get_table_definitions($app,$table);
1279
			$cols = array_keys($table_def['fd']);
1280
		}
1281
		else
1282
		{
1283
			// special handling for egw_cal, as old databases have a different column order!!!
1284
			$cols =& Api\Cache::getSession(__CLASS__,$table);
1285
1286
			if (is_null($cols))
1287
			{
1288
				$meta = $GLOBALS['egw']->db->metadata($table,true);
1289
				$cols = array_keys($meta['meta']);
1290
			}
1291
		}
1292
		return $cols;
1293
	}
1294
1295
	/**
1296
	 * Checks for conflicts
1297
	 */
1298
1299
/* folowing SQL checks for conflicts completly on DB level
1300
1301
SELECT cal_user_type, cal_user_id, SUM( cal_quantity )
1302
FROM egw_cal, egw_cal_dates, egw_cal_user
1303
LEFT JOIN egw_cal_repeats ON egw_cal.cal_id = egw_cal_repeats.cal_id
1304
WHERE egw_cal.cal_id = egw_cal_dates.cal_id
1305
AND egw_cal.cal_id = egw_cal_user.cal_id
1306
AND (
1307
recur_type IS NULL
1308
AND cal_recur_date =0
1309
OR cal_recur_date = cal_start
1310
)
1311
AND (
1312
(
1313
cal_user_type = 'u'			# user of the checked event
1314
AND cal_user_id
1315
IN ( 7, 5 )
1316
)
1317
AND 1118822400 < cal_end	# start- and end-time of the checked event
1318
AND cal_start <1118833200
1319
)
1320
AND egw_cal.cal_id !=26		# id of the checked event
1321
AND cal_non_blocking !=1
1322
AND cal_status != 'R'
1323
GROUP BY cal_user_type, cal_user_id
1324
ORDER BY cal_user_type, cal_usre_id
1325
1326
*/
1327
1328
	/**
1329
	 * Saves or creates an event
1330
	 *
1331
	 * We always set cal_modified and cal_modifier and for new events cal_uid.
1332
	 * All other column are only written if they are set in the $event parameter!
1333
	 *
1334
	 * @param array $event
1335
	 * @param boolean &$set_recurrences on return: true if the recurrences need to be written, false otherwise
1336
	 * @param int &$set_recurrences_start=0 on return: time from which on the recurrences should be rebuilt, default 0=all
1337
	 * @param int $change_since =0 time from which on the repetitions should be changed, default 0=all
1338
	 * @param int &$etag etag=null etag to check or null, on return new etag
1339
	 * @return boolean|int false on error, 0 if etag does not match, cal_id otherwise
1340
	 */
1341
	function save($event,&$set_recurrences,&$set_recurrences_start=0,$change_since=0,&$etag=null)
1342
	{
1343
		if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length']))
1344
		{
1345
			$minimum_uid_length = $GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'];
1346
			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!
1347
		}
1348
		else
1349
		{
1350
			$minimum_uid_length = 8;
1351
		}
1352
1353
		$old_min = $old_duration = 0;
1354
1355
		//error_log(__METHOD__.'('.array2string($event).",$set_recurrences,$change_since,$etag) ".function_backtrace());
1356
1357
		$cal_id = (int) $event['id'];
1358
		unset($event['id']);
1359
		$set_recurrences = $set_recurrences || !$cal_id && $event['recur_type'] != MCAL_RECUR_NONE;
1360
1361
		if ($event['recur_type'] != MCAL_RECUR_NONE &&
1362
			!(int)$event['recur_interval'])
1363
		{
1364
			$event['recur_interval'] = 1;
1365
		}
1366
1367
		// add colum prefix 'cal_' if there's not already a 'recur_' prefix
1368
		foreach($event as $col => $val)
1369
		{
1370
			if ($col[0] != '#' && substr($col,0,6) != 'recur_' && substr($col,0,6) != 'range_' && $col != 'alarm' && $col != 'tz_id' && $col != 'caldav_name')
1371
			{
1372
				$event['cal_'.$col] = $val;
1373
				unset($event[$col]);
1374
			}
1375
		}
1376
		// set range_start/_end, but only if we have cal_start/_end, as otherwise we destroy present values!
1377
		if (isset($event['cal_start'])) $event['range_start'] = $event['cal_start'];
1378
		if (isset($event['cal_end']))
1379
		{
1380
			$event['range_end'] = $event['recur_type'] == MCAL_RECUR_NONE ? $event['cal_end'] :
1381
				($event['recur_enddate'] ? $event['recur_enddate'] : null);
1382
		}
1383
		// ensure that we find mathing entries later on
1384
		if (!is_array($event['cal_category']))
1385
		{
1386
			$categories = array_unique(explode(',',$event['cal_category']));
1387
			sort($categories);
1388
		}
1389
		else
1390
		{
1391
			$categories = array_unique($event['cal_category']);
1392
		}
1393
		sort($categories, SORT_NUMERIC);
1394
1395
		$event['cal_category'] = implode(',',$categories);
1396
1397
		// make sure recurring events never reference to an other recurrent event
1398
		if ($event['recur_type'] != MCAL_RECUR_NONE) $event['cal_reference'] = 0;
1399
1400
		if ($cal_id)
1401
		{
1402
			// query old recurrance information, before updating main table, where recur_endate is now stored
1403
			if ($event['recur_type'] != MCAL_RECUR_NONE)
1404
			{
1405
				$old_repeats = $this->db->select($this->repeats_table, "$this->repeats_table.*,range_end AS recur_enddate",
1406
					"$this->repeats_table.cal_id=".(int)$cal_id, __LINE__, __FILE__,
1407
					false, '', 'calendar', 0, "JOIN $this->cal_table ON $this->repeats_table.cal_id=$this->cal_table.cal_id")->fetch();
1408
			}
1409
			$where = array('cal_id' => $cal_id);
1410
			// read only timezone id, to check if it is changed
1411
			if ($event['recur_type'] != MCAL_RECUR_NONE)
1412
			{
1413
				$old_tz_id = $this->db->select($this->cal_table,'tz_id',$where,__LINE__,__FILE__,'calendar')->fetchColumn();
1414
			}
1415
			if (!is_null($etag)) $where['cal_etag'] = $etag;
1416
1417
			unset($event['cal_etag']);
1418
			$event[] = 'cal_etag=cal_etag+1';	// always update the etag, even if none given to check
1419
1420
			$this->db->update($this->cal_table,$event,$where,__LINE__,__FILE__,'calendar');
1421
1422
			if (!is_null($etag) && $this->db->affected_rows() < 1)
1423
			{
1424
				return 0;	// wrong etag, someone else updated the entry
1425
			}
1426
			if (!is_null($etag)) ++$etag;
1427
		}
1428
		else
1429
		{
1430
			// new event
1431
			if (!$event['cal_owner']) $event['cal_owner'] = $GLOBALS['egw_info']['user']['account_id'];
1432
1433
			if (!$event['cal_id'] && !isset($event['cal_uid'])) $event['cal_uid'] = '';	// uid is NOT NULL!
1434
1435
			$this->db->insert($this->cal_table,$event,false,__LINE__,__FILE__,'calendar');
1436
			if (!($cal_id = $this->db->get_last_insert_id($this->cal_table,'cal_id')))
1437
			{
1438
				return false;
1439
			}
1440
			$etag = 0;
1441
		}
1442
		$update = array();
1443
		// event without uid or not strong enough uid
1444
		if (!isset($event['cal_uid']) || strlen($event['cal_uid']) < $minimum_uid_length)
1445
		{
1446
			$update['cal_uid'] = $event['cal_uid'] = Api\CalDAV::generate_uid('calendar',$cal_id);
1447
		}
1448
		// set caldav_name, if not given by caller
1449
		if (empty($event['caldav_name']) && version_compare($GLOBALS['egw_info']['apps']['calendar']['version'], '1.9.003', '>='))
1450
		{
1451
			$update['caldav_name'] = $event['caldav_name'] = $cal_id.'.ics';
1452
		}
1453
		if ($update)
1454
		{
1455
			$this->db->update($this->cal_table, $update, array('cal_id' => $cal_id),__LINE__,__FILE__,'calendar');
1456
		}
1457
1458
		if ($event['recur_type'] == MCAL_RECUR_NONE)
1459
		{
1460
			$this->db->delete($this->dates_table,array(
1461
				'cal_id' => $cal_id),
1462
				__LINE__,__FILE__,'calendar');
1463
1464
			// delete all user-records, with recur-date != 0
1465
			$this->db->delete($this->user_table,array(
1466
				'cal_id' => $cal_id, 'cal_recur_date != 0'),
1467
				__LINE__,__FILE__,'calendar');
1468
1469
			$this->db->delete($this->repeats_table,array(
1470
				'cal_id' => $cal_id),
1471
				__LINE__,__FILE__,'calendar');
1472
1473
			// add exception marker to master, so participants added to exceptions *only* get found
1474
			if ($event['cal_reference'])
1475
			{
1476
				$master_participants = array();
1477
				foreach($this->db->select($this->user_table, 'cal_user_type,cal_user_id,cal_user_attendee', array(
1478
					'cal_id' => $event['cal_reference'],
1479
					'cal_recur_date' => 0,
1480
					"cal_status != 'X'",	// deleted need to be replaced with exception marker too
1481
				), __LINE__, __FILE__, 'calendar') as $row)
1482
				{
1483
					$master_participants[] = self::combine_user($row['cal_user_type'], $row['cal_user_id'], $row['cal_user_attendee']);
1484
				}
1485
				foreach(array_diff(array_keys((array)$event['cal_participants']), $master_participants) as $uid)
1486
				{
1487
					$user_type = $user_id = null;
1488
					self::split_user($uid, $user_type, $user_id, true);
1489
					$this->db->insert($this->user_table, array(
1490
						'cal_status' => 'E',
1491
						'cal_user_attendee' => $user_type == 'e' ? substr($uid, 1) : null,
1492
					), array(
1493
						'cal_id' => $event['cal_reference'],
1494
						'cal_recur_date' => 0,
1495
						'cal_user_type' => $user_type,
1496
						'cal_user_id' => $user_id,
1497
					), __LINE__, __FILE__, 'calendar');
1498
				}
1499
			}
1500
		}
1501
		else // write information about recuring event, if recur_type is present in the array
1502
		{
1503
			// fetch information about the currently saved (old) event
1504
			$old_min = (int) $this->db->select($this->dates_table,'MIN(cal_start)',array('cal_id'=>$cal_id),__LINE__,__FILE__,false,'','calendar')->fetchColumn();
1505
			$old_duration = (int) $this->db->select($this->dates_table,'MIN(cal_end)',array('cal_id'=>$cal_id),__LINE__,__FILE__,false,'','calendar')->fetchColumn() - $old_min;
1506
			$old_exceptions = array();
1507
			foreach($this->db->select($this->dates_table, 'cal_start', array(
1508
				'cal_id' => $cal_id,
1509
				'recur_exception' => true
1510
			), __LINE__, __FILE__, false, 'ORDER BY cal_start', 'calendar') as $row)
1511
			{
1512
				$old_exceptions[] = $row['cal_start'];
1513
			}
1514
1515
			$event['recur_exception'] = is_array($event['recur_exception']) ? $event['recur_exception'] : array();
1516
			if (!empty($event['recur_exception']))
1517
			{
1518
				sort($event['recur_exception']);
1519
			}
1520
1521
			$where = array(
1522
				'cal_id' => $cal_id,
1523
				'cal_recur_date' => 0,
1524
			);
1525
			$old_participants = array();
1526
			foreach ($this->db->select($this->user_table,'cal_user_type,cal_user_id,cal_user_attendee,cal_status,cal_quantity,cal_role', $where,
1527
				__LINE__,__FILE__,false,'','calendar') as $row)
1528
			{
1529
				$uid = self::combine_user($row['cal_user_type'], $row['cal_user_id'], $row['cal_user_attendee']);
1530
				$status = self::combine_status($row['cal_status'], $row['cal_quantity'], $row['cal_role']);
1531
				$old_participants[$uid] = $status;
1532
			}
1533
1534
			// re-check: did so much recurrence data change that we have to rebuild it from scratch?
1535
			if (!$set_recurrences)
1536
			{
1537
				$set_recurrences = (isset($event['cal_start']) && (int)$old_min != (int) $event['cal_start']) ||
1538
				    $event['recur_type'] != $old_repeats['recur_type'] || $event['recur_data'] != $old_repeats['recur_data'] ||
1539
					(int)$event['recur_interval'] != (int)$old_repeats['recur_interval'] || $event['tz_id'] != $old_tz_id;
1540
			}
1541
1542
			if ($set_recurrences)
1543
			{
1544
				// too much recurrence data has changed, we have to do a rebuild from scratch
1545
				// delete all, but the lowest dates record
1546
				$this->db->delete($this->dates_table,array(
1547
					'cal_id' => $cal_id,
1548
					'cal_start > '.(int)$old_min,
1549
				),__LINE__,__FILE__,'calendar');
1550
1551
				// delete all user-records, with recur-date != 0
1552
				$this->db->delete($this->user_table,array(
1553
					'cal_id' => $cal_id,
1554
					'cal_recur_date != 0',
1555
				),__LINE__,__FILE__,'calendar');
1556
			}
1557
			else
1558
			{
1559
				// we adjust some possibly changed recurrences manually
1560
				// deleted exceptions: re-insert recurrences into the user and dates table
1561
				if (count($deleted_exceptions = array_diff($old_exceptions,$event['recur_exception'])))
1562
				{
1563
					if (isset($event['cal_participants']))
1564
					{
1565
						$participants = $event['cal_participants'];
1566
					}
1567
					else
1568
					{
1569
						// use old default
1570
						$participants = $old_participants;
1571
					}
1572
					foreach($deleted_exceptions as $id => $deleted_exception)
1573
					{
1574
						// rebuild participants for the re-inserted recurrence
1575
						$this->recurrence($cal_id, $deleted_exception, $deleted_exception + $old_duration, $participants);
1576
					}
1577
				}
1578
1579
				// check if recurrence enddate was adjusted
1580
				if(isset($event['recur_enddate']))
1581
				{
1582
					// recurrences need to be truncated
1583
					if((int)$event['recur_enddate'] > 0 &&
1584
						((int)$old_repeats['recur_enddate'] == 0 || (int)$old_repeats['recur_enddate'] > (int)$event['recur_enddate'])
1585
					)
1586
					{
1587
						$this->db->delete($this->user_table,array('cal_id' => $cal_id,'cal_recur_date >= '.($event['recur_enddate'] + 1*DAY_s)),__LINE__,__FILE__,'calendar');
1588
						$this->db->delete($this->dates_table,array('cal_id' => $cal_id,'cal_start >= '.($event['recur_enddate'] + 1*DAY_s)),__LINE__,__FILE__,'calendar');
1589
					}
1590
1591
					// recurrences need to be expanded
1592
					if(((int)$event['recur_enddate'] == 0 && (int)$old_repeats['recur_enddate'] > 0)
1593
						|| ((int)$event['recur_enddate'] > 0 && (int)$old_repeats['recur_enddate'] > 0 && (int)$old_repeats['recur_enddate'] < (int)$event['recur_enddate'])
1594
					)
1595
					{
1596
						$set_recurrences = true;
1597
						$set_recurrences_start = ($old_repeats['recur_enddate'] + 1*DAY_s);
1598
					}
1599
					//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");
1600
				}
1601
1602
				// truncate recurrences by given exceptions
1603
				if (count($event['recur_exception']))
1604
				{
1605
					// added and existing exceptions: delete the execeptions from the user table, it could be the first time
1606
					$this->db->delete($this->user_table,array('cal_id' => $cal_id,'cal_recur_date' => $event['recur_exception']),__LINE__,__FILE__,'calendar');
1607
					// update recur_exception flag based on current exceptions
1608
					$this->db->update($this->dates_table, 'recur_exception='.$this->db->expression($this->dates_table,array(
1609
						'cal_start' => $event['recur_exception'],
1610
					)), array(
1611
						'cal_id' => $cal_id,
1612
					), __LINE__, __FILE__, 'calendar');
1613
				}
1614
			}
1615
1616
			// write the repeats table
1617
			unset($event[0]);	// unset the 'etag=etag+1', as it's not in the repeats table
1618
			$this->db->insert($this->repeats_table,$event,array('cal_id' => $cal_id),__LINE__,__FILE__,'calendar');
1619
		}
1620
		// update start- and endtime if present in the event-array, evtl. we need to move all recurrences
1621
		if (isset($event['cal_start']) && isset($event['cal_end']))
1622
		{
1623
			$this->move($cal_id,$event['cal_start'],$event['cal_end'],!$cal_id ? false : $change_since, $old_min, $old_min +  $old_duration);
1624
		}
1625
		// update participants if present in the event-array
1626
		if (isset($event['cal_participants']))
1627
		{
1628
			$this->participants($cal_id,$event['cal_participants'],!$cal_id ? false : $change_since);
1629
		}
1630
		// Custom fields
1631
		foreach($event as $name => $value)
1632
		{
1633
			if ($name[0] == '#')
1634
			{
1635
				if (is_array($value) && array_key_exists('id',$value))
1636
				{
1637
					//error_log(__METHOD__.__LINE__."$name => ".array2string($value).function_backtrace());
1638
					$value = $value['id'];
1639
					//error_log(__METHOD__.__LINE__."$name => ".array2string($value));
1640
				}
1641
				if ($value)
1642
				{
1643
					$this->db->insert($this->extra_table,array(
1644
						'cal_extra_value'	=> is_array($value) ? implode(',',$value) : $value,
1645
					),array(
1646
						'cal_id'			=> $cal_id,
1647
						'cal_extra_name'	=> substr($name,1),
1648
					),__LINE__,__FILE__,'calendar');
1649
				}
1650
				else
1651
				{
1652
					$this->db->delete($this->extra_table,array(
1653
						'cal_id'			=> $cal_id,
1654
						'cal_extra_name'	=> substr($name,1),
1655
					),__LINE__,__FILE__,'calendar');
1656
				}
1657
			}
1658
		}
1659
		// updating or saving the alarms; new alarms have a temporary numeric id!
1660
		if (is_array($event['alarm']))
1661
		{
1662
			foreach ($event['alarm'] as $id => $alarm)
1663
			{
1664
				if ($alarm['id'] && strpos($alarm['id'], 'cal:'.$cal_id.':') !== 0)
1665
				{
1666
					unset($alarm['id']);	// unset the temporary id to add the alarm
1667
				}
1668
				if(!isset($alarm['offset']))
1669
				{
1670
					$alarm['offset'] = $event['cal_start'] - $alarm['time'];
1671
				}
1672
				elseif (!isset($alarm['time']))
1673
				{
1674
					$alarm['time'] = $event['cal_start'] - $alarm['offset'];
1675
				}
1676
1677
				if ($alarm['time'] < time() && !self::shift_alarm($event, $alarm))
1678
				{
1679
					continue;	// pgoerzen: don't add alarm in the past
1680
				}
1681
				$this->save_alarm($cal_id, $alarm, false);	// false: not update modified, we do it anyway
1682
			}
1683
		}
1684
		if (is_null($etag))
1685
		{
1686
			$etag = $this->db->select($this->cal_table,'cal_etag',array('cal_id' => $cal_id),__LINE__,__FILE__,false,'','calendar')->fetchColumn();
1687
		}
1688
1689
		// if event is an exception: update modified of master, to force etag, ctag and sync-token change
1690
		if ($event['cal_reference'])
1691
		{
1692
			$this->updateModified($event['cal_reference']);
1693
		}
1694
		return $cal_id;
1695
	}
1696
1697
	/**
1698
	 * Shift alarm on recurring events to next future recurrence
1699
	 *
1700
	 * @param array $_event event with optional 'cal_' prefix in keys
1701
	 * @param array &$alarm
1702
	 * @param int $timestamp For recurring events, this is the date we
1703
	 *	are dealing with, default is now.
1704
	 * @return boolean true if alarm could be shifted, false if not
1705
	 */
1706
	public static function shift_alarm(array $_event, array &$alarm, $timestamp=null)
1707
	{
1708
		if ($_event['recur_type'] == MCAL_RECUR_NONE)
1709
		{
1710
			return false;
1711
		}
1712
		$start = $timestamp ? $timestamp : (int)time() + $alarm['offset'];
1713
		$event = Api\Db::strip_array_keys($_event, 'cal_');
1714
		$rrule = calendar_rrule::event2rrule($event, false);
1715
		foreach ($rrule as $time)
1716
		{
1717
			if ($start < ($ts = Api\DateTime::to($time,'server')))
1718
			{
1719
				$alarm['time'] = $ts - $alarm['offset'];
1720
				return true;
1721
			}
1722
		}
1723
		return false;
1724
	}
1725
1726
	/**
1727
	 * moves an event to an other start- and end-time taken into account the evtl. recurrences of the event(!)
1728
	 *
1729
	 * @param int $cal_id
1730
	 * @param int $start new starttime
1731
	 * @param int $end new endtime
1732
	 * @param int|boolean $change_since =0 false=new entry, > 0 time from which on the repetitions should be changed, default 0=all
1733
	 * @param int $old_start =0 old starttime or (default) 0, to query it from the db
1734
	 * @param int $old_end =0 old starttime or (default) 0
1735
	 * @todo Recalculate recurrences, if timezone changes
1736
	 * @return int|boolean number of moved recurrences or false on error
1737
	 */
1738
	function move($cal_id,$start,$end,$change_since=0,$old_start=0,$old_end=0)
1739
	{
1740
		//echo "<p>socal::move($cal_id,$start,$end,$change_since,$old_start,$old_end)</p>\n";
1741
1742
		if (!(int) $cal_id) return false;
1743
1744
		if (!$old_start)
1745
		{
1746
			if ($change_since !== false) $row = $this->db->select($this->dates_table,'MIN(cal_start) AS cal_start,MIN(cal_end) AS cal_end',
1747
				array('cal_id'=>$cal_id),__LINE__,__FILE__,false,'','calendar')->fetch();
1748
			// if no recurrence found, create one with the new dates
1749
			if ($change_since === false || !$row || !$row['cal_start'] || !$row['cal_end'])
1750
			{
1751
				$this->db->insert($this->dates_table,array(
1752
					'cal_id'    => $cal_id,
1753
					'cal_start' => $start,
1754
					'cal_end'   => $end,
1755
				),false,__LINE__,__FILE__,'calendar');
1756
1757
				return 1;
1758
			}
1759
			$move_start = (int) ($start-$row['cal_start']);
1760
			$move_end   = (int) ($end-$row['cal_end']);
1761
		}
1762
		else
1763
		{
1764
			$move_start = (int) ($start-$old_start);
1765
			$move_end   = (int) ($end-$old_end);
1766
		}
1767
		$where = 'cal_id='.(int)$cal_id;
1768
1769 View Code Duplication
		if ($move_start)
1770
		{
1771
			// move the recur-date of the participants
1772
			$this->db->query("UPDATE $this->user_table SET cal_recur_date=cal_recur_date+$move_start WHERE $where AND cal_recur_date ".
1773
				((int)$change_since ? '>= '.(int)$change_since : '!= 0'),__LINE__,__FILE__);
1774
		}
1775 View Code Duplication
		if ($move_start || $move_end)
1776
		{
1777
			// move the event and it's recurrences
1778
			$this->db->query("UPDATE $this->dates_table SET cal_start=cal_start+$move_start,cal_end=cal_end+$move_end WHERE $where".
1779
				((int) $change_since ? ' AND cal_start >= '.(int) $change_since : ''),__LINE__,__FILE__);
1780
		}
1781
		return $this->db->affected_rows();
1782
	}
1783
1784
	/**
1785
	 * Format attendee as email
1786
	 *
1787
	 * @param string|array $attendee attendee information: email, json or array with attr cn and url
1788
	 * @return type
1789
	 */
1790
	static function attendee2email($attendee)
1791
	{
1792
		if (is_string($attendee) && $attendee[0] == '{' && substr($attendee, -1) == '}')
1793
		{
1794
			$user_attendee = json_decode($user_attendee, true);
0 ignored issues
show
Bug introduced by
The variable $user_attendee seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
1795
		}
1796
		if (is_array($attendee))
1797
		{
1798
			$email = !empty($attendee['email']) ? $user_attendee['email'] :
1799
				(strtolower(substr($attendee['url'], 0, 7)) == 'mailto:' ? substr($user_attendee['url'], 7) : $attendee['url']);
1800
			$attendee = !empty($attendee['cn']) ? $attendee['cn'].' <'.$email.'>' : $email;
1801
		}
1802
		return $attendee;
1803
	}
1804
	/**
1805
	 * combines user_type and user_id into a single string or integer (for users)
1806
	 *
1807
	 * @param string $user_type 1-char type: 'u' = user, ...
1808
	 * @param string|int $user_id id
1809
	 * @param string|array $attendee attendee information: email, json or array with attr cn and url
1810
	 * @return string|int combined id
1811
	 */
1812
	static function combine_user($user_type, $user_id, $attendee=null)
1813
	{
1814
		if (!$user_type || $user_type == 'u')
1815
		{
1816
			return (int) $user_id;
1817
		}
1818
		if ($user_type == 'e' && $attendee)
1819
		{
1820
			$user_id = self::attendee2email($attendee);
1821
		}
1822
		return $user_type.$user_id;
1823
	}
1824
1825
	/**
1826
	 * splits the combined user_type and user_id into a single values
1827
	 *
1828
	 * This is the only method building (normalized) md5 hashes for user_type="e",
1829
	 * if called with $md5_email=true parameter!
1830
	 *
1831
	 * @param string|int $uid
1832
	 * @param string &$user_type 1-char type: 'u' = user, ...
1833
	 * @param string|int &$user_id id
1834
	 * @param boolean $md5_email =false md5 hash user_id for email / user_type=="e"
1835
	 */
1836
	static function split_user($uid, &$user_type, &$user_id, $md5_email=false)
1837
	{
1838
		if (is_numeric($uid))
1839
		{
1840
			$user_type = 'u';
1841
			$user_id = (int) $uid;
1842
		}
1843
		// create md5 hash from lowercased and trimed raw email ("[email protected]", not "Ralf Becker <[email protected]>")
1844
		elseif ($md5_email && $uid[0] == 'e')
1845
		{
1846
			$user_type = $uid[0];
1847
			$email = substr($uid, 1);
1848
			$matches = null;
1849
			if (preg_match('/<([^<>]+)>$/', $email, $matches)) $email = $matches[1];
1850
			$user_id = md5(trim(strtolower($email)));
1851
		}
1852
		else
1853
		{
1854
			$user_type = $uid[0];
1855
			$user_id = substr($uid,1);
1856
		}
1857
	}
1858
1859
	/**
1860
	 * Combine status, quantity and role into one value
1861
	 *
1862
	 * @param string $status status letter: U, T, A, R
1863
	 * @param int $quantity =1
1864
	 * @param string $role ='REQ-PARTICIPANT'
1865
	 * @return string
1866
	 */
1867
	static function combine_status($status,$quantity=1,$role='REQ-PARTICIPANT')
1868
	{
1869
		if ((int)$quantity > 1) $status .= (int)$quantity;
1870
		if ($role != 'REQ-PARTICIPANT') $status .= $role;
1871
1872
		return $status;
1873
	}
1874
1875
	/**
1876
	 * splits the combined status, quantity and role
1877
	 *
1878
	 * @param string &$status I: combined value, O: status letter: U, T, A, R
1879
	 * @param int &$quantity=null only O: quantity
1880
	 * @param string &$role=null only O: role
1881
	 * @return string status U, T, A or R, same as $status parameter on return
1882
	 */
1883
	static function split_status(&$status,&$quantity=null,&$role=null)
1884
	{
1885
		$quantity = 1;
1886
		$role = 'REQ-PARTICIPANT';
1887
		//error_log(__METHOD__.__LINE__.array2string($status));
1888
		$matches = null;
1889
		if (is_string($status) && strlen($status) > 1 && preg_match('/^.([0-9]*)(.*)$/',$status,$matches))
1890
		{
1891
			if ((int)$matches[1] > 0) $quantity = (int)$matches[1];
1892
			if ($matches[2]) $role = $matches[2];
1893
			$status = $status[0];
1894
		}
1895
		elseif ($status === true)
1896
		{
1897
			$status = 'U';
1898
		}
1899
		return $status;
1900
	}
1901
1902
	/**
1903
	 * updates the participants of an event, taken into account the evtl. recurrences of the event(!)
1904
	 * this method just adds new participants or removes not longer set participants
1905
	 * this method does never overwrite existing entries (except the 0-recurrence and for delete)
1906
	 *
1907
	 * @param int $cal_id
1908
	 * @param array $participants uid => status pairs
1909
	 * @param int|boolean $change_since =0, false=new event,
1910
	 * 		0=all, > 0 time from which on the repetitions should be changed
1911
	 * @param boolean $add_only =false
1912
	 *		false = add AND delete participants if needed (full list of participants required in $participants)
1913
	 *		true = only add participants if needed, no participant will be deleted (participants to check/add required in $participants)
1914
	 * @return int|boolean number of updated recurrences or false on error
1915
	 */
1916
	function participants($cal_id,$participants,$change_since=0,$add_only=false)
1917
	{
1918
		//error_log(__METHOD__."($cal_id,".array2string($participants).",$change_since,$add_only");
1919
1920
		$recurrences = array();
1921
1922
		// remove group-invitations, they are NOT stored in the db
1923
		foreach($participants as $uid => $status)
1924
		{
1925
			if ($status[0] == 'G')
1926
			{
1927
				unset($participants[$uid]);
1928
			}
1929
		}
1930
		$where = array('cal_id' => $cal_id);
1931
1932
		if ((int) $change_since)
1933
		{
1934
			$where[] = '(cal_recur_date=0 OR cal_recur_date >= '.(int)$change_since.')';
1935
		}
1936
1937
		if ($change_since !== false)
1938
		{
1939
			// find all existing recurrences
1940 View Code Duplication
			foreach($this->db->select($this->user_table,'DISTINCT cal_recur_date',$where,__LINE__,__FILE__,false,'','calendar') as $row)
1941
			{
1942
				$recurrences[] = $row['cal_recur_date'];
1943
			}
1944
1945
			// update existing entries
1946
			$existing_entries = $this->db->select($this->user_table,'*',$where,__LINE__,__FILE__,false,'ORDER BY cal_recur_date DESC','calendar');
1947
1948
			// create a full list of participants which already exist in the db
1949
			// with status, quantity and role of the earliest recurence
1950
			$old_participants = array();
1951
			foreach($existing_entries as $row)
1952
			{
1953
				$uid = self::combine_user($row['cal_user_type'], $row['cal_user_id'], $row['cal_user_attendee']);
1954
				if ($row['cal_recur_date'] || !isset($old_participants[$uid]))
1955
				{
1956
					$old_participants[$uid] = self::combine_status($row['cal_status'],$row['cal_quantity'],$row['cal_role']);
1957
				}
1958
			}
1959
1960
			// tag participants which should be deleted
1961
			if($add_only === false)
1962
			{
1963
				$deleted = array();
1964
				foreach($existing_entries as $row)
1965
				{
1966
					$uid = self::combine_user($row['cal_user_type'], $row['cal_user_id'], $row['cal_user_attendee']);
1967
					// delete not longer set participants
1968
					if (!isset($participants[$uid]))
1969
					{
1970
						$deleted[$row['cal_user_type']][] = $row['cal_user_id'];
1971
					}
1972
				}
1973
			}
1974
1975
			// only keep added OR status (incl. quantity!) changed participants for further steps
1976
			// we do not touch unchanged (!) existing ones
1977
			foreach($participants as $uid => $status)
1978
			{
1979
				if ($old_participants[$uid] === $status)
1980
				{
1981
					unset($participants[$uid]);
1982
				}
1983
			}
1984
1985
			// delete participants tagged for delete
1986
			if ($add_only === false && count($deleted))
1987
			{
1988
				$to_or = array();
1989
				$table_def = $this->db->get_table_definitions('calendar',$this->user_table);
1990
				foreach($deleted as $type => $ids)
1991
				{
1992
					$to_or[] = $this->db->expression($table_def,array(
1993
						'cal_user_type' => $type,
1994
						'cal_user_id'   => $ids,
1995
					));
1996
				}
1997
				$where[] = '('.implode(' OR ',$to_or).')';
1998
				$where[] = "cal_status!='E'";	// do NOT delete exception marker
1999
				$this->db->update($this->user_table,array('cal_status'=>'X'),$where,__LINE__,__FILE__,'calendar');
2000
			}
2001
		}
2002
2003
		if (count($participants))	// participants which need to be added
2004
		{
2005
			if (!count($recurrences)) $recurrences[] = 0;   // insert the default recurrence
2006
2007
			$delete_deleted = array();
2008
2009
			// update participants
2010
			foreach($participants as $uid => $status)
2011
			{
2012
				$type = $id = $quantity = $role = null;
2013
				self::split_user($uid, $type, $id, true);
2014
				self::split_status($status,$quantity,$role);
2015
				$set = array(
2016
					'cal_status'	  => $status,
2017
					'cal_quantity'	  => $quantity,
2018
					'cal_role'        => $role,
2019
					'cal_user_attendee' => $type == 'e' ? substr($uid, 1) : null,
2020
				);
2021
				foreach($recurrences as $recur_date)
2022
				{
2023
					$this->db->insert($this->user_table,$set,array(
2024
						'cal_id'	      => $cal_id,
2025
						'cal_recur_date'  => $recur_date,
2026
						'cal_user_type'   => $type,
2027
						'cal_user_id' 	  => $id,
2028
					),__LINE__,__FILE__,'calendar');
2029
				}
2030
				// for new or changed group-invitations, remove previously deleted members, so they show up again
2031 View Code Duplication
				if ($uid < 0)
2032
				{
2033
					$delete_deleted = array_merge($delete_deleted, $GLOBALS['egw']->accounts->members($uid, true));
2034
				}
2035
			}
2036
			if ($delete_deleted)
2037
			{
2038
				$this->db->delete($this->user_table, $where=array(
2039
					'cal_id' => $cal_id,
2040
					'cal_recur_date' => $recurrences,
2041
					'cal_user_type' => 'u',
2042
					'cal_user_id' => array_unique($delete_deleted),
2043
					'cal_status' => 'X',
2044
				),__LINE__,__FILE__,'calendar');
2045
				//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');
2046
			}
2047
		}
2048
		return true;
2049
	}
2050
2051
	/**
2052
	 * set the status of one participant for a given recurrence or for all recurrences since now (includes recur_date=0)
2053
	 *
2054
	 * @param int $cal_id
2055
	 * @param char $user_type 'u' regular user, 'r' resource, 'c' contact
2056
	 * @param int|string $user_id
2057
	 * @param int|char $status numeric status (defines) or 1-char code: 'R', 'U', 'T' or 'A'
2058
	 * @param int $recur_date =0 date to change, or 0 = all since now
2059
	 * @param string $role =null role to set if !is_null($role)
2060
	 * @param string $attendee =null extra attendee information to set for all types (incl. accounts!)
2061
	 * @return int number of changed recurrences
2062
	 */
2063
	function set_status($cal_id,$user_type,$user_id,$status,$recur_date=0,$role=null,$attendee=null)
2064
	{
2065
		static $status_code_short = array(
2066
			REJECTED 	=> 'R',
2067
			NO_RESPONSE	=> 'U',
2068
			TENTATIVE	=> 'T',
2069
			ACCEPTED	=> 'A',
2070
			DELEGATED	=> 'D'
2071
		);
2072
		if (!(int)$cal_id || !(int)$user_id && $user_type != 'e')
2073
		{
2074
			return false;
2075
		}
2076
2077
		if (is_numeric($status)) $status = $status_code_short[$status];
2078
2079
		$uid = self::combine_user($user_type, $user_id);
2080
		$user_id_md5 = null;
2081
		self::split_user($uid, $user_type, $user_id_md5, true);
2082
2083
		$where = array(
2084
			'cal_id'		=> $cal_id,
2085
			'cal_user_type'	=> $user_type,
2086
			'cal_user_id'   => $user_id_md5,
2087
		);
2088
		if ((int) $recur_date)
2089
		{
2090
			$where['cal_recur_date'] = $recur_date;
2091
		}
2092
		else
2093
		{
2094
			$where[] = '(cal_recur_date=0 OR cal_recur_date >= '.time().')';
2095
		}
2096
2097
		if ($status == 'G')		// remove group invitations, as we dont store them in the db
2098
		{
2099
			$this->db->delete($this->user_table,$where,__LINE__,__FILE__,'calendar');
2100
			$ret = $this->db->affected_rows();
2101
		}
2102
		else
2103
		{
2104
			$set = array('cal_status' => $status);
2105
			if ($user_type == 'e' || $attendee) $set['cal_user_attendee'] = $attendee ? $attendee : $user_id;
2106
			if (!is_null($role) && $role != 'REQ-PARTICIPANT') $set['cal_role'] = $role;
2107
			$this->db->insert($this->user_table,$set,$where,__LINE__,__FILE__,'calendar');
2108
			// for new or changed group-invitations, remove previously deleted members, so they show up again
2109
			if (($ret = $this->db->affected_rows()) && $user_type == 'u' && $user_id < 0)
2110
			{
2111
				$where['cal_user_id'] = $GLOBALS['egw']->accounts->members($user_id, true);
2112
				$where['cal_status'] = 'X';
2113
				$this->db->delete($this->user_table, $where, __LINE__, __FILE__, 'calendar');
2114
				//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');
2115
			}
2116
		}
2117
		// update modified and modifier in main table
2118
		if ($ret)
2119
		{
2120
			$this->updateModified($cal_id, true);	// true = update series master too
2121
		}
2122
		//error_log(__METHOD__."($cal_id,$user_type,$user_id,$status,$recur_date) = $ret");
2123
		return $ret;
2124
	}
2125
2126
	/**
2127
	 * creates or update a recurrence in the dates and users table
2128
	 *
2129
	 * @param int $cal_id
2130
	 * @param int $start
2131
	 * @param int $end
2132
	 * @param array $participants uid => status pairs
2133
	 * @param boolean $exception =null true or false to set recure_exception flag, null leave it unchanged (new are by default no exception)
2134
	 */
2135
	function recurrence($cal_id,$start,$end,$participants,$exception=null)
2136
	{
2137
		//error_log(__METHOD__."($cal_id, $start, $end, ".array2string($participants).", ".array2string($exception));
2138
		$update = array('cal_end' => $end);
2139
		if (isset($exception)) $update['recur_exception'] = $exception;
2140
2141
		$this->db->insert($this->dates_table, $update, array(
2142
			'cal_id' => $cal_id,
2143
			'cal_start'  => $start,
2144
		),__LINE__,__FILE__,'calendar');
2145
2146
		if (!is_array($participants))
2147
		{
2148
			error_log(__METHOD__."($cal_id, $start, $end, ".array2string($participants).") participants is NO array! ".function_backtrace());
2149
		}
2150
		if ($exception !== true)
2151
		{
2152
			foreach($participants as $uid => $status)
2153
			{
2154
				if ($status == 'G') continue;	// dont save group-invitations
2155
2156
				$type = '';
2157
				$id = null;
2158
				self::split_user($uid, $type, $id, true);
2159
				$quantity = $role = null;
2160
				self::split_status($status,$quantity,$role);
2161
				$this->db->insert($this->user_table,array(
2162
					'cal_status'	=> $status,
2163
					'cal_quantity'	=> $quantity,
2164
					'cal_role'		=> $role,
2165
					'cal_user_attendee' => $type == 'e' ? substr($uid, 1) : null,
2166
				),array(
2167
					'cal_id'		 => $cal_id,
2168
					'cal_recur_date' => $start,
2169
					'cal_user_type'  => $type,
2170
					'cal_user_id' 	 => $id,
2171
				),__LINE__,__FILE__,'calendar');
2172
			}
2173
		}
2174
	}
2175
2176
	/**
2177
	 * Get all unfinished recuring events (or all users) after a given time
2178
	 *
2179
	 * @param int $time
2180
	 * @return array with cal_id => max(cal_start) pairs
2181
	 */
2182
	function unfinished_recuring($time)
2183
	{
2184
		$ids = array();
2185
		foreach($rs=$this->db->select($this->repeats_table, "$this->repeats_table.cal_id,MAX(cal_start) AS cal_start",
2186
			'(range_end IS NULL OR range_end > '.(int)$time.')',
2187
			__LINE__, __FILE__, false, "GROUP BY $this->repeats_table.cal_id,range_end", 'calendar', 0,
2188
			" JOIN $this->cal_table ON $this->repeats_table.cal_id=$this->cal_table.cal_id".
2189
			" JOIN $this->dates_table ON $this->repeats_table.cal_id=$this->dates_table.cal_id") as $row)
2190
		{
2191
			$ids[$row['cal_id']] = $row['cal_start'];
2192
		}
2193
		//error_log(__METHOD__."($time) query='$rs->sql' --> ids=".array2string($ids));
2194
		return $ids;
2195
	}
2196
2197
	/**
2198
	 * deletes an event incl. all recurrences, participants and alarms
2199
	 *
2200
	 * @param int $cal_id
2201
	 */
2202
	function delete($cal_id)
2203
	{
2204
		//echo "<p>socal::delete($cal_id)</p>\n";
2205
2206
		$this->delete_alarms($cal_id);
2207
2208
		// update timestamp of series master, updates own timestamp too, which does not hurt ;-)
2209
		$this->updateModified($cal_id, true);
2210
2211
		foreach($this->all_tables as $table)
2212
		{
2213
			$this->db->delete($table,array('cal_id'=>$cal_id),__LINE__,__FILE__,'calendar');
2214
		}
2215
	}
2216
2217
	/**
2218
	 * Delete all events that were before the given date.
2219
	 *
2220
	 * Recurring events that finished before the date will be deleted.
2221
	 * Recurring events that span the date will be ignored.  Non-recurring
2222
	 * events before the date will be deleted.
2223
	 *
2224
	 * @param int $date
2225
	 */
2226
	function purge($date)
2227
	{
2228
		// with new range_end we simple delete all with range_end < $date (range_end NULL is never returned)
2229
		foreach($this->db->select($this->cal_table, 'cal_id', 'range_end < '.(int)$date, __LINE__, __FILE__, false, '', 'calendar') as $row)
2230
		{
2231
			//echo __METHOD__." About to delete".$row['cal_id']."\r\n";
2232
			foreach($this->all_tables as $table)
2233
			{
2234
				$this->db->delete($table, array('cal_id'=>$row['cal_id']), __LINE__, __FILE__, 'calendar');
2235
			}
2236
			// handle sync
2237
			$this->db->update('egw_api_content_history',array(
2238
				'sync_deleted' => time(),
2239
			),array(
2240
				'sync_appname' => 'calendar',
2241
				'sync_contentid' => $row['cal_id'],	// sync_contentid is varchar(60)!
2242
			), __LINE__, __FILE__);
2243
			// handle links
2244
			Link::unlink('', 'calendar', $row['cal_id']);
2245
		}
2246
	}
2247
2248
	/**
2249
	 * Caches all alarms read from async table to not re-read them in same request
2250
	 *
2251
	 * @var array cal_id => array(async_id => data)
2252
	 */
2253
	static $alarm_cache;
2254
2255
	/**
2256
	 * read the alarms of one or more calendar-event(s) specified by $cal_id
2257
	 *
2258
	 * alarm-id is a string of 'cal:'.$cal_id.':'.$alarm_nr, it is used as the job-id too
2259
	 *
2260
	 * @param int|array $cal_id
2261
	 * @param boolean $update_cache =null true: re-read given $cal_id, false: delete given $cal_id
2262
	 * @return array of (cal_id => array of) alarms with alarm-id as key
2263
	 */
2264
	function read_alarms($cal_id, $update_cache=null)
2265
	{
2266
		if (!isset(self::$alarm_cache) && is_array($cal_id))
2267
		{
2268
			self::$alarm_cache = array();
2269
			if (($jobs = $this->async->read('cal:%')))
2270
			{
2271
				foreach($jobs as $id => $job)
2272
				{
2273
					$alarm         = $job['data'];	// text, enabled
2274
					$alarm['id']   = $id;
2275
					$alarm['time'] = $job['next'];
2276
2277
					self::$alarm_cache[$alarm['cal_id']][$id] = $alarm;
2278
				}
2279
			}
2280
			unset($update_cache);	// just done
2281
		}
2282
		$alarms = array();
2283
2284
		if (isset(self::$alarm_cache))
2285
		{
2286
			if (isset($update_cache))
2287
			{
2288
				foreach((array)$cal_id as $id)
2289
				{
2290
					if ($update_cache === false)
2291
					{
2292
						unset(self::$alarm_cache[$cal_id]);
2293
					}
2294
					elseif($update_cache === true)
2295
					{
2296
						self::$alarm_cache[$cal_id] = $this->read_alarms_nocache($cal_id);
2297
					}
2298
				}
2299
			}
2300
			if (!is_array($cal_id))
2301
			{
2302
				$alarms = (array)self::$alarm_cache[$cal_id];
2303
			}
2304
			else
2305
			{
2306
				foreach($cal_id as $id)
2307
				{
2308
					$alarms[$id] = (array)self::$alarm_cache[$id];
2309
				}
2310
			}
2311
			//error_log(__METHOD__."(".array2string($cal_id).", ".array2string($update_cache).") returning from cache ".array2string($alarms));
2312
			return $alarms;
2313
		}
2314
		return $this->read_alarms_nocache($cal_id);
2315
	}
2316
2317
	private function read_alarms_nocache($cal_id)
2318
	{
2319
		if (($jobs = $this->async->read('cal:'.(int)$cal_id.':%')))
2320
		{
2321
			foreach($jobs as $id => $job)
2322
			{
2323
				$alarm         = $job['data'];	// text, enabled
2324
				$alarm['id']   = $id;
2325
				$alarm['time'] = $job['next'];
2326
2327
				$alarms[$id] = $alarm;
2328
			}
2329
		}
2330
		//error_log(__METHOD__."(".array2string($cal_id).") returning ".array2string($alarms));
2331
		return $alarms ? $alarms : array();
2332
	}
2333
2334
	/**
2335
	 * read a single alarm specified by it's $id
2336
	 *
2337
	 * @param string $id alarm-id is a string of 'cal:'.$cal_id.':'.$alarm_nr, it is used as the job-id too
2338
	 * @return array with data of the alarm
2339
	 */
2340
	function read_alarm($id)
2341
	{
2342
		if (!($jobs = $this->async->read($id)))
2343
		{
2344
			return False;
2345
		}
2346
		list($alarm_id,$job) = each($jobs);
2347
		$alarm         = $job['data'];	// text, enabled
2348
		$alarm['id']   = $alarm_id;
2349
		$alarm['time'] = $job['next'];
2350
2351
		//echo "<p>read_alarm('$id')="; print_r($alarm); echo "</p>\n";
2352
		return $alarm;
2353
	}
2354
2355
	/**
2356
	 * saves a new or updated alarm
2357
	 *
2358
	 * @param int $cal_id Id of the calendar-entry
2359
	 * @param array $alarm array with fields: text, owner, enabled, ..
2360
	 * @param boolean $update_modified =true call update modified, default true
2361
	 * @return string id of the alarm
2362
	 */
2363
	function save_alarm($cal_id, $alarm, $update_modified=true)
2364
	{
2365
		//error_log(__METHOD__."($cal_id, ".array2string($alarm).', '.array2string($update_modified).') '.function_backtrace());
2366
		if (!($id = $alarm['id']))
2367
		{
2368
			$alarms = $this->read_alarms($cal_id);	// find a free alarm#
2369
			$n = count($alarms);
2370
			do
2371
			{
2372
				$id = 'cal:'.(int)$cal_id.':'.$n;
2373
				++$n;
2374
			}
2375
			while (@isset($alarms[$id]));
2376
		}
2377
		else
2378
		{
2379
			$this->async->cancel_timer($id);
2380
		}
2381
		$alarm['cal_id'] = $cal_id;		// we need the back-reference
2382
		// add an alarm uid, if none is given
2383
		if (empty($alarm['uid']) && class_exists('Horde_Support_Uuid')) $alarm['uid'] = (string)new Horde_Support_Uuid;
2384
		//error_log(__METHOD__.__LINE__.' Save Alarm for CalID:'.$cal_id.'->'.array2string($alarm).'-->'.$id.'#'.function_backtrace());
2385
		// allways store job with the alarm owner as job-owner to get eg. the correct from address
2386
		if (!$this->async->set_timer($alarm['time'],$id,'calendar.calendar_boupdate.send_alarm',$alarm,$alarm['owner']))
2387
		{
2388
			return False;
2389
		}
2390
2391
		// update the modification information of the related event
2392
		if ($update_modified) $this->updateModified($cal_id, true);
2393
2394
		// update cache, if used
2395
		if (isset(self::$alarm_cache)) $this->read_alarms($cal_id, true);
2396
2397
		return $id;
2398
	}
2399
2400
	/**
2401
	 * Delete all alarms of a calendar-entry
2402
	 *
2403
	 * Does not update timestamps of series master, therefore private!
2404
	 *
2405
	 * @param int $cal_id Id of the calendar-entry
2406
	 * @return int number of alarms deleted
2407
	 */
2408 View Code Duplication
	private function delete_alarms($cal_id)
2409
	{
2410
		//error_log(__METHOD__."($cal_id) ".function_backtrace());
2411
		if (($alarms = $this->read_alarms($cal_id)))
2412
		{
2413
			foreach(array_keys($alarms) as $id)
2414
			{
2415
				$this->async->cancel_timer($id);
2416
			}
2417
			// update cache, if used
2418
			if (isset(self::$alarm_cache)) $this->read_alarms($cal_id, false);
2419
		}
2420
		return count($alarms);
2421
	}
2422
2423
	/**
2424
	 * delete one alarms identified by its id
2425
	 *
2426
	 * @param string $id alarm-id is a string of 'cal:'.$cal_id.':'.$alarm_nr, it is used as the job-id too
2427
	 * @return int number of alarms deleted
2428
	 */
2429 View Code Duplication
	function delete_alarm($id)
2430
	{
2431
		//error_log(__METHOD__."('$id') ".function_backtrace());
2432
		// update the modification information of the related event
2433
		list(,$cal_id) = explode(':',$id);
2434
		if ($cal_id)
2435
		{
2436
			$this->updateModified($cal_id, true);
2437
		}
2438
		$ret = $this->async->cancel_timer($id);
2439
2440
		// update cache, if used
2441
		if (isset(self::$alarm_cache)) $this->read_alarms($cal_id, true);
2442
2443
		return $ret;
2444
	}
2445
2446
	/**
2447
	 * Delete account hook
2448
	 *
2449
	 * @param array|int $old_user integer old user or array with keys 'account_id' and 'new_owner' as the deleteaccount hook uses it
2450
	 * @param int $new_user =null
2451
	 */
2452
	function deleteaccount($old_user, $new_user=null)
2453
	{
2454
		if (is_array($old_user))
2455
		{
2456
			$new_user = $old_user['new_owner'];
2457
			$old_user = $old_user['account_id'];
2458
		}
2459
		if (!(int)$new_user)
2460
		{
2461
			$user_type = '';
2462
			$user_id = null;
2463
			self::split_user($old_user,$user_type,$user_id);
2464
2465
			if ($user_type == 'u')	// only accounts can be owners of events
2466
			{
2467
				foreach($this->db->select($this->cal_table,'cal_id',array('cal_owner' => $old_user),__LINE__,__FILE__,false,'','calendar') as $row)
2468
				{
2469
					$this->delete($row['cal_id']);
2470
				}
2471
			}
2472
			$this->db->delete($this->user_table,array(
2473
				'cal_user_type' => $user_type,
2474
				'cal_user_id'   => $user_id,
2475
			),__LINE__,__FILE__,'calendar');
2476
2477
			// delete calendar entries without participants (can happen if the deleted user is the only participants, but not the owner)
2478
			foreach($this->db->select($this->cal_table,"DISTINCT $this->cal_table.cal_id",'cal_user_id IS NULL',__LINE__,__FILE__,
2479
				False,'','calendar',0,"LEFT JOIN $this->user_table ON $this->cal_table.cal_id=$this->user_table.cal_id") as $row)
2480
			{
2481
				$this->delete($row['cal_id']);
2482
			}
2483
		}
2484
		else
2485
		{
2486
			$this->db->update($this->cal_table,array('cal_owner' => $new_user),array('cal_owner' => $old_user),__LINE__,__FILE__,'calendar');
2487
			// delete participation of old user, if new user is already a participant
2488
			$ids = array();
2489
			foreach($this->db->select($this->user_table,'cal_id',array(		// MySQL does NOT allow to run this as delete!
2490
				'cal_user_type' => 'u',
2491
				'cal_user_id' => $old_user,
2492
				"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')",
2493
			),__LINE__,__FILE__,false,'','calendar') as $row)
2494
			{
2495
				$ids[] = $row['cal_id'];
2496
			}
2497
			if ($ids) $this->db->delete($this->user_table,array(
2498
				'cal_user_type' => 'u',
2499
				'cal_user_id' => $old_user,
2500
				'cal_id' => $ids,
2501
			),__LINE__,__FILE__,'calendar');
2502
			// now change participant in the rest to contain new user instead of old user
2503
			$this->db->update($this->user_table,array(
2504
				'cal_user_id' => $new_user,
2505
			),array(
2506
				'cal_user_type' => 'u',
2507
				'cal_user_id' => $old_user,
2508
			),__LINE__,__FILE__,'calendar');
2509
		}
2510
	}
2511
2512
	/**
2513
	 * get stati of all recurrences of an event for a specific participant
2514
	 *
2515
	 * @param int $cal_id
2516
	 * @param int $uid =null  participant uid; if == null return only the recur dates
2517
	 * @param int $start =0  if != 0: startdate of the search/list (servertime)
2518
	 * @param int $end =0  if != 0: enddate of the search/list (servertime)
2519
	 *
2520
	 * @return array recur_date => status pairs (index 0 => main status)
2521
	 */
2522
	function get_recurrences($cal_id, $uid=null, $start=0, $end=0)
2523
	{
2524
		$participant_status = array();
2525
		$where = array('cal_id' => $cal_id);
2526 View Code Duplication
		if ($start != 0 && $end == 0) $where[] = '(cal_recur_date = 0 OR cal_recur_date >= ' . (int)$start . ')';
2527 View Code Duplication
		if ($start == 0 && $end != 0) $where[] = '(cal_recur_date = 0 OR cal_recur_date <= ' . (int)$end . ')';
2528 View Code Duplication
		if ($start != 0 && $end != 0)
2529
		{
2530
			$where[] = '(cal_recur_date = 0 OR (cal_recur_date >= ' . (int)$start .
2531
						' AND cal_recur_date <= ' . (int)$end . '))';
2532
		}
2533 View Code Duplication
		foreach($this->db->select($this->user_table,'DISTINCT cal_recur_date',$where,__LINE__,__FILE__,false,'','calendar') as $row)
2534
		{
2535
			// inititalize the array
2536
			$participant_status[$row['cal_recur_date']] = null;
2537
		}
2538
		if (is_null($uid)) return $participant_status;
2539
		$user_type = $user_id = null;
2540
		self::split_user($uid, $user_type, $user_id, true);
2541
2542
		$where2 = array(
2543
			'cal_id'		=> $cal_id,
2544
			'cal_user_type'	=> $user_type ? $user_type : 'u',
2545
			'cal_user_id'   => $user_id,
2546
		);
2547 View Code Duplication
		if ($start != 0 && $end == 0) $where2[] = '(cal_recur_date = 0 OR cal_recur_date >= ' . (int)$start . ')';
2548 View Code Duplication
		if ($start == 0 && $end != 0) $where2[] = '(cal_recur_date = 0 OR cal_recur_date <= ' . (int)$end . ')';
2549 View Code Duplication
		if ($start != 0 && $end != 0)
2550
		{
2551
			$where2[] = '(cal_recur_date = 0 OR (cal_recur_date >= ' . (int)$start .
2552
						' AND cal_recur_date <= ' . (int)$end . '))';
2553
		}
2554
		foreach ($this->db->select($this->user_table,'cal_recur_date,cal_status,cal_quantity,cal_role',$where2,
2555
				__LINE__,__FILE__,false,'','calendar') as $row)
2556
		{
2557
			$status = self::combine_status($row['cal_status'],$row['cal_quantity'],$row['cal_role']);
2558
			$participant_status[$row['cal_recur_date']] = $status;
2559
		}
2560
		return $participant_status;
2561
	}
2562
2563
	/**
2564
	 * get all participants of an event
2565
	 *
2566
	 * @param int $cal_id
2567
	 * @param int $recur_date =0 gives participants of this recurrence, default 0=all
2568
	 *
2569
	 * @return array participants
2570
	 */
2571
	/* seems NOT to be used anywhere, NOT ported to new md5-email schema!
2572
	function get_participants($cal_id, $recur_date=0)
2573
	{
2574
		$participants = array();
2575
		$where = array('cal_id' => $cal_id);
2576
		if ($recur_date)
2577
		{
2578
			$where['cal_recur_date'] = $recur_date;
2579
		}
2580
2581
		foreach ($this->db->select($this->user_table,'DISTINCT cal_user_type,cal_user_id', $where,
2582
				__LINE__,__FILE__,false,'','calendar') as $row)
2583
		{
2584
			$uid = self::combine_user($row['cal_user_type'], $row['cal_user_id']);
2585
			$id = $row['cal_user_type'] . $row['cal_user_id'];
2586
			$participants[$id]['type'] = $row['cal_user_type'];
2587
			$participants[$id]['id'] = $row['cal_user_id'];
2588
			$participants[$id]['uid'] = $uid;
2589
		}
2590
		return $participants;
2591
	}*/
2592
2593
	/**
2594
	 * get all releated events
2595
	 *
2596
	 * @param int $uid					UID of the series
2597
	 *
2598
	 * @return array of event exception ids for all events which share $uid
2599
	 */
2600
	function get_related($uid)
2601
	{
2602
		$where = array(
2603
			'cal_uid'		=> $uid,
2604
		);
2605
		$related = array();
2606
		foreach ($this->db->select($this->cal_table,'cal_id,cal_reference',$where,
2607
				__LINE__,__FILE__,false,'','calendar') as $row)
2608
		{
2609
			if ($row['cal_reference'] != 0)
2610
			{
2611
				// not the series master
2612
				$related[] = $row['cal_id'];
2613
			}
2614
		}
2615
		return $related;
2616
	}
2617
2618
	/**
2619
	 * Gets the exception days of a given recurring event caused by
2620
	 * irregular participant stati or timezone transitions
2621
	 *
2622
	 * @param array $event			Recurring Event.
2623
	 * @param string tz_id=null		timezone for exports (null for event's timezone)
2624
	 * @param int $start =0  if != 0: startdate of the search/list (servertime)
2625
	 * @param int $end =0  if != 0:	enddate of the search/list (servertime)
2626
	 * @param string $filter ='all'	string filter-name: all (not rejected),
2627
	 * 		accepted, unknown, tentative, rejected, delegated
2628
	 *      rrule					return array of remote exceptions in servertime
2629
	 * 		tz_rrule/tz_only,		return (only by) timezone transition affected entries
2630
	 * 		map						return array of dates with no pseudo exception
2631
	 * 									key remote occurrence date
2632
	 * 		tz_map					return array of all dates with no tz pseudo exception
2633
	 *
2634
	 * @return array		Array of exception days (false for non-recurring events).
2635
	 */
2636
	function get_recurrence_exceptions($event, $tz_id=null, $start=0, $end=0, $filter='all')
2637
	{
2638
		if (!is_array($event)) return false;
2639
		$cal_id = (int) $event['id'];
2640
		//error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2641
		//		"($cal_id, $tz_id, $filter): " . $event['tzid']);
2642
		if (!$cal_id || $event['recur_type'] == MCAL_RECUR_NONE) return false;
2643
2644
		$days = array();
2645
2646
		$expand_all = (!$this->isWholeDay($event) && $tz_id && $tz_id != $event['tzid']);
2647
2648
		if ($filter == 'tz_only' && !$expand_all) return $days;
2649
2650
		$remote = in_array($filter, array('tz_rrule', 'rrule'));
2651
2652
		$egw_rrule = calendar_rrule::event2rrule($event, false);
2653
		$egw_rrule->current = clone $egw_rrule->time;
2654
		if ($expand_all)
2655
		{
2656
			unset($event['recur_exception']);
2657
			$remote_rrule = calendar_rrule::event2rrule($event, false, $tz_id);
2658
			$remote_rrule->current = clone $remote_rrule->time;
2659
		}
2660
		while ($egw_rrule->valid())
2661
		{
2662
			while ($egw_rrule->exceptions &&
2663
				in_array($egw_rrule->current->format('Ymd'),$egw_rrule->exceptions))
2664
			{
2665
				if (in_array($filter, array('map','tz_map','rrule','tz_rrule')))
2666
				{
2667
					 // real exception
2668
					$locts = (int)Api\DateTime::to($egw_rrule->current(),'server');
2669
					if ($expand_all)
2670
					{
2671
						$remts = (int)Api\DateTime::to($remote_rrule->current(),'server');
2672
						if ($remote)
2673
						{
2674
							$days[$locts]= $remts;
2675
						}
2676
						else
2677
						{
2678
							$days[$remts]= $locts;
2679
						}
2680
					}
2681
					else
2682
					{
2683
						$days[$locts]= $locts;
2684
					}
2685
				}
2686
				if ($expand_all)
2687
				{
2688
					$remote_rrule->next_no_exception();
2689
				}
2690
				$egw_rrule->next_no_exception();
2691
				if (!$egw_rrule->valid()) return $days;
2692
			}
2693
			$day = $egw_rrule->current();
2694
			$locts = (int)Api\DateTime::to($day,'server');
2695
			$tz_exception = ($filter == 'tz_rrule');
2696
			//error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2697
			//	'()[EVENT Server]: ' . $day->format('Ymd\THis') . " ($locts)");
2698
			if ($expand_all)
2699
			{
2700
				$remote_day = $remote_rrule->current();
2701
				$remts = (int)Api\DateTime::to($remote_day,'server');
2702
			//	error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2703
			//	'()[EVENT Device]: ' . $remote_day->format('Ymd\THis') . " ($remts)");
2704
			}
2705
2706
2707
			if (!($end && $end < $locts) && $start <= $locts)
2708
			{
2709
				// we are within the relevant time period
2710
				if ($expand_all && $day->format('U') != $remote_day->format('U'))
2711
				{
2712
					$tz_exception = true;
2713 View Code Duplication
					if ($filter != 'map' && $filter != 'tz_map')
2714
					{
2715
						// timezone pseudo exception
2716
						//error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2717
						//	'() tz exception: ' . $day->format('Ymd\THis'));
2718
						if ($remote)
2719
						{
2720
							$days[$locts]= $remts;
2721
						}
2722
						else
2723
						{
2724
							$days[$remts]= $locts;
2725
						}
2726
					}
2727
				}
2728
				if ($filter != 'tz_map' && (!$tz_exception || $filter == 'tz_only') &&
2729
					$this->status_pseudo_exception($event['id'], $locts, $filter))
2730
				{
2731
					// status pseudo exception
2732
					//error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2733
					//	'() status exception: ' . $day->format('Ymd\THis'));
2734
					if ($expand_all)
2735
					{
2736
						if ($filter == 'tz_only')
2737
						{
2738
								unset($days[$remts]);
2739
						}
2740 View Code Duplication
						else
2741
						{
2742
							if ($filter != 'map')
2743
							{
2744
								if ($remote)
2745
								{
2746
									$days[$locts]= $remts;
2747
								}
2748
								else
2749
								{
2750
									$days[$remts]= $locts;
2751
								}
2752
							}
2753
						}
2754
					}
2755
					elseif ($filter != 'map')
2756
					{
2757
						$days[$locts]= $locts;
2758
					}
2759
				}
2760
				elseif (($filter == 'map' || filter == 'tz_map') &&
2761
						!$tz_exception)
2762
				{
2763
					// no pseudo exception date
2764
					if ($expand_all)
2765
					{
2766
2767
						$days[$remts]= $locts;
2768
					}
2769
					else
2770
					{
2771
						$days[$locts]= $locts;
2772
					}
2773
				}
2774
			}
2775
			if ($expand_all)
2776
			{
2777
				$remote_rrule->next_no_exception();
2778
			}
2779
			$egw_rrule->next_no_exception();
2780
		}
2781
		return $days;
2782
	}
2783
2784
	/**
2785
	 * Checks for status only pseudo exceptions
2786
	 *
2787
	 * @param int $cal_id		event id
2788
	 * @param int $recur_date	occurrence to check
2789
	 * @param string $filter	status filter criteria for user
2790
	 *
2791
	 * @return boolean			true, if stati don't match with defaults
2792
	 */
2793
	function status_pseudo_exception($cal_id, $recur_date, $filter)
2794
	{
2795
		static $recurrence_zero=null;
2796
		static $cached_id=null;
2797
		static $user=null;
2798
2799
		if (!isset($cached_id) || $cached_id != $cal_id)
2800
		{
2801
			// get default stati
2802
			$recurrence_zero = array();
2803
			$user = $GLOBALS['egw_info']['user']['account_id'];
2804
			$where = array(
2805
				'cal_id' => $cal_id,
2806
				'cal_recur_date' => 0,
2807
			);
2808 View Code Duplication
			foreach ($this->db->select($this->user_table,'cal_user_type,cal_user_id,cal_user_attendee,cal_status',$where,
2809
				__LINE__,__FILE__,false,'','calendar') as $row)
2810
			{
2811
				switch ($row['cal_user_type'])
2812
				{
2813
					case 'u':	// account
2814
					case 'c':	// contact
2815
					case 'e':	// email address
2816
						$uid = self::combine_user($row['cal_user_type'], $row['cal_user_id'], $row['cal_user_attendee']);
2817
						$recurrence_zero[$uid] = $row['cal_status'];
2818
				}
2819
			}
2820
			$cached_id = $cal_id;
2821
		}
2822
2823
		//error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2824
		//	"($cal_id, $recur_date, $filter)[DEFAULTS]: " .
2825
		//	array2string($recurrence_zero));
2826
2827
		$participants = array();
2828
		$where = array(
2829
			'cal_id' => $cal_id,
2830
			'cal_recur_date' => $recur_date,
2831
		);
2832 View Code Duplication
		foreach ($this->db->select($this->user_table,'cal_user_type,cal_user_id,cal_user_attendee,cal_status',$where,
2833
			__LINE__,__FILE__,false,'','calendar') as $row)
2834
		{
2835
			switch ($row['cal_user_type'])
2836
			{
2837
				case 'u':	// account
2838
				case 'c':	// contact
2839
				case 'e':	// email address
2840
					$uid = self::combine_user($row['cal_user_type'], $row['cal_user_id'], $row['cal_user_attendee']);
2841
					$participants[$uid] = $row['cal_status'];
2842
			}
2843
		}
2844
2845
		if (empty($participants)) return false; // occurrence does not exist at all yet
2846
2847
		foreach ($recurrence_zero as $uid => $status)
0 ignored issues
show
Bug introduced by
The expression $recurrence_zero of type array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
2848
		{
2849
			if ($uid == $user)
2850
			{
2851
				// handle filter for current user
2852
				switch ($filter)
2853
				{
2854
					case 'unknown':
2855
						if ($status != 'U')
2856
						{
2857
							unset($participants[$uid]);
2858
							continue;
2859
						}
2860
						break;
2861
					case 'accepted':
2862
						if ($status != 'A')
2863
						{
2864
							unset($participants[$uid]);
2865
							continue;
2866
						}
2867
						break;
2868
					case 'tentative':
2869
						if ($status != 'T')
2870
						{
2871
							unset($participants[$uid]);
2872
							continue;
2873
						}
2874
						break;
2875
					case 'rejected':
2876
						if ($status != 'R')
2877
						{
2878
							unset($participants[$uid]);
2879
							continue;
2880
						}
2881
						break;
2882
					case 'delegated':
2883
						if ($status != 'D')
2884
						{
2885
							unset($participants[$uid]);
2886
							continue;
2887
						}
2888
						break;
2889
					case 'default':
2890
						if ($status == 'R')
2891
						{
2892
							unset($participants[$uid]);
2893
							continue;
2894
						}
2895
						break;
2896
					default:
2897
						// All entries
2898
				}
2899
			}
2900
			if (!isset($participants[$uid])
2901
				|| $participants[$uid] != $status)
2902
				return true;
2903
			unset($participants[$uid]);
2904
		}
2905
		return (!empty($participants));
2906
	}
2907
2908
	/**
2909
	 * Check if the event is the whole day
2910
	 *
2911
	 * @param array $event event (all timestamps in servertime)
2912
	 * @return boolean true if whole day event within its timezone, false othwerwise
2913
	 */
2914
	function isWholeDay($event)
2915
	{
2916
		if (!isset($event['start']) || !isset($event['end'])) return false;
2917
2918
		if (empty($event['tzid']))
2919
		{
2920
			$timezone = Api\DateTime::$server_timezone;
2921
		}
2922
		else
2923
		{
2924 View Code Duplication
			if (!isset(self::$tz_cache[$event['tzid']]))
2925
			{
2926
				self::$tz_cache[$event['tzid']] = calendar_timezones::DateTimeZone($event['tzid']);
2927
			}
2928
			$timezone = self::$tz_cache[$event['tzid']];
2929
		}
2930
		$start_time = new Api\DateTime($event['start'],Api\DateTime::$server_timezone);
2931
		$start_time->setTimezone($timezone);
2932
		$end_time = new Api\DateTime($event['end'],Api\DateTime::$server_timezone);
2933
		$end_time->setTimezone($timezone);
2934
		//error_log(__FILE__.'['.__LINE__.'] '.__METHOD__.
2935
		//	'(): ' . $start . '-' . $end);
2936
		$start = Api\DateTime::to($start_time,'array');
2937
		$end = Api\DateTime::to($end_time,'array');
2938
2939
2940
		return !$start['hour'] && !$start['minute'] && $end['hour'] == 23 && $end['minute'] == 59;
2941
	}
2942
2943
	/**
2944
	 * Moves a datetime to the beginning of the day within timezone
2945
	 *
2946
	 * @param Api\DateTime	$time	the datetime entry
2947
	 * @param string tz_id		timezone
2948
	 *
2949
	 * @return DateTime
2950
	 */
2951
	function &startOfDay(Api\DateTime $time, $tz_id=null)
2952
	{
2953
		if (empty($tz_id))
2954
		{
2955
			$timezone = Api\DateTime::$server_timezone;
2956
		}
2957
		else
2958
		{
2959
			if (!isset(self::$tz_cache[$tz_id]))
2960
			{
2961
				self::$tz_cache[$tz_id] = calendar_timezones::DateTimeZone($tz_id);
2962
			}
2963
			$timezone = self::$tz_cache[$tz_id];
2964
		}
2965
		return new Api\DateTime($time->format('Y-m-d 00:00:00'), $timezone);
2966
	}
2967
2968
	/**
2969
	 * Updates the modification timestamp to force an etag, ctag and sync-token change
2970
	 *
2971
	 * @param int $id event id
2972
	 * @param int|boolean $update_master =false id of series master or true, to update series master too
2973
	 * @param int $time =null new timestamp, default current (server-)time
2974
	 * @param int $modifier =null uid of the modifier, default current user
2975
	 */
2976
	function updateModified($id, $update_master=false, $time=null, $modifier=null)
2977
	{
2978
		if (is_null($time) || !$time) $time = time();
2979
		if (is_null($modifier)) $modifier = $GLOBALS['egw_info']['user']['account_id'];
2980
2981
		$this->db->update($this->cal_table,
2982
			array('cal_modified' => $time, 'cal_modifier' => $modifier),
2983
			array('cal_id' => $id), __LINE__,__FILE__, 'calendar');
2984
2985
		// if event is an exception: update modified of master, to force etag, ctag and sync-token change
2986
		if ($update_master)
2987
		{
2988
			if ($update_master !== true || ($update_master = $this->db->select($this->cal_table, 'cal_reference', array('cal_id' => $id), __LINE__, __FILE__)->fetchColumn()))
2989
			{
2990
				$this->updateModified($update_master, false, $time, $modifier);
2991
			}
2992
		}
2993
	}
2994
}
2995