1<?php
2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
3//
4// All Rights Reserved. See copyright.txt for details and a complete list of authors.
5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
6// $Id$
7
8//this script may only be included - so its better to die if called directly.
9if (strpos($_SERVER["SCRIPT_NAME"], basename(__FILE__)) !== false) {
10	header("location: index.php");
11	exit;
12}
13
14if (! defined('ROLE_ORGANIZER')) {
15	define('ROLE_ORGANIZER', '6');
16}
17if (! defined('weekInSeconds')) {
18	define('weekInSeconds', 604800);
19}
20
21/**
22 *
23 */
24class CalendarLib extends TikiLib
25{
26	/**
27	 * @param $sort_mode
28	 * @return string
29	 */
30	function convertSortMode($sort_mode, $fields = null)
31	{
32		$tmp = explode("_", $sort_mode);
33		if (count($tmp) == 2) {
34			if ($tmp[0] == "categoryName" || $tmp[0] == "locationName") {
35				return "name " . $tmp[1];
36			}
37		}
38		return parent::convertSortMode($sort_mode, $fields);
39	}
40
41	/**
42	 * @param int $offset
43	 * @param $maxRecords
44	 * @param string $sort_mode
45	 * @param string $find
46	 * @param string $user
47	 * @return mixed
48	 */
49	function list_calendars($offset = 0, $maxRecords = -1, $sort_mode = 'name_asc', $find = '', $user = '')
50	{
51		$mid = '';
52		$res = [];
53		$bindvars = [];
54		$join = '';
55
56		if ($find) {
57			$mid = "and tcal.`name` like ?";
58			$bindvars[] = '%' . $find . '%';
59		}
60
61		if ($user) {
62			$mid = "and ( tcal.`user` = ? or tcali.`calendarInstanceId` IS NOT NULL )";
63			$bindvars[] = $user;
64			$join .= ' left join `tiki_calendar_instances` tcali on tcal.calendarId = tcali.calendarId and tcali.`user` = ?';
65			array_unshift($bindvars, $user);
66		}
67
68		$categlib = TikiLib::lib('categ');
69
70		if ($jail = $categlib->get_jail()) {
71			$categlib->getSqlJoin($jail, 'calendar', 'tcal.`calendarId`', $join, $mid, $bindvars);
72		}
73
74		$query = "select tcal.* from `tiki_calendars` as tcal $join where 1=1 $mid order by tcal." . $this->convertSortMode($sort_mode);
75		$result = $this->query($query, $bindvars, $maxRecords, $offset);
76		$query_cant = "select count(*) from `tiki_calendars` as tcal $join where 1=1 $mid";
77		$cant = $this->getOne($query_cant, $bindvars);
78
79		$res = [];
80		while ($r = $result->fetchRow()) {
81			$k = $r["calendarId"];
82			$res2 = $this->query("select `optionName`,`value` from `tiki_calendar_options` where `calendarId`=?", [(int)$k]);
83			while ($r2 = $res2->fetchRow()) {
84				$r[$r2['optionName']] = $r2['value'];
85			}
86			if ($user) {
87				// override with per user instance values if those exist
88				$query = "select * from `tiki_calendar_instances` where calendarId = ? and user = ?";
89				$instance_result = $this->query($query, [$r['calendarId'], $user]);
90				$instance = $instance_result->fetchRow();
91				if ($instance) {
92					$r['name'] = $instance['name'];
93					$r['description'] = $instance['description'];
94					$r['order'] = $instance['order'];
95					$r['color'] = $instance['color'];
96					$r['timezone'] = $instance['timezone'];
97					$r['access'] = $instance['access'];
98					$r['calendarInstanceId'] = $instance['calendarInstanceId'];
99				} else {
100					$r['calendarInstanceId'] = 0;
101				}
102			}
103			$r['name'] = tra($r['name']);
104			$res["$k"] = $r;
105		}
106		$retval["data"] = $res;
107		$retval["cant"] = $cant;
108		return $retval;
109	}
110
111	/**
112	 * @param $name
113	 * @return mixed
114	 */
115	function get_calendarId_from_name($name)
116	{
117		$query = 'select `calendarId` from `tiki_calendars` where `name`=?';
118		return $this->getOne($query, [$name]);
119	}
120
121	/**
122	 * @param $calendarId
123	 * @param $user
124	 * @param $name
125	 * @param $description
126	 * @param array $customflags
127	 * @param array $options
128	 * @param $instanceId
129	 * @return mixed
130	 */
131	function set_calendar($calendarId, $user, $name, $description, $customflags = [], $options = [], $instanceId = 0)
132	{
133		global $prefs;
134		$name = strip_tags($name);
135		$description = strip_tags($description);
136		$now = time();
137		if ($instanceId > 0) {
138			// modification of a calendar instance
139			$finalEvent = 'tiki.calendar.update';
140			$query = "update `tiki_calendar_instances` set `name`=?, `description`=?, `transparent`=?, `timezone`=?, `order`=?, `color`=? where `calendarInstanceId`=?";
141			$bindvars = [$name,$description, @$options['transparent'], @$options['timezone'], @$options['order'], @$options['custombgcolor'], $instanceId];
142			$result = $this->query($query, $bindvars);
143		}	elseif ($calendarId > 0) {
144			// modification of a calendar
145			$finalEvent = 'tiki.calendar.update';
146			$query = "update `tiki_calendars` set `name`=?, `user`=?, `description`=?, ";
147			$bindvars = [$name,$user,$description];
148			foreach ($customflags as $k => $v) {
149				$query .= "`$k`=?, ";
150				$bindvars[] = $v;
151			}
152			$query .= "`lastmodif`=?  where `calendarId`=?";
153			$bindvars[] = $now;
154			$bindvars[] = $calendarId;
155			$result = $this->query($query, $bindvars);
156			// merge existing options in case passed array does not contain the full list (e.g. caldav integration)
157			$res = $this->query("select `optionName`,`value` from `tiki_calendar_options` where `calendarId`=?", [(int)$calendarId]);
158			while ($r = $res->fetchRow()) {
159				if (!isset($options[$r['optionName']])) {
160					$options[$r['optionName']] = $r['optionName'] == 'viewdays' ? unserialize($r['value']) : $r['value'];
161				}
162			}
163		} else {
164			// create a new calendar
165			$finalEvent = 'tiki.calendar.create';
166			$query = 'insert into `tiki_calendars` (`name`,`user`,`description`,`created`,`lastmodif`';
167			$bindvars = [$name,$user,$description,$now,$now];
168			if (! empty($customflags)) {
169				$query .= ',`' . implode("`,`", array_keys($customflags)) . '`';
170			}
171			$query .= ') values (?,?,?,?,?';
172			if (! empty($customflags)) {
173				$query .= ',' . implode(",", array_fill(0, count($customflags), "?"));
174				foreach ($customflags as $k => $v) {
175					$bindvars[] = $v;
176				}
177			}
178			$query .= ')';
179			$result = $this->query($query, $bindvars);
180			$calendarId = $this->GetOne("select `calendarId` from `tiki_calendars` where `created`=?", [$now]);
181		}
182		if ($instanceId == 0) {
183			$this->query('delete from `tiki_calendar_options` where `calendarId`=?', [(int)$calendarId]);
184			if (count($options)) {
185				if (isset($options['viewdays'])) {
186					$options['viewdays'] = serialize($options['viewdays']);
187				} else {
188					$options['viewdays'] = serialize($prefs['calendar_view_days']);
189				}
190				foreach ($options as $name => $value) {
191					$name = preg_replace('/[^-_a-zA-Z0-9]/', '', $name);
192					$this->query('insert into `tiki_calendar_options` (`calendarId`,`optionName`,`value`) values (?,?,?)', [(int)$calendarId,$name,$value]);
193				}
194			}
195		}
196
197		TikiLib::events()->trigger($finalEvent, [
198			'type' => 'calendar',
199			'object' => $calendarId,
200			'user' => $GLOBALS['user'],
201		]);
202
203		return $calendarId;
204	}
205
206	/**
207	 * @param $calendarId
208	 * @return array
209	 */
210	function get_calendar($calendarId)
211	{
212		global $prefs;
213		$res = $this->query("select * from `tiki_calendars` where `calendarId`=?", [(int)$calendarId]);
214		$cal = $res->fetchRow();
215		$res2 = $this->query("select `optionName`,`value` from `tiki_calendar_options` where `calendarId`=?", [(int)$calendarId]);
216		while ($r = $res2->fetchRow()) {
217			$cal[$r['optionName']] = $r['value'];
218		}
219		if (! isset($cal['startday']) and ! isset($cal['endday'])) {
220			$cal['startday'] = 0;
221			$cal['endday'] = 23 * 60 * 60;
222		}
223		if (isset($cal['viewdays'])) {
224			$cal['viewdays'] = unserialize($cal['viewdays']);
225		} else {
226			$cal['viewdays'] = $prefs['calendar_view_days'];
227		}
228		$cal = array_merge(['allday' => 'n', 'nameoneachday' => 'n'], $cal);
229		return $cal;
230	}
231
232	function get_calendar_options($calendarId)
233	{
234		$opts = [];
235		$res = $this->query("select `optionName`,`value` from `tiki_calendar_options` where `calendarId`=?", [(int)$calendarId]);
236		while ($r = $res->fetchRow()) {
237			$opts[$r['optionName']] = $r['value'];
238		}
239		return $opts;
240	}
241
242	/**
243	 * @param $calitemId
244	 * @return mixed
245	 */
246	function get_calendarid($calitemId)
247	{
248		return $this->getOne("select `calendarId` from `tiki_calendar_items` where `calitemId`=?", [(int)$calitemId]);
249	}
250
251	/**
252	 * @param $calendarId
253	 */
254	function drop_calendar($calendarId)
255	{
256		$transaction = $this->begin();
257
258		// find and remove roles for all calendar items:
259		$query = "select `calitemId` from `tiki_calendar_items` where `calendarId`=?";
260		$result = $this->query($query, [ $calendarId ]);
261		$allItemsFromCalendar = [];
262		while ($res = $result->fetchRow()) {
263			$allItemsFromCalendar[] = $res['calitemId'];
264		}
265		if (count($allItemsFromCalendar) > 0) {
266			$query = "delete from `tiki_calendar_roles` where `calitemId` in (" . implode(',', array_fill(0, count($allItemsFromCalendar), '?')) . ")";
267			$this->query($query, [$allItemsFromCalendar]);
268		}
269		// remove calendar items, categories and locations:
270		$query = "delete from `tiki_calendar_items` where `calendarId`=?";
271		$this->query($query, [$calendarId]);
272		$query = "delete from `tiki_calendar_categories` where `calendarId`=?";
273		$this->query($query, [$calendarId]);
274		$query = "delete from `tiki_calendar_options` where `calendarId`=?";
275		$this->query($query, [$calendarId]);
276		$query = "delete from `tiki_calendar_locations` where `calendarId`=?";
277		$this->query($query, [$calendarId]);
278		$query = "delete from `tiki_calendar_instances` where `calendarId`=?";
279		$this->query($query, [$calendarId]);
280		$query = "delete from `tiki_calendar_changes` where `calendarId`=?";
281		$this->query($query, [$calendarId]);
282		// uncategorize calendar
283		$categlib = TikiLib::lib('categ');
284		$categlib->uncategorize_object('calendar', $calendarId);
285		// now remove the calendar itself:
286		$query = "delete from `tiki_calendars` where `calendarId`=?";
287		$dropResult = $this->query($query, [$calendarId]);
288
289		TikiLib::events()->trigger('tiki.calendar.delete', [
290			'type' => 'calendar',
291			'object' => $calendarId,
292			'user' => $GLOBALS['user'],
293		]);
294
295		$transaction->commit();
296		return $dropResult;
297	}
298
299	/* tsart ans tstop are in user time - the data base is in server time */
300	/**
301	 * @param $calIds
302	 * @param $user
303	 * @param $tstart
304	 * @param $tstop
305	 * @param $offset
306	 * @param $maxRecords
307	 * @param string $sort_mode
308	 * @param string $find
309	 * @param array $customs
310	 * @return array
311	 */
312	function list_raw_items($calIds, $user, $tstart, $tstop, $offset, $maxRecords, $sort_mode = 'start_asc', $find = '', $customs = [])
313	{
314
315		if (count($calIds) == 0) {
316			return [];
317		}
318
319		$where = [];
320		$bindvars = [];
321		foreach ($calIds as $calendarId) {
322			$where[] = "i.`calendarId`=?";
323			$bindvars[] = (int)$calendarId;
324		}
325
326		$cond = "(" . implode(" or ", $where) . ") and ";
327		$cond .= " ((i.`start` > ? and i.`end` < ?) or (i.`start` < ? and i.`end` > ?))";
328
329		$bindvars[] = (int)$tstart;
330		$bindvars[] = (int)$tstop;
331		$bindvars[] = (int)$tstop;
332		$bindvars[] = (int)$tstart;
333
334		$cond .= " and ((c.`personal`='y' and i.`user`=?) or c.`personal` != 'y')";
335		$bindvars[] = $user;
336
337		$query = "select i.`calitemId` as `calitemId` ";
338		$queryCompl = '';
339		$joinCompl = '';
340		$tblRef = 'i.';
341
342		if (substr($sort_mode, 0, 12) == "categoryName") {
343			$queryCompl = "`tiki_calendar_categories` as compl right join ";
344			$joinCompl = " on i.categoryId = compl.calcatid ";
345			$tblRef = "compl.";
346		} elseif (substr($sort_mode, 0, 12) == "locationName") {
347			$queryCompl = "`tiki_calendar_locations` as compl right join ";
348			$joinCompl = " on i.locationId = compl.callocid ";
349			$tblRef = "compl.";
350		}
351
352		$query .= 'from ' . $queryCompl . '`tiki_calendar_items` as i ' . $joinCompl .
353								" left join `tiki_calendars` as c on i.`calendarId`=c.`calendarId`" .
354								" where ($cond)" .
355								" order by " .
356								$tblRef . $this->convertSortMode($sort_mode) . ',i.' . $this->convertSortMode('calendarId_asc');
357
358		$result = $this->query($query, $bindvars, $maxRecords, $offset);
359		$ret = [];
360		while ($res = $result->fetchRow()) {
361			$ret[] = $this->get_item($res["calitemId"], $customs);
362		}
363		return $ret;
364	}
365
366	/**
367	 * @param $calIds
368	 * @param $user
369	 * @param $tstart
370	 * @param $tstop
371	 * @param $offset
372	 * @param $maxRecords
373	 * @param string $sort_mode
374	 * @param string $find
375	 * @param array $customs
376	 * @return array
377	 */
378	function list_items($calIds, $user, $tstart, $tstop, $offset, $maxRecords, $sort_mode = 'start_asc', $find = '', $customs = [])
379	{
380		global $tiki_p_change_events, $prefs;
381		$ret = [];
382		$list = $this->list_raw_items($calIds, $user, $tstart, $tstop, $offset, $maxRecords, $sort_mode, $find, $customs);
383		foreach ($list as $res) {
384			$mloop = TikiLib::date_format("%m", $res['start']);
385			$dloop = TikiLib::date_format("%d", $res['start']);
386			$yloop = TikiLib::date_format("%Y", $res['start']);
387			$dstart = TikiLib::make_time(0, 0, 0, $mloop, $dloop, $yloop);
388			$dend = TikiLib::make_time(0, 0, 0, TikiLib::date_format("%m", $res['end']), TikiLib::date_format("%d", $res['end']), TikiLib::date_format("%Y", $res['end']));
389			$tstart = TikiLib::date_format("%H%M", $res["start"]);
390			$tend = TikiLib::date_format("%H%M", $res["end"]);
391			for ($i = $dstart; $i <= $dend; $i = TikiLib::make_time(0, 0, 0, $mloop, ++$dloop, $yloop)) {
392				/* $head is in user time */
393				if ($dstart == $dend) {
394					$head = TikiLib::date_format($prefs['short_time_format'], $res["start"]) . " - " . TikiLib::date_format($prefs['short_time_format'], $res["end"]);
395				} elseif ($i == $dstart) {
396					$head = TikiLib::date_format($prefs['short_time_format'], $res["start"]) . " ...";
397				} elseif ($i == $dend) {
398					$head = " ... " . TikiLib::date_format($prefs['short_time_format'], $res["end"]);
399				} else {
400					$head = " ... " . tra("continued") . " ... ";
401				}
402
403				/* $i is timestamp unix of the beginning of a day */
404				$ret["$i"][] = [
405					'result' => $res,
406					'calitemId' => $res['calitemId'],
407					'calname' => tra($res['calname']),
408					'time' => $tstart, /* user time */
409					'end' => $tend, /* user time */
410					'type' => $res['status'],
411					'web' => $res['url'],
412					'startTimeStamp' => $res['start'],
413					'endTimeStamp' => $res['end'],
414					'nl' => $res['nlId'],
415					'prio' => $res['priority'],
416					'location' => $res['locationName'],
417					'category' => $res['categoryName'],
418					'name' => $res['name'],
419					'head' => $head,
420					'parsedDescription' => TikiLib::lib('parser')->parse_data($res['description'], ['is_html' => $prefs['calendar_description_is_html'] === 'y']),
421					'description' => str_replace("\n|\r", '', $res['description']),
422					'calendarId' => $res['calendarId'],
423					'status' => $res['status'],
424					'user' => $res['user']
425				];
426			}
427		}
428		return $ret;
429	}
430
431	/**
432	 * @param $calIds
433	 * @param $user
434	 * @param $tstart
435	 * @param $tstop
436	 * @param $offset
437	 * @param $maxRecords
438	 * @param string $sort_mode
439	 * @param string $find
440	 * @param array $customs
441	 * @return array
442	 */
443	function list_items_by_day($calIds, $user, $tstart, $tstop, $offset, $maxRecords, $sort_mode = 'start_asc', $find = '', $customs = [])
444	{
445		global $prefs;
446		$ret = [];
447		$list = $this->list_raw_items($calIds, $user, $tstart, $tstop, $offset, $maxRecords, $sort_mode, $find, $customs);
448		foreach ($list as $res) {
449			$mloop = TikiLib::date_format("%m", $res['start']);
450			$dloop = TikiLib::date_format("%d", $res['start']);
451			$yloop = TikiLib::date_format("%Y", $res['start']);
452			$dstart = TikiLib::make_time(0, 0, 0, $mloop, $dloop, $yloop);
453			$dend = TikiLib::make_time(0, 0, 0, TikiLib::date_format("%m", $res['end']), TikiLib::date_format("%d", $res['end']), TikiLib::date_format("%Y", $res['end']));
454			$tstart = TikiLib::date_format("%H%M", $res["start"]);
455			$tend = TikiLib::date_format("%H%M", $res["end"]);
456			for ($i = $dstart; $i <= $dend; $i = TikiLib::make_time(0, 0, 0, $mloop, ++$dloop, $yloop)) {
457				/* $head is in user time */
458				if ($res['allday'] == '1') {
459					$head = tra('All day');
460				} elseif ($dstart == $dend) {
461					$head = TikiLib::date_format($prefs['short_time_format'], $res["start"]) . " - " . TikiLib::date_format($prefs['short_time_format'], $res["end"]);
462				} elseif ($i == $dstart) {
463					$head = TikiLib::date_format($prefs['short_time_format'], $res["start"]) . " ...";
464				} elseif ($i == $dend) {
465					$head = " ... " . TikiLib::date_format($prefs['short_time_format'], $res["end"]);
466				} else {
467					$head = " ... " . tra("continued") . " ... ";
468				}
469
470				/* $i is timestamp unix of the beginning of a day */
471				$j = (isset($ret[$i]) && is_array($ret[$i])) ? count($ret[$i]) : 0;
472
473				$ret[$i][$j] = $res;
474				$ret[$i][$j]['head'] = $head;
475				$ret[$i][$j]['parsedDescription'] = TikiLib::lib('parser')->parse_data($res["description"], ['is_html' => $prefs['calendar_description_is_html'] === 'y']);
476				$ret[$i][$j]['description'] = str_replace("\n|\r", "", $res["description"]);
477				$ret[$i][$j]['visible'] = 'y';
478				$ret[$i][$j]['where'] = $res['locationName'];
479
480				$ret[$i][$j]['show_description'] = 'y';
481				/*	'time' => $tstart, /* user time */
482				/*	'end' => $tend, /* user time */
483
484				$ret[$i][$j]['group_description'] = htmlspecialchars($res['name']) . '<span class="calgrouptime">, ' . $head . '</span>';
485			}
486		}
487		return $ret;
488	}
489
490	/**
491	 * @param $calitemId
492	 * @param array $customs
493	 * @return mixed
494	 */
495	function get_item($calitemId, $customs = [])
496	{
497		global $user, $prefs;
498
499		$query = "select i.`calitemId` as `calitemId`, i.`calendarId` as `calendarId`, i.`user` as `user`, i.`start` as `start`, i.`end` as `end`, t.`name` as `calname`, ";
500		$query .= "i.`locationId` as `locationId`, l.`name` as `locationName`, i.`categoryId` as `categoryId`, c.`name` as `categoryName`, i.`priority` as `priority`, i.`nlId` as `nlId`, i.`uid` as `uid`, i.`uri` as `uri`, ";
501		$query .= "i.`status` as `status`, i.`url` as `url`, i.`lang` as `lang`, i.`name` as `name`, i.`description` as `description`, i.`created` as `created`, i.`lastmodif` as `lastModif`, i.`allday` as `allday`, ";
502		$query .= "t.`customlocations`, t.`customcategories`, t.`customlanguages`, t.`custompriorities`, t.`customsubscription`, t.`customparticipants`, i.`recurrenceId`, i.`recurrenceStart`, r.`uid` as `recurrenceUid`";
503
504		foreach ($customs as $k => $v) {
505			$query .= ", i.`$k` as `$v`";
506		}
507
508		$query .= " from `tiki_calendar_items` as i left join `tiki_calendar_locations` as l on i.`locationId`=l.`callocId` left join `tiki_calendar_recurrence` as r on i.`recurrenceId` = r.`recurrenceId`";
509		$query .= " left join `tiki_calendar_categories` as c on i.`categoryId`=c.`calcatId` left join `tiki_calendars` as t on i.`calendarId`=t.`calendarId` where `calitemId`=?";
510		$result = $this->query($query, [(int)$calitemId]);
511		$res = $result->fetchRow();
512
513		if ($res) {
514			$query
515				= "select `username`, `role`, `partstat` from `tiki_calendar_roles` where `calitemId`=? order by `role`";
516			$rezult = $this->query($query, [(int)$calitemId]);
517			$ppl = [];
518			$org = [];
519			while ($rez = $rezult->fetchRow()) {
520				if ($rez["role"] == ROLE_ORGANIZER) {
521					$org[] = $rez["username"];
522				} elseif ($rez["username"]) {
523					$email = TikiLib::lib('user')->get_user_email($rez['username']);
524					if (! $email) {
525						$email = $rez["username"];
526					}
527					$ppl[] = [
528						'username' => $rez["username"],
529						'email' => $email,
530						'role' => $rez["role"],
531						'partstat' => $rez['partstat']
532					];
533				}
534			}
535			$res["participants"] = $ppl;
536			$res["selected_participants"] = array_map(function($role){ return $role['username']; }, $ppl);
537			$res["organizers"] = $org;
538			$res['date_start'] = (int)$res['start'];
539			$res['date_end'] = (int)$res['end'];
540			$res['duration'] = $res['end'] - $res['start'];
541			$parserlib = TikiLib::lib('parser');
542			$res['parsed'] = $parserlib->parse_data(
543				$res['description'],
544				['is_html' => $prefs['calendar_description_is_html'] === 'y']
545			);
546			$res['parsedName'] = $parserlib->parse_data($res['name']);
547		}
548		return $res;
549	}
550
551	function get_item_by_uri($uri)
552	{
553		$result = $this->query("select calitemId from `tiki_calendar_items` where uri = ?", [$uri]);
554		$row = $result->fetchRow();
555		if ($row) {
556			return $this->get_item($row['calitemId']);
557		}
558		$result = $this->query("select recurrenceId from `tiki_calendar_recurrence` where uri = ?", [$uri]);
559		$row = $result->fetchRow();
560		if ($row) {
561			return new \CalRecurrence($row['recurrenceId']);
562		}
563		return null;
564	}
565
566	/**
567	 * @param       $user
568	 * @param       $calitemId
569	 * @param       $data
570	 * @param array $customs
571	 * @param bool  $isBulk
572	 *
573	 * @return bool
574	 * @throws Exception
575	 */
576	function set_item($user, $calitemId, $data, $customs = [], $isBulk = false)
577	{
578		global $prefs;
579		if (! isset($data['calendarId'])) {
580			return false;
581		}
582		$caldata = $this->get_calendar($data['calendarId']);
583
584		if ($caldata['customlocations'] == 'y') {
585			if (! $data["locationId"] and ! $data["newloc"]) {
586				$data['locationId'] = 0;
587			}
588			if (trim($data["newloc"])) {
589				$bindvars = [(int)$data["calendarId"],trim($data["newloc"])];
590				$query = "delete from `tiki_calendar_locations` where `calendarId`=? and `name`=?";
591				$this->query($query, $bindvars, -1, -1, false);
592				$query = "insert into `tiki_calendar_locations` (`calendarId`,`name`) values (?,?)";
593				$this->query($query, $bindvars);
594				$data["locationId"] = $this->getOne("select `callocId` from `tiki_calendar_locations` where `calendarId`=? and `name`=?", $bindvars);
595			}
596		} else {
597			$data['locationId'] = 0;
598		}
599
600		if ($caldata['customcategories'] == 'y') {
601			if (! $data["categoryId"] and ! $data["newcat"]) {
602				$data['categoryId'] = 0;
603			}
604			if (trim($data["newcat"])) {
605				$query = "delete from `tiki_calendar_categories` where `calendarId`=? and `name`=?";
606				$bindvars = [(int)$data["calendarId"],trim($data["newcat"])];
607				$this->query($query, $bindvars, -1, -1, false);
608				$query = "insert into `tiki_calendar_categories` (`calendarId`,`name`) values (?,?)";
609				$this->query($query, $bindvars);
610				$data["categoryId"] = $this->getOne("select `calcatId` from `tiki_calendar_categories` where `calendarId`=? and `name`=?", $bindvars);
611			}
612		} else {
613			$data['categoryId'] = 0;
614		}
615
616		if ($caldata['customparticipants'] == 'y') {
617			$roles = [];
618			if ($data["organizers"]) {
619				if (is_string($data['organizers'])) {
620					$data['organizers'] = preg_split('/\s*,\s*/', $data['organizers']);
621				}
622				foreach ($data['organizers'] as $o) {
623					if (trim($o)) {
624						$roles[] = [
625							'username' => trim($o),
626							'role' => ROLE_ORGANIZER
627						];
628					}
629				}
630			}
631			if ($data["participants"]) {
632				foreach ($data['participants'] as $pa) {
633					if (trim($pa['username'])) {
634						$roles[] = $pa;
635					}
636				}
637			}
638		}
639
640		if ($caldata['customlanguages'] == 'y') {
641			if (! isset($data['lang'])) {
642				$data['lang'] = '';
643			}
644		} else {
645			$data['lang'] = '';
646		}
647
648		if ($caldata['custompriorities'] == 'y') {
649			if (! isset($data['priority'])) {
650				$data['priority'] = 0;
651			}
652		} else {
653			$data['priority'] = 0;
654		}
655
656		if ($caldata['customsubscription'] == 'y') {
657			if (! isset($data['nlId'])) {
658				$data['nlId'] = 0;
659			}
660		} else {
661			$data['nlId'] = 0;
662		}
663
664		$data['user'] = $user;
665
666		$realcolumns = ['calitemId', 'calendarId', 'start', 'end', 'locationId', 'categoryId', 'nlId', 'priority', 'uri', 'uid',
667					 'status', 'url', 'lang', 'name', 'description', 'user', 'created', 'lastmodif', 'allday', 'recurrenceId', 'changed', 'recurrenceStart'];
668		foreach ($customs as $custom) {
669			$realcolumns[] = $custom;
670		}
671
672		if ($calitemId) {
673			$finalEvent = 'tiki.calendaritem.update';
674
675			$oldData = $this->get_item($calitemId);
676			if (empty($oldData)) {
677				return false;
678			}
679			$data = array_merge($oldData, $data);
680			$data['lastmodif'] = $this->now;
681
682			$l = [];
683			$r = [];
684
685			foreach ($data as $k => $v) {
686				if (! in_array($k, $realcolumns)) {
687					continue;
688				}
689				$l[] = "`$k`=?";
690				$r[] = $v;
691			}
692
693			if (! empty($data['changed']) && empty($data['recurrenceStart'])) {
694				$l[] = "`recurrenceStart` = ?";
695				$r[] = $oldData['start'];
696			}
697
698			if (! empty($data['recurrenceStart']) && empty($data['changed'])) {
699				$l[] = "`changed` = 1";
700			}
701
702			$query = 'UPDATE `tiki_calendar_items` SET ' . implode(',', $l) . ' WHERE `calitemId`=?';
703			$r[] = (int)$calitemId;
704
705			$result = $this->query($query, $r);
706			$this->add_change($data['calendarId'], $calitemId, 2);
707
708			$trackerItemsIds = $this->getAttachedTrackerItems($calitemId);
709
710			require_once 'lib/search/refresh-functions.php';
711			foreach ($trackerItemsIds as $trackerItemId) {
712				refresh_index('trackeritem', $trackerItemId);
713			}
714
715		} else {
716			$finalEvent = 'tiki.calendaritem.create';
717			$new = true;
718			$oldData = null;
719			$data['lastmodif'] = $this->now;
720			$data['created'] = $this->now;
721
722			$l = [];
723			$r = [];
724			$z = [];
725
726			foreach ($data as $k => $v) {
727				if (! in_array($k, $realcolumns)) {
728					continue;
729				}
730				$l[] = "`$k`";
731				$z[] = '?';
732				$r[] = ($k == 'priority') ? (string)$v : $v;
733			}
734
735			$query = 'INSERT INTO `tiki_calendar_items` (' . implode(',', $l) . ') VALUES (' . implode(',', $z) . ')';
736			$result = $this->query($query, $r);
737			$calitemId = $this->GetOne("SELECT MAX(`calitemId`) FROM `tiki_calendar_items` where `calendarId`=?", [$data["calendarId"]]);
738			$this->add_change($data['calendarId'], $calitemId, 1);
739		}
740
741		if ($calitemId) {
742			$wikilib = TikiLib::lib('wiki');
743			$wikilib->update_wikicontent_relations($data['description'], 'calendar event', $calitemId);
744			$wikilib->update_wikicontent_links($data['description'], 'calendar event', $calitemId);
745			$existing_roles = $this->fetchAll('select * from `tiki_calendar_roles` where `calitemId`=?', [$calitemId]);
746			$query = "delete from `tiki_calendar_roles` where `calitemId`=?";
747			$this->query($query, [(int)$calitemId]);
748		} else {
749			$existing_roles = [];
750		}
751
752		foreach ($roles as $role) {
753			if (empty($role['partstat'])) {
754				foreach ($existing_roles as $erole) {
755					if ($role['username'] == $erole['username']) {
756						$role['partstat'] = $erole['partstat'];
757					}
758				}
759			}
760			$query = "insert into `tiki_calendar_roles` (`calitemId`,`username`,`role`,`partstat`) values (?,?,?,?)";
761			$this->query($query, [(int)$calitemId, $role['username'], $role['role'] ?? 0, $role['partstat'] ?? null]);
762		}
763
764		if ($prefs['feature_user_watches'] == 'y') {
765			$this->watch($calitemId, $data);
766		}
767
768		TikiLib::events()->trigger($finalEvent, [
769			'type' => 'calendaritem',
770			'object' => $calitemId,
771			'user' => $GLOBALS['user'],
772			'bulk_import' => $isBulk,
773			'old_data' => $oldData,
774			'process_itip' => !empty($data['process_itip'])
775		]);
776
777		return $calitemId;
778	}
779
780	/**
781	 * Get all tracker items attached to a calender item
782	 *
783	 * @param $calitemId
784	 *
785	 * @return array
786	 * @throws Exception
787	 */
788	public function getAttachedTrackerItems($calitemId)
789	{
790		$trackerItems = [];
791		$attributes = TikiLib::lib('attribute')->find_objects_with('tiki.calendar.item', $calitemId);
792
793		foreach ($attributes as $attribute) {
794			$trackerItems[] = (int)$attribute['itemId'];
795		}
796
797		return $trackerItems;
798	}
799
800	/**
801	 * @param $calitemId
802	 * @param $data
803	 */
804	function watch($calitemId, $data)
805	{
806		global $prefs, $user;
807		$smarty = TikiLib::lib('smarty');
808		$tikilib = TikiLib::lib('tiki');
809		$nots = $tikilib->get_event_watches('calendar_changed', $data['calendarId']);
810
811		if ($prefs['calendar_watch_editor'] != "y" || $prefs['user_calendar_watch_editor'] != "y") {
812			for ($i = count($nots) - 1; $i >= 0; --$i) {
813				if ($nots[$i]['user'] == $data["user"]) {
814					unset($nots[$i]);
815					break;
816				}
817			}
818		}
819
820		if ($prefs['feature_daily_report_watches'] == 'y') {
821			$reportsManager = Reports_Factory::build('Reports_Manager');
822			$reportsManager->addToCache($nots, ['event' => 'calendar_changed', 'calitemId' => $calitemId, 'user' => $user]);
823		}
824
825		if ($nots) {
826			include_once('lib/webmail/tikimaillib.php');
827			$mail = new TikiMail();
828			$smarty->assign('mail_new', $new);
829			$smarty->assign('mail_data', $data);
830			$smarty->assign('mail_calitemId', $calitemId);
831			$foo = parse_url($_SERVER["REQUEST_URI"]);
832			$machine = $tikilib->httpPrefix(true) . dirname($foo["path"]);
833			$machine = preg_replace("!/$!", "", $machine); // just incase
834			 $smarty->assign('mail_machine', $machine);
835			$defaultLanguage = $prefs['site_language'];
836			foreach ($nots as $not) {
837				$mail->setUser($not['user']);
838				$mail_data = $smarty->fetchLang($defaultLanguage, "mail/user_watch_calendar_subject.tpl");
839				$mail->setSubject($mail_data);
840				$mail_data = $smarty->fetchLang($defaultLanguage, "mail/user_watch_calendar.tpl");
841				$mail->setText($mail_data);
842				$mail->send([$not['email']]);
843			}
844		}
845	}
846
847	/**
848	 * @param $user
849	 * @param $calitemId
850	 */
851	function drop_item($user, $calitemId, $isBulk = false, $process_itip = true)
852	{
853		if ($calitemId) {
854			$item = $this->get_item($calitemId);
855			$query = "delete from `tiki_calendar_items` where `calitemId`=?";
856			$this->query($query, [$calitemId]);
857			$query = "delete from `tiki_calendar_roles` where `calitemId`=?";
858			$this->query($query, [$calitemId]);
859			$this->remove_object('calendar event', $calitemId);
860			TikiLib::lib('calendar')->add_change($item['calendarId'], $calitemId, 3);
861
862			TikiLib::events()->trigger('tiki.calendaritem.delete', [
863				'type' => 'calendaritem',
864				'object' => $calitemId,
865				'user' => $user,
866				'bulk_import' => $isBulk,
867				'old_data' => $item,
868				'process_itip' => $process_itip
869			]);
870		}
871	}
872
873	/**
874	 * @param $calitemId
875	 * @param int $delay
876	 */
877	function move_item($calitemId, $delay = 0)
878	{
879		if ($delay != 0) {
880			$query = 'UPDATE `tiki_calendar_items` set start = start + ?, end = end + ? WHERE `calitemId`=?';
881			$this->query($query, [$delay,$delay,$calitemId]);
882		}
883	}
884
885	/**
886	 * @param $calitemId
887	 * @param int $delay
888	 */
889	function resize_item($calitemId, $delay = 0)
890	{
891		if ($delay != 0) {
892			$query = 'UPDATE `tiki_calendar_items` set end = end + ? WHERE `calitemId`=?';
893			$this->query($query, [$delay,$calitemId]);
894		}
895	}
896
897	/**
898	 * @param $calendarId
899	 * @return array
900	 */
901	function list_locations($calendarId)
902	{
903		$res = [];
904		if ($calendarId > 0) {
905			$query = "select `callocId` as `locationId`, `name` from `tiki_calendar_locations` where `calendarId`=? order by `name`";
906			return $this->fetchAll($query, [$calendarId]);
907		}
908		return $res;
909	}
910
911	/**
912	 * @param $calendarId
913	 * @return array
914	 */
915	function list_categories($calendarId)
916	{
917		$res = [];
918		if ($calendarId > 0) {
919			$query = "select `calcatId` as `categoryId`, `name` from `tiki_calendar_categories` where `calendarId`=? order by `name`";
920			return $this->fetchAll($query, [$calendarId]);
921		}
922		return $res;
923	}
924
925	// Returns the last $maxrows of modified events for an
926	// optional $calendarId
927	/**
928	 * @param $maxrows
929	 * @param int $calendarId
930	 * @return mixed
931	 */
932	function last_modif_events($maxrows = -1, $calendarId = 0)
933	{
934
935		if ($calendarId > 0) {
936			$cond = "where `calendarId` = ? ";
937			$bindvars = [$calendarId];
938		} else {
939			$cond = '';
940			$bindvars = [];
941		}
942
943		$query = "select `start`, `name`, `calitemId`, `calendarId`, `user`, `lastModif` from `tiki_calendar_items` " . $cond . "order by " . $this->convertSortMode('lastModif_desc');
944
945		return $this->fetchAll($query, $bindvars, $maxrows, 0);
946	}
947
948	/**
949	 * @param $fname
950	 * @param $calendarId
951	 * @return int
952	 */
953	function importCSV($fname, $calendarId)
954	{
955		global $user;
956		$smarty = TikiLib::lib('smarty');
957		$fields = false;
958		if ($fhandle = fopen($fname, 'r')) {
959			$fields = fgetcsv($fhandle, 1000);
960		}
961		if ($fields === false || ! array_search('name', $fields)) {
962			$smarty->assign('msg', tra("The file has incorrect syntax or is not a CSV file"));
963			$smarty->display("error.tpl");
964			die;
965		}
966		$nb = 0;
967		while (($data = fgetcsv($fhandle, 1000)) !== false) {
968			$d = [
969						'calendarId' => $calendarId,
970						'calitemId' => '0',
971						'name' => '',
972						'description' => '',
973						'locationId' => '',
974						'organizers' => '',
975						'participants' => '',
976						'status' => '1',
977						'priority' => '5',
978						'categoryId' => '0',
979						'newloc' => '0',
980						'newcat' => '',
981						'nlId' => '',
982						'lang' => '',
983						'start' => '',
984						'end' => ''
985			];
986
987			foreach ($fields as $field) {
988				$d[$field] = $data[array_search($field, $fields)];
989			}
990
991			if (isset($d["subject"]) && empty($d["name"])) {
992				$d["name"] = $d["subject"];
993			}
994			if (isset($d['start date'])) {
995				if (isset($d['start time'])) {
996					$d['start'] = strtotime($d['start time'], strtotime($d['start date']));
997				} else {
998					$d['start'] = strtotime($d['start date']);
999				}
1000			}
1001			if (isset($d['end date'])) {
1002				if (isset($d['end time'])) {
1003					$d['end'] = strtotime($d['end time'], strtotime($d['end date']));
1004				} else {
1005					$d['end'] = strtotime($d['end date']);
1006				}
1007			}
1008
1009			if ($d['organizers']) {
1010				$d['organizers'] = explode(',', $d['organizers']);
1011			}
1012
1013			if ($d['participants']) {
1014				$d['participants'] = array_map(function($part){
1015					$part = explode(':', $part);
1016					if (count($part) > 1) {
1017						$part = [
1018							'username' => $part[1],
1019							'role' => $part[0]
1020						];
1021					} else {
1022						$part = [
1023							'username' => $part[0]
1024						];
1025					}
1026					return $part;
1027				}, explode(',', $d['participants']));
1028			}
1029
1030			// TODO do a replace if name, calendarId, start, end exists
1031			if (! empty($d['start']) && ! empty($d['end'])) {
1032				$this->set_item($user, 0, $d);
1033				++$nb;
1034			}
1035		}
1036		fclose($fhandle);
1037		return $nb;
1038	}
1039
1040	/**
1041	 * Returns an array of a maximum of $maxrows upcoming (but possibly past) events in the given $order.
1042	 * If $calendarId is set, events not in the specified calendars are filtered. $calendarId
1043	 * can be a calendar identifier or an array of calendar identifiers. If $maxDaysEnd is
1044	 * a natural, events ending after $maxDaysEnd days are filtered. If $maxDaysStart is a
1045	 * natural, events starting after $maxDaysStart days are filtered.
1046	 * Events ending more than $priorDays in the past are filtered.
1047	 *
1048	 * Each event is represented by a string-indexed array with indices start, end,
1049	 * name, description, calitemId, calendarId, user, lastModif, url, allday
1050	 * in the same format as tiki_calendar_items fields, as well as location
1051	 * for the event's locations, parsed for the parsed description and category
1052	 * for the event's calendar category.
1053	 *
1054	 */
1055
1056	//Pagination
1057	function upcoming_events($maxrows = -1, $calendarId = null, $maxDaysEnd = -1, $order = 'start_asc', $priorDays = 0, $maxDaysStart = -1, $start = 0)
1058	{
1059
1060		global $prefs;
1061		$cond = '';
1062		$bindvars = [];
1063		if (isset($calendarId)) {
1064			if (is_array($calendarId)) {
1065				$cond = $cond . "and (0=1";
1066				foreach ($calendarId as $id) {
1067					$cond = $cond . " or i.`calendarId` = ? ";
1068				}
1069				$cond = $cond . ")";
1070				$bindvars = array_merge($bindvars, $calendarId);
1071			} else {
1072				$cond = $cond . " and i.`calendarId` = ? ";
1073				$bindvars[] = $calendarId;
1074			}
1075		}
1076		$cond .= " and `end` >= (unix_timestamp(now()) - ?*3600*24)";
1077		$bindvars[] = $priorDays;
1078
1079
1080		if ($maxDaysEnd > 0) {
1081			$maxSeconds = ($maxDaysEnd * 24 * 60 * 60);
1082			$cond .= " and `end` <= (unix_timestamp(now())) +" . $maxSeconds;
1083		}
1084		if ($maxDaysStart > 0) {
1085			$maxSeconds = ($maxDaysStart * 24 * 60 * 60);
1086			$cond .= " and `start` <= (unix_timestamp(now())) +" . $maxSeconds;
1087		}
1088		$ljoin = "left join `tiki_calendar_locations` as l on i.`locationId`=l.`callocId` left join `tiki_calendar_categories` as c on i.`categoryId`=c.`calcatId`";
1089
1090		$query = "select i.`start`, i.`end`, i.`name`, i.`description`, i.`status`," .
1091							" i.`calitemId`, i.`calendarId`, i.`user`, i.`lastModif`, i.`url`," .
1092							" l.`name` as location, i.`allday`, c.`name` as category" .
1093							" from `tiki_calendar_items` i $ljoin" .
1094							" where 1=1 " . $cond .
1095							" order by " . $this->convertSortMode($order);
1096
1097		$ret = $this->fetchAll($query, $bindvars, $maxrows, $start);
1098
1099		$query_cant = "select count(*) from `tiki_calendar_items` i $ljoin where 1=1 " . $cond . " GROUP BY i.calitemId order by " . $this->convertSortMode($order);
1100		$cant = $this->getOne($query_cant, $bindvars);
1101
1102		foreach ($ret as &$res) {
1103			$res['parsed'] = TikiLib::lib('parser')->parse_data($res['description'], ['is_html' => $prefs['calendar_description_is_html'] === 'y']);
1104		}
1105
1106		$retval = [];
1107		$retval['data'] = $ret;
1108		$retval['cant'] = $cant;
1109		return $retval;
1110	}
1111
1112	/**
1113	 * @param $maxrows
1114	 * @param null $calendarId
1115	 * @param $maxDaysEnd
1116	 * @param string $order
1117	 * @param int $priorDays
1118	 * @param $maxDaysStart
1119	 * @param int $start
1120	 * @return array
1121	 */
1122	function all_events($maxrows = -1, $calendarId = null, $maxDaysEnd = -1, $order = 'start_asc', $priorDays = 0, $maxDaysStart = -1, $start = 0, $itemIds = [])
1123	{
1124		global $prefs;
1125		$cond = '';
1126		$bindvars = [];
1127		if (isset($calendarId)) {
1128			if (is_array($calendarId)) {
1129				$cond = $cond . "and (0=1";
1130				foreach ($calendarId as $id) {
1131					$cond = $cond . " or i.`calendarId` = ? ";
1132				}
1133				$cond = $cond . ")";
1134				$bindvars = array_merge($bindvars, $calendarId);
1135			} else {
1136				$cond = $cond . " and i.`calendarId` = ? ";
1137				$bindvars[] = $calendarId;
1138			}
1139		}
1140		if (count($itemIds) > 0) {
1141			$cond .= " and i.calitemId in (".implode(',', array_fill(0, count($itemIds), '?')).")";
1142			$bindvars = array_merge($bindvars, $itemIds);
1143		}
1144		$condition = '';
1145		$cond .= " and  $condition (unix_timestamp(now()) - ?*3600*34)";
1146		$bindvars[] = $priorDays;
1147
1148		if ($maxDaysEnd > 0) {
1149			$maxSeconds = ($maxDaysEnd * 24 * 60 * 60);
1150			$cond .= " and `end` <= (unix_timestamp(now())) +" . $maxSeconds;
1151		}
1152		if ($maxDaysStart > 0) {
1153			$maxSeconds = ($maxDaysStart * 24 * 60 * 60);
1154			$cond .= " and `start` <= (unix_timestamp(now())) +" . $maxSeconds;
1155		}
1156		$ljoin = "left join `tiki_calendar_locations` as l on i.`locationId`=l.`callocId` left join `tiki_calendar_categories` as c on i.`categoryId`=c.`calcatId`";
1157
1158		$query = "select i.`start`, i.`end`, i.`name`, i.`description`, i.`status`," .
1159							" i.`calitemId`, i.`calendarId`, i.`user`, i.`lastModif`, i.`url`," .
1160							" l.`name` as location, i.`allday`, c.`name` as category, i.`created`, i.`priority`, i.`uid`" .
1161							" from `tiki_calendar_items` i" .
1162							" $ljoin" .
1163							" where 1=1 " . $cond .
1164							" order by " . $this->convertSortMode($order);
1165
1166		$ret = $this->fetchAll($query, $bindvars, $maxrows, $start);
1167
1168		$query_cant = "select count(*) from `tiki_calendar_items` i $ljoin where 1=1 " . $cond . " order by " . $this->convertSortMode($order);
1169		$cant = $this->getOne($query_cant, $bindvars);
1170
1171		foreach ($ret as &$res) {
1172			$res['parsed'] = TikiLib::lib('parser')->parse_data($res['description'], ['is_html' => $prefs['calendar_description_is_html'] === 'y']);
1173		}
1174
1175		$retval = [];
1176		$retval['data'] = $ret;
1177		$retval['cant'] = $cant;
1178		return $retval;
1179	}
1180
1181	public function get_events($calendarId, $itemIdsOrUris = [], $componenttype = null, $start = null, $end = null, $recurrenceId = null, $changed = null)
1182	{
1183		global $prefs;
1184
1185		$cond = ' and i.calendarId = ?';
1186		$bindvars = [$calendarId];
1187
1188		if (count($itemIdsOrUris) > 0) {
1189			$recurrences = array_filter($itemIdsOrUris, function($uri) {
1190				return substr($uri, 0, 1) == 'r';
1191			});
1192			$itemIdsOrUris = array_diff($itemIdsOrUris, $recurrences);
1193			if (! $itemIdsOrUris) {
1194				$itemIdsOrUris[] = '';
1195			}
1196			$recurrences = array_map(function($uri){
1197				return substr($uri, 1);
1198			}, $recurrences);
1199			if (! $recurrences) {
1200				$recurrences[] = '';
1201			}
1202			$cond .= " and (i.calitemId in (".implode(',', array_fill(0, count($itemIdsOrUris), '?')).") or i.uri in (".implode(',', array_fill(0, count($itemIdsOrUris), '?')).") or i.recurrenceId in (".implode(',', array_fill(0, count($recurrences), '?')).") or r.uri in (".implode(',', array_fill(0, count($itemIdsOrUris), '?'))."))";
1203			$bindvars = array_merge($bindvars, $itemIdsOrUris, $itemIdsOrUris, $recurrences, $itemIdsOrUris);
1204		}
1205
1206		// TODO: we support only events for now. This is meant for CalDAV access to support TODO items, for example.
1207		// if ($componenttype) {
1208		// 	$cond .= " and i.componenttype = ?";
1209		// 	$bindvars[] = $componenttype;
1210		// }
1211
1212		if ($start) {
1213			$cond .= " and i.end > ?";
1214			$bindvars[] = $start;
1215		}
1216
1217		if ($end) {
1218			$cond .= " and i.start < ?";
1219			$bindvars[] = $end;
1220		}
1221
1222		if ($recurrenceId) {
1223			$cond .= " and i.recurrenceId = ?";
1224			$bindvars[] = $recurrenceId;
1225		}
1226
1227		if (! is_null($changed)) {
1228			$cond .= " and i.changed = ?";
1229			$bindvars[] = $changed;
1230		}
1231
1232		$join = "left join `tiki_calendar_locations` as l on i.`locationId`=l.`callocId` left join `tiki_calendar_categories` as c on i.`categoryId`=c.`calcatId`";
1233
1234		$query = "select i.`start`, i.`end`, i.`name`, i.`description`, i.`status`," .
1235							" i.`calitemId`, i.`calendarId`, i.`user`, i.`lastmodif` as `lastModif`, i.`url`, i.`recurrenceId`, i.`recurrenceStart`, r.`uid` as `recurrenceUid`," .
1236							" l.`name` as location, c.`name` as category, i.`created`, i.`priority`, i.`uid`, i.`uri`" .
1237							" from `tiki_calendar_items` i" .
1238							" left join `tiki_calendar_recurrence` r on i.`recurrenceId` = r.`recurrenceId`" .
1239							" $join" .
1240							" where 1=1 " . $cond .
1241							" order by calitemId";
1242
1243		return $this->fetchAll($query, $bindvars);
1244	}
1245
1246	public function find_by_uid($user, $uid) {
1247		$query = "select i.`calendarId`, i.`calitemId`, i.`uri`, i.`recurrenceId` from `tiki_calendar_items` i left join `tiki_calendars` c on i.`calendarId` = c.`calendarId` left join `tiki_calendar_recurrence` r on i.`recurrenceId` = r.`recurrenceId` where (i.`uid` = ? or r.uid = ?)";
1248		$bindvars = [$uid, $uid];
1249		if ($user) {
1250			$query .= " and c.user = ?";
1251			$bindvars[] = $user;
1252		}
1253		$result = $this->query($query, $bindvars);
1254		return $result->fetchRow();
1255	}
1256
1257	/**
1258	 * @param $maxrows
1259	 * @param null $calendarId
1260	 * @param $maxDaysEnd
1261	 * @param string $order
1262	 * @param int $priorDays
1263	 * @param $maxDaysStart
1264	 * @param int $start
1265	 * @return array
1266	 */
1267	function past_events($maxrows = -1, $calendarId = null, $maxDaysEnd = -1, $order = 'start_asc', $priorDays = 0, $maxDaysStart = -1, $start = 0)
1268	{
1269		global $prefs;
1270		$cond = '';
1271		$bindvars = [];
1272		if (isset($calendarId)) {
1273			if (is_array($calendarId)) {
1274				$cond = $cond . "and (0=1";
1275				foreach ($calendarId as $id) {
1276					$cond = $cond . " or i.`calendarId` = ? ";
1277				}
1278				$cond = $cond . ")";
1279				$bindvars = array_merge($bindvars, $calendarId);
1280			} else {
1281				$cond = $cond . " and i.`calendarId` = ? ";
1282				$bindvars[] = $calendarId;
1283			}
1284		}
1285		$cond .= " and `end` <= (unix_timestamp(now()) - ?*3600*34)";
1286		$bindvars[] = $priorDays;
1287
1288		if ($maxDaysEnd > 0) {
1289			$maxSeconds = ($maxDaysEnd * 24 * 60 * 60);
1290			$cond .= " and `end` <= (unix_timestamp(now())) +" . $maxSeconds;
1291		}
1292		if ($maxDaysStart > 0) {
1293			$maxSeconds = ($maxDaysStart * 24 * 60 * 60);
1294			$cond .= " and `start` <= (unix_timestamp(now())) +" . $maxSeconds;
1295		}
1296
1297		$ljoin = "left join `tiki_calendar_locations` as l on i.`locationId`=l.`callocId` left join `tiki_calendar_categories` as c on i.`categoryId`=c.`calcatId`";
1298		$query = "select i.`start`, i.`end`, i.`name`, i.`description`," .
1299							" i.`calitemId`, i.`calendarId`, i.`user`, i.`lastModif`," .
1300							" i.`url`, l.`name` as location, i.`allday`," .
1301							"c.`name` as category" .
1302							" from `tiki_calendar_items` i $ljoin where 1=1 " . $cond .
1303							" order by " . $this->convertSortMode($order);
1304
1305		$ret = $this->fetchAll($query, $bindvars, $maxrows, $start);
1306
1307		$query_cant = "select count(*) from `tiki_calendar_items` i $ljoin where 1=1 " . $cond . " order by " . $this->convertSortMode($order);
1308		$cant = $this->getOne($query_cant, $bindvars);
1309
1310		foreach ($ret as &$res) {
1311			$res['parsed'] = TikiLib::lib('parser')->parse_data($res['description'], ['is_html' => $prefs['calendar_description_is_html'] === 'y']);
1312		}
1313
1314		$retval = [];
1315		$retval['data'] = $ret;
1316		$retval['cant'] = $cant;
1317		return $retval;
1318	}
1319
1320	/**
1321	 * @param $calendarId
1322	 * @param $days
1323	 * @return TikiDb_Pdo_Result|TikiDb_Adodb_Result
1324	 */
1325	function cleanEvents($calendarId, $days)
1326	{
1327		global $tikilib;
1328		$mid[] = " `end` < ? ";
1329		$bindvars[] = $tikilib->now - $days * 24 * 60 * 60;
1330		if ($calendarId > 0) {
1331			$mid[] = " `calendarId` = ? ";
1332			$bindvars[] = $calendarId;
1333		}
1334		$query = "delete from `tiki_calendar_items` where " . implode(' and ', $mid);
1335		$result = $tikilib->query($query, $bindvars);
1336		return $result;
1337	}
1338
1339	/**
1340	 * @return int
1341	 */
1342	function firstDayofWeek()
1343	{
1344		global $prefs;
1345		if ($prefs['calendar_firstDayofWeek'] == 'user') {
1346			$firstDayofWeek = (int)tra('First day of week: Sunday (its ID is 0) - Translators, you need to localize this string!');
1347			if ($firstDayofWeek < 1 || $firstDayofWeek > 6) {
1348				$firstDayofWeek = 0;
1349			}
1350		} else {
1351			$firstDayofWeek = $prefs['calendar_firstDayofWeek'];
1352		}
1353		return $firstDayofWeek;
1354	}
1355	// return detail on a date
1356	/**
1357	 * @param $focusDate
1358	 * @return array
1359	 */
1360	function infoDate($focusDate)
1361	{
1362		$focus = [
1363			'day' => (int)TikiLib::date_format('%d', $focusDate),
1364			'month' => (int)TikiLib::date_format('%m', $focusDate),
1365			'year' => TikiLib::date_format('%Y', $focusDate),
1366			'date' => $focusDate,
1367			'weekDay' => TikiLib::date_format('%w', $focusDate) // in (0, 6)
1368		];
1369		$focus['daysInMonth'] = Date_Calc::daysInMonth($focus['month'], $focus['year']);
1370		return $focus;
1371	}
1372	// Compute the start date (the 1 first of the month of the focus date or the day) and the next start date from the period around a focus date
1373	/**
1374	 * @param $focus
1375	 * @param string $view
1376	 * @param string $beginMonth
1377	 * @param $start
1378	 * @param $startNext
1379	 */
1380	function focusStartEnd($focus, $view = 'month', $beginMonth = 'y', &$start, &$startNext)
1381	{
1382		$nbMonths = ['month' => 1, 'bimester' => 2, 'trimester' => 3, 'quarter' => 4, 'semester' => 6, 'year' => 12];
1383		// start of the period
1384		$start = $focus;
1385		if ($beginMonth == 'y') {
1386			$start['day'] = 1;
1387		}
1388		$start['date'] = TikiLib::make_time(0, 0, 0, $start['month'], $start['day'], $start['year']);
1389		$start['weekDay'] = TikiLib::date_format('%w', $start['date']); // in (0, 6)
1390		// start of the next period - just shift some months
1391		$startNext['date'] = TikiLib::make_time(0, 0, 0, $start['month'] + $nbMonths[$view], $start['day'], $start['year']);
1392		$startNext['day'] = TikiLib::date_format('%d', $startNext['date']);
1393		$startNext['month'] = TikiLib::date_format('%m', $startNext['date']);
1394		$startNext['year'] = TikiLib::date_format('%Y', $startNext['date']);
1395		$startNext['weekDay'] = TikiLib::date_format('%w', $startNext['date']);
1396	}
1397	// Compute the date just $view from the focus
1398	/**
1399	 * @param $focus
1400	 * @param string $view
1401	 * @return array
1402	 */
1403	function focusPrevious($focus, $view = 'month')
1404	{
1405		$nbMonths = ['day' => 0, 'week' => 0, 'month' => 1, 'bimester' => 2, 'trimester' => 3, 'quarter' => 4, 'semester' => 6, 'year' => 12];
1406		$nbDays = ['day' => 1, 'week' => 7, 'month' => 0, 'bimester' => 0, 'trimester' => 0, 'quarter' => 0, 'semester' => 0, 'year' => 0];
1407		$previous = $focus;
1408		$previous['day'] -= $nbDays[$view];
1409		// $tikilib->make_time() used with timezones doesn't support month = 0
1410		if ($previous['month'] - $nbMonths[$view] <= 0) { // need to change year
1411			$previous['month'] = ($previous['month'] + 11 - $nbMonths[$view]) % 12 + 1;
1412			$previous['year'] -= 1;
1413		} else {
1414			$previous['month'] -= $nbMonths[$view];
1415		}
1416		$previous['daysInMonth'] = Date_Calc::daysInMonth($previous['month'], $previous['year']);
1417		if ($previous['day'] > $previous['daysInMonth']) {
1418			$previous['day'] = $previous['daysInMonth'];
1419		}
1420		$previous['date'] = Tikilib::make_time(0, 0, 0, $previous['month'], $previous['day'], $previous['year']);
1421		$previous = $this->infoDate($previous['date']); // get back real day, month, year
1422		return $previous;
1423	}
1424	// Compute the date just $view after the focus
1425	/**
1426	 * @param $focus
1427	 * @param string $view
1428	 * @return array
1429	 */
1430	function focusNext($focus, $view = 'month')
1431	{
1432		$nbMonths = ['day' => 0, 'week' => 0, 'month' => 1, 'bimester' => 2, 'trimester' => 3, 'quarter' => 4, 'semester' => 6, 'year' => 12];
1433		$nbDays = ['day' => 1, 'week' => 7, 'month' => 0, 'bimester' => 0, 'trimester' => 0, 'quarter' => 0, 'semester' => 0, 'year' => 0];
1434		$next = $focus;
1435		$next['day'] += $nbDays[$view];
1436		if ($next['month'] + $nbMonths[$view] > 12) {
1437			$next['month'] = ($next['month'] - 1 + $nbMonths[$view]) % 12 + 1;
1438			$next['year'] += 1;
1439		} else {
1440			$next['month'] += $nbMonths[$view];
1441		}
1442		$next['daysInMonth'] = Date_Calc::daysInMonth($next['month'], $next['year']);
1443		if ($next['day'] > $next['daysInMonth']) {
1444			$next['day'] = $next['daysInMonth'];
1445		}
1446		$next['date'] = Tikilib::make_time(0, 0, 0, $next['month'], $next['day'], $next['year']);
1447		$next = $this->infoDate($next['date']); // get back real day, month, year
1448		return $next;
1449	}
1450	// Compute a table view of dates (one line per week)
1451	// $firstWeekDay = 0 (Sunday), 1 (Monday)
1452	/**
1453	 * @param $start
1454	 * @param $startNext
1455	 * @param string $view
1456	 * @param int $firstWeekDay
1457	 * @return array
1458	 */
1459	function getTableViewCells($start, $startNext, $view = 'month', $firstWeekDay = 0)
1460	{
1461		// start of the view
1462		$viewStart = $start;
1463		$nbBackDays = $start['weekDay'] < $firstWeekDay ? 6 : $start['weekDay'] - $firstWeekDay;
1464		if ($nbBackDays == 0) {
1465			$viewStart['daysInMonth'] = Date_Calc::daysInMonth($viewStart['month'], $viewStart['year']);
1466		} elseif ($start['day'] - $nbBackDays < 0) {
1467			$viewStart['month'] = $start['month'] == 1 ? 12 : $start['month'] - 1;
1468			$viewStart['year'] = $start['month'] == 1 ? $start['year'] - 1 : $start['year'];
1469			$viewStart['daysInMonth'] = Date_Calc::daysInMonth($viewStart['month'], $viewStart['year']);
1470			$viewStart['day'] = $viewStart['daysInMonth'] - $nbBackDays + 1;
1471			$viewStart['date'] = TikiLib::make_time(0, 0, 0, $viewStart['month'], $viewStart['day'], $viewStart['year']);
1472		} else {
1473			$viewStart['daysInMonth'] = Date_Calc::daysInMonth($viewStart['month'], $viewStart['year']);
1474			$viewStart['day'] = $viewStart['day'] - $nbBackDays;
1475			$viewStart['date'] = TikiLib::make_time(0, 0, 0, $viewStart['month'], $viewStart['day'], $viewStart['year']);
1476		}
1477		// echo '<br/>VIEWSTART'; print_r($viewStart);
1478		// end of the period
1479		$cell = [];
1480
1481		for ($ilign = 0, $icol = 0, $loop = $viewStart, $weekDay = $viewStart['weekDay'];;) {
1482			if ($loop['date'] >= $startNext['date'] && $icol == 0) {
1483				break;
1484			}
1485			$cell[$ilign][$icol] = $loop;
1486			$cell[$ilign][$icol]['focus'] = $loop['date'] < $start['date'] || $loop['date'] >= $startNext['date'] ? false : true;
1487			$cell[$ilign][$icol]['weekDay'] = $weekDay;
1488			$weekDay = ($weekDay + 1) % 7;
1489			if ($icol >= 6) {
1490				++$ilign;
1491				$icol = 0;
1492			} else {
1493				++$icol;
1494			}
1495			if ($loop['day'] >= $loop['daysInMonth']) {
1496				$loop['day'] = 1;
1497				if ($loop['month'] == 12) {
1498					$loop['month'] = 1;
1499					$loop['year'] += 1;
1500				} else {
1501					$loop['month'] += 1;
1502				}
1503				$loop['daysInMonth'] = Date_Calc::daysInMonth($loop['month'], $loop['year']);
1504			} else {
1505				$loop['day'] = $loop['day'] + 1;
1506			}
1507			$loop['date'] = TikiLib::make_time(0, 0, 0, $loop['month'], $loop['day'], $loop['year']);
1508		}
1509		//echo '<pre>CELL'; print_r($cell); echo '</pre>';
1510		return $cell;
1511	}
1512
1513	/**
1514	 * @param int $firstDayofWeek
1515	 * @param $daysnames
1516	 * @param $daysnames_abr
1517	 */
1518	function getDayNames($firstDayofWeek = 0, &$daysnames, &$daysnames_abr)
1519	{
1520		$daysnames = [];
1521		$daysnames_abr = [];
1522		if ($firstDayofWeek == 0) {
1523			$daysnames[] = tra('Sunday');
1524			$daysnames_abr[] = tra('Su');
1525		}
1526		array_push(
1527			$daysnames,
1528			tra('Monday'),
1529			tra('Tuesday'),
1530			tra('Wednesday'),
1531			tra('Thursday'),
1532			tra('Friday'),
1533			tra('Saturday')
1534		);
1535		array_push(
1536			$daysnames_abr,
1537			tra('Mo'),
1538			tra('Tu'),
1539			tra('We'),
1540			tra('Th'),
1541			tra('Fr'),
1542			tra('Sa')
1543		);
1544		if ($firstDayofWeek != 0) {
1545			$daysnames[] = tra('Sunday');
1546			$daysnames_abr[] = tra('Su');
1547		}
1548	}
1549
1550	/**
1551	 * Get calendar and its events
1552	 *
1553	 * @param $calIds
1554	 * @param $viewstart
1555	 * @param $viewend
1556	 * @param $group_by
1557	 * @param $item_name
1558	 * @param bool $listmode if set to true populate listevents key of the returned array
1559	 * @return array
1560	 */
1561	function getCalendar($calIds, &$viewstart, &$viewend, $group_by = '', $item_name = 'events', $listmode = false)
1562	{
1563		global $user, $prefs;
1564
1565		// Global vars used by tiki-calendar_setup.php (this has to be changed)
1566		global $calendarViewMode, $request_day, $request_month;
1567		global $request_year, $dayend, $myurl;
1568		global $weekdays, $daysnames, $daysnames_abr;
1569		include('tiki-calendar_setup.php');
1570
1571		$smarty = TikiLib::lib('smarty');
1572		$tikilib = TikiLib::lib('tiki');
1573
1574		//FIXME : maxrecords = 50
1575		$listtikievents = $this->list_items_by_day($calIds, $user, $viewstart, $viewend, 0, 50);
1576
1577		$mloop = TikiLib::date_format('%m', $viewstart);
1578		$dloop = TikiLib::date_format('%d', $viewstart);
1579		$yloop = TikiLib::date_format('%Y', $viewstart);
1580		$curtikidate = new TikiDate();
1581		$display_tz = $tikilib->get_display_timezone();
1582		if ($display_tz == '') {
1583			$display_tz = 'UTC';
1584		}
1585		$curtikidate->setTZbyID($display_tz);
1586		$curtikidate->setLocalTime($dloop, $mloop, $yloop, 0, 0, 0, 0);
1587		$listevents = [];
1588
1589		// note that number of weeks starts at ZERO (i.e., zero = 1 week to display).
1590		for ($i = 0; $i <= $numberofweeks; $i++) {
1591			$weeks[] = $curtikidate->getWeekOfYear();
1592
1593			foreach ($weekdays as $w) {
1594				$leday = [];
1595				if ($group_by == 'day') {
1596					$key = 0;
1597				}
1598				if ($calendarViewMode['casedefault'] == 'day') {
1599					$dday = $daystart;
1600				} else {
1601					$dday = $curtikidate->getTime();
1602					$curtikidate->addDays(1);
1603				}
1604				$cell[$i][$w]['day'] = $dday;
1605
1606				if ($calendarViewMode['casedefault'] == 'day' or ( $dday >= $daystart && $dday <= $dayend )) {
1607					$cell[$i][$w]['focus'] = true;
1608				} else {
1609					$cell[$i][$w]['focus'] = false;
1610				}
1611				if (isset($listtikievents["$dday"])) {
1612					$e = -1;
1613
1614					foreach ($listtikievents["$dday"] as $lte) {
1615						$lte['desc_name'] = $lte['name'];
1616						if ($group_by_item != 'n') {
1617							if ($group_by != 'day') {
1618								$key = $lte['id'] . '|' . $lte['type'];
1619							}
1620							if (! isset($leday[$key])) {
1621								$leday[$key] = $lte;
1622								if ($group_by == 'day') {
1623									$leday[$key]['description'] = [$lte['where'] => [$lte['group_description']]];
1624									$leday[$key]['head'] = TikiLib::date_format($prefs['short_date_format'], $cell[$i][$w]['day']);
1625								} else {
1626									$leday[$key]['description'] = ' - <b>' . $lte['when'] . '</b> : ' . tra($lte['action']) . ' ' . $lte['description'];
1627									$leday[$key]['head'] = $lte['name'] . ', <i>' . tra('in') . ' ' . $lte['where'] . '</i>';
1628								}
1629								$leday[$key]['desc_name'] = '';
1630							} else {
1631								$leday_item =& $leday[$key];
1632								$leday_item['user'] .= ', ' . $lte['user'];
1633
1634								if (! isset($leday_item['action']) || ! is_integer($leday_item['action'])) {
1635									$leday_item['action'] = 1;
1636								}
1637								$leday_item['action']++;
1638
1639								if ($group_by == 'day') {
1640									$leday_item['name'] .= '<br />' . $lte['name'];
1641									$leday_item['desc_name'] = $leday_item['action'] . ' ' . tra($item_name) . ': ';
1642									$leday_item['description'][$lte['where']][] = $lte['group_description'];
1643								} else {
1644									$leday_item['name'] = $lte['name'] . ' (x ' . $leday_item['action'] . ')';
1645									$leday_item['desc_name'] = $leday_item['action'] . ' ' . tra($item_name);
1646									if ($lte['show_description'] == 'y' && ! empty($lte['description'])) {
1647										$leday_item['description'] .= ",\n<br /> - <b>" . $lte['when'] . '</b> : ' . tra($lte['action']) . ' ' . $lte['description'];
1648										$leday_item['show_description'] = 'y';
1649									}
1650								}
1651							}
1652						} else {
1653							$e++;
1654							$key = "{$lte['time']}$e";
1655							$leday[$key] = $lte;
1656							$lte['desc_name'] .= tra($lte['action']);
1657						}
1658					}
1659					foreach ($leday as $key => $lte) {
1660						if ($group_by == 'day') {
1661							$desc = '';
1662							foreach ($lte['description'] as $desc_where => $desc_items) {
1663								$desc_items = array_unique($desc_items);
1664								foreach ($desc_items as $desc_item) {
1665									if ($desc != '') {
1666										$desc .= '<br />';
1667									}
1668									$desc .= '- ' . $desc_item;
1669									if (! empty($lte['show_location']) && $lte['show_location'] == 'y' && $desc_where != '') {
1670										$desc .= ' <i>[' . $desc_where . ']</i>';
1671									}
1672								}
1673							}
1674							$lte['description'] = $desc;
1675						}
1676
1677						$smarty->assign('calendar_type', ( $myurl == 'tiki-action_calendar.php' ? 'tiki_actions' : 'calendar' ));
1678						$smarty->assign_by_ref('item_url', $lte["url"]);
1679						$smarty->assign_by_ref('cellhead', $lte["head"]);
1680						$smarty->assign_by_ref('cellprio', $lte["prio"]);
1681						$smarty->assign_by_ref('cellcalname', $lte["calname"]);
1682						$smarty->assign('celllocation', "");
1683						$smarty->assign('cellcategory', "");
1684						$smarty->assign_by_ref('cellname', $lte["desc_name"]);
1685						$smarty->assign('cellid', "");
1686						$smarty->assign_by_ref('celldescription', $lte["description"]);
1687						$smarty->assign('show_description', $lte["show_description"]);
1688
1689						if (! isset($leday[$key]["over"])) {
1690							$leday[$key]["over"] = '';
1691						} else {
1692							$leday[$key]["over"] .= "<br />\n";
1693						}
1694						$leday[$key]["over"] .= $smarty->fetch("tiki-calendar_box.tpl");
1695					}
1696				}
1697
1698				if (is_array($leday)) {
1699					ksort($leday);
1700					$cell[$i][$w]['items'] = array_values($leday);
1701				}
1702			}
1703		}
1704
1705		if ((isset($_SESSION['CalendarViewList']) && $_SESSION['CalendarViewList'] == 'list') || $listmode) {
1706			if (is_array($listtikievents)) {
1707				foreach ($listtikievents as $le) {
1708					if (is_array($le)) {
1709						foreach ($le as $e) {
1710							$listevents[] = $e;
1711						}
1712					}
1713				}
1714			}
1715		}
1716
1717		return [
1718			'cell' => $cell,
1719			'listevents' => $listevents,
1720			'weeks' => $weeks,
1721			'weekdays' => $weekdays,
1722			'daysnames' => $daysnames,
1723			'daysnames_abr' => $daysnames_abr,
1724			'trunc' => $trunc
1725		];
1726	}
1727
1728	/**
1729	 * @param $calitemId
1730	 * @param null $adds
1731	 * @param null $dels
1732	 */
1733	function update_participants($calitemId, $adds = null, $dels = null)
1734	{
1735		if (! empty($dels)) {
1736			foreach ($dels as $del) {
1737				$this->query('delete from `tiki_calendar_roles` where `calitemId`=? and `username`=? and `role`!=?', [$calitemId, $del, ROLE_ORGANIZER]);
1738			}
1739		}
1740		if (! empty($adds)) {
1741			$all = $this->fetchAll('select * from `tiki_calendar_roles` where `calitemId`=?', [$calitemId]);
1742			foreach ($adds as $add) {
1743				if (! isset($add['role']) || $add['role'] == ROLE_ORGANIZER) {
1744					$add['role'] = 0;
1745				}
1746				$found = false;
1747				foreach ($all as $u) {
1748					if ($u['username'] == $add['name'] && $u['role'] != ROLE_ORGANIZER) {
1749						if ($u['role'] != $add['role']) {
1750							$this->query('update `tiki_calendar_roles` set `role`=? where `calitemId`=? and `username`=?', [$add['role'], $calitemId, $add['name']]);
1751						}
1752						$found = true;
1753						break;
1754					}
1755				}
1756				if (! $found) {
1757					$this->query('insert into `tiki_calendar_roles`(`calitemId`, `username`, `role`) values(?, ? ,?)', [$calitemId, $add['name'], $add['role']]);
1758				}
1759			}
1760		}
1761	}
1762
1763	/**
1764	 * Update participant status (partstat) for an attendee
1765	 * @param $calitemId
1766	 * @param $username
1767	 * @param $partstat - ACCEPTED, TENTATIVE, DECLINED
1768	 */
1769	public function update_partstat($calitemId, $username, $partstat) {
1770		return $this->query("update `tiki_calendar_roles` SET `partstat` = ? where calitemId = ? and username = ?", [$partstat, $calitemId, $username]);
1771	}
1772
1773	/**
1774	 * Adds a change record to the calendarchanges table.
1775	 *
1776	 * @param int $calendarId
1777	 * @param int $calitemId
1778	 * @param int $operation 1 = add, 2 = modify, 3 = delete.
1779	 * @return void
1780	 */
1781	public function add_change($calendarId, $calitemId, $operation)
1782	{
1783		$options = $this->get_calendar_options($calendarId);
1784
1785		$this->query('insert into `tiki_calendar_changes`(`calitemId`, `synctoken`, `calendarId`, `operation`) values(?, ?, ?, ?)', [
1786			$calitemId,
1787			$options['synctoken'] ?? 1,
1788			$calendarId,
1789			$operation,
1790		]);
1791
1792		$this->query('replace into tiki_calendar_options(calendarId, optionName, value) values(?, ?, ?)', [
1793			$calendarId,
1794			'synctoken',
1795			$options['synctoken']+1,
1796		]);
1797	}
1798
1799	/**
1800	 * Gets latest change for each item in the calendar.
1801	 *
1802	 * @param int $calendarId
1803	 * @param int $synctoken
1804	 * @param int $maxrecords
1805	 * @return array
1806	 */
1807	public function get_changes($calendarId, $synctoken, $maxRecords = -1)
1808	{
1809		$query = 'select c1.calitemId, c1.operation, ci.uri
1810			from `tiki_calendar_changes` c1
1811			left join `tiki_calendar_changes` c2 on c1.calitemId = c2.calitemId and c1.synctoken < c2.synctoken
1812			left join `tiki_calendar_items` ci on c1.calitemId = ci.calitemId
1813			where c1.calendarId = ? and c1.synctoken > ? and c2.calitemId is null';
1814		$bindvars = [$calendarId, $synctoken];
1815		return $this->fetchAll($query, $bindvars, $maxRecords);
1816	}
1817
1818	public function fill_uid($calitemId, $uid) {
1819		$this->query("update `tiki_calendar_items` set `uid` = ? where `calitemId` = ?", [$uid, $calitemId]);
1820	}
1821
1822	/**
1823	 * Calendar instance methods - deal with invites and shares
1824	 */
1825	public function get_calendar_instances($calendarId)
1826	{
1827		$query = "select user, access, share_href, share_name, share_invite_status from `tiki_calendar_instances` where calendarId = ?";
1828		$bindvars = [$calendarId];
1829		return $this->fetchAll($query, $bindvars);
1830	}
1831
1832	public function get_calendar_instance($instanceId)
1833	{
1834		$query = "select user, access, share_href, share_name, share_invite_status from `tiki_calendar_instances` where calendarInstanceId = ?";
1835		$bindvars = [$instanceId];
1836		$result = $this->query($query, $bindvars);
1837		return $result->fetchRow();
1838	}
1839
1840	public function create_calendar_instance($data)
1841	{
1842		$query = 'insert into `tiki_calendar_instances` (`'.implode('`, `', array_keys($data)).'`) values ('.implode(",", array_fill(0, count($data), "?")).')';
1843		$bindvars = array_values($data);
1844		$this->query($query, $bindvars);
1845		return $this->lastInsertId();
1846	}
1847
1848	public function update_calendar_instance($calendarId, $share_href, $data)
1849	{
1850		$query = 'update `tiki_calendar_instances` set '.implode(' = ?, ', array_keys($data)).' = ? where calendarId = ? and share_href = ?';
1851		$bindvars = array_values($data) + [$calendarId, $share_href];
1852		return $this->query($query, $bindvars);
1853	}
1854
1855	public function remove_calendar_instance($calendarId, $share_href = null, $instanceId = null)
1856	{
1857		if ($shared_href) {
1858			return $this->query('delete from `tiki_calendar_instances` where calendarId = ? and share_href = ?', [$calendarId, $share_href]);
1859		}
1860		if ($instanceId) {
1861			return $this->query('delete from `tiki_calendar_instances` where calendarId = ? and calendarInstanceId = ?', [$calendarId, $instanceId]);
1862		}
1863	}
1864
1865	/**
1866	 * Subscription methods
1867	 */
1868	public function get_subscriptions($user)
1869	{
1870		$query = "select subscriptionId, calendarId, user, source, name, refresh_rate, `order`, color, strip_todos, strip_alarms, strip_attachments, lastmodif from tiki_calendar_subscriptions where user = ?";
1871		$bindvars = [$user];
1872		return $this->fetchAll($query, $bindvars);
1873	}
1874
1875	public function create_subscription($data)
1876	{
1877		$data['lastmodif'] = time();
1878		$query = 'insert into `tiki_calendar_subscriptions` (`'.implode('`, `', array_keys($data)).'`) values ('.implode(",", array_fill(0, count($data), "?")).')';
1879		$bindvars = array_values($data);
1880		$this->query($query, $bindvars);
1881		return $this->lastInsertId();
1882	}
1883
1884	public function update_subscription($subscriptionId, $data)
1885	{
1886		$data['lastmodif'] = time();
1887		$query = 'update `tiki_calendar_subscriptions` set '.implode(' = ?, ', array_keys($data)).' = ? where subscriptionId = ?';
1888		$bindvars = array_values($data) + [$subscriptionId];
1889		return $this->query($query, $bindvars);
1890	}
1891
1892	public function delete_subscription($subscriptionId)
1893	{
1894		return $this->query('delete from `tiki_calendar_subscriptions` where subscriptionId = ?', [$subscriptionId]);
1895	}
1896
1897	/**
1898	 * Scheduling methods
1899	 */
1900	public function get_scheduling_object($user, $uri)
1901	{
1902		$query = "SELECT uri, calendardata, lastmodif, etag, size FROM `tiki_calendar_scheduling_objects` WHERE user = ? AND uri = ?";
1903		$bindvars = [$user, $uri];
1904		$result = $this->query($query, $bindvars);
1905		return $result->fetchRow();
1906	}
1907
1908	public function get_scheduling_objects($user)
1909	{
1910		$query = "SELECT schedulingObjectId, calendardata, uri, lastmodif, etag, size FROM `tiki_calendar_scheduling_objects` WHERE user = ?";
1911		$bindvars = [$user];
1912		return $this->fetchAll($query, $bindvars);
1913	}
1914
1915	public function delete_scheduling_object($user, $uri)
1916	{
1917		return $this->query('delete from `tiki_calendar_scheduling_objects` where user = ? and uri = ?', [$user, $uri]);
1918	}
1919
1920	public function create_scheduling_object($user, $uri, $data)
1921	{
1922		$query = "insert into `tiki_calendar_scheduling_objects` (user, calendardata, uri, lastmodif, etag, size) values (?, ?, ?, ?, ?, ?)";
1923		$bindvars = [$user, $data, $uri, time(), md5($data), strlen($data)];
1924		$this->query($query, $bindvars);
1925		return $this->lastInsertId();
1926	}
1927}
1928