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/**
9 * Tracker Library
10 *
11 * \brief Functions to support accessing and processing of the Trackers.
12 *
13 * @package		Tiki
14 * @subpackage		Trackers
15 * @author		Luis Argerich, Garland Foster, Eduardo Polidor, et. al.
16 * @copyright		Copyright (c) 2002-2009, All Rights Reserved.
17 * 			See copyright.txt for details and a complete list of authors.
18 * @license		LGPL - See license.txt for details.
19 * @version		SVN $Rev: 25023 $
20 * @filesource
21 * @link		http://dev.tiki.org/Trackers
22 * @since		Always
23 */
24/**
25 * This script may only be included, so it is better to die if called directly.
26 */
27if (strpos($_SERVER["SCRIPT_NAME"], basename(__FILE__)) !== false) {
28	header("location: index.php");
29	exit;
30}
31
32/**
33 * TrackerLib Class
34 *
35 * This class extends the TikiLib class.
36 */
37class TrackerLib extends TikiLib
38{
39
40	public $trackerinfo_cache;
41	private $sectionFormats = [];
42
43	function __construct()
44	{
45		$this->now = time();
46		$this->registerSectionFormat('flat', 'view', 'trackeroutput/layout_flat.tpl', tr('Flat'));
47		$this->registerSectionFormat('flat', 'edit', 'trackerinput/layout_flat.tpl', tr('Flat'));
48		$this->registerSectionFormat('tab', 'view', 'trackeroutput/layout_tab.tpl', tr('Tabs'));
49		$this->registerSectionFormat('tab', 'edit', 'trackerinput/layout_tab.tpl', tr('Tabs'));
50	}
51
52	function registerSectionFormat($layout, $mode, $template, $label)
53	{
54		if ($template) {
55			$this->sectionFormats[$layout][$mode] = [
56				'template' => $template,
57				'label' => $label,
58			];
59		}
60	}
61
62	function unregisterSectionFormat($layout)
63	{
64		unset($this->sectionFormats[$layout]);
65	}
66
67	function getSectionFormatTemplate($layout, $mode)
68	{
69		if (isset($this->sectionFormats[$layout][$mode])) {
70			return $this->sectionFormats[$layout][$mode]['template'];
71		} elseif ($layout == 'config' || $layout === 'n') {
72			// Special handling for config, fallback to default flat (also for when sectionFormat gets saved as "n" in legacy trackers)
73			return $this->getSectionFormatTemplate('flat', $mode);
74		} else {
75			throw new Exception(tr('No template available for %0 - %1', $layout, $mode));
76		}
77	}
78
79	function getGlobalSectionFormats()
80	{
81		$out = [];
82		foreach ($this->sectionFormats as $layout => $modes) {
83			if (count($modes) == 2) {
84				$first = reset($modes);
85				$out[$layout] = $first['label'];
86			}
87		}
88
89		$out['config'] = tr('Configured');
90
91		return $out;
92	}
93
94	private function attachments()
95	{
96		return $this->table('tiki_tracker_item_attachments');
97	}
98
99	private function comments()
100	{
101		return $this->table('tiki_comments');
102	}
103
104	private function itemFields()
105	{
106		return $this->table('tiki_tracker_item_fields', false);
107	}
108
109	private function trackers()
110	{
111		return $this->table('tiki_trackers');
112	}
113
114	private function items()
115	{
116		return $this->table('tiki_tracker_items');
117	}
118
119	private function fields()
120	{
121		return $this->table('tiki_tracker_fields');
122	}
123
124	private function options()
125	{
126		return $this->table('tiki_tracker_options');
127	}
128
129	private function logs()
130	{
131		return $this->table('tiki_tracker_item_field_logs', false);
132	}
133
134	private function groupWatches()
135	{
136		return $this->table('tiki_group_watches');
137	}
138
139	private function userWatches()
140	{
141		return $this->table('tiki_user_watches');
142	}
143
144	public function remove_field_images($fieldId)
145	{
146		$itemFields = $this->itemFields();
147		$values = $itemFields->fetchColumn('value', ['fieldId' => (int) $fieldId]);
148		foreach ($values as $file) {
149			if (file_exists($file)) {
150				unlink($file);
151			}
152		}
153	}
154
155	public function add_item_attachment_hit($id)
156	{
157		global $prefs, $user;
158		if (StatsLib::is_stats_hit()) {
159			$attachments = $this->attachments();
160			$attachments->update(['hits' => $attachments->increment(1)], ['attId' => (int) $id]);
161		}
162		return true;
163	}
164
165	public function get_item_attachment_owner($attId)
166	{
167		return $this->attachments()->fetchOne('user', ['attId' => (int) $attId]);
168	}
169
170	public function list_item_attachments($itemId, $offset = 0, $maxRecords = -1, $sort_mode = 'attId_asc', $find = '')
171	{
172		$attachments = $this->attachments();
173
174		$order = $attachments->sortMode($sort_mode);
175		$fields = ['user', 'attId', 'itemId', 'filename', 'filesize', 'filetype', 'hits', 'created', 'comment', 'longdesc', 'version'];
176
177		$conditions = [
178			'itemId' => (int) $itemId,
179		];
180
181		if ($find) {
182			$conditions['filename'] = $attachments->like("%$find%");
183		}
184
185		return [
186			'data' => $attachments->fetchAll($fields, $conditions, $maxRecords, $offset, $order),
187			'cant' => $attachments->fetchCount($conditions),
188		];
189	}
190
191	public function get_item_nb_attachments($itemId)
192	{
193		$attachments = $this->attachments();
194
195		$ret = $attachments->fetchRow(
196			['hits' => $attachments->sum('hits'), 'attachments' => $attachments->count()],
197			['itemId' => $itemId]
198		);
199
200		return $ret ? $ret : [];
201	}
202
203	public function get_item_nb_comments($itemId)
204	{
205		return $this->comments()->fetchCount(['object' => (int) $itemId, 'objectType' => 'trackeritem']);
206	}
207
208	public function list_all_attachements($offset = 0, $maxRecords = -1, $sort_mode = 'created_desc', $find = '')
209	{
210		$attachments = $this->attachments();
211
212		$fields = ['user', 'attId', 'itemId', 'filename', 'filesize', 'filetype', 'hits', 'created', 'comment', 'path'];
213		$order = $attachments->sortMode($sort_mode);
214		$conditions = [];
215
216		if ($find) {
217			$conditions['filename'] = $attachments->like("%$find%");
218		}
219
220		return [
221			'data' => $attachments->fetchAll($fields, $conditions, $maxRecords, $offset, $order),
222			'cant' => $attachments->fetchCount($conditions),
223		];
224	}
225
226	public function file_to_db($path, $attId)
227	{
228		if (is_readable($path)) {
229			$updateResult = $this->attachments()->update(
230				['data' => file_get_contents($path),	'path' => ''],
231				['attId' => (int) $attId]
232			);
233
234			if ($updateResult) {
235				unlink($path);
236			}
237		}
238	}
239
240	public function db_to_file($path, $attId)
241	{
242		$attachments = $this->attachments();
243
244		$data = $attachments->fetchOne('data', ['attId' => (int) $attId]);
245		if (false !== file_put_contents($path, $data)) {
246			$attachments->update(['data' => '', 'path' => basename($path)], ['attId' => (int) $attId]);
247		}
248	}
249
250	public function get_item_attachment($attId)
251	{
252		return $this->attachments()->fetchFullRow(['attId' => (int) $attId]);
253	}
254
255	public function remove_item_attachment($attId = 0, $itemId = 0)
256	{
257		global $prefs;
258		$attachments = $this->attachments();
259		$paths = [];
260
261		if (empty($attId) && ! empty($itemId)) {
262			if ($prefs['t_use_db'] === 'n') {
263				$paths = $attachments->fetchColumn('path', ['itemId' => $itemId]);
264			}
265
266			$this->query('update `tiki_tracker_item_fields` ttif left join `tiki_tracker_fields` ttf using (`fieldId`) set `value`=? where ttif.`itemId`=? and ttf.`type`=?', ['', (int) $itemId, 'A']);
267			$attachments->deleteMultiple(['itemId' => $itemId]);
268		} elseif (! empty($attId)) {
269			if ($prefs['t_use_db'] === 'n') {
270				$paths = $attachments->fetchColumn('path', ['attId' => (int) $attId]);
271			}
272			$this->query('update `tiki_tracker_item_fields` ttif left join `tiki_tracker_fields` ttf using (`fieldId`) set `value`=? where ttif.`value`=? and ttf.`type`=?', ['', (int) $attId, 'A']);
273			$attachments->delete(['attId' => (int) $attId]);
274		}
275		foreach (array_filter($paths) as $path) {
276			@unlink($prefs['t_use_dir'] . $path);
277		}
278	}
279
280	public function replace_item_attachment($attId, $filename, $type, $size, $data, $comment, $user, $fhash, $version, $longdesc, $trackerId = 0, $itemId = 0, $options = '', $notif = true)
281	{
282		global $prefs;
283		$attachments = $this->attachments();
284
285		$comment = strip_tags($comment);
286		$now = $this->now;
287		if (empty($attId)) {
288			$attId = $attachments->insert(
289				[
290					'itemId' => (int) $itemId,
291					'filename' => $filename,
292					'filesize' => $size,
293					'filetype' => $type,
294					'data' => $data,
295					'created' => $now,
296					'hits' => 0,
297					'user' => $user,
298					'comment' => $comment,
299					'path' => $fhash,
300					'version' => $version,
301					'longdesc' => $longdesc,
302				]
303			);
304		} elseif (empty($filename)) {
305			$attachments->update(
306				[
307					'user' => $user,
308					'comment' => $comment,
309					'version' => $version,
310					'longdesc' => $longdesc,
311				],
312				['attId' => $attId]
313			);
314		} else {
315			$path = $attachments->fetchOne('path', ['attId' => (int) $attId]);
316			if ($path) {
317				@unlink($prefs['t_use_dir'] . $path);
318			}
319
320			$attachments->update(
321				[
322					'filename' => $filename,
323					'filesize' => $size,
324					'filetype' => $type,
325					'data' => $data,
326					'user' => $user,
327					'comment' => $comment,
328					'path' => $fhash,
329					'version' => $version,
330					'longdesc' => $longdesc,
331				],
332				['attId' => (int) $attId]
333			);
334		}
335
336		if (! $notif) {
337			return $attId;
338		}
339
340		$options["attachment"] = ["attId" => $attId, "filename" => $filename, "comment" => $comment];
341		$watchers = $this->get_notification_emails($trackerId, $itemId, $options);
342
343		if (count($watchers > 0)) {
344			$smarty = TikiLib::lib('smarty');
345			$trackerName = $this->trackers()->fetchOne('name', ['trackerId' => (int) $trackerId]);
346
347			$smarty->assign('mail_date', $this->now);
348			$smarty->assign('mail_user', $user);
349			$smarty->assign('mail_action', 'New File Attached to Item:' . $itemId . ' at tracker ' . $trackerName);
350			$smarty->assign('mail_itemId', $itemId);
351			$smarty->assign('mail_trackerId', $trackerId);
352			$smarty->assign('mail_trackerName', $trackerName);
353			$smarty->assign('mail_attId', $attId);
354			$smarty->assign('mail_data', $filename . "\n" . $comment . "\n" . $version . "\n" . $longdesc);
355			$foo = parse_url($_SERVER["REQUEST_URI"]);
356			$machine = $this->httpPrefix(true) . $foo["path"];
357			$smarty->assign('mail_machine', $machine);
358			$parts = explode('/', $foo['path']);
359			if (count($parts) > 1) {
360				unset($parts[count($parts) - 1]);
361			}
362			$smarty->assign('mail_machine_raw', $this->httpPrefix(true) . implode('/', $parts));
363			if (! isset($_SERVER["SERVER_NAME"])) {
364				$_SERVER["SERVER_NAME"] = $_SERVER["HTTP_HOST"];
365			}
366			include_once('lib/webmail/tikimaillib.php');
367			$smarty->assign('server_name', $_SERVER['SERVER_NAME']);
368			$desc = $this->get_isMain_value($trackerId, $itemId);
369			$smarty->assign('mail_item_desc', $desc);
370			foreach ($watchers as $w) {
371				$mail = new TikiMail($w['user']);
372
373				if (! isset($w['template'])) {
374					$w['template'] = '';
375				}
376				$content = $this->parse_notification_template($w['template']);
377
378				$mail->setSubject($smarty->fetchLang($w['language'], $content['subject']));
379				$mail_data = $smarty->fetchLang($w['language'], $content['template']);
380				if (isset($w['templateFormat']) && $w['templateFormat'] == 'html') {
381					$mail->setHtml($mail_data, str_replace('&nbsp;', ' ', strip_tags($mail_data)));
382				} else {
383					$mail->setText(str_replace('&nbsp;', ' ', strip_tags($mail_data)));
384				}
385				$mail->send([$w['email']]);
386			}
387		}
388
389		return $attId;
390	}
391
392	public function list_last_comments($trackerId = 0, $itemId = 0, $offset = -1, $maxRecords = -1)
393	{
394		global $user;
395		$mid = "1=1";
396		$bindvars = [];
397
398		if ($itemId != 0) {
399			$mid .= " and `itemId`=?";
400			$bindvars[] = (int) $itemId;
401		}
402
403		if ($trackerId != 0) {
404			$query = "select t.*, t.object itemId from `tiki_comments` t left join `tiki_tracker_items` a on t.`object`=a.`itemId` where $mid and a.`trackerId`=? and t.`objectType` = 'trackeritem' order by t.`commentDate` desc";
405			$bindvars[] = $trackerId;
406			$query_cant = "select count(*) from `tiki_comments` t left join `tiki_tracker_items` a on t.`object`=a.`itemId` where $mid and a.`trackerId`=? AND t.`objectType` = 'trackeritem' order by t.`commentDate` desc";
407		} else {
408			$query = "select t.*, t.object itemId, a.`trackerId` from `tiki_comments` t left join `tiki_tracker_items` a on t.`object`=a.`itemId` where $mid AND t.`objectType` = 'trackeritem' order by `commentDate` desc";
409			$query_cant = "select count(*) from `tiki_comments` where $mid AND `objectType` = 'trackeritem'";
410		}
411
412		$ret = $this->fetchAll($query, $bindvars, $maxRecords, $offset);
413		$cant = $this->getOne($query_cant, $bindvars);
414
415		foreach ($ret as $key => &$res) {
416			$itemObject = Tracker_Item::fromId($res['itemId']);
417			if (! $itemObject->canView()) {
418				--$cant;
419				unset($ret[$key]);
420				continue;
421			}
422			$res["parsed"] = $this->parse_comment($res["data"]);
423		}
424
425		return [
426			'data' => array_values($ret),
427			'cant' => $cant,
428		];
429	}
430
431	public function get_last_position($trackerId)
432	{
433		$fields = $this->fields();
434		return $fields->fetchOne($fields->max('position'), ['trackerId' => (int) $trackerId]);
435	}
436
437	public function get_tracker_item($itemId)
438	{
439		$res = $this->items()->fetchFullRow(['itemId' => (int) $itemId]);
440		if (! $res) {
441			return false;
442		}
443
444		$itemFields = $this->itemFields();
445		$fields = $itemFields->fetchMap('fieldId', 'value', ['itemId' => (int) $itemId]);
446
447		foreach ($fields as $id => $value) {
448			$res[$id] = $value;
449		}
450		return $res;
451	}
452
453	function get_all_item_id($trackerId, $fieldId, $value)
454	{
455		$query = "select distinct ttif.`itemId` from `tiki_tracker_items` tti, `tiki_tracker_item_fields` ttif ";
456		$query .= " where tti.`itemId`=ttif.`itemId` and tti.`trackerId`=? and ttif.`fieldId`=? ";
457		$value = "%$value%";
458		$query .= " and ttif.`value` LIKE ?";
459
460		$result = $this->fetchAll($query, [(int) $trackerId, (int)$fieldId, $value]);
461
462		$itemIds = [];
463		foreach ($result as $row) {
464			$itemIds[] = $row['itemId'];
465		}
466		return $itemIds;
467	}
468
469	public function get_item_id($trackerId, $fieldId, $value, $partial = false)
470	{
471		$query = "select ttif.`itemId` from `tiki_tracker_items` tti, `tiki_tracker_fields` ttf, `tiki_tracker_item_fields` ttif ";
472		$query .= " where tti.`trackerId`=ttf.`trackerId` and ttif.`fieldId`=ttf.`fieldId` and tti.`itemId`=ttif.`itemId` and ttf.`trackerId`=? and ttf.`fieldId`=? ";
473		if ($partial) {
474			$value = "%$value%";
475			$query .= " and ttif.`value` LIKE ?";
476		} else {
477			$query .= " and ttif.`value`=?";
478		}
479		return $this->getOne($query, [(int) $trackerId, (int) $fieldId, $value]);
480	}
481
482	public function get_item($trackerId, $fieldId, $value)
483	{
484		$itemId = $this->get_item_id($trackerId, $fieldId, $value);
485		return $this->get_tracker_item($itemId);
486	}
487
488	/* experimental shared */
489	/* trackerId is useless */
490	public function get_item_value($trackerId, $itemId, $fieldId)
491	{
492		global $prefs;
493
494		static $cache = [];
495		$cacheKey = "$fieldId.$itemId";
496		if (isset($cache[$cacheKey])) {
497			return $cache[$cacheKey];
498		}
499
500		$value = $this->itemFields()->fetchOne('value', ['fieldId' => (int) $fieldId, 'itemId' => (int) $itemId]);
501
502		if ($this->is_multilingual($fieldId) == 'y') {
503			$list = json_decode($value, true);
504			if (isset($list[$prefs['language']])) {
505				return $list[$prefs['language']];
506			}
507		}
508
509		if (TikiLib::lib('tiki')->get_memory_avail() < 1048576 * 10) {
510			$cache = [];
511		}
512		$cache[$cacheKey] = $value;
513
514		return $value;
515	}
516
517	public function get_item_status($itemId)
518	{
519		$status = $this->items()->fetchOne('status', ['itemId' => (int) $itemId]);
520		return $status;
521	}
522
523	/*shared*/
524	public function list_tracker_items($trackerId, $offset, $maxRecords, $sort_mode, $fields, $status = '', $initial = '')
525	{
526
527		$filters = [];
528		if ($fields) {
529			$temp_max = count($fields["data"]);
530			for ($i = 0; $i < $temp_max; $i++) {
531				$fieldId = $fields["data"][$i]["fieldId"];
532				$filters[$fieldId] = $fields["data"][$i];
533			}
534		}
535		$csort_mode = '';
536		if (substr($sort_mode, 0, 2) == "f_") {
537			list($a,$csort_mode,$corder) = explode('_', $sort_mode, 3);
538		}
539		$trackerId = (int) $trackerId;
540		if ($trackerId == -1) {
541			$mid = " where 1=1 ";
542			$bindvars = [];
543		} else {
544			$mid = " where tti.`trackerId`=? ";
545			$bindvars = [$trackerId];
546		}
547		if ($status) {
548			$mid .= " and tti.`status`=? ";
549			$bindvars[] = $status;
550		}
551		if ($initial) {
552			$mid .= "and ttif.`value` like ?";
553			$bindvars[] = $initial . '%';
554		}
555		if (! $sort_mode) {
556			$temp_max = count($fields["data"]);
557			for ($i = 0; $i < $temp_max; $i++) {
558				if ($fields['data'][$i]['isMain'] == 'y') {
559					$csort_mode = $fields['data'][$i]['name'];
560					break;
561				}
562			}
563		}
564		if ($csort_mode) {
565			$sort_mode = $csort_mode . "_desc";
566			$bindvars[] = $csort_mode;
567			$query = "select tti.*, ttif.`value` from `tiki_tracker_items` tti, `tiki_tracker_item_fields` ttif, `tiki_tracker_fields` ttf ";
568			$query .= " $mid and tti.`itemId`=ttif.`itemId` and ttf.`fieldId`=ttif.`fieldId` and ttf.`name`=? order by ttif.`value`";
569			$query_cant = "select count(*) from `tiki_tracker_items` tti, `tiki_tracker_item_fields` ttif, `tiki_tracker_fields` ttf ";
570			$query_cant .= " $mid and tti.`itemId`=ttif.`itemId` and ttf.`fieldId`=ttif.`fieldId` and ttf.`name`=? ";
571		} else {
572			if (! $sort_mode) {
573				$sort_mode = "lastModif_desc";
574			}
575			$query = "select * from `tiki_tracker_items` tti $mid order by " . $this->convertSortMode($sort_mode);
576			$query_cant = "select count(*) from `tiki_tracker_items` tti $mid ";
577		}
578		$result = $this->fetchAll($query, $bindvars, $maxRecords, $offset);
579		$cant = $this->getOne($query_cant, $bindvars);
580		$ret = [];
581		foreach ($result as $res) {
582			$fields = [];
583			$itid = $res["itemId"];
584			$query2 = "select ttif.`fieldId`,`name`,`value`,`type`,`isTblVisible`,`isMain`,`position`
585				from `tiki_tracker_item_fields` ttif, `tiki_tracker_fields` ttf
586				where ttif.`fieldId`=ttf.`fieldId` and `itemId`=? order by `position` asc";
587			$result2 = $this->fetchAll($query2, [(int) $res["itemId"]]);
588			$pass = true;
589			$kx = "";
590			foreach ($result2 as $res2) {
591				// Check if the field is visible!
592				$fieldId = $res2["fieldId"];
593				if (count($filters) > 0) {
594					if (isset($filters[$fieldId]["value"]) and $filters[$fieldId]["value"]) {
595						if (in_array($filters[$fieldId]["type"], ['a', 't'])) {
596							if (! stristr($res2["value"], $filters[$fieldId]["value"])) {
597								$pass = false;
598							}
599						} else {
600							if (strtolower($res2["value"]) != strtolower($filters[$fieldId]["value"])) {
601								$pass = false;
602							}
603						}
604					}
605					if (preg_replace("/[^a-zA-Z0-9]/", "", $res2["name"]) == $csort_mode) {
606						$kx = $res2["value"] . $itid;
607					}
608				}
609				$fields[] = $res2;
610			}
611			$res["field_values"] = $fields;
612			$res["comments"] = $this->table('tiki_comments')->fetchCount(['object' => (int) $itid, 'objectType' => 'trackeritem']);
613			if ($pass) {
614				$kl = $kx . $itid;
615				$ret["$kl"] = $res;
616			}
617		}
618		ksort($ret);
619		//$ret=$this->sort_items_by_condition($ret,$sort_mode);
620		$retval = [];
621		$retval["data"] = array_values($ret);
622		$retval["cant"] = $cant;
623		return $retval;
624	}
625
626	/*shared*/
627	public function get_user_items($auser, $with_groups = true)
628	{
629		global $user;
630		$items = [];
631
632		$query = "select ttf.`trackerId`, tti.`itemId` from `tiki_tracker_fields` ttf, `tiki_tracker_items` tti, `tiki_tracker_item_fields` ttif";
633		$query .= " where ttf.`fieldId`=ttif.`fieldId` and ttif.`itemId`=tti.`itemId` and `type`=? and tti.`status`=? and `value`=?";
634		$result = $this->fetchAll($query, ['u','o',$auser]);
635		$ret = [];
636
637		$trackers = $this->table('tiki_trackers');
638		$trackerFields = $this->table('tiki_tracker_fields');
639		$trackerItemFields = $this->table('tiki_tracker_item_fields');
640		//FIXME Perm:filter ?
641		foreach ($result as $res) {
642			$itemObject = Tracker_Item::fromId($res['itemId']);
643			if (! $itemObject->canView()) {
644				continue;
645			}
646			$itemId = $res["itemId"];
647
648			$trackerId = $res["trackerId"];
649			// Now get the isMain field for this tracker
650			$fieldId = $trackerFields->fetchOne('fieldId', ['isMain' => 'y', 'trackerId' => (int) $trackerId]);
651			// Now get the field value
652			$value = $trackerItemFields->fetchOne('value', ['fieldId' => (int) $fieldId, 'itemId' => (int) $itemId]);
653			$tracker = $trackers->fetchOne('name', ['trackerId' => (int) $trackerId]);
654
655			$aux["trackerId"] = $trackerId;
656			$aux["itemId"] = $itemId;
657			$aux["value"] = $value;
658			$aux["name"] = $tracker;
659
660			if (! in_array($itemId, $items)) {
661				$ret[] = $aux;
662				$items[] = $itemId;
663			}
664		}
665
666		if ($with_groups) {
667			$groups = $this->get_user_groups($auser);
668
669			foreach ($groups as $group) {
670				$query = "select ttf.`trackerId`, tti.`itemId` from `tiki_tracker_fields` ttf, `tiki_tracker_items` tti, `tiki_tracker_item_fields` ttif ";
671				$query .= " where ttf.`fieldId`=ttif.`fieldId` and ttif.`itemId`=tti.`itemId` and `type`=? and tti.`status`=? and `value`=?";
672				$result = $this->fetchAll($query, ['g', 'o', $group]);
673
674				foreach ($result as $res) {
675					$itemId = $res["itemId"];
676
677					$trackerId = $res["trackerId"];
678					// Now get the isMain field for this tracker
679					$fieldId = $trackerFields->fetchOne('fieldId', ['isMain' => 'y', 'trackerId' => (int)$trackerId]);
680					// Now get the field value
681					$value = $trackerItemFields->fetchOne('value', ['fieldId' => (int)$fieldId, 'itemId' => (int)$itemId]);
682					$tracker = $trackers->fetchOne('name', ['trackerId' => (int)$trackerId]);
683
684					$aux["trackerId"] = $trackerId;
685					$aux["itemId"] = $itemId;
686					$aux["value"] = $value;
687					$aux["name"] = $tracker;
688
689					if (! in_array($itemId, $items)) {
690						$ret[] = $aux;
691						$items[] = $itemId;
692					}
693				}
694			}
695		}
696		return $ret;
697	}
698
699	/* experimental shared */
700	public function get_items_list($trackerId, $fieldId, $value, $status = 'o', $multiple = false, $sortFieldIds = null)
701	{
702		static $cache = [];
703		$cacheKey = implode('.', [
704			$trackerId, $fieldId, $value, $status, $multiple,
705			is_array($sortFieldIds) ? implode($sortFieldIds) : $sortFieldIds
706		]);
707		if (isset($cache[$cacheKey])) {
708			return $cache[$cacheKey];
709		}
710		$query = "select distinct tti.`itemId`, tti.`itemId` i from `tiki_tracker_items` tti, `tiki_tracker_item_fields` ttif ";
711		$bindvars = [];
712		if (is_string($sortFieldIds)) {
713			$sortFieldIds = preg_split('/\|/', $sortFieldIds, -1, PREG_SPLIT_NO_EMPTY);
714		}
715		if (! empty($sortFieldIds)) {
716			foreach ($sortFieldIds as $i => $sortFieldId) {
717				$query .= " left join `tiki_tracker_item_fields` ttif$i on ttif.`itemId` = ttif$i.`itemId` and ttif$i.`fieldId` = ?";
718				$bindvars[] = (int)$sortFieldId;
719			}
720		}
721		$query .= " where tti.`itemId`=ttif.`itemId` and ttif.`fieldId`=?";
722		$bindvars[] = (int)$fieldId;
723		if ($multiple) {
724			$query .= " and ttif.`value` REGEXP CONCAT('[[:<:]]', ?, '[[:>:]]')";
725		} else {
726			$query .= " and ttif.`value`=?";
727		}
728		$bindvars[] = $value;
729		if (! empty($status)) {
730			$query .= ' and ' . $this->in('tti.status', str_split($status, 1), $bindvars);
731		}
732		if (! empty($sortFieldIds)) {
733			$query .= " order by " . implode(
734				',',
735				array_map(
736					function ($i) {
737						return "ttif$i.value";
738					},
739					array_keys($sortFieldIds)
740				)
741			);
742		}
743		$items = $this->fetchAll($query, $bindvars);
744		$items = array_map(
745			function ($row) {
746				return $row['itemId'];
747			},
748			$items
749		);
750		if (TikiLib::lib('tiki')->get_memory_avail() < 1048576 * 10) {
751			$cache = [];
752		}
753		$cache[$cacheKey] = $items;
754		return $items;
755	}
756
757	public function get_tracker($trackerId)
758	{
759		return $this->table('tiki_trackers')->fetchFullRow(['trackerId' => (int) $trackerId]);
760	}
761
762	public function get_field_info($fieldId)
763	{
764		return $this->table('tiki_tracker_fields')->fetchFullRow(['fieldId' => (int) $fieldId]);
765	}
766
767	/**
768	 * Marks fields as empty
769	 * @param array $fields
770	 * @return array
771	 */
772	public function mark_fields_as_empty($fields)
773	{
774		$lastHeader = -1;
775		$elemSinceLastHeader = 0;
776		foreach ($fields as $key => $trac) {
777			if (! (empty($trac['value']) && empty($trac['cat'])
778					&& empty($trac['links']) && $trac['type'] != 's'
779					&& $trac['type'] != 'STARS' && $trac['type'] != 'h'
780					&& $trac['type'] != 'l' && $trac['type'] != 'W')
781					&& ! ($trac['options_array'][0] == 'password' && $trac['type'] == 'p')) {
782				if ($trac['type'] == 'h') {
783					if ($lastHeader > 0 && $elemSinceLastHeader == 0) {
784						$fields[$lastHeader]['field_is_empty'] = true;
785					}
786					$lastHeader = $key;
787					$elemSinceLastHeader = 0;
788				} else {
789					$elemSinceLastHeader++;
790				}
791				// this has a value
792				continue;
793			}
794			$fields[$key]['field_is_empty'] = true;
795		}
796		if ($lastHeader > 0 && $elemSinceLastHeader == 0) {
797			$fields[$lastHeader]['field_is_empty'] = true;
798		}
799		return $fields;
800	}
801
802	// includePermissions: Include the permissions of each tracker in its element's "permissions" subelement
803	public function list_trackers($offset = 0, $maxRecords = -1, $sort_mode = 'name_asc', $find = '', $includePermissions = false)
804	{
805		$categlib = TikiLib::lib('categ');
806		$join = '';
807		$where = '';
808		$bindvars = [];
809		if ($jail = $categlib->get_jail()) {
810			$categlib->getSqlJoin($jail, 'tracker', '`tiki_trackers`.`trackerId`', $join, $where, $bindvars);
811		}
812		if ($find) {
813			$findesc = '%' . $find . '%';
814			$where .= ' and (`tiki_trackers`.`name` like ? or `tiki_trackers`.`description` like ?)';
815			$bindvars = array_merge($bindvars, [$findesc, $findesc]);
816		}
817		$query = "select * from `tiki_trackers` $join where 1=1 $where order by `tiki_trackers`." . $this->convertSortMode($sort_mode);
818		$query_cant = "select count(*) from `tiki_trackers` $join where 1=1 $where";
819		$result = $this->fetchAll($query, $bindvars, $maxRecords, $offset);
820		$cant = $this->getOne($query_cant, $bindvars);
821		$ret = [];
822		$list = [];
823		//FIXME Perm:filter ?
824		foreach ($result as $res) {
825			global $user;
826			$add = $this->user_has_perm_on_object($user, $res['trackerId'], 'tracker', 'tiki_p_view_trackers');
827			if ($add) {
828				if ($includePermissions) {
829					$res['permissions'] = Perms::get('tracker', $res['trackerId']);
830				}
831				$ret[] = $res;
832				$list[$res['trackerId']] = $res['name'];
833			}
834		}
835		$retval = [];
836		$retval["list"] = $list;
837		$retval["data"] = $ret;
838		$retval["cant"] = $cant;
839		return $retval;
840	}
841
842	// This function gets the prefix alias page name e.g. Org:230 for the pretty tracker
843	// wiki page corresponding to a tracker item (230 in the example) using prefix aliases
844	// Returns false if no such page is found.
845	public function get_trackeritem_pagealias($itemId)
846	{
847		global $prefs;
848		$trackerId = $this->table('tiki_tracker_items')->fetchOne('trackerId', ['itemId' => $itemId]);
849
850		$semanticlib = TikiLib::lib('semantic');
851		$t_links = $semanticlib->getLinksUsing('trackerid', ['toPage' => $trackerId]);
852
853		if (count($t_links)) {
854			if ($prefs['feature_multilingual'] == 'y' && count($t_links) > 1) {
855				foreach ($t_links as $t) {
856					if ($prefs['language'] == TikiLib::lib('multilingual')->getLangOfPage($t['fromPage'])) {
857						$target = $t['fromPage'];
858						break;
859					}
860				}
861			} else {
862				$target = $t_links[0]['fromPage'];
863			}
864
865			$p_links = $semanticlib->getLinksUsing('prefixalias', ['fromPage' => $target]);
866			if (count($p_links)) {
867				$ret = $p_links[0]['toPage'] . $itemId;
868				return $ret;
869			} else {
870				return false;
871			}
872		} else {
873			return false;
874		}
875	}
876
877	public function concat_item_from_fieldslist($trackerId, $itemId, $fieldsId, $status = 'o', $separator = ' ', $list_mode = '', $strip_tags = false, $format = '', $item = [])
878	{
879		$res = '';
880		$values = [];
881		if (is_string($fieldsId)) {
882			$fieldsId = preg_split('/\|/', $fieldsId, -1, PREG_SPLIT_NO_EMPTY);
883		}
884		$definition = Tracker_Definition::get($trackerId);
885		if ($definition) {
886			foreach ($fieldsId as $k => $field) {
887				$myfield = $definition->getField($field);
888
889				$myfield['value'] = $this->get_item_value(
890					$trackerId, $itemId, $field
891				);
892				if (! isset($item['itemId'])) {
893					$item['itemId'] = $itemId;
894				}
895				$value = trim($this->field_render_value(
896					['field' => $myfield, 'process' => 'y', 'list_mode' => $list_mode, 'item' => $item])
897				);
898
899				if ($format) {
900					$values[] = $value;
901				} else {
902					if ($k > 0) {
903						$res .= $separator;
904					}
905					$res .= $value;
906				}
907			}
908			if ($format) {
909				// use the underlying translation function to replace the %0 etc placeholders (and translate if necessary)
910				$res = tra($format, '', false, $values);
911			}
912			if ($strip_tags) {
913				$res = strip_tags($res);
914			}
915		} else {
916			Feedback::error(tr('Tracker %0 not found for Field %1', $trackerId, implode(',', $fieldsId)));
917		}
918		return $res;
919	}
920
921	public function concat_all_items_from_fieldslist($trackerId, $fieldsId, $status = 'o', $separator = ' ', $strip_tags = false)
922	{
923		if (is_string($fieldsId)) {
924			$fieldsId = preg_split('/\|/', $fieldsId, -1, PREG_SPLIT_NO_EMPTY);
925		}
926		$res = [];
927		$definition = Tracker_Definition::get($trackerId);
928		foreach ($fieldsId as $field) {
929			if ($myfield = $definition->getField($field)) {
930				$is_date = ($myfield['type'] == 'f');
931				$is_trackerlink = ($myfield['type'] == 'r');
932				$tmp = $this->get_all_items($trackerId, $field, $status);
933				$options = $myfield['options_map'];
934				foreach ($tmp as $key => $value) {
935					if ($is_date) {
936						$value = $this->date_format("%e/%m/%y", $value);
937					}
938					if ($is_trackerlink && $options['displayFieldsList'] && ! empty($options['displayFieldsList'][0])) {
939						$item = $this->get_tracker_item($key);
940						$itemId = $item[$field];
941						$value = $this->concat_item_from_fieldslist($options['trackerId'], $itemId, $options['displayFieldsList'], $status, $separator, '', $strip_tags);
942					}
943					if (! empty($res[$key])) {
944						$res[$key] .= $separator . $value;
945					} else {
946						$res[$key] = $value;
947					}
948				}
949			}
950		}
951		return $res;
952	}
953
954	public function get_fields_from_fieldslist($trackerId, $fieldsId)
955	{
956		if (is_string($fieldsId)) {
957			$fieldsId = preg_split('/\|/', $fieldsId, -1, PREG_SPLIT_NO_EMPTY);
958		}
959		$res = [];
960		$definition = Tracker_Definition::get($trackerId);
961		foreach ($fieldsId as $field) {
962			if ($myfield = $definition->getField($field)) {
963				$res[$field] = $myfield['permName'];
964			}
965		}
966		return $res;
967	}
968
969
970	public function valid_status($status)
971	{
972		return in_array($status, ['o', 'c', 'p', 'op', 'oc', 'pc', 'opc']);
973	}
974
975
976	/**
977	 * Gets an array of itemId => rendered value for a certain field for use in ItemLinks (mainly)
978	 *
979	 * @param int $trackerId
980	 * @param int $fieldId
981	 * @param string $status
982	 * @return array
983	 */
984	public function get_all_items($trackerId, $fieldId, $status = 'o')
985	{
986		global $prefs, $user;
987		$cachelib = TikiLib::lib('cache');
988
989		if (! $trackerId) {
990			return [tr('*** ERROR: Tracker ID not set ***', $fieldId)];
991		}
992		if (! $fieldId) {
993			return [tr('*** ERROR: Field ID not set ***', $fieldId)];
994		}
995
996		$definition = Tracker_Definition::get($trackerId);
997		if (! $definition) {
998			// could be a deleted field referred to by a list type field
999			return [tr('*** ERROR: Tracker %0 not found ***', $trackerId)];
1000		}
1001		$field = $definition->getField($fieldId);
1002
1003		if (! $field) {
1004			// could be a deleted field referred to by a list type field
1005			return [tr('*** ERROR: Field %0 not found ***', $fieldId)];
1006		}
1007
1008		$jail = '';
1009		if ($prefs['feature_categories'] == 'y') {
1010			$categlib = TikiLib::lib('categ');
1011			$jail = $categlib->get_jail();
1012		}
1013
1014		$sort_mode = "value_asc";
1015		$cacheKey = 'trackerfield' . $fieldId . $status . $user;
1016		if ($this->is_multilingual($fieldId) == 'y') {
1017			$cacheKey .= $prefs['language'];
1018		}
1019		if (! empty($jail)) {
1020			$cacheKey .= serialize($jail);
1021		}
1022
1023		$cacheKey = md5($cacheKey);
1024
1025		if (( ! $ret = $cachelib->getSerialized($cacheKey) ) || ! $this->valid_status($status)) {
1026			$sts = preg_split('//', $status, -1, PREG_SPLIT_NO_EMPTY);
1027			$mid = " (" . implode('=? or ', array_fill(0, count($sts), 'tti.`status`')) . "=?) ";
1028			$fieldIdArray = preg_split('/\|/', $fieldId, -1, PREG_SPLIT_NO_EMPTY);
1029			$mid .= " and (" . implode('=? or ', array_fill(0, count($fieldIdArray), 'ttif.`fieldId`')) . "=?) ";
1030			$bindvars = array_merge($sts, $fieldIdArray);
1031			$join = '';
1032			if (! empty($jail)) {
1033				$categlib->getSqlJoin($jail, 'trackeritem', 'tti.`itemId`', $join, $mid, $bindvars);
1034			}
1035			$query = "select ttif.`itemId` , ttif.`value` FROM `tiki_tracker_items` tti,`tiki_tracker_item_fields` ttif $join ";
1036			$query .= " WHERE $mid and tti.`itemId` = ttif.`itemId` order by " . $this->convertSortMode($sort_mode);
1037			$items = $this->fetchAll($query, $bindvars);
1038			Perms::bulk(['type' => 'trackeritem', 'parentId' => $trackerId], 'object', array_map(function ($res) {
1039				return $res['itemId'];
1040			}, $items));
1041			$ret = [];
1042			foreach ($items as $res) {
1043				$itemId = $res['itemId'];
1044				$itemObject = Tracker_Item::fromId($itemId);
1045				if (! $itemObject) {
1046					Feedback::error(tr('TrackerLib::get_all_items: No item for itemId %0', $itemId));
1047				} elseif ($itemObject->canView()) {
1048					$ret[] = $res;
1049				}
1050			}
1051			$cachelib->cacheItem($cacheKey, serialize($ret));
1052		}
1053
1054		$ret2 = [];
1055		foreach ($ret as $res) {
1056			$itemId = $res['itemId'];
1057			$field['value'] = $res['value'];
1058			$rendered = $this->field_render_value(['field' => $field, 'process' => 'y']);
1059			$ret2[$itemId] = trim(strip_tags($rendered), " \t\n\r\0\x0B\xC2\xA0");
1060		}
1061		return $ret2;
1062	}
1063
1064	public function need_to_check_categ_perms($allfields = '')
1065	{
1066		global $prefs;
1067		if ($allfields === false) {
1068			// use for itemlink field - otherwise will be too slow
1069			return false;
1070		}
1071		$needToCheckCategPerms = false;
1072		if ($prefs['feature_categories'] == 'y') {
1073			$categlib = TikiLib::lib('categ');
1074			if (empty($allfields['data'])) {
1075				$needToCheckCategPerms = true;
1076			} else {
1077				foreach ($allfields['data'] as $f) {
1078					if ($f['type'] == 'e') {
1079						$needToCheckCategPerms = true;
1080						break;
1081					}
1082				}
1083			}
1084		}
1085		return $needToCheckCategPerms;
1086	}
1087
1088	public function get_all_tracker_items($trackerId)
1089	{
1090		return $this->items()->fetchColumn('itemId', ['trackerId' => (int) $trackerId]);
1091	}
1092
1093	public function getSqlStatus($status, &$mid, &$bindvars, $trackerId, $skip_status_perm_check = false)
1094	{
1095		global $user;
1096		if (is_array($status)) {
1097			$status = implode('', $status);
1098		}
1099
1100		// Check perms
1101		if (! $skip_status_perm_check && $status && ! $this->user_has_perm_on_object($user, $trackerId, 'tracker', 'tiki_p_view_trackers_pending') && ! $this->group_creator_has_perm($trackerId, 'tiki_p_view_trackers_pending')) {
1102			$status = str_replace('p', '', $status);
1103		}
1104		if (! $skip_status_perm_check && $status && ! $this->user_has_perm_on_object($user, $trackerId, 'tracker', 'tiki_p_view_trackers_closed') && ! $this->group_creator_has_perm($trackerId, 'tiki_p_view_trackers_closed')) {
1105			$status = str_replace('c', '', $status);
1106		}
1107
1108		if (! $status) {
1109			return false;
1110		} elseif ($status == 'opc') {
1111				return true;
1112		} elseif (strlen($status) > 1) {
1113			$sts = preg_split('//', $status, -1, PREG_SPLIT_NO_EMPTY);
1114			if (count($sts)) {
1115				$mid .= " and (" . implode('=? or ', array_fill(0, count($sts), '`status`')) . "=?) ";
1116				$bindvars = array_merge($bindvars, $sts);
1117			}
1118		} else {
1119			$mid .= " and tti.`status`=? ";
1120			$bindvars[] = $status;
1121		}
1122		return true;
1123	}
1124
1125	public function group_creator_has_perm($trackerId, $perm)
1126	{
1127		global $prefs;
1128		$definition = Tracker_Definition::get($trackerId);
1129		if ($definition && $groupCreatorFieldId = $definition->getWriterGroupField()) {
1130			$tracker_info = $definition->getInformation();
1131			$perms = $this->get_special_group_tracker_perm($tracker_info);
1132			return empty($perms[$perm]) ? false : true;
1133		} else {
1134			return false;
1135		}
1136	}
1137
1138	/* group creator perms can only add perms,they can not take away perm
1139	   and they are only used if tiki_p_view_trackers is not set for the tracker and if the tracker ha a group creator field
1140	   must always be combined with a filter on the groups
1141	*/
1142	public function get_special_group_tracker_perm($tracker_info, $global = false)
1143	{
1144		global $prefs;
1145		$userlib = TikiLib::lib('user');
1146		$smarty = TikiLib::lib('smarty');
1147		$ret = [];
1148		$perms = $userlib->get_object_permissions($tracker_info['trackerId'], 'tracker', $prefs['trackerCreatorGroupName']);
1149		foreach ($perms as $perm) {
1150			$ret[$perm['permName']] = 'y';
1151			if ($global) {
1152				$p = $perm['permName'];
1153				global $$p;
1154				$$p = 'y';
1155				$smarty->assign("$p", 'y');
1156			}
1157		}
1158		if ($tracker_info['writerGroupCanModify'] == 'y') {
1159			// old configuration
1160			$ret['tiki_p_modify_tracker_items'] = 'y';
1161			if ($global) {
1162				$tiki_p_modify_tracker_items = 'y';
1163				$smarty->assign('tiki_p_modify_tracker_items', 'y');
1164			}
1165		}
1166		return $ret;
1167	}
1168
1169	/* to filter filterfield is an array of fieldIds
1170	 * and the value of each field is either filtervalue or exactvalue
1171	 * ex: filterfield=array('1','2', 'sqlsearch'=>array('3', '4'), '5')
1172	 * ex: filtervalue=array(array('this', '*that'), '')
1173	 * ex: exactvalue= array('', array('there', 'those'), 'these', array('>'=>10))
1174	 * will filter items with fielId 1 with a value %this% or %that, and fieldId 2 with the value there or those, and fieldId 3 or 4 containing these and fieldId 5 > 10
1175	 * listfields = array(fieldId=>array('type'=>, 'name'=>...), ...)
1176	 * allfields is only for performance issue - check if one field is a category
1177	 */
1178	public function list_items($trackerId, $offset = 0, $maxRecords = -1, $sort_mode = '', $listfields = '', $filterfield = '', $filtervalue = '', $status = '', $initial = '', $exactvalue = '', $filter = '', $allfields = null, $skip_status_perm_check = false, $skip_permission_check = false)
1179	{
1180		//echo '<pre>FILTERFIELD:'; print_r($filterfield); echo '<br />FILTERVALUE:';print_r($filtervalue); echo '<br />EXACTVALUE:'; print_r($exactvalue); echo '<br />STATUS:'; print_r($status); echo '<br />FILTER:'; print_r($filter); /*echo '<br />LISTFIELDS'; print_r($listfields);*/ echo '</pre>';
1181		global $prefs;
1182
1183		$cat_table = '';
1184		$sort_tables = '';
1185		$sort_join_clauses = '';
1186		$csort_mode = '';
1187		$corder = '';
1188		$trackerId = (int) $trackerId;
1189		$numsort = false;
1190
1191		$mid = ' WHERE tti.`trackerId` = ? ';
1192		$bindvars = [$trackerId];
1193		$join = '';
1194
1195		if (! empty($filter)) {
1196			$mid2 = [];
1197			if (! empty($filter['comment'])) {
1198				$cat_table .= ' LEFT JOIN `tiki_comments` tc ON tc.`object` = tti.`itemId` AND tc.`objectType` = "trackeritem"';
1199				$mid2[] = '(tc.`title` LIKE ? OR tc.`data` LIKE ?)';
1200				$bindvars[] = '%' . $filter['comment'] . '%';
1201				$bindvars[] = '%' . $filter['comment'] . '%';
1202				unset($filter['comment']);
1203			}
1204			$this->parse_filter($filter, $mid2, $bindvars);
1205			if (! empty($mid2)) {
1206				$mid .= ' AND ' . implode(' AND ', $mid2);
1207			}
1208		}
1209
1210		if (! $this->getSqlStatus($status, $mid, $bindvars, $trackerId, $skip_status_perm_check) && ! $skip_status_perm_check && $status) {
1211			return ['cant' => 0, 'data' => ''];
1212		}
1213		if (substr($sort_mode, 0, 2) == 'f_') {
1214			list($a, $asort_mode, $corder) = preg_split('/_/', $sort_mode);
1215		}
1216		if ($initial) {
1217			$mid .= ' AND ttif.`value` LIKE ?';
1218			$bindvars[] = $initial . '%';
1219			if (isset($asort_mode)) {
1220				$mid .= ' AND ttif.`fieldId` = ?';
1221				$bindvars[] = $asort_mode;
1222			}
1223		}
1224		if (! $sort_mode) {
1225			$sort_mode = 'lastModif_desc';
1226		}
1227
1228		if (substr($sort_mode, 0, 2) == 'f_' or ! empty($filterfield)) {
1229			if (substr($sort_mode, 0, 2) == 'f_') {
1230				$csort_mode = 'sttif.`value` ';
1231				$sort_tables = ' LEFT JOIN (`tiki_tracker_item_fields` sttif)'
1232					. ' ON (tti.`itemId` = sttif.`itemId`'
1233					. (! empty($asort_mode) ? " AND sttif.`fieldId` = $asort_mode" : '')
1234					. ')';
1235				// Do we need a numerical sort on the field ?
1236				$field = $this->get_tracker_field($asort_mode);
1237				switch ($field['type']) {
1238					case 'C':
1239					case '*':
1240					case 'q':
1241					case 'n':
1242					case 'f':	// DateTime
1243					case 'j':	// JsCalendar
1244					case 'CAL':	// CalendarItem
1245						$numsort = true;
1246						break;
1247					case 'l':
1248						// Do nothing, value is dynamic and thus cannot be sorted on
1249						$csort_mode = 1;
1250						$csort_tables = '';
1251						break;
1252					case 'r':
1253						$link_field = (int)$field['fieldId'];
1254						$remote_field = (int)$field['options_array'][1];
1255						$sort_tables = '
1256							LEFT JOIN `tiki_tracker_item_fields` itemlink ON tti.itemId = itemlink.itemId AND itemlink.fieldId = ' . $link_field . '
1257							LEFT JOIN `tiki_tracker_item_fields` sttif ON itemlink.value = sttif.itemId AND sttif.fieldId = ' . $remote_field . '
1258						';
1259						break;
1260					case 's':
1261//						if ($field['name'] == 'Rating' || $field['name'] == tra('Rating')) { // No need to have that string, isn't it? Admins can replace for a more suited string in their use case
1262							$numsort = true;
1263//						}
1264						break;
1265				}
1266			} else {
1267				list($csort_mode, $corder) = preg_split('/_/', $sort_mode);
1268				$csort_mode = 'tti.`' . $csort_mode . '` ';
1269			}
1270
1271			if (empty($filterfield)) {
1272				$nb_filtered_fields = 0;
1273			} elseif (! is_array($filterfield)) {
1274				$fv = $filtervalue;
1275				$ev = $exactvalue;
1276				$ff = (int) $filterfield;
1277				$nb_filtered_fields = 1;
1278			} else {
1279				$nb_filtered_fields = count($filterfield);
1280			}
1281
1282			$last = 0;
1283			for ($i = 0; $i < $nb_filtered_fields; $i++) {
1284				if (is_array($filterfield)) {
1285					//multiple filter on an exact value or a like value - each value can be simple or an array
1286					$ff = (int) $filterfield[$i];
1287					$ff_array = $filterfield[$i]; // Need value as array used below
1288					$ev = ! empty($exactvalue[$i]) ? $exactvalue[$i] : null;
1289					$fv = ! empty($filtervalue[$i]) ? $filtervalue[$i] : null;
1290				}
1291				$filter = $this->get_tracker_field($ff);
1292
1293				// Determine if field is an item list field and postpone filtering till later if so
1294				if ($filter["type"] == 'l' && isset($filter['options_array'][2]) && isset($filter['options_array'][2]) && isset($filter['options_array'][3])) {
1295					$linkfilter[] = ['filterfield' => $ff, 'exactvalue' => $ev, 'filtervalue' => $fv];
1296					continue;
1297				}
1298
1299				$value = empty($fv) ? $ev : $fv;
1300				$search_for_blank = ( is_null($ev) && is_null($fv) )
1301					|| ( is_array($value) && count($value) == 1
1302						&& ( empty($value[0])
1303							|| ( is_array($value[0]) && count($value[0]) == 1 && empty($value[0][0]) )
1304						)
1305					);
1306
1307				$cat_table .= ' ' . ( $search_for_blank ? 'LEFT' : 'INNER' ) . " JOIN `tiki_tracker_item_fields` ttif$i ON ttif$i.`itemId` = tti.`itemId`";
1308				$last++;
1309
1310				if (isset($ff_array['sqlsearch']) && is_array($ff_array['sqlsearch'])) {
1311					$mid .= " AND ttif$i.`fieldId` in (" . implode(',', array_fill(0, count($ff_array['sqlsearch']), '?')) . ')';
1312					$bindvars = array_merge($bindvars, $ff_array['sqlsearch']);
1313				} elseif (isset($ff_array['usersearch']) && is_array($ff_array['usersearch'])) {
1314					$mid .= " AND ttif$i.`fieldId` in (" . implode(',', array_fill(0, count($ff_array['usersearch']), '?')) . ')';
1315					$bindvars = array_merge($bindvars, $ff_array['usersearch']);
1316				} elseif ($ff) {
1317					if ($search_for_blank) {
1318						$cat_table .= " AND ttif$i.`fieldId` = " . (int)$ff;
1319					} else {
1320						$mid .= " AND ttif$i.`fieldId`=? ";
1321						$bindvars[] = $ff;
1322					}
1323				}
1324
1325				if ($filter['type'] == 'e' && $prefs['feature_categories'] == 'y' && (! empty($ev) || ! empty($fv))) {
1326					//category
1327
1328					$value = empty($fv) ? $ev : $fv;
1329					if (! is_array($value) && $value != '') {
1330						$value = [$value];
1331						$not = '';
1332					} elseif (is_array($value) && array_key_exists('not', $value)) {
1333						$value = [$value['not']];
1334						$not = 'not';
1335					}
1336					if (empty($not) && count($value) == 1 && ( empty($value[0]) || ( is_array($value[0]) && count($value[0]) == 1 && empty($value[0][0]) ) )) {
1337						$cat_table .= " left JOIN `tiki_objects` tob$ff ON (tob$ff.`itemId` = tti.`itemId` AND tob$ff.`type` = 'trackeritem')"
1338							. " left JOIN `tiki_category_objects` tco$ff ON (tob$ff.`objectId` = tco$ff.`catObjectId`)";
1339						$mid .= " AND tco$ff.`categId` IS NULL ";
1340						continue;
1341					}
1342					if (empty($not)) {
1343						$cat_table .= " INNER JOIN `tiki_objects` tob$ff ON (tob$ff.`itemId` = tti.`itemId`)"
1344							. " INNER JOIN `tiki_category_objects` tco$ff ON (tob$ff.`objectId` = tco$ff.`catObjectId`)";
1345						$mid .= " AND tob$ff.`type` = 'trackeritem' AND tco$ff.`categId` IN ( ";
1346					} else {
1347						$cat_table .= " left JOIN `tiki_objects` tob$ff ON (tob$ff.`itemId` = tti.`itemId`)"
1348							. " left JOIN `tiki_category_objects` tco$ff ON (tob$ff.`objectId` = tco$ff.`catObjectId`)";
1349						$mid .= " AND tob$ff.`type` = 'trackeritem' AND tco$ff.`categId` NOT IN ( ";
1350					}
1351					$first = true;
1352					foreach ($value as $k => $catId) {
1353						if (is_array($catId)) {
1354							// this is a grouped AND logic for optimization indicated by the value being array
1355							$innerfirst = true;
1356							foreach ($catId as $c) {
1357								if (is_array($c)) {
1358									$innerfirst = true;
1359									foreach ($c as $d) {
1360										$bindvars[] = $d;
1361										if ($innerfirst) {
1362											$innerfirst = false;
1363										} else {
1364											$mid .= ',';
1365										}
1366										$mid .= '?';
1367									}
1368								} else {
1369									$bindvars[] = $c;
1370									$mid .= '?';
1371								}
1372							}
1373							if ($k < count($value) - 1) {
1374								$mid .= " ) AND ";
1375								if (empty($not)) {
1376									$ff2 = $ff . '_' . $k;
1377									$cat_table .= " INNER JOIN `tiki_category_objects` tco$ff2 ON (tob$ff.`objectId` = tco$ff2.`catObjectId`)";
1378									$mid .= "tco$ff2.`categId` IN ( ";
1379								} else {
1380									$ff2 = $ff . '_' . $k;
1381									$cat_table .= " left JOIN `tiki_category_objects` tco$ff2 ON (tob$ff.`objectId` = tco$ff2.`catObjectId`)";
1382									$mid .= "tco$ff2.`categId` NOT IN ( ";
1383								}
1384							}
1385						} else {
1386							$bindvars[] = $catId;
1387							if ($first) {
1388								$first = false;
1389							} else {
1390								$mid .= ',';
1391							}
1392							$mid .= '?';
1393						}
1394					}
1395					$mid .= " ) ";
1396					if (! empty($not)) {
1397						$mid .= " OR tco$ff.`categId` IS NULL ";
1398					}
1399				} elseif ($filter['type'] == 'usergroups') {
1400					$definition = Tracker_Definition::get($trackerId);
1401					$userFieldId = $definition->getUserField();
1402					$cat_table .= " INNER JOIN `tiki_tracker_item_fields` ttifu ON (tti.`itemId`=ttifu.`itemId`) INNER JOIN `users_users` uu ON ttifu.`value` REGEXP CONCAT('[[:<:]]', uu.`login`, '[[:>:]]') INNER JOIN `users_usergroups` uug ON (uug.`userId`=uu.`userId`)";
1403					$mid .= ' AND ttifu.`fieldId`=? AND uug.`groupName`=? ';
1404					$bindvars[] = $userFieldId;
1405					$bindvars[] = empty($ev) ? $fv : $ev;
1406				} elseif ($filter['type'] == 'u' && $ev > '') { // user selector and exact value
1407					if (is_array($ev)) {
1408						$keys = array_keys($ev);
1409						if ($keys[0] === 'not') {
1410							$mid .= " AND ( ttif$i.`value` NOT REGEXP " . implode(' OR ttif$i.`value` NOT REGEXP ', array_fill(0, count($ev), '?')) . " OR ttif$i.`value` IS NULL )";
1411						} else {
1412							$mid .= " AND ( ttif$i.`value` REGEXP " . implode(' OR ttif$i.`value` REGEXP ', array_fill(0, count($ev), '?')) . " )";
1413						}
1414						$bindvars = array_merge(
1415							$bindvars,
1416							array_values(array_map(function ($ev) {
1417								return "[[:<:]]{$ev}[[:>:]]";
1418							}, $ev))
1419						);
1420					} else {
1421						$mid .= " AND ttif$i.`value` REGEXP ? ";
1422						$bindvars[] = "[[:<:]]{$ev}[[:>:]]";
1423					}
1424				} elseif ($filter['type'] == '*') { // star
1425					$mid .= " AND ttif$i.`value`*1>=? ";
1426					$bindvars[] = $ev;
1427					if (($j = array_search($ev, $filter['options_array'])) !== false && $j + 1 < count($filter['options_array'])) {
1428						$mid .= " AND ttif$i.`value`*1<? ";
1429						$bindvars[] = $filter['options_array'][$j + 1];
1430					}
1431				} elseif ($filter['type'] == 'r' && ($fv || $ev)) {
1432					$cv = $fv ? $fv : $ev;
1433
1434					$cat_table .= " LEFT JOIN tiki_tracker_item_fields ttif{$i}_remote ON ttif$i.`value` = ttif{$i}_remote.`itemId` AND ttif{$i}_remote.`fieldId` = " . (int)$filter['options_array'][1] . ' ';
1435					if (is_numeric($cv)) {
1436						$mid .= " AND ( ttif{$i}_remote.`value` LIKE ? OR ttif$i.`value` = ? ) ";
1437						$bindvars[] = $ev ? $ev : "%$fv%";
1438						$bindvars[] = $cv;
1439					} else {
1440						$mid .= " AND ttif{$i}_remote.`value` LIKE ? ";
1441						$bindvars[] = $ev ? $ev : "%$fv%";
1442					}
1443				} elseif ($filter['type'] == 'REL' && ($fv || $ev)) {
1444					$rv = $ev ?: $fv;
1445					$options = explode("\n", $rv);
1446					foreach ($options as $option) {
1447						$mid .= " AND (ttif$i.`value` LIKE ? OR ttif$i.`value` LIKE ?)";
1448						$option = trim($option);
1449						$bindvars[] = "%$option";
1450						$bindvars[] = "%$option\n%";
1451					}
1452				} elseif ($ev > '') {
1453					if (is_array($ev)) {
1454						$keys = array_keys($ev);
1455						if (in_array((string) $keys[0], ['<', '>'])) {
1456							$mid .= " AND ttif$i.`value`" . $keys[0] . "? + 0";
1457							$bindvars[] = $ev[$keys[0]];
1458						} elseif (in_array((string) $keys[0], ['<=', '>='])) {
1459							$mid .= " AND (ttif$i.`value`" . $keys[0] . "? + 0 OR ttif$i.`value` = ?)";
1460							$bindvars[] = $ev[$keys[0]];
1461							$bindvars[] = $ev[$keys[0]];
1462						} elseif ($keys[0] === 'not') {
1463							$mid .= " AND ( ttif$i.`value` not in (" . implode(',', array_fill(0, count($ev), '?')) . ") OR ttif$i.`value` IS NULL )";
1464							$bindvars = array_merge($bindvars, array_values($ev));
1465						} else {
1466							$mid .= " AND ttif$i.`value` in (" . implode(',', array_fill(0, count($ev), '?')) . ")";
1467							$bindvars = array_merge($bindvars, array_values($ev));
1468						}
1469					} elseif (isset($ff_array['sqlsearch']) && is_array($ff_array['sqlsearch'])) {
1470						$mid .= " AND MATCH(ttif$i.`value`) AGAINST(? IN BOOLEAN MODE)";
1471						$bindvars[] = $ev;
1472					} elseif (isset($ff_array['usersearch']) && is_array($ff_array['usersearch'])) {
1473						$mid .= " AND ttif$i.`value` REGEXP ? ";
1474						$bindvars[] = "[[:<:]]{$ev}[[:>:]]";
1475					} else {
1476						$mid .= " AND ttif$i.`value`=? ";
1477						$bindvars[] = $ev == '' ? $fv : $ev;
1478					}
1479				} elseif ($fv > '') {
1480					if (! is_array($fv)) {
1481						$value = [$fv];
1482					} else {
1483						$value = $fv;
1484					}
1485					$mid .= ' AND(';
1486					$cpt = 0;
1487					foreach ($value as $v) {
1488						if ($cpt++) {
1489							$mid .= ' OR ';
1490						}
1491						$mid .= " upper(ttif$i.`value`) like upper(?) ";
1492						if (substr($v, 0, 1) == '*' || substr($v, 0, 1) == '%') {
1493							$bindvars[] = '%' . substr($v, 1);
1494						} elseif (substr($v, -1, 1) == '*' || substr($v, -1, 1) == '%') {
1495							$bindvars[] = substr($v, 0, strlen($v) - 1) . '%';
1496						} else {
1497							$bindvars[] = '%' . $v . '%';
1498						}
1499					}
1500					$mid .= ')';
1501				} elseif (is_null($ev) && is_null($fv)) { // test null value
1502					$mid .= " AND ( ttif$i.`value`=? OR ttif$i.`value` IS NULL )";
1503					$bindvars[] = '';
1504				}
1505			}
1506		} else {
1507			if (strpos($sort_mode, '_') !== false) {
1508				list($csort_mode, $corder) = preg_split('/_/', $sort_mode);
1509			} else {
1510				$csort_mode = $sort_mode;
1511				$corder = 'asc';
1512			}
1513			$csort_mode = "`" . $csort_mode . "`";
1514			if ($csort_mode == '`itemId`') {
1515				$csort_mode = 'tti.`itemId`';
1516				$numsort = true;
1517			}
1518			$sort_tables = '';
1519			$cat_tables = '';
1520		}
1521
1522		$categlib = TikiLib::lib('categ');
1523		if ($jail = $categlib->get_jail()) {
1524			$categlib->getSqlJoin($jail, 'trackeritem', 'tti.`itemId`', $join, $mid, $bindvars);
1525		}
1526
1527		$base_tables = '('
1528			. ' `tiki_tracker_items` tti'
1529			. ' INNER JOIN `tiki_tracker_item_fields` ttif ON tti.`itemId` = ttif.`itemId`'
1530			. ' INNER JOIN `tiki_tracker_fields` ttf ON ttf.`fieldId` = ttif.`fieldId`'
1531			. ')' . $join;
1532
1533		$fieldIds = [];
1534		foreach ($listfields as $k => $f) {
1535			if (isset($f['fieldId'])) {
1536				$fieldIds[] = $f['fieldId'];
1537			} else {
1538				$fieldIds[] = $k;	// sometimes filterfields are provided with the fieldId only on the array keys
1539			}
1540		}
1541		if (! empty($filterfield)) {
1542			// fix: could be that there is just one field. in this case it might be a scalar,
1543			// not an array due to not handle $filterfield proper somewhere else in the code
1544			if (! is_array($filterfield)) {
1545				$filterfield = [$filterfield];
1546			}
1547			foreach ($filterfield as $f) {
1548				if (! empty($f['sqlsearch'])) {
1549					foreach ($f['sqlsearch'] as $subf) {
1550						if (! in_array($subf, $fieldIds)) {
1551							$fieldIds[] = $subf;
1552						}
1553					}
1554				} elseif (! empty($f['usersearch'])) {
1555					foreach ($f['usersearch'] as $subf) {
1556						if (! in_array($subf, $fieldIds)) {
1557							$fieldIds[] = $subf;
1558						}
1559					}
1560				} else {
1561					if (! in_array($f, $fieldIds)) {
1562						$fieldIds[] = $f;
1563					}
1564				}
1565			}
1566		}
1567
1568		if (! empty($fieldIds)) {
1569			$mid .= ' AND ' . $this->in('ttif.fieldId', $fieldIds, $bindvars);
1570		}
1571
1572		if ($csort_mode == '`created`') {
1573			$csort_mode = 'tti.created';
1574		}
1575		$query = 'SELECT tti.*'
1576				. ', ' . ( ($numsort) ? "cast(max($csort_mode) as decimal)" : "max($csort_mode)") . ' as `sortvalue`'
1577			. ' FROM ' . $base_tables . $sort_tables . $cat_table
1578			. $mid
1579			. ' GROUP BY tti.`itemId`, tti.`trackerId`, tti.`created`, tti.`createdBy`, tti.`status`, tti.`lastModif`, tti.`lastModifBy`, ' . $csort_mode
1580			. ' ORDER BY ' . $this->convertSortMode('sortvalue_' . $corder);
1581		if ($numsort) {
1582			$query .= ',' . $this->convertSortMode($csort_mode);
1583		}
1584		//echo htmlentities($query); print_r($bindvars);
1585		$query_cant = 'SELECT count(DISTINCT ttif.`itemId`) FROM ' . $base_tables . $sort_tables . $cat_table . $mid;
1586
1587		// save the result
1588		$ret = [];
1589
1590		// Start loop to get the required number of items if permissions / filters are in use.
1591		// The problem: If $maxItems and $offset are given,
1592		// but the sql query returns items the user has no permissions or the filter criteria does not match,
1593		// then only a subset of what is available  would be returned.
1594
1595		// original requested number of items
1596		$maxRecordsRequested = $maxRecords;
1597		// original page (from pagination)
1598		$offsetRequested = $offset;
1599		// offset calculated on  $offsetRequested
1600		$currentOffset = 0;
1601		// set to true when we have enough records or no records left.
1602		$finished = false;
1603		// used internaly - one time query that returns the total number of records without taking into account filter or permissions
1604		$cant = $this->getOne($query_cant, $bindvars);
1605		// $cant will be modified bc its used otherwise. so save the totalCount value
1606		$totalCount = $cant;
1607		// total number of records read so far
1608		$currentCount = 0;
1609		// number of records in the result set
1610		$resultCount = 0;
1611
1612		// outer loop - grab more records bc it might be we must filter out records.
1613		// 300 seems to be ok, bc paganination offers this as well as the size of the resultset
1614		// NOTE: This value is important with respect to memory usage and performance - especially when lots of items (like 10k+) are in use.
1615		$maxRecords = 300;
1616		// offset used for sql query
1617		$offset = 0;
1618
1619		// optimize permission check - preload ownership fields to be able to quickly enforce canSeeOwn or wrtier group can modify permissions
1620		$definition = Tracker_Definition::get($trackerId);
1621		$ownershipFields = $definition->getItemOwnerFields();
1622		$groupOwnershipFields = $definition->getItemGroupOwnerFields();
1623		if ($groupField = $definition->getWriterGroupField()) {
1624			$groupOwnershipFields[] = $groupField;
1625		}
1626
1627		while (! $finished) {
1628			$ret1 = $this->fetchAll($query, $bindvars, $maxRecords, $offset);
1629			// add. security - should not be necessary bc of check at the end. no records left - end outer loop
1630			if (count($ret1) == 0) {
1631				$finished = true;
1632			}
1633
1634			if (! $skip_permission_check) {
1635				// preload permissions for all items to be checked
1636				Perms::bulk(['type' => 'trackeritem', 'parentId' => $trackerId], 'object', $ret1, 'itemId');
1637
1638				// preload ownership field values for all items to be checked
1639				$ownershipData = [];
1640				$table = $this->itemFields();
1641				$rows = $table->fetchAll(['itemId', 'fieldId', 'value'], [
1642					'itemId' => $table->in(array_map(function($row){ return $row['itemId']; }, $ret1)),
1643					'fieldId' => $table->in($ownershipFields)
1644				]);
1645				foreach ($rows as $row) {
1646					$ownershipData[$row['itemId']][$row['fieldId']] = $this->parse_user_field($row['value']);
1647				}
1648				$rows = $table->fetchAll(['itemId', 'fieldId', 'value'], [
1649					'itemId' => $table->in(array_map(function($row){ return $row['itemId']; }, $ret1)),
1650					'fieldId' => $table->in($groupOwnershipFields)
1651				]);
1652				foreach ($rows as $row) {
1653					$ownershipData[$row['itemId']][$row['fieldId']] = $row['value'];
1654				}
1655			}
1656
1657			foreach ($ret1 as $res) {
1658				$mem = TikiLib::lib('tiki')->get_memory_avail();
1659				if ($mem < 1048576 * 10) {	// Less than 10MB left?
1660					// post an error even though it doesn't get displayed when using export as the output goes into the output file
1661					Feedback::error(tr('Tracker list_items ran out of memory after %0 items.', count($ret)));
1662					break;
1663				}
1664
1665				if (! $skip_permission_check) {
1666					// this is needed by permission checking inside tracker item
1667					$res += $ownershipData[$res['itemId']] ?? [];
1668					$itemObject = Tracker_Item::fromInfo($res);
1669					if (! $itemObject->canView()) {
1670						$cant--;
1671						// skipped record bc of permissions - need to count for outer loop
1672						$currentCount++;
1673						continue;
1674					}
1675				}
1676
1677				$res['itemUsers'] = [];
1678				if ($listfields !== null) {
1679					$res['field_values'] = $this->get_item_fields($trackerId, $res['itemId'], $listfields, $res['itemUsers']);
1680				}
1681
1682				if (! empty($asort_mode)) {
1683					foreach ($res['field_values'] as $i => $field) {
1684						if ($field['fieldId'] == $asort_mode) {
1685							$kx = $field['value'] . '.' . $res['itemId'];
1686						}
1687					}
1688				}
1689				if (isset($linkfilter) && $linkfilter) {
1690					$filterout = false;
1691					// NOTE: This implies filterfield if is link field has to be in fields set
1692					foreach ($res['field_values'] as $i => $field) {
1693						foreach ($linkfilter as $lf) {
1694							if ($field['fieldId'] == $lf["filterfield"]) {
1695								// extra comma at the front and back of filtervalue to avoid ambiguity in partial match
1696								if ($lf["filtervalue"] && strpos(',' . implode(',', $field['items']) . ',', $lf["filtervalue"]) === false) {
1697									$filterout = true;
1698									break 2;
1699								} elseif ($lf["exactvalue"] && ! in_array($lf['exactvalue'], $field['items'])) {
1700									$filterout = true;
1701									break 2;
1702								}
1703							}
1704						}
1705					}
1706					if ($filterout) {
1707						$cant--;
1708						// skipped record bc of filter criteria - need to count for outer loop
1709						$currentCount++;
1710						continue;
1711					}
1712				}
1713
1714				$res['geolocation'] = TikiLib::lib('geo')->get_coordinates('trackeritem', $res['itemId']);
1715
1716				// have a field, adjust counter and check if we have enough items
1717				$currentCount++;
1718				$currentOffset++;
1719
1720				// field is stored in $res. See wether we can add it to the resultset, based on the requested offset
1721				if (($currentOffset > $offsetRequested)) {
1722					$resultCount++;
1723					if (empty($kx)) {
1724						// ex: if the sort field is non visible, $kx is null
1725						$ret[] = $res;
1726					} else {
1727						$ret[$kx] = $res;
1728					}
1729				}
1730
1731				if ($resultCount == $maxRecordsRequested) {
1732					$finished = true;
1733					break;
1734				}
1735			} // foreach
1736
1737			// are items left?
1738			if ($currentCount == $totalCount) {
1739				$finished = true;
1740			} else {
1741				$offset += $maxRecords;
1742			}
1743		} // while
1744
1745// End loop to get the required number of items if permissions / filters are in use
1746		$retval = [];
1747		$retval['data'] = array_values($ret);
1748		$retval['cant'] = $cant;
1749		return $retval;
1750	}
1751
1752	/* listfields fieldId=>fielddefinition */
1753	public function get_item_fields($trackerId, $itemId, $listfields, &$itemUsers, $alllang = false)
1754	{
1755		global $prefs, $user, $tiki_p_admin_trackers;
1756
1757		$definition = Tracker_Definition::get($trackerId);
1758		$info = $this->get_tracker_item((int) $itemId);
1759		$factory = $definition->getFieldFactory();
1760
1761		$itemUsers = array_map(function ($userField) use ($info) {
1762			return isset($info[$userField]) ? $this->parse_user_field($info[$userField]) : [];
1763		}, $definition->getItemOwnerFields());
1764
1765		if ($itemUsers) {
1766			$itemUsers = call_user_func_array('array_merge', $itemUsers);
1767		}
1768
1769		$fields = [];
1770		foreach ($listfields as $fieldId => $fopt) {
1771			if (empty($fopt['fieldId'])) {
1772				// to accept listfield as a simple table
1773				$fopt['fieldId'] = $fieldId;
1774			}
1775
1776			$fopt['trackerId'] = $trackerId;
1777			$fopt['itemId'] = (int)$itemId;
1778
1779			$handler = $factory->getHandler($fopt, $info);
1780			if ($handler) {
1781				$get = $this->extend_GET($fopt); // extend context
1782				$fopt = array_merge($fopt, $handler->getFieldData());
1783				$fields[] = $fopt;
1784				$this->restore_GET($get); // restore context
1785			}
1786		}
1787
1788		return($fields);
1789	}
1790
1791	/**
1792	 * Make sure $_GET is extended with the $fopt (in get_item_fields) before calling $handler->getFieldData()
1793	 * Some trackers use tiki syntax replacement, that uses $_GET in ParserLib::parse_wiki_argvariable, extending
1794	 * with $fopt makes sure that that the wiki syntax parser gets the right context variables
1795	 *
1796	 * @param Array $array Values to add to $_GET
1797	 * @return Array a copy of the original $_GET array
1798	 */
1799	protected function extend_GET($array)
1800	{
1801		$get = $_GET;
1802		foreach ($array as $key => $value) {
1803			$_GET[$key] = $value;
1804		}
1805		return $get;
1806	}
1807
1808	/**
1809	 * Use to restore the $_GET context with the copy of $_GET returned by self::extend_GET
1810	 *
1811	 * @param Array $get the array to restore as $_GET
1812	 */
1813	protected function restore_GET($get)
1814	{
1815		$_GET = $get;
1816	}
1817
1818	public function replace_item($trackerId, $itemId, $ins_fields, $status = '', $ins_categs = 0, $bulk_import = false)
1819	{
1820		global $user, $prefs, $tiki_p_admin_trackers, $tiki_p_admin_users;
1821		$final_event = 'tiki.trackeritem.update';
1822
1823		if (! $bulk_import) {
1824			$transaction = $this->begin();
1825		}
1826
1827		$categlib = TikiLib::lib('categ');
1828		$cachelib = TikiLib::lib('cache');
1829		$smarty = TikiLib::lib('smarty');
1830		$logslib = TikiLib::lib('logs');
1831		$userlib = TikiLib::lib('user');
1832		$tikilib = TikiLib::lib('tiki');
1833		$notificationlib = TikiLib::lib('notification');
1834
1835		$items = $this->items();
1836		$itemFields = $this->itemFields();
1837		$fields = $this->fields();
1838
1839		if (! empty($itemId)) {	// check the item really exists
1840			$itemId = (int) $this->items()->fetchOne('itemId', [ 'itemId' => $itemId]);
1841		}
1842
1843		$fil = [];
1844		if (! empty($itemId)) {
1845			$fil = $itemFields->fetchMap('fieldId', 'value', ['itemId' => $itemId]);
1846		}
1847
1848		$old_values = $fil;
1849
1850		$tracker_definition = Tracker_Definition::get($trackerId);
1851
1852		if (method_exists($tracker_definition, 'getInformation') == false) {
1853			return -1;
1854		}
1855
1856		$tracker_info = $tracker_definition->getInformation();
1857
1858		if (! empty($itemId)) {
1859			$new_itemId = 0;
1860			$oldStatus = $this->items()->fetchOne('status', ['itemId' => $itemId]);
1861
1862			$status = $status ? $status : $oldStatus;
1863			$fil['status'] = $status;
1864			$old_values['status'] = $oldStatus;
1865
1866			if ($status != $oldStatus) {
1867				$this->change_status([$itemId], $status);
1868			} else {
1869				$this->update_items(
1870					[$itemId],
1871					[
1872						'lastModif' => $tikilib->now,
1873						'lastModifBy' => $user,
1874					],
1875					false
1876				);
1877			}
1878
1879			$version = $this->last_log_version($itemId) + 1;
1880		} else {
1881			if (empty($status) && isset($tracker_info['newItemStatus'])) {
1882				// set status based on tracker setting of status not explicitly requested
1883				$status = $tracker_info['newItemStatus'];
1884			}
1885			if (empty($status)) {
1886				$status = 'o';
1887			}
1888			$fil['status'] = $status;
1889			$old_values['status'] = '';
1890			$oldStatus = '';
1891
1892			$new_itemId = $items->insert(
1893				[
1894					'trackerId' => (int) $trackerId,
1895					'created' => $this->now,
1896					'createdBy' => $user,
1897					'lastModif' => $this->now,
1898					'lastModifBy' => $user,
1899					'status' => $status,
1900				]
1901			);
1902
1903			$logslib->add_action('Created', $new_itemId, 'trackeritem');
1904			$version = 0;
1905
1906			$final_event = 'tiki.trackeritem.create';
1907		}
1908
1909		$currentItemId = $itemId ? $itemId : $new_itemId;
1910		$item_info = $this->get_item_info($currentItemId);
1911
1912		if (! empty($oldStatus) || ! empty($status)) {
1913			if (! empty($itemId) && $oldStatus != $status) {
1914				 $this->log($version, $itemId, -1, $oldStatus);
1915			}
1916		}
1917
1918		// If this is a user tracker it needs to be detected right here before actual looping of fields happen
1919		$trackersync_user = $user;
1920		foreach ($ins_fields["data"] as $i => $array) {
1921			if (isset($array['type']) && $array['type'] == 'u' && isset($array['options_array'][0]) && $array['options_array'][0] == '1') {
1922				if ($prefs['user_selector_realnames_tracker'] == 'y' && $array['type'] == 'u') {
1923					if (! $userlib->user_exists($array['value'])) {
1924						$finalusers = $userlib->find_best_user([$array['value']], '', 'login');
1925						if (! empty($finalusers[0]) && ! (isset($_REQUEST['register']) && isset($_REQUEST['name']) && $_REQUEST['name'] == $array['value'])) {
1926							// It could be in fact that a new user is required (when no match is found or during registration even if match is found)
1927							$ins_fields['data'][$i]['value'] = $finalusers[0];
1928						}
1929					}
1930				}
1931				$trackersync_user = $array['value'];
1932			}
1933		}
1934
1935		$final = [];
1936		$postSave = [];
1937		$suppliedFields = [];
1938
1939		foreach ($ins_fields["data"] as $i => $array) {
1940			// Old values were prefilled at the begining of the function and only replaced at the end of the iteration
1941			$fieldId = $array['fieldId'];
1942			$suppliedFields[] = $fieldId;
1943			$old_value = isset($fil[$fieldId]) ? $fil[$fieldId] : null;
1944
1945			$handler = $this->get_field_handler($array, array_merge($item_info, $fil));
1946
1947			if (method_exists($handler, 'postSaveHook')) {
1948				// postSaveHook will be called with final value saved
1949				// after saving all item fields
1950				$postSave[] = [
1951					'fieldId' => $fieldId,
1952					'handler' => $handler,
1953				];
1954			}
1955
1956			if (method_exists($handler, 'handleFinalSave')) {
1957				// handleFinalSave will be called after all other fields are saved, and
1958				// will get as parameter all other field data (other than ones that also
1959				// use finalSave).
1960				$final[] = [
1961					'field' => $array,
1962					'handler' => $handler,
1963				];
1964				continue;
1965			}
1966
1967			if (method_exists($handler, 'handleSave')) {
1968				$array = array_merge($array, $handler->handleSave(! isset($array['value']) ? null : $array['value'], $old_value));
1969				$value = ! isset($array['value']) ? null : $array['value'];
1970
1971				if ($value !== false) {
1972					$this->modify_field($currentItemId, $array['fieldId'], $value);
1973
1974					if ($itemId && $old_value != $value) {
1975						// On update, save old value
1976						$this->log($version, $itemId, $array['fieldId'], $old_value);
1977					}
1978					$fil[$fieldId] = $value;
1979				}
1980				continue;
1981			}
1982
1983			$value = isset($array["value"]) ? $array["value"] : null;
1984
1985			if (isset($array['type']) && $array['type'] == 'p' && ($user == $trackersync_user || $tiki_p_admin_users == 'y')) {
1986				if ($array['options_array'][0] == 'password') {
1987					if (! empty($array['value']) && $prefs['change_password'] == 'y' && ($e = $userlib->check_password_policy($array['value'])) == '') {
1988						$userlib->change_user_password($trackersync_user, $array['value']);
1989					}
1990					if (! empty($itemId)) {
1991						$this->log($version, $itemId, $array['fieldId'], '?');
1992					}
1993				} elseif ($array['options_array'][0] == 'email') {
1994					if (! empty($array['value']) && validate_email($array['value']) && ($prefs['user_unique_email'] != 'y' || ! $userlib->other_user_has_email($trackersync_user, $array['value']))) {
1995						$old_value = $userlib->get_user_email($trackersync_user);
1996						$userlib->change_user_email($trackersync_user, $array['value']);
1997					}
1998					if (! empty($itemId) && $old_value != $array['value']) {
1999						$this->log($version, $itemId, $array['fieldId'], $old_value);
2000					}
2001				} else {
2002					$old_value = $tikilib->get_user_preference($trackersync_user, $array['options_array'][0]);
2003					$tikilib->set_user_preference($trackersync_user, $array['options_array'][0], $array['value']);
2004					if (! empty($itemId) && $old_value != $array['value']) {
2005						$this->log($version, $itemId, $array['fieldId'], $array['value']);
2006					}
2007				}
2008				// Should not store value in tracker database as it won't be reliable (what if pref is changed afterwards?)
2009				$value = '';
2010				$fil[$fieldId] = $value;
2011				$this->modify_field($currentItemId, $array['fieldId'], $value);
2012			} elseif (isset($array['type']) && $array['type'] == 'k') { //page selector
2013				if ($array['value'] != '') {
2014					$this->modify_field($currentItemId, $array['fieldId'], $value);
2015					if ($itemId) {
2016						// On update, save old value
2017						$this->log($version, $itemId, $array['fieldId'], $old_value);
2018					}
2019					$fil[$fieldId] = $value;
2020					if (! $this->page_exists($array['value'])) {
2021						$opts = $array['options_array'];
2022						if (! empty($opts[2])) {
2023							$IP = $this->get_ip_address();
2024							$info = $this->get_page_info($opts[2]);
2025							$this->create_page($array['value'], 0, $info['data'], $this->now, '', $user, $IP, $info['description'], $info['lang'], $info['is_html'], [], $info['wysiwyg'], $info['wiki_authors_style']);
2026						}
2027					}
2028				}
2029			} else {
2030				$is_date = isset($array['type']) ? in_array($array["type"], ['f', 'j']) : false;
2031
2032				if ($currentItemId || ( isset($array['type']) && $array['type'] !== 'q')) {	// autoincrement
2033					$this->modify_field($currentItemId, $fieldId, $value);
2034					if ($old_value != $value) {
2035						if ($is_date) {
2036							$dformat = $prefs['short_date_format'] . ' ' . $prefs['short_time_format'];
2037							$old_value = $this->date_format($dformat, (int) $old_value);
2038							$new_value = $this->date_format($dformat, (int) $value);
2039						} else {
2040							$new_value = $value;
2041						}
2042						if ($old_value != $new_value && ! empty($itemId) &&
2043								$array['type'] !== 'W' // not for webservices
2044								) {
2045							$this->log($version, $itemId, $array['fieldId'], $old_value);
2046						}
2047					}
2048				}
2049
2050				$fil[$fieldId] = $value;
2051			}
2052		}
2053
2054		// delete empty actionlog version to prevent history date overlap
2055		if ($version > 0 && $this->last_log_version($itemId)+1 == $version) {
2056			$logslib->delete_action('Updated', $itemId, 'trackeritem', $version);
2057		}
2058
2059		// get permnames
2060		$permNames = [];
2061		foreach ($fil as $fieldId => $value) {
2062			$field = $tracker_definition->getField($fieldId);
2063			if ($field['type'] !== 'W') {    // not for webservices
2064				$permNames[$fieldId] = $field['permName'];
2065			} else {
2066				unset($fil[$fieldId], $old_values[$fieldId]);	// webservice values are just a cache and not useful for diffs etc
2067			}
2068		}
2069
2070		if (count($final)) {
2071			$data = [];
2072			foreach ($fil as $fieldId => $value) {
2073				$data[$permNames[$fieldId]] = $value;
2074			}
2075
2076			foreach ($final as $job) {
2077				$value = $job['handler']->handleFinalSave($data);
2078				$data[$job['field']['permName']] = $value;
2079				$this->modify_field($currentItemId, $job['field']['fieldId'], $value);
2080			}
2081		}
2082
2083		foreach ($postSave as $job) {
2084			$value = $fil[$job['fieldId']];
2085			$job['handler']->postSaveHook($value);
2086		}
2087
2088		$values_by_permname = [];
2089		$old_values_by_permname = [];
2090		foreach ($fil as $fieldId => $value) {
2091			$values_by_permname[$permNames[$fieldId]] = $value;
2092		}
2093		foreach ($old_values as $fieldId => $value) {
2094			$old_values_by_permname[$permNames[$fieldId]] = $value;
2095		}
2096
2097		$arguments = [
2098			'type' => 'trackeritem',
2099			'object' => $currentItemId,
2100			'user' => $GLOBALS['user'],
2101			'version' => $version,
2102			'trackerId' => $trackerId,
2103			'supplied' => $suppliedFields,
2104			'values' => $fil,
2105			'old_values' => $old_values,
2106			'values_by_permname' => $values_by_permname,
2107			'old_values_by_permname' => $old_values_by_permname,
2108			'bulk_import' => $bulk_import,
2109			'aggregate' => sha1("trackeritem/$currentItemId"),
2110		];
2111
2112		// this needs to trigger no matter of the size as trackeritem categorization depends on this and other event types as well
2113		TikiLib::events()->trigger(
2114			$final_event,
2115			$arguments
2116		);
2117
2118		if (! $bulk_import) {
2119			$transaction->commit();
2120		}
2121
2122		return $currentItemId;
2123	}
2124
2125	public function modify_field($itemId, $fieldId, $value)
2126	{
2127		$conditions = [
2128			'itemId' => (int) $itemId,
2129			'fieldId' => (int) $fieldId,
2130		];
2131
2132		$this->itemFields()->insertOrUpdate(['value' => $value], $conditions);
2133	}
2134
2135	public function groupName($tracker_info, $itemId)
2136	{
2137		if (empty($tracker_info['autoCreateGroupInc'])) {
2138			$groupName = $tracker_info['name'];
2139		} else {
2140			$userlib = TikiLib::lib('user');
2141			$group_info = $userlib->get_groupId_info($tracker_info['autoCreateGroupInc']);
2142			$groupName = $group_info['groupName'];
2143		}
2144		return "$groupName $itemId";
2145	}
2146
2147	public function _format_data($field, $data)
2148	{
2149		$data = trim($data);
2150		if ($field['type'] == 'a') {
2151			if (isset($field["options_array"][3]) and $field["options_array"][3] > 0 and strlen($data) > $field["options_array"][3]) {
2152				$data = substr($data, 0, $field["options_array"][3]) . " (...)";
2153			}
2154		} elseif ($field['type'] == 'c') {
2155			if ($data != 'y') {
2156				$data = 'n';
2157			}
2158		}
2159		return $data;
2160	}
2161
2162	/**
2163	 * Called from tiki-list_trackers.php import button
2164	 *
2165	 * @param int $trackerId
2166	 * @param resource $csvHandle file handle to import
2167	 * @param bool $replace_rows make new items for those with existing itemId
2168	 * @param string $dateFormat used for item fields of type date
2169	 * @param string $encoding defaults "UTF8"
2170	 * @param string $csvDelimiter defaults to ","
2171	 * @param bool $updateLastModif default true
2172	 * @param bool $convertItemLinkValues default false		attempts to find a linked or related item for ItemLink and Relations fields
2173	 * @return number items imported
2174	 */
2175	public function import_csv($trackerId, $csvHandle, $replace_rows = true, $dateFormat = '', $encoding = 'UTF8', $csvDelimiter = ',', $updateLastModif = true, $convertItemLinkValues = false)
2176	{
2177		$tikilib = TikiLib::lib('tiki');
2178		$unifiedsearchlib = TikiLib::lib('unifiedsearch');
2179
2180		$items = $this->items();
2181		$itemFields = $this->itemFields();
2182
2183		$tracker_info = $this->get_tracker_options($trackerId);
2184		if (($header = fgetcsv($csvHandle, 100000, $csvDelimiter)) === false) {
2185			return 'Illegal first line';
2186		}
2187		if ($encoding == 'UTF-8') {
2188			// See en.wikipedia.org/wiki/Byte_order_mark
2189			if (substr($header[0], 0, 3) == "\xef\xbb\xbf") {
2190				$header[0] = substr($header[0], 3);
2191			}
2192		}
2193		$max = count($header);
2194		if ($max === 1 and strpos($header, "\t") !== false) {
2195			Feedback::error(tr('No fields found in header, not a comma-separated values file?'));
2196			return 0;
2197		}
2198		for ($i = 0; $i < $max; $i++) {
2199			if ($encoding == 'ISO-8859-1') {
2200				$header[$i] = utf8_encode($header[$i]);
2201			}
2202			$header[$i] = preg_replace('/ -- [0-9]*$/', ' -- ', $header[$i]);
2203		}
2204		if (count($header) != count(array_unique($header))) {
2205			return 'Duplicate header names';
2206		}
2207		$total = 0;
2208		$need_reindex = [];
2209		$fields = $this->list_tracker_fields($trackerId, 0, -1, 'position_asc', '');
2210
2211		// prepare autoincrement fields
2212		$auto_fields = [];
2213		foreach ($fields['data'] as $field) {
2214			if ($field['type'] === 'q') {
2215				$auto_fields[(int) $field['fieldId']] = $field;
2216			}
2217		}
2218
2219		// prepare ItemLink fields
2220		if ($convertItemLinkValues) {
2221			$itemlink_options = [];
2222			foreach ($fields['data'] as $field) {
2223				if ($field['type'] === 'r') {
2224					$itemlink_options[(int) $field['fieldId']] = $field['options_array'];
2225				}
2226			}
2227		}
2228
2229		// mandatory fields check
2230		$utilities = new \Services_Tracker_Utilities;
2231		$definition = Tracker_Definition::get($trackerId);
2232		$line = 0;
2233		$errors = [];
2234		while (($data = fgetcsv($csvHandle, 100000, $csvDelimiter)) !== false) {
2235			$line++;
2236			if ($encoding == 'ISO-8859-1') {
2237				for ($i = 0; $i < $max; $i++) {
2238					$data[$i] = utf8_encode($data[$i]);
2239				}
2240			}
2241			$itemId = 0;
2242			$datafields = [];
2243			for ($i = 0; $i < $max; ++$i) {
2244				if ($header[$i] == 'itemId') {
2245					$itemId = $data[$i];
2246				}
2247				if (! preg_match('/ -- $/', $header[$i])) {
2248					continue;
2249				}
2250				$h = preg_replace('/ -- $/', '', $header[$i]);
2251				foreach ($fields['data'] as $field) {
2252					if ($field['name'] == $h) {
2253						$datafields[$field['permName']] = $data[$i];
2254					}
2255				}
2256			}
2257			$lineErrors = $utilities->validateItem($definition, ['itemId' => $itemId, 'fields' => $datafields]);
2258			foreach ($lineErrors as $error) {
2259				$errors[] = tr('Line %0:', $line) . ' ' . $error;
2260			}
2261		}
2262
2263		if (count($errors) > 0) {
2264			Feedback::error([
2265				'title' => tr('Import file contains errors. Please review and fix before importing.'),
2266				'mes' => $errors
2267			]);
2268			return 0;
2269		}
2270
2271		// back to first row excluding header
2272		fseek($csvHandle, 0);
2273		fgetcsv($csvHandle, 100000, $csvDelimiter);
2274
2275		while (($data = fgetcsv($csvHandle, 100000, $csvDelimiter)) !== false) {
2276			$status = 'o';
2277			$itemId = 0;
2278			$created = $tikilib->now;
2279			$lastModif = $created;
2280			$cats = '';
2281			for ($i = 0; $i < $max; $i++) {
2282				if ($encoding == 'ISO-8859-1') {
2283					$data[$i] = utf8_encode($data[$i]);
2284				}
2285				if ($header[$i] == 'status') {
2286					if ($data[$i] == 'o' || $data[$i] == 'p' || $data[$i] == 'c') {
2287						$status = $data[$i];
2288					}
2289				} elseif ($header[$i] == 'itemId') {
2290					$itemId = $data[$i];
2291				} elseif ($header[$i] == 'created') {
2292					$created = $this->parse_imported_date($data[$i], $dateFormat);
2293					;
2294				} elseif ($header[$i] == 'lastModif') {
2295					$lastModif = $this->parse_imported_date($data[$i], $dateFormat);
2296				} elseif ($header[$i] == 'categs') { // for old compatibility
2297					$cats = preg_split('/,/', trim($data[$i]));
2298				}
2299			}
2300			$t = $this->get_tracker_for_item($itemId);
2301			if ($itemId && $t && $t == $trackerId && $replace_rows) {
2302				if (in_array('status', $header)) {
2303					$update['status'] = $status;
2304				}
2305				if (in_array('created', $header)) {
2306					$update['created'] = (int) $created;
2307				}
2308				if ($updateLastModif) {
2309					$update['lastModif'] = (int) $lastModif;
2310				}
2311				if (! empty($update)) {
2312					$items->update($update, ['itemId' => (int) $itemId]);
2313				}
2314			} else {
2315				$itemId = $items->insert(
2316					[
2317						'trackerId' => (int) $trackerId,
2318						'created' => (int) $created,
2319						'lastModif' => (int) $lastModif,
2320						'status' => $status,
2321					]
2322				);
2323				if (empty($itemId) || $itemId < 1) {
2324					Feedback::error(tr(
2325						'Problem inserting tracker item: trackerId=%0, created=%1, lastModif=%2, status=%3',
2326						$trackerId,
2327						$created,
2328						$lastModif,
2329						$status
2330					));
2331				} else {
2332					// deal with autoincrement fields
2333					foreach ($auto_fields as $afield) {
2334						$auto_handler = $this->get_field_handler($afield, $this->get_item_info($itemId));
2335						if (! empty($auto_handler)) {
2336							$auto_val = $auto_handler->handleSave(null, null);
2337							$itemFields->insert(['itemId' => (int) $itemId, 'fieldId' => (int) $afield['fieldId'], 'value' => $auto_val['value']]);
2338						}
2339					}
2340				}
2341			}
2342			$need_reindex[] = $itemId;
2343			if (! empty($cats)) {
2344				$this->categorized_item($trackerId, $itemId, "item $itemId", $cats);
2345			}
2346			for ($i = 0; $i < $max; ++$i) {
2347				if (! preg_match('/ -- $/', $header[$i])) {
2348					continue;
2349				}
2350				$h = preg_replace('/ -- $/', '', $header[$i]);
2351				foreach ($fields['data'] as $field) {
2352					if ($field['name'] == $h) {
2353						if ($field['type'] == 'p' && $field['options_array'][0] == 'password') {
2354							//$userlib->change_user_password($user, $ins_fields['data'][$i]['value']);
2355							continue;
2356						}
2357
2358						if ($data[$i] === 'NULL') {
2359							$data[$i] = '';
2360						}
2361						// remove escaped quotes \" etc
2362						$data[$i] = stripslashes($data[$i]);
2363
2364						switch ($field['type']) {
2365							case 'e':
2366								$cats = preg_split('/%%%/', trim($data[$i]));
2367								$catIds = [];
2368								if (! empty($cats)) {
2369									foreach ($cats as $c) {
2370										$categlib = TikiLib::lib('categ');
2371										if ($cId = $categlib->get_category_id(trim($c))) {
2372											$catIds[] = $cId;
2373										}
2374									}
2375									if (! empty($catIds)) {
2376										$this->categorized_item($trackerId, $itemId, "item $itemId", $catIds);
2377									}
2378								}
2379								$data[$i] = '';
2380								break;
2381							case 's':
2382								$data[$i] = '';
2383								break;
2384							case 'y':	// Country selector
2385								$data[$i] = preg_replace('/ /', "_", $data[$i]);
2386								break;
2387							case 'a':
2388								$data[$i] = preg_replace('/\%\%\%/', "\r\n", $data[$i]);
2389								break;
2390							case 'c':
2391								if (strtolower($data[$i]) == 'yes' || strtolower($data[$i]) == 'on' || $data[$i] == 1 || strtolower($data[$i]) == 'y') {
2392									$data[$i] = 'y';
2393								} else {
2394									$data[$i] = 'n';
2395								}
2396								break;
2397							case 'f':
2398							case 'j':
2399								$data[$i] = $this->parse_imported_date($data[$i], $dateFormat);
2400								break;
2401							case 'r':
2402								if ($convertItemLinkValues && $data[$i]) {
2403									$val = $this->get_item_id(
2404										$itemlink_options[$field['fieldId']][0], // other trackerId (option 0)
2405										$itemlink_options[$field['fieldId']][1], // other fieldId (option 1)
2406										$data[$i]									// value
2407									);
2408									if ($val !== null) {
2409										$data[$i] = $val;
2410									} else {
2411										Feedback::error(
2412											tr(
2413												'Problem converting tracker item link field: trackerId=%0, fieldId=%1, itemId=%2',
2414												$trackerId,
2415												$field['fieldId'],
2416												$itemId
2417											)
2418										);
2419									}
2420								}
2421								break;
2422							case 'REL':	// Relations
2423								if ($convertItemLinkValues && $data[$i] && ! $field['options_map']['readonly']) {
2424									$filter = [];
2425									$results = [];
2426
2427									parse_str($field['options_map']['filter'], $filter);
2428									$filter['title'] = $data[$i];
2429
2430									$query = $unifiedsearchlib->buildQuery($filter);
2431									$query->setRange(0, 1);
2432
2433									try {
2434										$results = $query->search($unifiedsearchlib->getIndex());
2435									} catch (Search_Elastic_TransportException $e) {
2436										Feedback::error(tr('Search functionality currently unavailable.'));
2437									} catch (Exception $e) {
2438										Feedback::error($e->getMessage());
2439									}
2440
2441									if (count($results)) {
2442										$data[$i] = $results[0]['object_id'];
2443										TikiLib::lib('relation')->add_relation($field['options_map']['relation'], 'trackeritem', $itemId, $results[0]['object_type'], $data[$i]);
2444									} else {
2445										Feedback::error(
2446											tr(
2447												'Problem converting tracker relation field: trackerId=%0, fieldId=%1, itemId=%2 from value "%3"',
2448												$trackerId,
2449												$field['fieldId'],
2450												$itemId,
2451												$data[$i]
2452											)
2453										);
2454									}
2455								}
2456								break;
2457						}
2458
2459						if ($this->get_item_value($trackerId, $itemId, $field['fieldId']) !== false) {
2460							$itemFields->update(['value' => $data[$i]], ['itemId' => (int) $itemId, 'fieldId' => (int) $field['fieldId']]);
2461						} else {
2462							$itemFields->insert(['itemId' => (int) $itemId, 'fieldId' => (int) $field['fieldId'], 'value' => $data[$i]]);
2463						}
2464						break;
2465					}
2466				}
2467			}
2468			$total++;
2469		}
2470
2471		$cant_items = $items->fetchCount(['trackerId' => (int) $trackerId]);
2472		$this->trackers()->update(['items' => (int) $cant_items, 'lastModif' => $this->now], ['trackerId' => (int) $trackerId]);
2473
2474		global $prefs;
2475		if ($prefs['feature_search'] === 'y' && $prefs['unified_incremental_update'] === 'y') {
2476			$unifiedsearchlib = TikiLib::lib('unifiedsearch');
2477
2478			foreach ($need_reindex as $id) {
2479				$unifiedsearchlib->invalidateObject('trackeritem', $id);
2480			}
2481			$unifiedsearchlib->processUpdateQueue();
2482		}
2483
2484		return $total;
2485	}
2486
2487	function parse_imported_date($dateString, $dateFormat)
2488	{
2489
2490		$tikilib = TikiLib::lib('tiki');
2491		$date = 0;
2492
2493		if (is_numeric($dateString)) {
2494			$date = (int)$dateString;
2495		} elseif ($dateFormat == 'mm/dd/yyyy') {
2496			list($m, $d, $y) = preg_split('#/#', $dateString);
2497			if ($y && $m && $d) {
2498				$date = $tikilib->make_time(0, 0, 0, $m, $d, $y);
2499			}
2500		} elseif ($dateFormat == 'dd/mm/yyyy') {
2501			list($d, $m, $y) = preg_split('#/#', $dateString);
2502			if ($y && $m && $d) {
2503				$date = $tikilib->make_time(0, 0, 0, $m, $d, $y);
2504			}
2505		} elseif ($dateFormat == 'yyyy-mm-dd') {
2506			list($y, $m, $d) = preg_split('#-#', $dateString);
2507			if ($y && $m && $d) {
2508				$date = $tikilib->make_time(0, 0, 0, $m, $d, $y);
2509			}
2510		}
2511
2512		if (! $date) {    // previous attempts failed, try a more flexible approach
2513			$date = strtotime($dateString);
2514		}
2515
2516		return $date;
2517	}
2518
2519	public function dump_tracker_csv($trackerId)
2520	{
2521		$tikilib = TikiLib::lib('tiki');
2522		$tracker_info = $this->get_tracker_options($trackerId);
2523		$fields = $this->list_tracker_fields($trackerId, 0, -1, 'position_asc', '');
2524
2525		$trackerId = (int) $trackerId;
2526
2527		// Check if can view field otherwise exclude it
2528		$item = Tracker_Item::newItem($trackerId);
2529		foreach ($fields['data'] as $k => $field) {
2530			if (!$item->canViewField($field['fieldId'])) {
2531				unset($fields['data'][$k]);
2532			}
2533		}
2534
2535		// write out file header
2536		session_write_close();
2537		$this->write_export_header('UTF-8', $trackerId);
2538
2539		// then "field names -- index" as first line
2540		$str = '';
2541		$str .= 'itemId,status,created,lastModif,';	// these headings weren't quoted in the previous export function
2542		if (count($fields['data']) > 0) {
2543			foreach ($fields['data'] as $field) {
2544				$str .= '"' . $field['name'] . ' -- ' . $field['fieldId'] . '",';
2545			}
2546		}
2547		echo $str;
2548
2549		// prepare queries
2550		$mid = ' WHERE tti.`trackerId` = ? ';
2551		$bindvars = [$trackerId];
2552		$join = '';
2553
2554		$query_items = 'SELECT tti.itemId, tti.status, tti.created, tti.lastModif'
2555						. ' FROM `tiki_tracker_items` tti'
2556						. $mid
2557						. ' ORDER BY tti.`itemId` ASC';
2558		$query_fields = 'SELECT tti.itemId, ttif.`value`, ttf.`type`'
2559						. ' FROM ('
2560						. ' `tiki_tracker_items` tti'
2561						. ' INNER JOIN `tiki_tracker_item_fields` ttif ON tti.`itemId` = ttif.`itemId`'
2562						. ' INNER JOIN `tiki_tracker_fields` ttf ON ttf.`fieldId` = ttif.`fieldId`'
2563						. ')'
2564						. $mid
2565						. ' ORDER BY tti.`itemId` ASC, ttf.`position` ASC';
2566		$base_tables = '('
2567			. ' `tiki_tracker_items` tti'
2568			. ' INNER JOIN `tiki_tracker_item_fields` ttif ON tti.`itemId` = ttif.`itemId`'
2569			. ' INNER JOIN `tiki_tracker_fields` ttf ON ttf.`fieldId` = ttif.`fieldId`'
2570			. ')' . $join;
2571
2572
2573		$query_cant = 'SELECT count(DISTINCT ttif.`itemId`) FROM ' . $base_tables . $mid;
2574		$cant = $this->getOne($query_cant, $bindvars);
2575
2576		$avail_mem = $tikilib->get_memory_avail();
2577		$maxrecords_items = (int)(($avail_mem - 10 * 1024 * 1025) / 5000);		// depends on size of items table (fixed)
2578		if ($maxrecords_items < 0) {
2579			// cope with memory_limit = -1
2580			$maxrecords_items = -1;
2581		}
2582		$offset_items = 0;
2583
2584		$items = $this->get_dump_items_array($query_items, $bindvars, $maxrecords_items, $offset_items);
2585
2586		$avail_mem = $tikilib->get_memory_avail();							// update avail after getting first batch of items
2587		$maxrecords = (int) ($avail_mem / 40000) * count($fields['data']);	// depends on number of fields
2588		if ($maxrecords < 0) {
2589			// cope with memory_limit = -1
2590			$maxrecords = $cant * count($fields['data']);
2591		}
2592		$canto = $cant * count($fields['data']);
2593		$offset = 0;
2594		$lastItem = -1;
2595		$count = 0;
2596		$icount = 0;
2597		$field_values = [];
2598
2599		// write out rows
2600		for ($offset = 0; $offset < $canto; $offset = $offset + $maxrecords) {
2601			$field_values = $this->fetchAll($query_fields, $bindvars, $maxrecords, $offset);
2602			$mem = memory_get_usage(true);
2603
2604			foreach ($field_values as $res) {
2605				if ($lastItem != $res['itemId']) {
2606					$lastItem = $res['itemId'];
2607					echo "\n" . $items[$lastItem]['itemId'] . ',' . $items[$lastItem]['status'] . ',' . $items[$lastItem]['created'] . ',' . $items[$lastItem]['lastModif'] . ',';	// also these fields weren't traditionally escaped
2608					$count++;
2609					$icount++;
2610					if ($icount > $maxrecords_items && $maxrecords_items > 0) {
2611						$offset_items += $maxrecords_items;
2612						$items = $this->get_dump_items_array($query_items, $bindvars, $maxrecords_items, $offset_items);
2613						$icount = 0;
2614					}
2615				}
2616				echo '"' . str_replace(['"', "\r\n", "\n"], ['\\"', '%%%', '%%%'], $res['value']) . '",';
2617			}
2618			ob_flush();
2619			flush();
2620			//if ($offset == 0) { $maxrecords = 1000 * count($fields['data']); }
2621		}
2622		echo "\n";
2623		ob_end_flush();
2624	}
2625
2626	public function get_dump_items_array($query, $bindvars, $maxrecords, $offset)
2627	{
2628		$items_array = $this->fetchAll($query, $bindvars, $maxrecords, $offset);
2629		$items = [];
2630		foreach ($items_array as $item) {
2631			$items[$item['itemId']] = $item;
2632		}
2633		unset($items_array);
2634		return $items;
2635	}
2636
2637	public function write_export_header($encoding = null, $trackerId = null)
2638	{
2639		if (! $encoding) {
2640			$encoding = $_REQUEST['encoding'];
2641		}
2642		if (! $trackerId) {
2643			$trackerId = $_REQUEST['trackerId'];
2644		}
2645		if (! empty($_REQUEST['file'])) {
2646			if (preg_match('/.csv$/', $_REQUEST['file'])) {
2647				$file = $_REQUEST['file'];
2648			} else {
2649				$file = $_REQUEST['file'] . '.csv';
2650			}
2651		} else {
2652			$file = tra('tracker') . '_' . $trackerId . '.csv';
2653		}
2654		header("Content-type: text/comma-separated-values; charset:" . $encoding);
2655		header("Content-Disposition: attachment; filename=$file");
2656		header("Expires: 0");
2657		header("Cache-Control: must-revalidate, post-check=0,pre-check=0");
2658		header("Pragma: public");
2659	}
2660
2661	// check the validity of each field values of a tracker item
2662	// and the presence of mandatory fields
2663	public function check_field_values($ins_fields, $categorized_fields = '', $trackerId = '', $itemId = '')
2664	{
2665		global $prefs;
2666		$mandatory_fields = [];
2667		$erroneous_values = [];
2668		if (isset($ins_fields)&&isset($ins_fields['data'])) {
2669			foreach ($ins_fields['data'] as $f) {
2670				if ($f['type'] == 'b' && ! empty($f['value'])){
2671					if (is_numeric($f['value'])) {
2672						$f['name']= $f['name'].' Currency';
2673						$mandatory_fields[] = $f;
2674					}
2675				}
2676				if ($f['type'] == 'f' && $f['isMandatory'] != 'y' && empty($f['value'])) {
2677					$ins_id = 'ins_' . $f['fieldId'];
2678					if (! empty($_REQUEST[$ins_id . 'Month']) || ! empty($_REQUEST[$ins_id . 'Day']) || ! empty($_REQUEST[$ins_id . 'Year']) ||
2679								! empty($_REQUEST[$ins_id . 'Hour']) || ! empty($_REQUEST[$ins_id . 'Minute'])) {
2680						$erroneous_values[] = $f;
2681					}
2682				}
2683				if ($f['type'] != 'q' and isset($f['isMandatory']) && $f['isMandatory'] == 'y') {
2684					if (($f['type'] == 'e' || in_array($f['fieldId'], $categorized_fields)) && empty($f['value'])) {	// category: value is now categ id's
2685						$mandatory_fields[] = $f;
2686					} elseif (in_array($f['type'], ['a', 't']) && ($this->is_multilingual($f['fieldId']) == 'y')) {
2687						if (! isset($multi_languages)) {
2688							$multi_languages = $prefs['available_languages'];
2689						}
2690						//Check recipient
2691						if (isset($f['lingualvalue'])) {
2692							foreach ($f['lingualvalue'] as $val) {
2693								foreach ($multi_languages as $num => $tmplang) {
2694									//Check if trad is empty
2695									if (! isset($val['lang']) ||! isset($val['value']) ||(($val['lang'] == $tmplang) && strlen($val['value']) == 0)) {
2696										$mandatory_fields[] = $f;
2697									}
2698								}
2699							}
2700						} elseif (is_array($f['value'])) {
2701							foreach ($f['value'] as $key => $val) {
2702								foreach ($multi_languages as $num => $tmplang) {
2703									if ($key == $tmplang && empty($val)) {
2704										$mandatory_fields[] = $f;
2705									}
2706								}
2707							}
2708						} else {
2709							$mandatory_fields[] = $f;
2710						}
2711					} elseif (in_array($f['type'], ['u', 'g']) && $f['options_array'][0] == 1) {
2712						;
2713					} elseif ($f['type'] == 'c' && (empty($f['value']) || $f['value'] == 'n')) {
2714						$mandatory_fields[] = $f;
2715					} elseif ($f['type'] == 'A' && ! empty($itemId) && empty($f['value'])) {
2716						$val = $this->get_item_value($trackerId, $itemId, $f['fieldId']);
2717						if (empty($val)) {
2718							$mandatory_fields[] = $f;
2719						}
2720					} elseif ($f['type'] == 'r' && empty(array_filter((array) $f['value']))) {	// ItemLink - '0' counts as empty
2721						$mandatory_fields[] = $f;
2722					} elseif (! isset($f['value']) || ! is_array($f['value']) && strlen($f['value']) == 0 || is_array($f['value']) && empty($f['value'])) {
2723						$mandatory_fields[] = $f;
2724					}
2725				}
2726				if (! empty($f['value'])) {
2727					switch ($f['type']) {
2728						// IP address (only for IPv4)
2729						case 'I':
2730							$validator = new Zend\Validator\Ip;
2731							if (! $validator->isValid($f['value'])) {
2732								$erroneous_values[] = $f;
2733							}
2734							break;
2735						// numeric
2736						case 'n':
2737							if (! is_numeric($f['value'])) {
2738								$f['error'] = tra('Field is not numeric');
2739								$erroneous_values[] = $f;
2740							}
2741							break;
2742
2743						// email
2744						case 'm':
2745							if (! validate_email($f['value'], $prefs['validateEmail'])) {
2746								$erroneous_values[] = $f;
2747							}
2748							break;
2749
2750						// password
2751						case 'p':
2752							if ($f['options_array'][0] == 'password') {
2753								$userlib = TikiLib::lib('user');
2754								if (($e = $userlib->check_password_policy($f['value'])) != '') {
2755									 $erroneous_values[] = $f;
2756								}
2757							} elseif ($f['options_array'][0] == 'email') {
2758								if (! validate_email($f['value'])) {
2759									$erroneous_values[] = $f;
2760								}
2761							}
2762							break;
2763						case 'a':
2764							if (isset($f['options_array'][5]) && $f['options_array'][5] > 0) {
2765								if (count(preg_split('/\s+/', trim($f['value']))) > $f['options_array'][5]) {
2766									$erroneous_values[] = $f;
2767								}
2768							}
2769							if (isset($f['options_array'][6]) && $f['options_array'][6] == 'y') {
2770								if (in_array($f['value'], $this->list_tracker_field_values($trackerId, $f['fieldId'], 'opc', 'y', '', $itemId))) {
2771									$erroneous_values[] = $f;
2772								}
2773							}
2774							break;
2775					}
2776
2777					$handler = $this->get_field_handler($f, $this->get_item_info($itemId));
2778					if (method_exists($handler, 'isValid')) {
2779						$validationResponse = $handler->isValid($ins_fields['data']);
2780						if ($validationResponse !== true) {
2781							if (! empty($f['validationMessage'])) {
2782								$f['errorMsg'] = $f['validationMessage'];
2783							} elseif (! empty($validationResponse)) {
2784								$f['errorMsg'] = $validationResponse;
2785							} else {
2786								$f['errorMsg'] = tr('Unknown error');
2787							}
2788							$erroneous_values[] = $f;
2789						}
2790					}
2791				}
2792			}
2793		}
2794
2795		$res = [];
2796		$res['err_mandatory'] = $mandatory_fields;
2797		$res['err_value'] = $erroneous_values;
2798		return $res;
2799	}
2800
2801	public function remove_tracker_item($itemId, $bulk_mode = false)
2802	{
2803		global $user, $prefs;
2804		$res = $this->items()->fetchFullRow(['itemId' => (int) $itemId]);
2805		$trackerId = $res['trackerId'];
2806		$status = $res['status'];
2807
2808		// keep copy of item for putting info into final event
2809		$itemInfo = $this->get_tracker_item($itemId);
2810
2811		// ---- save image list before sql query ---------------------------------
2812		$fieldList = $this->list_tracker_fields($trackerId, 0, -1, 'name_asc', '');
2813
2814		$statusTypes = $this->status_types();
2815		$statusString = isset($statusTypes[$status]['label']) ? $statusTypes[$status]['label'] : '';
2816
2817		$imgList = [];
2818		foreach ($fieldList['data'] as $f) {
2819			$data_field[] = ['name' => tr($f['name']),'value' => $this->get_item_value($trackerId, $itemId, $f['fieldId'])];
2820			if ($f['type'] == 'i') {
2821				$imgList[] = $this->get_item_value($trackerId, $itemId, $f['fieldId']);
2822			}
2823		}
2824
2825		if (! $bulk_mode) {
2826			$watchers = $this->get_notification_emails($trackerId, $itemId, $this->get_tracker_options($trackerId));
2827
2828			if (count($watchers > 0)) {
2829				$smarty = TikiLib::lib('smarty');
2830				$trackerName = $this->trackers()->fetchOne('name', ['trackerId' => (int) $trackerId]);
2831				$smarty->assign('mail_date', $this->now);
2832				$smarty->assign('mail_user', $user);
2833				$smarty->assign('mail_action', 'deleted');
2834				$smarty->assign('mail_itemId', $itemId);
2835				$smarty->assign('mail_item_desc', $itemId);
2836				$smarty->assign('mail_fields', $data_field);
2837				$smarty->assign('mail_field_status', $statusString);
2838				$smarty->assign('mail_trackerId', $trackerId);
2839				$smarty->assign('mail_trackerName', $trackerName);
2840				$smarty->assign('mail_data', '');
2841				$foo = parse_url($_SERVER["REQUEST_URI"]);
2842				$machine = $this->httpPrefix(true) . $foo["path"];
2843				$smarty->assign('mail_machine', $machine);
2844				$parts = explode('/', $foo['path']);
2845				if (count($parts) > 1) {
2846					unset($parts[count($parts) - 1]);
2847				}
2848				$smarty->assign('mail_machine_raw', $this->httpPrefix(true) . implode('/', $parts));
2849				if (! isset($_SERVER["SERVER_NAME"])) {
2850					$_SERVER["SERVER_NAME"] = $_SERVER["HTTP_HOST"];
2851				}
2852				include_once('lib/webmail/tikimaillib.php');
2853				$smarty->assign('server_name', $_SERVER['SERVER_NAME']);
2854				foreach ($watchers as $w) {
2855					$mail = new TikiMail($w['user']);
2856
2857					if (! isset($w['template'])) {
2858						$w['template'] = '';
2859					}
2860					$content = $this->parse_notification_template($w['template']);
2861
2862					$mail->setSubject($smarty->fetchLang($w['language'], $content['subject']));
2863					$mail_data = $smarty->fetchLang($w['language'], $content['template']);
2864					if (isset($w['templateFormat']) && $w['templateFormat'] == 'html') {
2865						$mail->setHtml($mail_data, str_replace('&nbsp;', ' ', strip_tags($mail_data)));
2866					} else {
2867						$mail->setText(str_replace('&nbsp;', ' ', strip_tags($mail_data)));
2868					}
2869					$mail->send([$w['email']]);
2870				}
2871			}
2872		}
2873
2874		// remove the object and uncategorize etc while the item still exists
2875		$this->remove_object("trackeritem", $itemId);
2876		$itemFields = $this->itemFields()->fetchAll(['fieldId'], ['itemId' => $itemId]);
2877		foreach ($itemFields as $itemField) {
2878			$this->remove_object("trackeritemfield", sprintf("%d:%d", (int)$itemId, (int)$itemField['fieldId']));
2879		}
2880
2881		$this->trackers()->update(
2882			['lastModif' => $this->now, 'items' => $this->trackers()->decrement(1)],
2883			['trackerId' => (int) $trackerId]
2884		);
2885
2886		$this->itemFields()->deleteMultiple(['itemId' => (int) $itemId]);
2887		$this->comments()->deleteMultiple(['object' => (int) $itemId, 'objectType' => 'trackeritem']);
2888		$this->attachments()->deleteMultiple(['itemId' => (int) $itemId]);
2889		$this->groupWatches()->deleteMultiple(['object' => (int) $itemId, 'event' => 'tracker_item_modified']);
2890		$this->userWatches()->deleteMultiple(['object' => (int) $itemId, 'event' => 'tracker_item_modified']);
2891		$this->items()->delete(['itemId' => (int) $itemId]);
2892
2893		$this->remove_stale_comment_watches();
2894
2895		// ---- delete image from disk -------------------------------------
2896		foreach ($imgList as $img) {
2897			if (file_exists($img)) {
2898				unlink($img);
2899			}
2900		}
2901
2902		// remove votes/ratings
2903		$userVotings = $this->table('tiki_user_votings');
2904		$userVotings->delete(['id' => $userVotings->like("tracker.$trackerId.$itemId.%")]);
2905
2906		$cachelib = TikiLib::lib('cache');
2907		$cachelib->invalidate('trackerItemLabel' . $itemId);
2908		foreach ($fieldList['data'] as $f) {
2909			$this->invalidate_field_cache($f['fieldId']);
2910		}
2911
2912		$options = $this->get_tracker_options($trackerId);
2913		if (isset($option) && isset($option['autoCreateCategories']) && $option['autoCreateCategories'] == 'y') {
2914			$categlib = TikiLib::lib('categ');
2915			$currentCategId = $categlib->get_category_id("Tracker Item $itemId");
2916			$categlib->remove_category($currentCategId);
2917		}
2918
2919		if (isset($options['autoCreateGroup']) && $options['autoCreateGroup'] == 'y') {
2920			$userlib = TikiLib::lib('user');
2921			$groupName = $this->groupName($options, $itemId);
2922			$userlib->remove_group($groupName);
2923		}
2924		$this->remove_item_log($itemId);
2925		$todolib = TikiLib::lib('todo');
2926		$todolib->delObjectTodo('trackeritem', $itemId);
2927
2928		$multilinguallib = TikiLib::lib('multilingual');
2929		$multilinguallib->detachTranslation('trackeritem', $itemId);
2930
2931		$tx = TikiDb::get()->begin();
2932
2933		$child = $this->findLinkedItems(
2934			$itemId,
2935			function ($field, $handler) use ($trackerId) {
2936				return $handler->cascadeDelete($trackerId);
2937			}
2938		);
2939
2940		foreach ($child as $i) {
2941			$this->remove_tracker_item($i);
2942		}
2943
2944		$tx->commit();
2945
2946		TikiLib::events()->trigger(
2947			'tiki.trackeritem.delete',
2948			[
2949				'type' => 'trackeritem',
2950				'object' => $itemId,
2951				'trackerId' => $trackerId,
2952				'user' => $GLOBALS['user'],
2953				'values' => $itemInfo,
2954			]
2955		);
2956
2957		return true;
2958	}
2959
2960	public function findUncascadedDeletes($itemId, $trackerId)
2961	{
2962		$fields = [];
2963		$child = $this->findLinkedItems(
2964			$itemId,
2965			function ($field, $handler) use ($trackerId, & $fields) {
2966				if (! $handler->cascadeDelete($trackerId)) {
2967					$fields[] = $field['fieldId'];
2968					return true;
2969				}
2970
2971				return false;
2972			}
2973		);
2974
2975		return ['itemIds' => $child, 'fieldIds' => array_unique($fields)];
2976	}
2977
2978	public function replaceItemReferences($replacement, $itemIds, $fieldIds)
2979	{
2980		$table = $this->itemFields();
2981		$table->update(['value' => $replacement], [
2982			'itemId' => $table->in($itemIds),
2983			'fieldId' => $table->in($fieldIds),
2984		]);
2985
2986		$events = TikiLib::events();
2987		foreach ($itemIds as $itemId) {
2988			$events->trigger('tiki.trackeritem.update', [
2989				'type' => 'trackeritem',
2990				'object' => $itemId,
2991				'user' => $GLOBALS['user'],
2992			]);
2993		}
2994	}
2995
2996	// filter examples: array('fieldId'=>array(1,2,3)) to look for a list of fields
2997	// array('or'=>array('isSearchable'=>'y', 'isTplVisible'=>'y')) for fields that are visible ou searchable
2998	// array('not'=>array('isHidden'=>'y')) for fields that are not hidden
2999	public function parse_filter($filter, &$mids, &$bindvars)
3000	{
3001		$tikilib = TikiLib::lib('tiki');
3002		foreach ($filter as $type => $val) {
3003			if ($type == 'or') {
3004				$midors = [];
3005				$this->parse_filter($val, $midors, $bindvars);
3006				$mids[] = '(' . implode(' or ', $midors) . ')';
3007			} elseif ($type == 'not') {
3008				$midors = [];
3009				$this->parse_filter($val, $midors, $bindvars);
3010				$mids[] = '!(' . implode(' and ', $midors) . ')';
3011			} elseif ($type == 'createdBefore') {
3012				$mids[] = 'tti.`created` < ?';
3013				$bindvars[] = $val;
3014			} elseif ($type == 'createdAfter') {
3015				$mids[] = 'tti.`created` > ?';
3016				$bindvars[] = $val;
3017			} elseif ($type == 'lastModifBefore') {
3018				$mids[] = 'tti.`lastModif` < ?';
3019				$bindvars[] = $val;
3020			} elseif ($type == 'lastModifAfter') {
3021				$mids[] = 'tti.`lastModif` > ?';
3022				$bindvars[] = $val;
3023			} elseif ($type == 'notItemId') {
3024				$mids[] = 'tti.`itemId` NOT IN(' . implode(",", array_fill(0, count($val), '?')) . ')';
3025				$bindvars = $val;
3026			} elseif (is_array($val)) {
3027				if (count($val) > 0) {
3028					if (! strstr($type, '`')) {
3029						$type = "`$type`";
3030					}
3031					$mids[] = "$type in (" . implode(",", array_fill(0, count($val), '?')) . ')';
3032					$bindvars = array_merge($bindvars, $val);
3033				}
3034			} else {
3035				if (! strstr($type, '`')) {
3036					$type = "`$type`";
3037				}
3038				$mids[] = "$type=?";
3039				$bindvars[] = $val;
3040			}
3041		}
3042	}
3043
3044	// Lists all the fields for an existing tracker
3045	public function list_tracker_fields($trackerId, $offset = 0, $maxRecords = -1, $sort_mode = 'position_asc', $find = '', $tra_name = true, $filter = '', $fields = '')
3046	{
3047		global $prefs;
3048		$smarty = TikiLib::lib('smarty');
3049		$fieldsTable = $this->fields();
3050
3051		if (! empty($trackerId)) {
3052			$conditions = ['trackerId' => (int) $trackerId];
3053		} else {
3054			return [];
3055		}
3056		if ($find) {
3057			$conditions['name'] = $fieldsTable->like("%$find%");
3058		}
3059		if (! empty($fields)) {
3060			$conditions['fieldId'] = $fieldsTable->in($fields);
3061		}
3062
3063		if (! empty($filter)) {
3064			$mids = [];
3065			$bindvars = [];
3066			$this->parse_filter($filter, $mids, $bindvars);
3067			$conditions['filter'] = $fieldsTable->expr(implode(' AND ', $mids), $bindvars);
3068		}
3069
3070		$result = $fieldsTable->fetchAll($fieldsTable->all(), $conditions, $maxRecords, $offset, $fieldsTable->sortMode($sort_mode));
3071		$cant = $fieldsTable->fetchCount($conditions);
3072
3073		$factory = new Tracker_Field_Factory;
3074		foreach ($result as & $res) {
3075			$typeInfo = $factory->getFieldInfo($res['type']);
3076			$options = Tracker_Options::fromSerialized($res['options'], $typeInfo);
3077			$res['options_array'] = $options->buildOptionsArray();
3078			$res['options_map'] = $options->getAllParameters();
3079			$res['itemChoices'] = ( $res['itemChoices'] != '' ) ? unserialize($res['itemChoices']) : [];
3080			$res['visibleBy'] = ($res['visibleBy'] != '') ? unserialize($res['visibleBy']) : [];
3081			$res['editableBy'] = ($res['editableBy'] != '') ? unserialize($res['editableBy']) : [];
3082			if ($tra_name && $prefs['feature_multilingual'] == 'y' && $prefs['language'] != 'en') {
3083				$res['name'] = tra($res['name']);
3084			}
3085			if ($res['type'] == 'p' && $res['options_array'][0] == 'language') {
3086				$langLib = TikiLib::lib('language');
3087				$smarty->assign('languages', $langLib->list_languages());
3088			}
3089			$ret[] = $res;
3090		}
3091
3092		return [
3093			'data' => $result,
3094			'cant' => $cant,
3095		];
3096	}
3097
3098	// Inserts or updates a tracker
3099	public function replace_tracker($trackerId, $name, $description, $options, $descriptionIsParsed)
3100	{
3101		$trackers = $this->trackers();
3102
3103		if ($descriptionIsParsed == 'y') {
3104			$parserlib = TikiLib::lib('parser');
3105			$description = $parserlib->process_save_plugins(
3106				$description,
3107				[
3108					'type' => 'tracker',
3109					'itemId' => $trackerId,
3110				]
3111			);
3112		}
3113
3114				$data = [
3115			'name' => $name,
3116			'description' => $description,
3117			'descriptionIsParsed' => $descriptionIsParsed,
3118			'lastModif' => $this->now,
3119				];
3120
3121				$logOption = 'Updated';
3122				if ($trackerId) {
3123					$finalEvent = 'tiki.tracker.update';
3124					$conditions = ['trackerId' => (int) $trackerId];
3125					if ($trackers->fetchCount($conditions)) {
3126						$trackers->update($data, $conditions);
3127					} else {
3128						$data['trackerId'] = (int) $trackerId;
3129						$data['items'] = 0;
3130						$data['created'] = $this->now;
3131						$trackers->insert($data);
3132						$logOption = 'Created';
3133					}
3134				} else {
3135					$finalEvent = 'tiki.tracker.create';
3136					$data['created'] = $this->now;
3137					$trackerId = $trackers->insert($data);
3138				}
3139
3140				$wikiParsed = $descriptionIsParsed == 'y';
3141				$wikilib = TikiLib::lib('wiki');
3142				$wikilib->update_wikicontent_relations($description, 'tracker', (int)$trackerId, $wikiParsed);
3143				$wikilib->update_wikicontent_links($description, 'tracker', (int)$trackerId, $wikiParsed);
3144
3145				$optionTable = $this->options();
3146				$optionTable->deleteMultiple(['trackerId' => (int) $trackerId]);
3147
3148				foreach ($options as $kopt => $opt) {
3149					$this->replace_tracker_option((int) $trackerId, $kopt, $opt);
3150				}
3151
3152				$definition = Tracker_Definition::get($trackerId);
3153				$ratingId = $definition->getRateField();
3154
3155				if (isset($options['useRatings']) && $options['useRatings'] == 'y') {
3156					if (! $ratingId) {
3157						$ratingId = 0;
3158					}
3159
3160					$ratingoptions = isset($options['ratingOptions']) ? $options['ratingOptions'] : '';
3161					$showratings = isset($options['showRatings']) ? $options['showRatings'] : 'n';
3162					$this->replace_tracker_field($trackerId, $ratingId, 'Rating', 's', '-', '-', $showratings, 'y', 'n', '-', 0, $ratingoptions);
3163				}
3164				$this->clear_tracker_cache($trackerId);
3165				$this->update_tracker_summary(['trackerId' => $trackerId]);
3166
3167				if ($logOption) {
3168					$logslib = TikiLib::lib('logs');
3169					$logslib->add_action(
3170						$logOption,
3171						$trackerId,
3172						'tracker',
3173						[
3174						'name' => $data['name'],
3175						]
3176					);
3177				}
3178
3179				TikiLib::events()->trigger($finalEvent, [
3180				'type' => 'tracker',
3181				'object' => $trackerId,
3182				'user' => $GLOBALS['user'],
3183				]);
3184
3185		return $trackerId;
3186	}
3187
3188	public function replace_tracker_option($trackerId, $name, $value)
3189	{
3190		$optionTable = $this->options();
3191		$optionTable->insertOrUpdate(['value' => $value], ['trackerId' => $trackerId, 'name' => $name]);
3192	}
3193
3194	public function clear_tracker_cache($trackerId)
3195	{
3196		global $prefs;
3197
3198		$cachelib = TikiLib::lib('cache');
3199
3200		foreach ($this->get_all_tracker_items($trackerId) as $itemId) {
3201				$cachelib->invalidate('trackerItemLabel' . $itemId);
3202		}
3203		if (in_array('trackerrender', $prefs['unified_cached_formatters'])) {
3204			$cachelib->empty_type_cache('search_valueformatter');
3205		}
3206	}
3207
3208
3209	public function replace_tracker_field($trackerId, $fieldId, $name, $type, $isMain, $isSearchable, $isTblVisible, $isPublic, $isHidden, $isMandatory, $position, $options, $description = '', $isMultilingual = '', $itemChoices = null, $errorMsg = '', $visibleBy = null, $editableBy = null, $descriptionIsParsed = 'n', $validation = '', $validationParam = '', $validationMessage = '', $permName = null, $rules = null)
3210	{
3211		// Serialize choosed items array (items of the tracker field to be displayed in the list proposed to the user)
3212		if (is_array($itemChoices) && count($itemChoices) > 0 && ! empty($itemChoices[0])) {
3213			$itemChoices = serialize($itemChoices);
3214		} else {
3215			$itemChoices = '';
3216		}
3217		if (is_array($visibleBy) && count($visibleBy) > 0 && ! empty($visibleBy[0])) {
3218			$visibleBy = serialize($visibleBy);
3219		} else {
3220			$visibleBy = '';
3221		}
3222		if (is_array($editableBy) && count($editableBy) > 0 && ! empty($editableBy[0])) {
3223			$editableBy = serialize($editableBy);
3224		} else {
3225			$editableBy = '';
3226		}
3227		if ($descriptionIsParsed == 'y') {
3228			$parserlib = TikiLib::lib('parser');
3229			$description = $parserlib->process_save_plugins(
3230				$description,
3231				[
3232					'type' => 'trackerfield',
3233					'itemId' => $fieldId,
3234				]
3235			);
3236		}
3237
3238		$fields = $this->fields();
3239
3240		$data = [
3241			'name' => $name,
3242			'permName' => empty($permName) ? null : $permName,
3243			'type' => $type,
3244			'isMain' => $isMain,
3245			'isSearchable' => $isSearchable,
3246			'isTblVisible' => $isTblVisible,
3247			'isPublic' => $isPublic,
3248			'isHidden' => $isHidden,
3249			'isMandatory' => $isMandatory,
3250			'position' => (int) $position,
3251			'options' => $options,
3252			'isMultilingual' => $isMultilingual,
3253			'description' => $description,
3254			'itemChoices' => $itemChoices,
3255			'errorMsg' => $errorMsg,
3256			'visibleBy' => $visibleBy,
3257			'editableBy' => $editableBy,
3258			'descriptionIsParsed' => $descriptionIsParsed,
3259			'validation' => $validation,
3260			'validationParam' => $validationParam,
3261			'validationMessage' => $validationMessage,
3262			'rules' => $rules,
3263		];
3264
3265		$logOption = null;
3266
3267		if ($fieldId) {
3268			// -------------------------------------
3269			// remove images when needed
3270			$old_field = $this->get_tracker_field($fieldId);
3271			if (! empty($old_field['fieldId'])) {
3272				if ($old_field['type'] == 'i' && $type != 'i') {
3273					$this->remove_field_images($fieldId);
3274				}
3275
3276				$fields->update($data, ['fieldId' => (int) $fieldId]);
3277				$logOption = 'modify_field';
3278
3279				$data['trackerId'] = (int) $old_field['trackerId'];
3280			} else {
3281				$data['trackerId'] = (int) $trackerId;
3282				$data['fieldId'] = (int) $fieldId;
3283				$fields->insert($data);
3284				$logOption = 'add_field';
3285			}
3286		} else {
3287			$data['trackerId'] = (int) $trackerId;
3288			$fieldId = $fields->insert($data);
3289			$logOption = 'add_field';
3290
3291			if (! $permName) {
3292				// Apply a default value to perm name when not specified
3293				$fields->update(['permName' => 'f_' . $fieldId], ['fieldId' => $fieldId]);
3294			}
3295
3296			$itemFields = $this->itemFields();
3297			foreach ($this->get_all_tracker_items($trackerId) as $itemId) {
3298				$itemFields->deleteMultiple(['itemId' => (int) $itemId, 'fieldId' => $fieldId]);
3299				$itemFields->insert(['itemId' => (int) $itemId, 'fieldId' => (int) $fieldId, 'value' => '']);
3300			}
3301		}
3302
3303		$wikiParsed = $descriptionIsParsed == 'y';
3304		$wikilib = TikiLib::lib('wiki');
3305		$wikilib->update_wikicontent_relations($description, 'trackerfield', (int)$fieldId, $wikiParsed);
3306		$wikilib->update_wikicontent_links($description, 'trackerfield', (int)$fieldId, $wikiParsed);
3307
3308		if ($logOption) {
3309			$logslib = TikiLib::lib('logs');
3310			$logslib->add_action(
3311				'Updated',
3312				$data['trackerId'],
3313				'tracker',
3314				[
3315					'operation' => $logOption,
3316					'fieldId' => $fieldId,
3317					'name' => $data['name'],
3318				]
3319			);
3320
3321			TikiLib::events()->trigger(
3322				$logOption == 'add_field' ? 'tiki.trackerfield.create' : 'tiki.trackerfield.update',
3323				['type' => 'trackerfield', 'object' => $fieldId]
3324			);
3325		}
3326
3327		$this->clear_tracker_cache($trackerId);
3328		return $fieldId;
3329	}
3330
3331	public function replace_rating($trackerId, $itemId, $fieldId, $user, $new_rate)
3332	{
3333		global $tiki_p_tracker_vote_ratings, $tiki_p_tracker_revote_ratings;
3334		$itemFields = $this->itemFields();
3335
3336		if ($new_rate === null) {
3337			$new_rate = 0;
3338		}
3339
3340		if ($tiki_p_tracker_vote_ratings != 'y') {
3341			return;
3342		}
3343		$key = "tracker.$trackerId.$itemId";
3344		$olrate = $this->get_user_vote($key, $user) ?: 0;
3345		$allow_revote = $tiki_p_tracker_revote_ratings == 'y';
3346		$count = $itemFields->fetchCount(['itemId' => (int) $itemId, 'fieldId' => (int) $fieldId]);
3347		$tikilib = TikiLib::lib('tiki');
3348		if (! $tikilib->register_user_vote($user, $key, $new_rate, [], $allow_revote)) {
3349			return;
3350		}
3351
3352		if (! $count) {
3353			$itemFields->insert(['value' => (int) $new_rate, 'itemId' => (int) $itemId, 'fieldId' => (int) $fieldId]);
3354			$outValue = $new_rate;
3355		} else {
3356			$conditions = [
3357				'itemId' => (int) $itemId,
3358				'fieldId' => (int) $fieldId,
3359			];
3360
3361			$val = $itemFields->fetchOne('value', $conditions);
3362			$outValue = $val - $olrate + $new_rate;
3363
3364			$itemFields->update(['value' => $outValue], $conditions);
3365		}
3366
3367		TikiLib::events()->trigger('tiki.trackeritem.rating', [
3368			'type' => 'trackeritem',
3369			'object' => (int) $itemId,
3370			'trackerId' => (int) $trackerId,
3371			'fieldId' => (int) $fieldId,
3372			'user' => $user,
3373			'rating' => $new_rate, // User's selected value, not the stored one
3374		]);
3375
3376		return $outValue;
3377	}
3378
3379	public function replace_star($userValue, $trackerId, $itemId, &$field, $user, $updateField = true)
3380	{
3381		global $tiki_p_tracker_vote_ratings, $tiki_p_tracker_revote_ratings, $prefs;
3382		if ($field['type'] != '*' && $field['type'] != 'STARS') {
3383			return;
3384		}
3385		if ($userValue != 'NULL' && isset($field['rating_options']) && ! in_array($userValue, $field['rating_options'])) {
3386			return;
3387		}
3388		if ($userValue != 'NULL' && ! isset($field['rating_options']) && ! in_array($userValue, $field['options_array'])) {
3389			// backward compatibility with trackerlist rating which does not have rating options
3390			return;
3391		}
3392		if ($tiki_p_tracker_vote_ratings != 'y') {
3393			return;
3394		}
3395		$key = "tracker.$trackerId.$itemId." . $field['fieldId'];
3396
3397		$allow_revote = $tiki_p_tracker_revote_ratings == 'y';
3398		$tikilib = TikiLib::lib('tiki');
3399		$result = $tikilib->register_user_vote($user, $key, $userValue, [], $allow_revote);
3400
3401		$votings = $this->table('tiki_user_votings');
3402		$data = $votings->fetchRow(['count' => $votings->count(), 'total' => $votings->sum('optionId')], ['id' => $key]);
3403		$field['numvotes'] = $data['count'];
3404		$field['my_rate'] = $userValue;
3405		$field['voteavg'] = $field['value'] = $data['total'] / $field['numvotes'];
3406
3407		if ($result) {
3408			TikiLib::events()->trigger('tiki.trackeritem.rating', [
3409				'type' => 'trackeritem',
3410				'object' => $itemId,
3411				'trackerId' => $trackerId,
3412				'fieldId' => $field['fieldId'],
3413				'user' => $user,
3414				'rating' => $userValue,
3415			]);
3416		}
3417
3418		return $result;
3419	}
3420
3421	public function remove_tracker($trackerId)
3422	{
3423		$transaction = $this->begin();
3424
3425		// ---- delete image from disk -------------------------------------
3426		$fieldList = $this->list_tracker_fields($trackerId, 0, -1, 'name_asc', '');
3427		foreach ($fieldList['data'] as $f) {
3428			if ($f['type'] == 'i') {
3429				$this->remove_field_images($f['fieldId']);
3430			}
3431		}
3432
3433		$option = $this->get_tracker_options($trackerId);
3434		if (isset($option) && isset($option['autoCreateCategories']) && $option['autoCreateCategories'] == 'y') {
3435			$categlib = TikiLib::lib('categ');
3436			$currentCategId = $categlib->get_category_id("Tracker $trackerId");
3437			$categlib->remove_category($currentCategId);
3438		}
3439
3440		foreach ($this->get_all_tracker_items($trackerId) as $itemId) {
3441			$this->remove_tracker_item($itemId);
3442		}
3443
3444		$fields = $this->fields()->fetchAll(['fieldId'], ['trackerId' => $trackerId]);
3445		foreach ($fields as $field) {
3446			$this->remove_object("trackerfield", $field['fieldId']);
3447		}
3448
3449		$conditions = [
3450			'trackerId' => (int) $trackerId,
3451		];
3452
3453		$this->fields()->deleteMultiple($conditions);
3454		$this->options()->deleteMultiple($conditions);
3455		$this->trackers()->delete($conditions);
3456
3457		// remove votes/ratings
3458		$userVotings = $this->table('tiki_user_votings');
3459		$userVotings->delete(['id' => $userVotings->like("tracker.$trackerId.%")]);
3460
3461		$this->remove_object('tracker', $trackerId);
3462
3463		$logslib = TikiLib::lib('logs');
3464		$logslib->add_action('Removed', $trackerId, 'tracker');
3465
3466		$this->clear_tracker_cache($trackerId);
3467
3468		TikiLib::events()->trigger('tiki.tracker.delete', [
3469			'type' => 'tracker',
3470			'object' => $trackerId,
3471			'user' => $GLOBALS['user'],
3472		]);
3473
3474		$transaction->commit();
3475
3476		return true;
3477	}
3478
3479	public function remove_tracker_field($fieldId, $trackerId)
3480	{
3481		$cachelib = TikiLib::lib('cache');
3482		$logslib = TikiLib::lib('logs');
3483
3484		// -------------------------------------
3485		// remove images when needed
3486		$field = $this->get_tracker_field($fieldId);
3487		if ($field['type'] == 'i') {
3488			$this->remove_field_images($fieldId);
3489		}
3490
3491		$handler = $this->get_field_handler($field);
3492		if ($handler && method_exists($handler, 'handleFieldRemove')) {
3493			$handler->handleFieldRemove();
3494		}
3495
3496		$conditions = [
3497			'fieldId' => (int) $fieldId,
3498		];
3499
3500		$this->fields()->delete($conditions);
3501		$this->itemFields()->deleteMultiple($conditions);
3502
3503		$this->invalidate_field_cache($fieldId);
3504
3505		$this->clear_tracker_cache($trackerId);
3506
3507		$logslib = TikiLib::lib('logs');
3508		$logslib->add_action(
3509			'Updated',
3510			$trackerId,
3511			'tracker',
3512			[
3513				'operation' => 'remove_field',
3514				'fieldId' => $fieldId,
3515			]
3516		);
3517		$this->remove_object('trackerfield', $fieldId);
3518		TikiLib::events()->trigger(
3519			'tiki.trackerfield.delete',
3520			['type' => 'trackerfield', 'object' => $fieldId]
3521		);
3522
3523		return true;
3524	}
3525
3526	/**
3527	 * get_trackers_containing
3528	 *
3529	 * \brief Get tracker names containing ... (useful for auto-complete)
3530	 *
3531	 * @author luci
3532	 * @param mixed $name
3533	 * @access public
3534	 * @return
3535	 */
3536	function get_trackers_containing($name)
3537	{
3538		if (empty($name)) {
3539			return [];
3540		}
3541		//FIXME: perm filter ?
3542		$result = $this->fetchAll(
3543			'SELECT `name` FROM `tiki_trackers` WHERE `name` LIKE ?',
3544			[$name . '%'],
3545			10
3546		);
3547		$names = [];
3548		foreach ($result as $row) {
3549			$names[] = $row['name'];
3550		}
3551		return $names;
3552	}
3553
3554	/**
3555	 * Returns the trackerId of the tracker possessing the item ($itemId)
3556	 */
3557	public function get_tracker_for_item($itemId)
3558	{
3559		return $this->items()->fetchOne('trackerId', ['itemId' => (int) $itemId]);
3560	}
3561
3562	public function get_tracker_options($trackerId)
3563	{
3564		return $this->options()->fetchMap('name', 'value', ['trackerId' => (int) $trackerId]);
3565	}
3566
3567	public function get_trackers_options($trackerId, $option = '', $find = '', $not = '')
3568	{
3569		$options = $this->options();
3570		$conditions = [];
3571
3572		if (! empty($trackerId)) {
3573			$conditions['trackerId'] = (int) $trackerId;
3574		}
3575
3576		if (! empty($option)) {
3577			$conditions['name'] = $option;
3578		}
3579
3580		if ($not == 'null' || $not == 'empty') {
3581			$conditions['value'] = $options->not('');
3582		}
3583
3584		if (! empty($find)) {
3585			$conditions['value'] = $options->like("%$find%");
3586		}
3587
3588		return $options->fetchAll($options->all(), $conditions);
3589	}
3590
3591	public function get_tracker_field($fieldIdOrPermName)
3592	{
3593		static $cache = [];
3594		if (isset($cache[$fieldIdOrPermName])) {
3595			return $cache[$fieldIdOrPermName];
3596		}
3597		if ((int)$fieldIdOrPermName > 0) {
3598			$res = $this->fields()->fetchFullRow(['fieldId' => (int)$fieldIdOrPermName]);
3599		} else {
3600			$res = $this->fields()->fetchFullRow(['permName' => $fieldIdOrPermName]);
3601		}
3602		if ($res) {
3603			$factory = new Tracker_Field_Factory;
3604			$options = Tracker_Options::fromSerialized($res['options'], $factory->getFieldInfo($res['type']));
3605			$res['options_array'] = $options->buildOptionsArray();
3606			$res['itemChoices'] = ! empty($res['itemChoices']) ? unserialize($res['itemChoices']) : [];
3607			$res['visibleBy'] = ! empty($res['visibleBy']) ? unserialize($res['visibleBy']) : [];
3608			$res['editableBy'] = ! empty($res['editableBy']) ? unserialize($res['editableBy']) : [];
3609			if (TikiLib::lib('tiki')->get_memory_avail() < 1048576 * 10) {
3610				$cache = [];
3611			}
3612			$cache[$fieldIdOrPermName] = $res;
3613			return $res;
3614		}
3615	}
3616
3617	public function get_field_id($trackerId, $name, $lookup = 'name')
3618	{
3619		return $this->fields()->fetchOne('fieldId', ['trackerId' => (int) $trackerId, $lookup => $name]);
3620	}
3621
3622	/**
3623	 * Return a tracker field id from it's type. By default
3624	 * it return only the first field of the searched type.
3625	 *
3626	 * @param int $trackerId tracker id
3627	 * @param string $type field type (in general an one letter code)
3628	 * @param string $option a value (or values separated by comma) that a tracker field must have in its options (it will be used inside a LIKE statement so most of the times it is a good idea to use %)
3629	 * @param bool $first if true return only the first field of the searched type, if false return all the fields of the searched type
3630	 * @param string $name filter by tracker field name
3631	 * @return int|array tracker field id or list of tracker fields ids
3632	 */
3633	public function get_field_id_from_type($trackerId, $type, $option = null, $first = true, $name = null)
3634	{
3635		static $memo;
3636		if (! is_array($type) && isset($memo[$trackerId][$type][$option])) {
3637			return $memo[$trackerId][$type][$option];
3638		}
3639
3640		$conditions = [
3641			'trackerId' => (int) $trackerId,
3642		];
3643		$fields = $this->fields();
3644
3645		if (is_array($type)) {
3646			$conditions['type'] = $fields->in($type, true);
3647		} else {
3648			$conditions['type'] = $fields->exactly($type);
3649		}
3650
3651		if (! empty($option)) {
3652			throw new Exception("\$option parameter no longer supported. Code needs fixing.");
3653		}
3654
3655		if (! empty($name)) {
3656			$conditions['name'] = $name;
3657		}
3658
3659		if ($first) {
3660			$fieldId = $fields->fetchOne('fieldId', $conditions);
3661			$memo[$trackerId][$type][$option] = $fieldId;
3662			return $fieldId;
3663		} else {
3664			return $fields->fetchColumn('fieldId', $conditions);
3665		}
3666	}
3667
3668	public function get_page_field($trackerId)
3669	{
3670		$definition = Tracker_Definition::get($trackerId);
3671		$score = 0;
3672		$out = null;
3673
3674		foreach ($definition->getFields() as $field) {
3675			if ($field['type'] == 'k') {
3676				if ($score < 3 && $field['options_map']['autoassign'] == '1') {
3677					$score = 3;
3678					$out = $field;
3679				} elseif ($score < 2 && $field['options_map']['create'] == '1') {
3680					// Not sure about this one, old code used to say "has a 1 somewhere in the options string"
3681					// Create seems to be the most likely candidate
3682					$score = 2;
3683					$out = $field;
3684				} else {
3685					$score = 1;
3686					$out = $field;
3687				}
3688			}
3689		}
3690
3691		return $out;
3692	}
3693
3694	/*
3695	** function only used for the popup for more infos on attachements
3696	*  returns an array with field=>value
3697	*/
3698	public function get_moreinfo($attId)
3699	{
3700		$query = "select o.`value`, o.`trackerId` from `tiki_tracker_options` o";
3701		$query .= " left join `tiki_tracker_items` i on o.`trackerId`=i.`trackerId` ";
3702		$query .= " left join `tiki_tracker_item_attachments` a on i.`itemId`=a.`itemId` ";
3703		$query .= " where a.`attId`=? and o.`name`=?";
3704		$result = $this->query($query, [(int) $attId, 'orderAttachments']);
3705		$resu = $result->fetchRow();
3706		if ($resu) {
3707			$resu['orderAttachments'] = $resu['value'];
3708		} else {
3709			$query = "select `orderAttachments`, t.`trackerId` from `tiki_trackers` t ";
3710			$query .= " left join `tiki_tracker_items` i on t.`trackerId`=i.`trackerId` ";
3711			$query .= " left join `tiki_tracker_item_attachments` a on i.`itemId`=a.`itemId` ";
3712			$query .= " where a.`attId`=? ";
3713			$result = $this->query($query, [(int) $attId]);
3714			$resu = $result->fetchRow();
3715		}
3716		if (strstr($resu['orderAttachments'], '|')) {
3717			$fields = preg_split('/,/', substr($resu['orderAttachments'], strpos($resu['orderAttachments'], '|') + 1));
3718			$res = $this->attachments()->fetchRow($fields, ['attId' => (int) $attId]);
3719			$res["trackerId"] = $resu['trackerId'];
3720			$res["longdesc"] = isset($res['longdesc']) ? TikiLib::lib('parser')->parse_data($res['longdesc']) : '';
3721		} else {
3722			$res = [tra("Message") => tra("No extra information for that attached file. ")];
3723			$res['trackerId'] = 0;
3724		}
3725		return $res;
3726	}
3727
3728	public function field_types()
3729	{
3730
3731		$types = [];
3732
3733		$factory = new Tracker_Field_Factory(false);
3734		foreach ($factory->getFieldTypes() as $key => $info) {
3735			$types[$key] = [
3736				'label' => $info['name'],
3737				'opt' => count($info['params']) === 0,
3738				'help' => $this->build_help_for_type($info),
3739			];
3740		}
3741
3742		return $types;
3743	}
3744
3745	private function build_help_for_type($info)
3746	{
3747		$function = tr('Function');
3748		$text = "<p><strong>$function:</strong> {$info['description']}</p>";
3749
3750		if (count($info['params'])) {
3751			$text .= '<dl>';
3752			foreach ($info['params'] as $key => $param) {
3753				if (isset($param['count'])) {
3754					$text .= "<dt>{$param['name']}[{$param['count']}]</dt>";
3755				} else {
3756					$text .= "<dt>{$param['name']}</dt>";
3757				}
3758
3759				$text .= "<dd>{$param['description']}</dd>";
3760
3761				if (isset($param['options'])) {
3762					$text .= "<dd><ul>";
3763					foreach ($param['options'] as $k => $label) {
3764						$text .= "<li><strong>{$k}</strong> = <em>$label</em></li>";
3765					}
3766					$text .= "</ul></dd>";
3767				}
3768			}
3769			$text .= '</dl>';
3770		}
3771
3772		return "<div>{$text}</div>";
3773	}
3774
3775	/**
3776	 * @param string $lg The language key to translate the status labels, if different than preferences.
3777	 * @return mixed
3778	 */
3779	public function status_types($lg = '')
3780	{
3781		$status['o'] = ['name' => 'open', 'label' => tra('Open', $lg),'perm' => 'tiki_p_view_trackers',
3782			'image' => 'img/icons/status_open.gif', 'iconname' => 'status-open'];
3783		$status['p'] = ['name' => 'pending', 'label' => tra('Pending', $lg),'perm' => 'tiki_p_view_trackers_pending',
3784			'image' => 'img/icons/status_pending.gif', 'iconname' => 'status-pending'];
3785		$status['c'] = ['name' => 'closed', 'label' => tra('Closed', $lg),'perm' => 'tiki_p_view_trackers_closed',
3786			'image' => 'img/icons/status_closed.gif', 'iconname' => 'status-closed'];
3787		return $status;
3788	}
3789
3790	public function get_isMain_value($trackerId, $itemId)
3791	{
3792		global $prefs;
3793
3794		$query = "select tif.`value` from `tiki_tracker_item_fields` tif, `tiki_tracker_items` i, `tiki_tracker_fields` tf where i.`itemId`=? and i.`itemId`=tif.`itemId` and tf.`fieldId`=tif.`fieldId` and tf.`isMain`=? ORDER BY tf.`position`";
3795		$result = $this->getOne($query, [ (int) $itemId, "y"]);
3796
3797		$main_field_type = $this->get_main_field_type($trackerId);
3798
3799		if (in_array($main_field_type, ['r','q', 'p'])) {	// for ItemLink, AutoIncrement and UserPref fields use the proper output method
3800			$definition = Tracker_Definition::get($trackerId);
3801			$field = $definition->getField($this->get_main_field($trackerId));
3802			$item = $this->get_tracker_item($itemId);
3803			$handler = $this->get_field_handler($field, $item);
3804			$result = $handler->renderOutput(['list_mode' => 'csv']);
3805		}
3806
3807		if (strlen($result) && $result{0} === '{') {
3808			$result = json_decode($result, true);
3809			if (isset($result[$prefs['language']])) {
3810				return $result[$prefs['language']];
3811			} elseif (is_array($result)) {
3812				return reset($result);
3813			}
3814		}
3815
3816		return $result;
3817	}
3818
3819	public function get_main_field_type($trackerId)
3820	{
3821		return $this->fields()->fetchOne('type', ['isMain' => 'y', 'trackerId' => $trackerId], ['position' => 'ASC']);
3822	}
3823
3824	public function get_main_field($trackerId)
3825	{
3826		return $this->fields()->fetchOne('fieldId', ['isMain' => 'y', 'trackerId' => $trackerId], ['position' => 'ASC']);
3827	}
3828
3829	public function categorized_item($trackerId, $itemId, $mainfield, $ins_categs, $parent_categs_only = [], $override_perms = false, $managed_fields = null)
3830	{
3831		global $prefs;
3832
3833		// Collect the list of possible categories, those provided by a complete form
3834		// The update_object_categories function will limit changes to those
3835		$managed_categories = [];
3836
3837		$definition = Tracker_Definition::get($trackerId);
3838		foreach ($definition->getCategorizedFields() as $t) {
3839			if ($managed_fields && ! in_array($t, $managed_fields)) {
3840				continue;
3841			}
3842
3843			$this->itemFields()->insert(['itemId' => $itemId, 'fieldId' => $t,	'value' => ''], true);
3844
3845			$field = $definition->getField($t);
3846			$handler = $this->get_field_handler($field);
3847			$data = $handler->getFieldData();
3848			$datalist = $data['list'];
3849			if (! empty($parent_categs_only)) {
3850				foreach ($datalist as $k => $entry) {
3851					$parentId = TikiLib::lib('categ')->get_category_parent($entry['categId']);
3852					if (! in_array($parentId, $parent_categs_only)) {
3853						unset($datalist[$k]);
3854					}
3855				}
3856			}
3857
3858			$managed_categories = array_merge(
3859				$managed_categories,
3860				array_map(
3861					function ($entry) {
3862						return $entry['categId'];
3863					},
3864					$datalist
3865				)
3866			);
3867		}
3868
3869		$this->update_item_categories($itemId, $managed_categories, $ins_categs, $override_perms);
3870
3871		$items = $this->findLinkedItems(
3872			$itemId,
3873			function ($field, $handler) use ($trackerId) {
3874				return $handler->cascadeCategories($trackerId);
3875			}
3876		);
3877
3878		$searchlib = TikiLib::lib('unifiedsearch');
3879		$index = $prefs['feature_search'] === 'y' && $prefs['unified_incremental_update'] === 'y';
3880
3881		foreach ($items as $child) {
3882			$this->update_item_categories($child, $managed_categories, $ins_categs, $override_perms);
3883
3884			if ($index) {
3885				$searchlib->invalidateObject('trackeritem', $child);
3886			}
3887		}
3888	}
3889
3890	private function update_item_categories($itemId, $managed_categories, $ins_categs, $override_perms)
3891	{
3892		$categlib = TikiLib::lib('categ');
3893		$cat_desc = '';
3894		$cat_name = $this->get_isMain_value(null, $itemId);
3895
3896		// The following needed to ensure category field exist for item (to be readable by list_items)
3897		// Update 2016: Needs to be the non-sefurl in case the feature is disabled later as this is stored in tiki_objects
3898		// and used in tiki-browse_categories.php and other places
3899		$cat_href = "tiki-view_tracker_item.php?itemId=$itemId";
3900
3901		$categlib->update_object_categories($ins_categs, $itemId, 'trackeritem', $cat_desc, $cat_name, $cat_href, $managed_categories, $override_perms);
3902	}
3903
3904	public function move_up_last_fields($trackerId, $fieldId, $delta = 1)
3905	{
3906		$type = ($delta > 0) ? 'increment' : 'decrement';
3907
3908		$this->fields()->update(
3909			['position' => $this->fields()->$type(abs($delta))],
3910			['trackerId' => (int) $trackerId, 'fieldId' => (int) $fieldId]
3911		);
3912	}
3913
3914	/* list all the values of a field
3915	 */
3916	public function list_tracker_field_values($trackerId, $fieldId, $status = 'o', $distinct = 'y', $lang = '', $exceptItemId = '')
3917	{
3918		$mid = '';
3919		$bindvars[] = (int) $fieldId;
3920		if (! $this->getSqlStatus($status, $mid, $bindvars, $trackerId)) {
3921			return null;
3922		}
3923		$sort_mode = "value_asc";
3924		$distinct = $distinct == 'y' ? 'distinct' : '';
3925		if (! empty($exceptItemId)) {
3926			$mid .= ' and ttif.`itemId` != ? ';
3927			$bindvars[] = $exceptItemId;
3928		}
3929		$query = "select $distinct(ttif.`value`) from `tiki_tracker_item_fields` ttif, `tiki_tracker_items` tti where tti.`itemId`= ttif.`itemId`and ttif.`fieldId`=? $mid order by " . $this->convertSortMode($sort_mode);
3930		$result = $this->query($query, $bindvars);
3931		$ret = [];
3932		while ($res = $result->fetchRow()) {
3933			$ret[] = $res['value'];
3934		}
3935		return $ret;
3936	}
3937
3938	/* tests if a value exists in a field
3939	 */
3940	public function check_field_value_exists($value, $fieldId, $exceptItemId = 0)
3941	{
3942		$itemFields = $this->itemFields();
3943
3944		$conditions = [
3945			'fieldId' => (int) $fieldId,
3946			'value' => $value,
3947		];
3948
3949		if ($exceptItemId > 0) {
3950			$conditions['itemId'] = $itemFields->not((int) $exceptItemId);
3951		}
3952
3953		return $itemFields->fetchCount($conditions) > 0;
3954	}
3955
3956	public function is_multilingual($fieldId)
3957	{
3958		global $prefs;
3959
3960		if ($fieldId < 1) {
3961			return 'n';
3962		}
3963
3964		if ($prefs['feature_multilingual'] != 'y') {
3965			return 'n';
3966		}
3967
3968		$is = $this->fields()->fetchOne('isMultilingual', ['fieldId' => (int) $fieldId]);
3969
3970		return ($is == 'y') ? 'y' : 'n';
3971	}
3972
3973	/* return the values of $fieldIdOut of an item that has a value $value for $fieldId */
3974	public function get_filtered_item_values($fieldId, $value, $fieldIdOut)
3975	{
3976		$query = "select ttifOut.`value` from `tiki_tracker_item_fields` ttifOut, `tiki_tracker_item_fields` ttif
3977			where ttifOut.`itemId`= ttif.`itemId`and ttif.`fieldId`=? and ttif.`value`=? and ttifOut.`fieldId`=?";
3978		$bindvars = [$fieldId, $value, $fieldIdOut];
3979		$result = $this->query($query, $bindvars);
3980		$ret = [];
3981		while ($res = $result->fetchRow()) {
3982			$ret[] = $res['value'];
3983		}
3984		return $ret;
3985	}
3986
3987	/* look if a tracker has only one item per user and if an item has already being created for the user or the IP*/
3988	public function get_user_item(&$trackerId, $trackerOptions, $userparam = null, $user = null, $status = '')
3989	{
3990		global $prefs;
3991		$tikilib = TikiLib::lib('tiki');
3992		$userlib = TikiLib::lib('user');
3993		if (empty($user)) {
3994			$user = $GLOBALS['user'];
3995		}
3996		if (empty($trackerId) && $prefs['userTracker'] == 'y') {
3997			$utid = $userlib->get_tracker_usergroup($user);
3998			if (! empty($utid['usersTrackerId'])) {
3999				$trackerId = $utid['usersTrackerId'];
4000				$itemId = $this->get_item_id($trackerId, $utid['usersFieldId'], $user);
4001			}
4002			return $itemId;
4003		}
4004
4005		$definition = Tracker_Definition::get($trackerId);
4006		$userreal = $userparam != null ? $userparam : $user;
4007		if (! empty($userreal)) {
4008			if ($fieldId = $definition->getUserField()) {
4009				// user creator field
4010				$value = $userreal;
4011				$items = $this->get_items_list($trackerId, $fieldId, $value, $status, true);
4012				if (! empty($items)) {
4013					return $items[0];
4014				}
4015			}
4016		}
4017		if ($fieldId = $definition->getAuthorIpField()) {
4018			// IP creator field
4019			$IP = $tikilib->get_ip_address();
4020			$items = $this->get_items_list($trackerId, $fieldId, $IP, $status);
4021			if (! empty($items)) {
4022				return $items[0];
4023			} else {
4024				return 0;
4025			}
4026		}
4027	}
4028
4029	public function get_item_creators($trackerId, $itemId)
4030	{
4031		$definition = Tracker_Definition::get($trackerId);
4032
4033		$owners = array_map(function ($fieldId) use ($trackerId, $itemId) {
4034
4035			$owners = $this->get_item_value($trackerId, $itemId, $fieldId);
4036			return $this->parse_user_field($owners);
4037		}, $definition->getItemOwnerFields());
4038
4039		if ($owners) {
4040			return call_user_func_array('array_merge', $owners);
4041		} else {
4042			return [];
4043		}
4044	}
4045
4046	/* find the best fieldwhere you can do a filter on the initial
4047	 * 1) if sort_mode and sort_mode is a text and the field is visible
4048	 * 2) the first main taht is text
4049	 */
4050	public function get_initial_field($list_fields, $sort_mode)
4051	{
4052		if (preg_match('/^f_([^_]*)_?.*/', $sort_mode, $matches)) {
4053			if (isset($list_fields[$matches[1]])) {
4054				$type = $list_fields[$matches[1]]['type'];
4055				if (in_array($type, ['t', 'a', 'm'])) {
4056					return $matches[1];
4057				}
4058			}
4059		}
4060		foreach ($list_fields as $fieldId => $field) {
4061			if ($field['isMain'] == 'y' && in_array($field['type'], ['t', 'a', 'm'])) {
4062				return $fieldId;
4063			}
4064		}
4065	}
4066
4067	public function get_nb_items($trackerId)
4068	{
4069		return $this->items()->fetchCount(['trackerId' => (int) $trackerId]);
4070	}
4071
4072	public function duplicate_tracker($trackerId, $name, $description = '', $descriptionIsParsed = 'n')
4073	{
4074		$tracker_info = $this->get_tracker($trackerId);
4075
4076		if ($options = $this->get_tracker_options($trackerId)) {
4077			$tracker_info = array_merge($tracker_info, $options);
4078		} else {
4079			$options = [];
4080		}
4081
4082		$newTrackerId = $this->replace_tracker(0, $name, $description, [], $descriptionIsParsed);
4083		$fields = $this->list_tracker_fields($trackerId, 0, -1, 'position_asc', '');
4084		foreach ($fields['data'] as $field) {
4085			$newFieldId = $this->replace_tracker_field($newTrackerId, 0, $field['name'], $field['type'], $field['isMain'], $field['isSearchable'], $field['isTblVisible'], $field['isPublic'], $field['isHidden'], $field['isMandatory'], $field['position'], $field['options'], $field['description'], $field['isMultilingual'], $field['itemChoices']);
4086			if ($options['defaultOrderKey'] == $field['fieldId']) {
4087				$options['defaultOrderKey'] = $newFieldId;
4088			}
4089		}
4090
4091		foreach ($options as $name => $val) {
4092			$this->options()->insert(['trackerId' => $newTrackerId, 'name' => $name, 'value' => $val]);
4093		}
4094		return $newTrackerId;
4095	}
4096
4097	public function get_notification_emails($trackerId, $itemId, $options, $status = '', $oldStatus = '')
4098	{
4099		global $prefs, $user;
4100		$watchers_global = $this->get_event_watches('tracker_modified', $trackerId);
4101		$watchers_local = $this->get_local_notifications($itemId, $status, $oldStatus);
4102		$watchers_item = $itemId ? $this->get_event_watches('tracker_item_modified', $itemId, ['trackerId' => $trackerId]) : [];
4103
4104		if ($this->get_user_preference($user, 'user_tracker_watch_editor') != "y") {
4105			for ($i = count($watchers_global) - 1; $i >= 0; --$i) {
4106				if ($watchers_global[$i]['user'] == $user) {
4107					unset($watchers_global[$i]);
4108					break;
4109				}
4110			}
4111			for ($i = count($watchers_local) - 1; $i >= 0; --$i) {
4112				if ($watchers_local[$i]['user'] == $user) {
4113					unset($watchers_local[$i]);
4114					break;
4115				}
4116			}
4117			for ($i = count($watchers_item) - 1; $i >= 0; --$i) {
4118				if ($watchers_item[$i]['user'] == $user) {
4119					unset($watchers_item[$i]);
4120					break;
4121				}
4122			}
4123		}
4124
4125		// use daily reports feature if tracker item has been added or updated
4126		if ($prefs['feature_daily_report_watches'] == 'y' && ! empty($status)) {
4127			$reportsManager = Reports_Factory::build('Reports_Manager');
4128			$reportsManager->addToCache(
4129				$watchers_global,
4130				['event' => 'tracker_item_modified', 'itemId' => $itemId, 'trackerId' => $trackerId, 'user' => $user]
4131			);
4132			$reportsManager->addToCache(
4133				$watchers_item,
4134				['event' => 'tracker_item_modified', 'itemId' => $itemId, 'trackerId' => $trackerId, 'user' => $user]
4135			);
4136		}
4137
4138		// use daily reports feature if a file was attached or removed from a tracker item
4139		if ($prefs['feature_daily_report_watches'] == 'y' && isset($options["attachment"])) {
4140			$reportsManager = Reports_Factory::build('Reports_Manager');
4141			$reportsManager->addToCache(
4142				$watchers_global,
4143				[
4144					'event' => 'tracker_file_attachment',
4145					'itemId' => $itemId,
4146					'trackerId' => $trackerId,
4147					'user' => $user,
4148					"attachment" => $options["attachment"]
4149				]
4150			);
4151			$reportsManager->addToCache(
4152				$watchers_item,
4153				[
4154					'event' => 'tracker_file_attachment',
4155					'itemId' => $itemId,
4156					'trackerId' => $trackerId,
4157					'user' => $user,
4158					'attachment' => $options['attachment']
4159				]
4160			);
4161		}
4162
4163		$watchers_outbound = [];
4164		if (array_key_exists("outboundEmail", $options) && $options["outboundEmail"]) {
4165			$emails3 = preg_split('/,/', $options['outboundEmail']);
4166			foreach ($emails3 as $w) {
4167				global $user_preferences;
4168				$tikilib = TikiLib::lib('tiki');
4169				$userlib = TikiLib::lib('user');
4170				$u = $userlib->get_user_by_email($w);
4171				$tikilib->get_user_preferences($u, ['user', 'language', 'mailCharset']);
4172				if (empty($user_preferences[$u]['language'])) {
4173					$user_preferences[$u]['language'] = $prefs['site_language'];
4174				}
4175				if (empty($user_preferences[$u]['mailCharset'])) {
4176					$user_preferences[$u]['mailCharset'] = $prefs['users_prefs_mailCharset'];
4177				}
4178				$watchers_outbound[] = ['email' => $w, 'user' => $u, 'language' => $user_preferences[$u]['language'], 'mailCharset' => $user_preferences[$u]['mailCharset']];
4179			}
4180		}
4181
4182		$emails = [];
4183		$watchers = [];
4184		foreach (['watchers_global', 'watchers_local', 'watchers_item', 'watchers_outbound'] as $ws) {
4185			if (! empty($$ws)) {
4186				foreach ($$ws as $w) {
4187					$wl = strtolower($w['email']);
4188					if (! in_array($wl, $emails)) {
4189						$emails[] = $wl;
4190						$watchers[] = $w;
4191					}
4192				}
4193			}
4194		}
4195		return $watchers;
4196	}
4197
4198	/* sort allFileds function of a list of fields */
4199	public function sort_fields($allFields, $listFields)
4200	{
4201		$tmp = [];
4202		foreach ($listFields as $fieldId) {
4203			if (substr($fieldId, 0, 1) == '-') {
4204				$fieldId = substr($fieldId, 1);
4205			}
4206			foreach ($allFields['data'] as $i => $field) {
4207				if ($field['fieldId'] == $fieldId && $field['fieldId']) {
4208					$tmp[] = $field;
4209					$allFields['data'][$i]['fieldId'] = 0;
4210					break;
4211				}
4212			}
4213		}
4214		// do not forget the admin fields like user selector
4215		foreach ($allFields['data'] as $field) {
4216			if ($field['fieldId']) {
4217				$tmp[] = $field;
4218			}
4219		}
4220		$allFields['data'] = $tmp;
4221		$allFields['cant'] = count($tmp);
4222		return $allFields;
4223	}
4224
4225	/* return all the values+field options of an item for a type field (ex: return all the user selector value for an item) */
4226	public function get_item_values_by_type($itemId, $typeField)
4227	{
4228		$query = "select ttif.`value`, ttf.`options` from `tiki_tracker_fields` ttf, `tiki_tracker_item_fields` ttif";
4229		$query .= " where ttif.`itemId`=? and ttf.`type`=? and ttf.`fieldId`=ttif.`fieldId`";
4230		$ret = $this->fetchAll($query, [$itemId, $typeField]);
4231		$factory = new Tracker_Field_Factory;
4232		$typeInfo = $factory->getFieldInfo($typeField);
4233		foreach ($ret as &$res) {
4234			$options = Tracker_Options::fromSerialized($res['options'], $typeInfo);
4235			$res['options_map'] = $options->getAllParameters();
4236		}
4237		return $ret;
4238	}
4239
4240	/* return all the emails that are locally watching an item */
4241	public function get_local_notifications($itemId, $status = '', $oldStatus = '')
4242	{
4243		global $user_preferences, $prefs, $user;
4244		$tikilib = TikiLib::lib('tiki');
4245		$userlib = TikiLib::lib('user');
4246		$emails = [];
4247		// user field watching item
4248		$res = $this->get_item_values_by_type($itemId, 'u');
4249		if (is_array($res)) {
4250			foreach ($res as $f) {
4251				if (isset($f['options_map']['notify']) && $f['options_map']['notify'] != 0 && ! empty($f['value'])) {
4252					$fieldUsers = $this->parse_user_field($f['value']);
4253					foreach ($fieldUsers as $fieldUser) {
4254						if ($f['options_map']['notify'] == 2 && $user == $fieldUser) {
4255							// Don't send email to oneself
4256							continue;
4257						}
4258						$email = $userlib->get_user_email($fieldUser);
4259						if (! empty($fieldUser) && ! empty($email)) {
4260							$tikilib->get_user_preferences($fieldUser, ['email', 'user', 'language', 'mailCharset']);
4261							$emails[] = ['email' => $email, 'user' => $fieldUser, 'language' => $user_preferences[$fieldUser]['language'],
4262								'mailCharset' => $user_preferences[$fieldUser]['mailCharset'], 'template' => $f['options_map']['notify_template'], 'templateFormat' => $f['options_map']['notify_template_format']];
4263						}
4264					}
4265				}
4266			}
4267		}
4268		// email field watching status change
4269		if ($status != $oldStatus) {
4270			$res = $this->get_item_values_by_type($itemId, 'm');
4271			if (is_array($res)) {
4272				foreach ($res as $f) {
4273					if ((isset($f['options_map']['watchopen']) && $f['options_map']['watchopen'] == 'o' && $status == 'o')
4274						|| (isset($f['options_map']['watchpending']) && $f['options_map']['watchpending'] == 'p' && $status == 'p')
4275						|| (isset($f['options_map']['watchclosed']) && $f['options_map']['watchclosed'] == 'c' && $status == 'c')) {
4276						$emails[] = ['email' => $f['value'], 'user' => '', 'language' => $prefs['language'], 'mailCharset' => $prefs['users_prefs_mailCharset'], 'action' => 'status'];
4277					}
4278				}
4279			}
4280		}
4281		return $emails;
4282	}
4283
4284	public function get_join_values($trackerId, $itemId, $fieldIds, $finalTrackerId = '', $finalFields = '', $separator = ' ', $status = '')
4285	{
4286		$smarty = TikiLib::lib('smarty');
4287		$select[] = "`tiki_tracker_item_fields` t0";
4288		$where[] = " t0.`itemId`=?";
4289		$bindVars[] = $itemId;
4290		$mid = '';
4291		for ($i = 0, $tmp_count = count($fieldIds) - 1; $i < $tmp_count; $i += 2) {
4292			$j = $i + 1;
4293			$k = $j + 1;
4294			$select[] = "`tiki_tracker_item_fields` t$j";
4295			$select[] = "`tiki_tracker_item_fields` t$k";
4296			$where[] = "t$i.`value`=t$j.`value` and t$i.`fieldId`=? and t$j.`fieldId`=?";
4297			$bindVars[] = $fieldIds[$i];
4298			$bindVars[] = $fieldIds[$j];
4299			$where[] = "t$j.`itemId`=t$k.`itemId` and t$k.`fieldId`=?";
4300			$bindVars[] = $fieldIds[$k];
4301		}
4302		if (! empty($status)) {
4303			$this->getSqlStatus($status, $mid, $bindVars, $trackerId);
4304			$select[] = '`tiki_tracker_items` tti';
4305			$mid .= " and tti.`itemId`=t$k.`itemId`";
4306		}
4307		$query = "select t$k.* from " . implode(',', $select) . ' where ' . implode(' and ', $where) . $mid;
4308		$result = $this->query($query, $bindVars);
4309		$ret = [];
4310		while ($res = $result->fetchRow()) {
4311			$field_value['value'] = $res['value'];
4312			$field_value['trackerId'] = $trackerId;
4313			$field_value['type'] = $this->fields()->fetchOne('type', ['fieldId' => (int) $res['fieldId']]);
4314			if (! $field_value['type']) {
4315				$ret[$res['itemId']] = tra('Tracker field setup error - display field not found: ') . '#' . $res['fieldId'];
4316			} else {
4317				$ret[$res['itemId']] = $this->get_field_handler($field_value, $res)->renderOutput(['showlinks' => 'n', 'list_mode' => 'n']);
4318			}
4319			if (is_array($finalFields) && count($finalFields)) {
4320				$i = 0;
4321				foreach ($finalFields as $f) {
4322					if (! $i++) {
4323						continue;
4324					}
4325					$field_value = $this->get_tracker_field($f);
4326					$ff = $this->get_item_value($finalTrackerId, $res['itemId'], $f);
4327					;
4328					$field_value['value'] = $ff;
4329					$ret[$res['itemId']] = $this->get_field_handler($field_value, $res)->renderOutput(['showlinks' => 'n']);
4330				}
4331			}
4332		}
4333		return $ret;
4334	}
4335
4336	public function get_left_join_sql($fieldIds)
4337	{
4338		$sql = '';
4339		for ($i = 0, $tmp_count = count($fieldIds); $i < $tmp_count; $i += 3) {
4340			$j = $i + 1;
4341			$k = $j + 1;
4342			$tti = $i ? "t$i" : 'tti';
4343			$sttif = $k < $tmp_count - 1 ? "t$k" : 'sttif';
4344			$sql .= " LEFT JOIN (`tiki_tracker_item_fields` t$i) ON ($tti.`itemId`= t$i.`itemId` and t$i.`fieldId`=" . $fieldIds[$i] . ")";
4345			$sql .= " LEFT JOIN (`tiki_tracker_item_fields` t$j) ON (t$i.`value`= t$j.`value` and t$j.`fieldId`=" . $fieldIds[$j] . ")";
4346			$sql .= " LEFT JOIN (`tiki_tracker_item_fields` $sttif) ON (t$j.`itemId`= $sttif.`itemId` and $sttif.`fieldId`=" . $fieldIds[$k] . ")";
4347		}
4348		return $sql;
4349	}
4350
4351	public function get_item_info($itemId)
4352	{
4353		return $this->items()->fetchFullRow(['itemId' => (int) $itemId]);
4354	}
4355
4356	public function rename_page($old, $new)
4357	{
4358		global $prefs;
4359
4360		$query = "update `tiki_tracker_item_fields` ttif left join `tiki_tracker_fields` ttf on (ttif.fieldId = ttf.fieldId) set ttif.`value`=? where ttif.`value`=? and (ttf.`type` = ? or ttf.`type` = ?)";
4361		$this->query($query, [$new, $old, 'k', 'wiki']);
4362
4363		$relationlib = TikiLib::lib('relation');
4364		$wikilib = TikiLib::lib('wiki');
4365		$relatedfields = $relationlib->get_object_ids_with_relations_from('wiki page', $new, 'tiki.wiki.linkedfield'); // $new because attributes have been changed
4366		$relateditems = $relationlib->get_object_ids_with_relations_from('wiki page', $new, 'tiki.wiki.linkeditem');
4367		foreach ($relateditems as $itemId) {
4368			foreach ($relatedfields as $fieldId) {
4369				$field = $this->get_tracker_field($fieldId);
4370				$toSync = false;
4371				$nameFieldId = 0;
4372				if ($field['type'] == 'wiki') {
4373					$trackerId = $field['trackerId'];
4374					$definition = Tracker_Definition::get($trackerId);
4375					$field = $definition->getField($fieldId);
4376					if ($field['options_map']['syncwikipagename'] != 'n') {
4377						$toSync = true;
4378					}
4379										$nameFieldId = $field['options_map']['fieldIdForPagename'];
4380				} elseif ($prefs['tracker_wikirelation_synctitle'] == 'y') {
4381					$toSync = true;
4382				}
4383				if ($toSync) {
4384					$value = $this->get_item_value(0, $itemId, $fieldId);
4385					if ($wikilib->get_namespace($value) && $value != $new) {
4386						$this->modify_field($itemId, $fieldId, $new);
4387					} elseif (! $wikilib->get_namespace($value) && $value != $wikilib->get_without_namespace($new)) {
4388						$this->modify_field($itemId, $fieldId, $wikilib->get_without_namespace($new));
4389					}
4390					if ($nameFieldId) {
4391						$this->modify_field($itemId, $nameFieldId, $wikilib->get_without_namespace($new));
4392					}
4393				}
4394			}
4395		}
4396	}
4397
4398	/**
4399	 * Note that this is different from function rename_page
4400	 */
4401	public function rename_linked_page($args)
4402	{
4403		global $prefs;
4404		$relationlib = TikiLib::lib('relation');
4405		$wikilib = TikiLib::lib('wiki');
4406		$wikipages = $relationlib->get_object_ids_with_relations_to('trackeritem', $args['object'], 'tiki.wiki.linkeditem');
4407		foreach ($wikipages as $pageName) {
4408			// determine if field has changed
4409			$relatedfields = $relationlib->get_object_ids_with_relations_from('wiki page', $pageName, 'tiki.wiki.linkedfield');
4410			foreach ($relatedfields as $fieldId) {
4411				if (isset($args['values'][$fieldId]) and isset($args['old_values'][$fieldId])
4412					&& $args['values'][$fieldId] != $args['old_values'][$fieldId] ) {
4413					if ($wikilib->get_namespace($args['values'][$fieldId])) {
4414						$newname = $args['values'][$fieldId];
4415					} elseif ($namespace = $wikilib->get_namespace($pageName)) {
4416						$newname = $namespace . $prefs['namespace_separator'] . $wikilib->get_without_namespace($args['values'][$fieldId]);
4417					} else {
4418						$newname = $args['values'][$fieldId];
4419					}
4420					$wikilib->wiki_rename_page($pageName, $newname, false);
4421				}
4422			}
4423		}
4424	}
4425
4426	public function setup_wiki_fields($args)
4427	{
4428		$definition = Tracker_Definition::get($args['trackerId']);
4429		$itemId = $args['object'];
4430		$values = $args['values'];
4431
4432		if ($definition && $fieldIds = $definition->getWikiFields()) {
4433			foreach ($fieldIds as $fieldId) {
4434				if (! empty($values[$fieldId])) {
4435					TikiLib::lib('relation')->add_relation('tiki.wiki.linkeditem', 'wiki page', $values[$fieldId], 'trackeritem', $itemId);
4436					TikiLib::lib('relation')->add_relation('tiki.wiki.linkedfield', 'wiki page', $values[$fieldId], 'trackerfield', $fieldId);
4437				}
4438			}
4439		}
4440	}
4441
4442	public function update_wiki_fields($args)
4443	{
4444		global $prefs;
4445		$wikilib = TikiLib::lib('wiki');
4446		$definition = Tracker_Definition::get($args['trackerId']);
4447		$values = $args['values'];
4448		$old_values = $args['old_values'];
4449		$itemId = $args['object'];
4450
4451		if ($definition && $fieldIds = $definition->getWikiFields()) {
4452			foreach ($fieldIds as $fieldId) {
4453				$field = $definition->getField($fieldId);
4454				if ($field['options_map']['syncwikipagename'] != 'n') {
4455					$nameFieldId = $field['options_map']['fieldIdForPagename'];
4456					if (! empty($values[$nameFieldId]) && ! empty($old_values[$nameFieldId]) && ! empty($old_values[$fieldId])
4457						&& $values[$nameFieldId] != $old_values[$nameFieldId] ) {
4458						if ($namespace = $wikilib->get_namespace($old_values[$fieldId])) {
4459							$newname = $namespace . $prefs['namespace_separator'] . $wikilib->get_without_namespace($values[$nameFieldId]);
4460						} else {
4461							$newname = $values[$nameFieldId];
4462						}
4463							$args['values'][$fieldId] = $newname;
4464							$this->modify_field($itemId, $fieldId, $newname);
4465							$wikilib->wiki_rename_page($old_values[$fieldId], $newname, false);
4466					}
4467				}
4468			}
4469		}
4470	}
4471
4472	public function delete_wiki_fields($args)
4473	{
4474		$definition = Tracker_Definition::get($args['trackerId']);
4475		$itemId = $args['object'];
4476
4477		if ($definition && $fieldIds = $definition->getWikiFields()) {
4478			foreach ($fieldIds as $fieldId) {
4479				$field = $definition->getField($fieldId);
4480
4481				if ($field['options_map']['syncwikipagedelete'] == 'y' && ! empty($args['values'][$fieldId])) {
4482					$pagename = $args['values'][$fieldId];
4483					TikiLib::lib('tiki')->remove_all_versions($pagename);
4484				}
4485			}
4486		}
4487	}
4488
4489	public function build_date($input, $format, $ins_id)
4490	{
4491		if (is_array($format)) {
4492			$format = $format['options_array'][0];
4493		}
4494
4495		$tikilib = TikiLib::lib('tiki');
4496		$value = '';
4497		$monthIsNull = empty($input[$ins_id . 'Month']) || $input[$ins_id . 'Month'] == null || $input[$ins_id . 'Month'] == 'null'|| $input[$ins_id . 'Month'] == '';
4498		$dayIsNull = empty($input[$ins_id . 'Day']) || $input[$ins_id . 'Day'] == null || $input[$ins_id . 'Day'] == 'null' || $input[$ins_id . 'Day'] == '';
4499		$yearIsNull = empty($input[$ins_id . 'Year']) || $input[$ins_id . 'Year'] == null || $input[$ins_id . 'Year'] == 'null' || $input[$ins_id . 'Year'] == '';
4500		$hourIsNull = ! isset($input[$ins_id . 'Hour']) || $input[$ins_id . 'Hour'] == null || $input[$ins_id . 'Hour'] == 'null' || $input[$ins_id . 'Hour'] == ''|| $input[$ins_id . 'Hour'] == ' ';
4501		$minuteIsNull = empty($input[$ins_id . 'Minute']) || $input[$ins_id . 'Minute'] == null || $input[$ins_id . 'Minute'] == 'null' || $input[$ins_id . 'Minute'] == '' || $input[$ins_id . 'Minute'] == ' ';
4502		if ($format == 'd') {
4503			if ($monthIsNull || $dayIsNull || $yearIsNull) {
4504				// all the values must be blank
4505				$value = '';
4506			} else {
4507				$value = $tikilib->make_time(0, 0, 0, $input[$ins_id . 'Month'], $input[$ins_id . 'Day'], $input[$ins_id . 'Year']);
4508			}
4509		} elseif ($format == 't') { // all the values must be blank
4510			if ($hourIsNull || $minuteIsNull) {
4511				$value = '';
4512			} else {
4513				//if (isset($input[$ins_id.'Meridian']) && $input[$ins_id.'Meridian'] == 'pm') $input[$ins_id.'Hour'] += 12;
4514				$now = $tikilib->now;
4515				//Convert 12-hour clock hours to 24-hour scale to compute time
4516				if (isset($input[$ins_id . 'Meridian'])) {
4517					$input[$ins_id . 'Hour'] = date('H', strtotime($input[$ins_id . 'Hour'] . ':00 ' . $input[$ins_id . 'Meridian']));
4518				}
4519				$value = $tikilib->make_time($input[$ins_id . 'Hour'], $input[$ins_id . 'Minute'], 0, $tikilib->date_format("%m", $now), $tikilib->date_format("%d", $now), $tikilib->date_format("%Y", $now));
4520			}
4521		} else {
4522			if ($monthIsNull || $dayIsNull || $yearIsNull || $hourIsNull || $minuteIsNull) {
4523				// all the values must be blank
4524				$value = '';
4525			} else {
4526				//if (isset($input[$ins_id.'Meridian']) && $input[$ins_id.'Meridian'] == 'pm') $input[$ins_id.'Hour'] += 12;
4527				//Convert 12-hour clock hours to 24-hour scale to compute time
4528				if (isset($input[$ins_id . 'Meridian'])) {
4529					$input[$ins_id . 'Hour'] = date('H', strtotime($input[$ins_id . 'Hour'] . ':00 ' . $input[$ins_id . 'Meridian']));
4530				}
4531				$value = $tikilib->make_time($input[$ins_id . 'Hour'], $input[$ins_id . 'Minute'], 0, $input[$ins_id . 'Month'], $input[$ins_id . 'Day'], $input[$ins_id . 'Year']);
4532			}
4533		}
4534		return $value;
4535	}
4536
4537	/* get the fields from the pretty tracker template
4538		 * return a list of fieldIds
4539		 */
4540	public function get_pretty_fieldIds($resource, $type = 'wiki', &$prettyModifier, $trackerId = 0)
4541	{
4542		$tikilib = TikiLib::lib('tiki');
4543		$smarty = TikiLib::lib('smarty');
4544		if ($type == 'wiki') {
4545			$wiki_info = $tikilib->get_page_info($resource);
4546			if (! empty($wiki_info)) {
4547				$f = $wiki_info['data'];
4548			}
4549		} else {
4550			if (strpos($resource, 'templates/') === 0) {
4551				$resource = substr($resource, 10);
4552			}
4553			$resource_name = $smarty->get_filename($resource);
4554			$f = file_get_contents($resource_name);
4555		}
4556		if (! empty($f)) {
4557			//matches[1] = field name
4558			//matches[2] = trailing modifier text
4559			//matches[3] = modifier name ('output' or 'template')
4560			//matches[4] = modifier parameter (template name in this case)
4561			preg_match_all('/\$f_(\w+)(\|(output|template):?([^}]*))?}/', $f, $matches);
4562			$ret = [];
4563			foreach ($matches[1] as $i => $val) {
4564				if (ctype_digit($val)) {
4565					$ret[] = $val;
4566				} elseif ($fieldId = $this->table('tiki_tracker_fields')->fetchOne('fieldId', ['permName' => $val, 'trackerId' => $trackerId])) {
4567					$ret[] = $fieldId;
4568				}
4569			}
4570
4571			/*
4572			 * Check through modifiers in the pretty tracker template.
4573			 * If |output, store modifier as output. In wikiplugin_tracker, this will make it such that the field is output only
4574			 * If |template, it will check to see if a template is specified (e.g. $f_title|template:"title.tpl"). If not, default to tracker_input_field tpl
4575			 */
4576			foreach ($matches[3] as $i => $val) {
4577				if ($val == 'output') {
4578					$v = $matches[1][$i];
4579					if (ctype_digit($v)) {
4580						$prettyModifier[$v] = "output";
4581					} elseif ($fieldId = $this->table('tiki_tracker_fields')->fetchOne('fieldId', ['permName' => $v, 'trackerId' => $trackerId])) {
4582						$prettyModifier[$fieldId] = "output";
4583					}
4584				} elseif ($val == "template") {
4585					$v = $matches[1][$i];
4586					$tpl = ! empty($matches[4][$i]) ? $matches[4][$i] : "tracker_input_field.tpl"; //fetches template from pretty tracker template. if none, set to default
4587					$tpl = trim($tpl, '"\''); //trim quotations from template name
4588					if (ctype_digit($v)) {
4589						$prettyModifier[$v] = $tpl;
4590					} elseif ($fieldId = $this->table('tiki_tracker_fields')->fetchOne('fieldId', ['permName' => $v, 'trackerId' => $trackerId])) {
4591						$prettyModifier[$fieldId] = $tpl;
4592					}
4593				}
4594			}
4595			return $ret;
4596		}
4597		return [];
4598	}
4599
4600	/**
4601	 * @param mixed $value		string or array to process
4602	 */
4603	public function replace_pretty_tracker_refs(&$value)
4604	{
4605		$smarty = TikiLib::lib('smarty');
4606
4607		if (is_array($value)) {
4608			foreach ($value as &$v) {
4609				$this->replace_pretty_tracker_refs($v);
4610			}
4611		} else {
4612			// array syntax for callback function needed for some versions of PHP (5.2.0?) - thanks to mariush on http://php.net/preg_replace_callback
4613			$value = preg_replace_callback('/\{\$(f_\w+)\}/', [ &$this, '_pretty_tracker_replace_value'], $value);
4614		}
4615	}
4616
4617	public static function _pretty_tracker_replace_value($matches)
4618	{
4619		$smarty = TikiLib::lib('smarty');
4620		$s_var = null;
4621		if (! empty($matches[1])) {
4622			$s_var = $smarty->getTemplateVars($matches[1]);
4623		}
4624		if (! is_null($s_var)) {
4625			$r = $s_var;
4626		} else {
4627			$r = $matches[0];
4628		}
4629		return $r;
4630	}
4631
4632	public function nbComments($user)
4633	{
4634		return $this->comments()->fetchCount(['userName' => $user, 'objectType' => 'trackeritem']);
4635	}
4636
4637	public function lastModif($trackerId)
4638	{
4639		return $this->items()->fetchOne($this->items()->max('lastModif'), ['trackerId' => (int) $trackerId]);
4640	}
4641
4642	public function get_field($fieldId, $fields)
4643	{
4644		foreach ($fields as $f) {
4645			if ($f['fieldId'] == $fieldId) {
4646				return $f;
4647			}
4648		}
4649		return false;
4650	}
4651
4652	public function flaten($fields)
4653	{
4654		$new = [];
4655		if (empty($fields)) {
4656			return $new;
4657		}
4658		foreach ($fields as $field) {
4659			if (is_array($field)) {
4660				$new = array_merge($new, $this->flaten($field));
4661			} else {
4662				$new[] = $field;
4663			}
4664		}
4665		return $new;
4666	}
4667
4668	public function test_field_type($fields, $types)
4669	{
4670		$new = $this->flaten($fields);
4671		$table = $this->fields();
4672
4673		return $table->fetchCount(['fieldId' => $table->in($new),'type' => $table->in($types, true)]);
4674	}
4675
4676	public function get_computed_info($options, $trackerId = 0, &$fields = null)
4677	{
4678		preg_match_all('/#([0-9]+)/', $options, $matches);
4679		$nbDates = 0;
4680		foreach ($matches[1] as $k => $match) {
4681			if (empty($fields)) {
4682				$allfields = $this->list_tracker_fields($trackerId, 0, -1, 'position_asc', '');
4683				$fields = $allfields['data'];
4684			}
4685			foreach ($fields as $k => $field) {
4686				if ($field['fieldId'] == $match && in_array($field['type'], ['f', 'j'])) {
4687					++$nbDates;
4688					$info = $field;
4689					break;
4690				} elseif ($field['fieldId'] == $match && $field['type'] == 'C') {
4691					$info = $this-> get_computed_info($field['options'], $trackerId, $fields);
4692					if (! empty($info) && ($info['computedtype'] == 'f' || $info['computedtype'] == 'j')) {
4693						++$nbDates;
4694						break;
4695					}
4696				}
4697			}
4698		}
4699		if ($nbDates == 0) {
4700			return null;
4701		} elseif ($nbDates % 2 == 0) {
4702			return ['computedtype' => 'duration', 'options' => $info['options'] ,'options_array' => $info['options_array']];
4703		} else {
4704			return ['computedtype' => 'f', 'options' => $info['options'] ,'options_array' => $info['options_array']];
4705		}
4706	}
4707
4708	public function change_status($items, $status)
4709	{
4710		global $prefs, $user;
4711		$tikilib = TikiLib::lib('tiki');
4712
4713		if (! count($items)) {
4714			return;
4715		}
4716
4717		$toUpdate = [];
4718
4719		foreach ($items as $i) {
4720			if (is_array($i) && isset($i['itemId'])) {
4721				$i = $i['itemId'];
4722			}
4723
4724			$toUpdate[] = $i;
4725		}
4726
4727		$table = $this->items();
4728		$map = $table->fetchMap(
4729			'itemId',
4730			'trackerId',
4731			[
4732				'itemId' => $table->in($toUpdate),
4733			]
4734		);
4735
4736		foreach ($toUpdate as $itemId) {
4737			$trackerId = $map[$itemId];
4738			$child = $this->findLinkedItems(
4739				$itemId,
4740				function ($field, $handler) use ($trackerId) {
4741					return $handler->cascadeStatus($trackerId);
4742				}
4743			);
4744
4745			$toUpdate = array_merge($toUpdate, $child);
4746		}
4747
4748		$this->update_items(
4749			$toUpdate,
4750			[
4751				'status' => $status,
4752				'lastModif' => $tikilib->now,
4753				'lastModifBy' => $user,
4754			],
4755			true
4756		);
4757	}
4758
4759	private function update_items(array $toUpdate, array $fields, $refresh_index)
4760	{
4761		global $prefs;
4762		$logslib = TikiLib::lib('logs');
4763		$table = $this->items();
4764		$table->updateMultiple(
4765			$fields,
4766			['itemId' => $table->in($toUpdate)]
4767		);
4768
4769		foreach ($toUpdate as $itemId) {
4770			$version = $this->last_log_version($itemId) + 1;
4771			if (($logslib->add_action('Updated', $itemId, 'trackeritem', $version)) == 0) {
4772				$version = 0;
4773			}
4774		}
4775
4776		if ($prefs['feature_search'] === 'y' && $prefs['unified_incremental_update'] === 'y') {
4777			$searchlib = TikiLib::lib('unifiedsearch');
4778
4779			foreach ($toUpdate as $child) {
4780				$searchlib->invalidateObject('trackeritem', $child);
4781			}
4782
4783			if ($refresh_index && $toUpdate) {
4784				require_once('lib/search/refresh-functions.php');
4785				refresh_index('trackeritem', $toUpdate[0]);
4786			}
4787		}
4788	}
4789
4790	public function log($version, $itemId, $fieldId, $value = '')
4791	{
4792		if (empty($version)) {
4793			 return;
4794		}
4795		if ($value === null) {
4796			$value = ''; // we want to log it after all, so change is in history
4797		}
4798		$values = (array) $value;
4799		foreach ($values as $v) {
4800			$this->logs()->insert(['version' => $version, 'itemId' => $itemId, 'fieldId' => $fieldId, 'value' => $v]);
4801		}
4802	}
4803
4804	public function last_log_version($itemId)
4805	{
4806		$logs = $this->logs();
4807
4808		return $logs->fetchOne($logs->max('version'), ['itemId' => $itemId]);
4809	}
4810
4811	public function remove_item_log($itemId)
4812	{
4813		$this->logs()->deleteMultiple(['itemId' => $itemId]);
4814	}
4815
4816	public function get_item_history($item_info = null, $fieldId = 0, $filter = '', $offset = 0, $max = -1)
4817	{
4818		global $prefs;
4819		if (! empty($fieldId)) {
4820			$mid2[] = $mid[] = 'ttifl.`fieldId`=?';
4821			$bindvars[] = $fieldId;
4822		}
4823		if (! empty($item_info['itemId'])) {
4824			$mid[] = 'ttifl.`itemId`=?';
4825			$bindvars[] = $item_info['itemId'];
4826			if ($prefs['feature_categories'] == 'y') {
4827				$categlib = TikiLib::lib('categ');
4828				$item_categs = $categlib->get_object_categories('trackeritem', $item_info['itemId']);
4829			}
4830		}
4831		$query = 'select ttifl.*, ttf.* from `tiki_tracker_item_fields` ttifl left join `tiki_tracker_fields` ttf on (ttf.`fieldId`=ttifl.`fieldId`) where ' . implode(' and ', $mid);
4832		$all = $this->fetchAll($query, $bindvars, -1, 0);
4833		foreach ($all as $f) {
4834			if (! empty($item_categs) && $f['type'] == 'e') {
4835				//category
4836				$f['options_array'] = explode(',', $f['options']);
4837				if (ctype_digit($f['options_array'][0]) && $f['options_array'][0] > 0) {
4838					$type = (isset($f['options_array'][3]) && $f['options_array'][3] == 1) ? 'descendants' : 'children';
4839					$cfilter = ['identifier' => $f['options_array'][0], 'type' => $type];
4840					$field_categs = $categlib->getCategories($cfilter, true, false);
4841				} else {
4842					$field_categs = [];
4843				}
4844				$aux = [];
4845				foreach ($field_categs as $cat) {
4846					$aux[] = $cat['categId'];
4847				}
4848				$field_categs = $aux;
4849				$check = array_intersect($field_categs, $item_categs);
4850				if (! empty($check)) {
4851					$f['value'] = implode(',', $check);
4852				}
4853			}
4854			$last[$f['fieldId']] = $f['value'];
4855		}
4856
4857		$last[-1] = $item_info['status'];
4858		if (empty($item_info['itemId'])) {
4859			$join = 'ttifl.`itemId`';
4860			$bindvars = array_merge(['trackeritem'], $bindvars);
4861		} else {
4862			$join = '?';
4863			$bindvars = array_merge(['trackeritem', $item_info['itemId']], $bindvars);
4864		}
4865		$count = $this->getOne('SELECT COUNT(DISTINCT `version`) FROM `tiki_tracker_item_field_logs` WHERE `itemId`=?', [$item_info['itemId']]);
4866		$page = $this->fetchAll(
4867			'SELECT DISTINCT ttifl.`version` FROM `tiki_tracker_item_field_logs` ttifl WHERE ttifl.`itemId`=? ORDER BY `version` DESC',
4868			[$item_info['itemId']],
4869			$max,
4870			$offset
4871		);
4872
4873		if (! empty($page)) {
4874			$mid[] = 'ttifl.`version`<=?';
4875			$bindvars[] = $page[0]['version'];
4876			$mid[] = 'ttifl.`version`>=?';
4877			$bindvars[] = $page[count($page) - 1]['version'];
4878		}
4879
4880		$itemObject = Tracker_Item::fromId($item_info['itemId']);
4881
4882		$query = 'SELECT ttifl.`version`, ttifl.`fieldId`, ttifl.`value`, ta.`user`, ta.`lastModif` ' .
4883					'FROM `tiki_tracker_item_field_logs` ttifl ' .
4884					'LEFT JOIN `tiki_actionlog` ta ON (ta.`comment`=ttifl.`version` AND ta.`objectType`=? AND ta.`object`=' . $join . ') ' .
4885					'WHERE ' . implode(' AND ', $mid) . ' ORDER BY ttifl.`itemId` ASC, ttifl.`version` DESC, ttifl.`fieldId` ASC';
4886
4887		$all = $this->fetchAll($query, $bindvars);
4888		$history['data'] = [];
4889		foreach ($all as $hist) {
4890			$hist['new'] = isset($last[$hist['fieldId']]) ? $last[$hist['fieldId']] : '';
4891			if ($hist['new'] == $hist['value']) {
4892				continue;
4893			}
4894			$last[$hist['fieldId']] = $hist['value'];
4895			if (! $itemObject->canViewField($hist['fieldId'])) {
4896				continue;
4897			}
4898			if (! empty($filter['version']) && $filter['version'] != $hist['version']) {
4899				continue;
4900			}
4901			$history['data'][] = $hist;
4902		}
4903		$history['cant'] = $count;
4904		return $history;
4905	}
4906
4907	public function item_has_history($itemId)
4908	{
4909		return $this->table('tiki_tracker_item_fields')->fetchCount([ 'itemId' => $itemId ]);
4910	}
4911
4912	public function move_item($trackerId, $itemId, $newTrackerId)
4913	{
4914		$newFields = $this->list_tracker_fields($newTrackerId, 0, -1, 'name_asc');
4915		foreach ($newFields['data'] as $field) {
4916			$translation[$field['name']] = $field;
4917		}
4918		$this->items()->update(['trackerId' => $newTrackerId], ['itemId' => $itemId]);
4919		$this->trackers()->update(['items' => $this->trackers()->decrement(1)], ['trackerId' => $trackerId]);
4920		$this->trackers()->update(['items' => $this->trackers()->increment(1)], ['trackerId' => $newTrackerId]);
4921
4922		$newFields = $this->list_tracker_fields($newTrackerId, 0, -1, 'name_asc');
4923		$query = 'select ttif.*, ttf.`name`, ttf.`type`, ttf.`options` from `tiki_tracker_item_fields` ttif, `tiki_tracker_fields` ttf where ttif.itemId=? and ttif.`fieldId`=ttf.`fieldId`';
4924		$fields = $this->fetchAll($query, [$itemId]);
4925
4926		foreach ($fields as $field) {
4927			if (empty($translation[$field['name']]) || $field['type'] != $translation[$field['name']]['type'] || $field['options'] != $translation[$field['name']]['options']) {
4928				// delete the field
4929				$this->itemFields()->delete(['itemId' => $field['itemId'], 'fieldId' => $field['fieldId']]);
4930			} else {
4931				// transfer
4932				$this->itemFields()->update(
4933					[
4934						'fieldId' => $translation[$field['name']]['fieldId'],
4935					],
4936					[
4937						'itemId' => $field['itemId'],
4938						'fieldId' => $field['fieldId'],
4939					]
4940				);
4941			}
4942		}
4943	}
4944
4945	/* copy the fields of one item ($from) to another one ($to) of the same tracker - except/only for some fields */
4946	/* note: can not use the generic function as they return not all the multilingual fields */
4947	public function copy_item($from, $to, $except = null, $only = null, $status = null)
4948	{
4949		global $user, $prefs;
4950
4951		if ($prefs['feature_categories'] == 'y') {
4952			$categlib = TikiLib::lib('categ');
4953			$cats = $categlib->get_object_categories('trackeritem', $from);
4954		}
4955		if (empty($to)) {
4956			$is_new = 'y';
4957			$info_to['trackerId'] = $this->items()->fetchOne('trackerId', ['itemId' => $from]);
4958			$info_to['status'] = empty($status) ? $this->items()->fetchOne('status', ['itemId' => $from]) : $status;
4959			$info_to['created'] = $info_to['lastModif'] = $this->now;
4960			$info_to['createdBy'] = $info_to['lastModifBy'] = $user;
4961			$to = $this->items()->insert($info_to);
4962		}
4963
4964		$query = 'select ttif.*, ttf.`type`, ttf.`options` from `tiki_tracker_item_fields` ttif left join `tiki_tracker_fields` ttf on (ttif.`fieldId` = ttf.`fieldId`) where `itemId`=?';
4965		$result = $this->fetchAll($query, [$from]);
4966		$clean = [];
4967		$factory = new Tracker_Field_Factory;
4968		foreach ($result as $res) {
4969			$typeInfo = $factory->getFieldInfo($res['type']);
4970			$options = Tracker_Options::fromSerialized($res['options'], $typeInfo);
4971			$res['options_array'] = $options->buildOptionsArray();
4972
4973			if ($prefs['feature_categories'] == 'y' && $res['type'] == 'e') {
4974				//category
4975				if ((! empty($except) && in_array($res['fieldId'], $except))
4976					|| (! empty($only) && ! in_array($res['fieldId'], $only))) {
4977					// take away the categories from $cats
4978					if (ctype_digit($res['options_array'][0]) && $res['options_array'][0] > 0) {
4979						$filter = ['identifier' => $res['options_array'][0], 'type' => 'children'];
4980					} else {
4981						$filter = null;
4982					}
4983					$children = $categlib->getCategories($filter, true, false);
4984					$local = [];
4985					foreach ($children as $child) {
4986						$local[] = $child['categId'];
4987					}
4988					$cats = array_diff($cats, $local);
4989				}
4990			}
4991
4992			if ((! empty($except) && in_array($res['fieldId'], $except))
4993				|| (! empty($only) && ! in_array($res['fieldId'], $only))
4994				|| ($res['type'] == 'q')
4995				) {
4996				continue;
4997			}
4998			if (! empty($is_new) && in_array($res['type'], ['u', 'g', 'I']) && ($res['options_array'][0] == 1 || $res['options_array'][0] == 2)) {
4999				$res['value'] = ($res['type'] == 'u') ? $user : (($res['type'] == 'g') ? $_SESSION['u_info']['group'] : TikiLib::get_ip_address());
5000			}
5001			if (in_array($res['type'], ['A', 'N'])) {
5002				// attachment - image
5003				continue; //not done yet
5004			}
5005			//echo "duplic".$res['fieldId'].' '. $res['value'].'<br>';
5006			if (! in_array($res['fieldId'], $clean)) {
5007				$this->itemFields()->delete(['itemId' => $to, 'fieldId' => $res['fieldId']]);
5008				$clean[] = $res['fieldId'];
5009			}
5010
5011			$data = [
5012				'itemId' => $to,
5013				'fieldId' => $res['fieldId'],
5014				'value' => $res['value'],
5015			];
5016
5017			$this->itemFields()->insert($data);
5018		}
5019
5020		if (! empty($cats)) {
5021			$trackerId = $this->items()->fetchOne('trackerId', ['itemId' => $from]);
5022			$this->categorized_item($trackerId, $to, "item $to", $cats);
5023		}
5024		return $to;
5025	}
5026
5027	public function export_attachment($itemId, $archive)
5028	{
5029		global $prefs;
5030		$files = $this->list_item_attachments($itemId, 0, -1, 'attId_asc');
5031		foreach ($files['data'] as $file) {
5032			$localZip = "item_$itemId/" . $file['filename'];
5033			$complete = $this->get_item_attachment($file['attId']);
5034			if (! empty($complete['path']) && file_exists($prefs['t_use_dir'] . $complete['path'])) {
5035				if (! $archive->addFile($prefs['t_use_dir'] . $complete['path'], $localZip)) {
5036					return false;
5037				}
5038			} elseif (! empty($complete['data'])) {
5039				if (! $archive->addFromString($localZip, $complete['data'])) {
5040					return false;
5041				}
5042			}
5043		}
5044		return true;
5045	}
5046
5047	/* fill a calendar structure with items
5048	 * fieldIds contains one date or 2 dates
5049	 */
5050	public function fillTableViewCell($items, $fieldIds, &$cell)
5051	{
5052		$smarty = TikiLib::lib('smarty');
5053		if (empty($items)) {
5054			return;
5055		}
5056		$iStart = -1;
5057		$iEnd = -1;
5058		foreach ($items[0]['field_values'] as $i => $field) {
5059			if ($field['fieldId'] == $fieldIds[0]) {
5060				$iStart = $i;
5061				$iEnd = $i; //$end can be the same as start
5062			} elseif (count($fieldIds) > 1 && $field['fieldId'] == $fieldIds[1]) {
5063				$iEnd = $i;
5064			}
5065		}
5066		foreach ($cell as $i => $line) {
5067			foreach ($line as $j => $day) {
5068				if (! $day['focus']) {
5069					continue;
5070				}
5071				$overs = [];
5072				foreach ($items as $item) {
5073					$endDay = TikiLib::make_time(23, 59, 59, $day['month'], $day['day'], $day['year']);
5074					if ((count($fieldIds) == 1 && $item['field_values'][$iStart]['value'] >= $day['date'] && $item['field_values'][$iStart]['value'] <= $endDay)
5075						|| (count($fieldIds) > 1 && $item['field_values'][$iStart]['value'] <= $endDay && $item['field_values'][$iEnd]['value'] >= $day['date'])) {
5076							$cell[$i][$j]['items'][] = $item;
5077							$overs[] = preg_replace('|(<br /> *)*$|m', '', $item['over']);
5078					}
5079				}
5080				if (! empty($overs)) {
5081					$smarty->assign_by_ref('overs', $overs);
5082					$cell[$i][$j]['over'] = $smarty->fetch('tracker_calendar_over.tpl');
5083				}
5084			}
5085		}
5086		//echo '<pre>'; print_r($cell); echo '</pre>';
5087	}
5088
5089	public function get_tracker_by_name($name)
5090	{
5091		return $this->trackers()->fetchOne('trackerId', ['name' => $name]);
5092	}
5093
5094	public function get_field_by_name($trackerId, $fieldName)
5095	{
5096		return $this->fields()->fetchOne('fieldId', ['trackerId' => $trackerId, 'name' => $fieldName]);
5097	}
5098
5099	public function get_field_by_names($trackerName, $fieldName)
5100	{
5101		$trackerId = $this->trackers()->fetchOne('trackerId', ['name' => $trackerName]);
5102		return $fieldId = $this->fields()->fetchOne('fieldId', ['trackerId' => $trackerId, 'name' => $fieldName]);
5103	}
5104
5105	public function get_fields_by_names($trackerName, $fieldNames)
5106	{
5107		$fields = [];
5108		foreach ($fieldNames as $fieldName) {
5109			$fields[$fieldName] = $this->get_field_by_names($trackerName, $fieldName);
5110		}
5111		return $fields;
5112	}
5113
5114	/**
5115	 * Get a field handler for a specific fieldtype. The handler comes initialized with the field / item data passed.
5116	 * @param array $field.
5117	 * <pre>
5118	 * $field = array(
5119	 * 		// required
5120	 * 		'trackerId' => 1 // trackerId
5121	 * );
5122	 * </pre
5123	 * @param array $item - array('itemId1' => value1, 'itemid2' => value2)
5124	 * @return Tracker_Field_Abstract $tracker_field_handler - i.e. Tracker_Field_Text
5125	 */
5126	public function get_field_handler($field, $item = [])
5127	{
5128		if (! isset($field['trackerId'])) {
5129			return false;
5130		}
5131
5132		$trackerId = (int) $field['trackerId'];
5133		$definition = Tracker_Definition::get($trackerId);
5134
5135		if (! $definition) {
5136			return false;
5137		}
5138
5139		return $definition->getFieldFactory()->getHandler($field, $item);
5140	}
5141
5142	public function get_field_value($field, $item)
5143	{
5144		$handler = $this->get_field_handler($field, $item);
5145		$values = $handler->getFieldData();
5146
5147		return isset($values['value']) ? $values['value'] : null;
5148	}
5149
5150	private function parse_comment($data)
5151	{
5152		return nl2br(htmlspecialchars($data));
5153	}
5154
5155	public function send_replace_item_notifications($args)
5156	{
5157		global $prefs, $user;
5158
5159		// Don't send a notification if this operation is part of a bulk import
5160		if ($args['bulk_import']) {
5161			return;
5162		}
5163
5164		$trackerId = $args['trackerId'];
5165		$itemId = $args['object'];
5166
5167		$new_values = $args['values'];
5168		$old_values = $args['old_values'];
5169
5170		$tracker_definition = Tracker_Definition::get($trackerId);
5171		if (! $tracker_definition) {
5172			return;
5173		}
5174		$tracker_info = $tracker_definition->getInformation();
5175
5176		$watchers = $this->get_notification_emails($trackerId, $itemId, $tracker_info, $new_values['status'], $old_values['status']);
5177
5178		if (count($watchers) > 0) {
5179			$simpleEmail = isset($tracker_info['simpleEmail']) ? $tracker_info['simpleEmail'] : "n";
5180
5181			$trackerName = $tracker_info['name'];
5182			if (! isset($_SERVER["SERVER_NAME"])) {
5183				$_SERVER["SERVER_NAME"] = $_SERVER["HTTP_HOST"];
5184			}
5185			include_once('lib/webmail/tikimaillib.php');
5186			if ($simpleEmail == "n") {
5187				$mail_main_value_fieldId = $this->get_main_field($trackerId);
5188				$mail_main_value_field = $tracker_definition->getField($mail_main_value_fieldId);
5189				if (in_array($mail_main_value_field['type'], ['r', 'q'])) {
5190					// Item Link & auto-inc are special cases as field value is not the displayed text. There might be other such field types.
5191					$handler = $this->get_field_handler($mail_main_value_field);
5192					$desc = $handler->renderOutput(['list_mode' => 'csv']);
5193				} else {
5194					$desc = $this->get_item_value($trackerId, $itemId, $mail_main_value_fieldId);
5195				}
5196				$smarty = TikiLib::lib('smarty');
5197
5198				$smarty->assign('mail_date', $this->now);
5199				$smarty->assign('mail_user', $user);
5200				$smarty->assign('mail_itemId', $itemId);
5201				$smarty->assign('mail_item_desc', $desc);
5202				$smarty->assign('mail_trackerId', $trackerId);
5203				$smarty->assign('mail_trackerName', $trackerName);
5204				$smarty->assign('server_name', $_SERVER['SERVER_NAME']);
5205				$foo = parse_url($_SERVER["REQUEST_URI"]);
5206				$machine = $this->httpPrefix(true) . $foo["path"];
5207				$smarty->assign('mail_machine', $machine);
5208				$parts = explode('/', $foo['path']);
5209				if (count($parts) > 1) {
5210					unset($parts[count($parts) - 1]);
5211				}
5212				$smarty->assign('mail_machine_raw', $this->httpPrefix(true) . implode('/', $parts));
5213				// not a great test for a new item but we don't get the event type here
5214				$created = empty($old_values) || $old_values === ['status' => ''];
5215				foreach ($watchers as $watcher) {
5216					// assign these variables inside the loop as this->tracker_render_values overrides them in case trackeroutput or similar is used
5217					$smarty->assign_by_ref('status', $new_values['status']);
5218					$smarty->assign_by_ref('status_old', $old_values['status']);
5219					// expose the pretty tracker fields to the email tpls
5220					foreach ($tracker_definition->getFields() as $field) {
5221						$fieldId = $field['fieldId'];
5222						$old_value = isset($old_values[$fieldId]) ? $old_values[$fieldId] : '';
5223						$new_value = isset($new_values[$fieldId]) ? $new_values[$fieldId] : '';
5224						$smarty->assign('f_' . $fieldId, $new_value);
5225						$smarty->assign('f_' . $field['permName'], $new_value);
5226						$smarty->assign('f_old_' . $fieldId, $old_value);
5227						$smarty->assign('f_old_' . $field['permName'], $old_value);
5228						$smarty->assign('f_name_' . $fieldId, $field['name']);
5229						$smarty->assign('f_name_' . $field['permName'], $field['name']);
5230					}
5231					$watcher['language'] = $this->get_user_preference($watcher['user'], 'language', $prefs['site_language']);
5232					if ($created) {
5233						$label = tra('Item Creation', $watcher['language']);
5234					} else {
5235						$label = tra('Item Modification', $watcher['language']);
5236					}
5237					$mail_action = "\r\n$label\r\n\r\n";
5238					$mail_action .= tra('Tracker', $watcher['language']) . ":\n   " . tra($trackerName, $watcher['language']) . "\r\n";
5239					$mail_action .= tra('Item', $watcher['language']) . ":\n   $itemId $desc";
5240
5241					$smarty->assign('mail_action', $mail_action);
5242
5243					if (! isset($watcher['template'])) {
5244						$watcher['template'] = '';
5245					}
5246					$content = $this->parse_notification_template($watcher['template']);
5247
5248					$subject = $smarty->fetchLang($watcher['language'], $content['subject']);
5249
5250					// get the diff for changes for this watcher
5251					$the_data = $this->generate_watch_data($old_values, $new_values, $trackerId, $itemId, $args['version'], $watcher['user']);
5252
5253					if (empty($the_data) && $prefs['tracker_always_notify'] !== 'y') {
5254						continue;
5255					}
5256
5257					if ($tracker_info['doNotShowEmptyField'] === 'y') {
5258						// remove empty fields if tracker says so
5259						$the_data = preg_replace('/\[-\[.*?\]-\] -\[\(.*?\)\]-:\n\n----------\n/', '', $the_data);
5260					}
5261
5262					list($watcher_data, $watcher_subject) = $this->translate_watch_data($the_data, $subject, $watcher['language']);
5263
5264					$smarty->assign('mail_data', $watcher_data);
5265					if (isset($watcher['action'])) {
5266						$smarty->assign('mail_action', $watcher['action']);
5267					}
5268					$smarty->assign('mail_to_user', $watcher['user']);
5269					$mail_data = $smarty->fetchLang($watcher['language'], $content['template']);
5270
5271					// if the tpl returns nothing then don't send the mail
5272					if (! empty($mail_data)) {
5273						$mail = new TikiMail($watcher['user']);
5274						$mail->setSubject($watcher_subject);
5275						if (isset($watcher['templateFormat']) && $watcher['templateFormat'] == 'html') {
5276							$mail->setHtml($mail_data, str_replace('&nbsp;', ' ', strip_tags($mail_data)));
5277						} else {
5278							$mail->setText(str_replace('&nbsp;', ' ', strip_tags($mail_data)));
5279						}
5280						$mail->send([$watcher['email']]);
5281					}
5282				}
5283			} else {
5284					// Use simple email
5285				$foo = parse_url($_SERVER["REQUEST_URI"]);
5286				$machine = $this->httpPrefix(true) . $foo["path"];
5287				$parts = explode('/', $foo['path']);
5288				if (count($parts) > 1) {
5289					unset($parts[count($parts) - 1]);
5290				}
5291				$machine = $this->httpPrefix(true) . implode('/', $parts);
5292
5293				$userlib = TikiLib::lib('user');
5294
5295				if (! empty($user)) {
5296					$my_sender = $userlib->get_user_email($user);
5297				} else {
5298					// look if a email field exists
5299					$fieldId = $this->get_field_id_from_type($trackerId, 'm');
5300					if (! empty($fieldId)) {
5301						$my_sender = $this->get_item_value($trackerId, $itemId, $fieldId);
5302					}
5303				}
5304
5305				$the_data = $this->generate_watch_data($old_values, $new_values, $trackerId, $itemId, $args['version']);
5306
5307				if (empty($the_data) && $prefs['tracker_always_notify'] !== 'y') {
5308					return;
5309				}
5310
5311				// Try to find a Subject in $the_data looking for strings marked "-[Subject]-" TODO: remove the tra (language translation by submitter)
5312				$the_string = '/^\[-\[' . tra('Subject') . '\]-\] -\[[^\]]*\]-:\n(.*)/m';
5313				$subject_test_unchanged = preg_match($the_string, $the_data, $unchanged_matches);
5314				$the_string = '/^\[-\[' . tra('Subject') . '\]-\]:\n(.*)\n(.*)\n\n(.*)\n(.*)/m';
5315				$subject_test_changed = preg_match($the_string, $the_data, $matches);
5316				$subject = '';
5317
5318				if ($subject_test_unchanged == 1) {
5319					$subject = $unchanged_matches[1];
5320				}
5321				if ($subject_test_changed == 1) {
5322					$subject = $matches[1] . ' ' . $matches[2] . ' ' . $matches[3] . ' ' . $matches[4];
5323				}
5324
5325				$i = 0;
5326				foreach ($watchers as $watcher) {
5327					$watcher['language'] = $this->get_user_preference($watcher['user'], 'language', $prefs['site_language']);
5328					$mail = new TikiMail($watcher['user']);
5329					list($watcher_data, $watcher_subject) = $this->translate_watch_data($the_data, $subject, $watcher['language']);
5330
5331					$mail->setSubject('[' . $trackerName . '] ' . str_replace('> ', '', $watcher_subject) . ' (' . tra('Tracker was modified at %0 by %1', $watcher['language'], false, [$_SERVER["SERVER_NAME"], $user]) . ')');
5332					$mail->setText(tra('View the tracker item at:', $watcher['language']) . " $machine/tiki-view_tracker_item.php?itemId=$itemId\n\n" . $watcher_data);
5333					if (! empty($my_sender)) {
5334						$mail->setReplyTo($my_sender);
5335					}
5336					$mail->send([$watcher['email']]);
5337					$i++;
5338				}
5339			}
5340		}
5341	}
5342
5343	private function parse_notification_template($template)
5344	{
5345		$tikilib = TikiLib::lib('tiki');
5346		$subject = "";
5347		if (! empty($template)) { //tpl
5348			if (! preg_match('/^(:?tpl)?wiki\:/', $template, $match)) {
5349				if (! preg_match('/\.tpl$/', $template)) {		// template file
5350					$template .= '.tpl';
5351				}
5352				$template = 'mail/' . $template;
5353				$subject = str_replace('.tpl', '_subject.tpl', $template);
5354			} else {	// wiki template
5355				$pageName = substr($template, strlen($match[0]));
5356				if (! $tikilib->page_exists($pageName)) {
5357					Feedback::error(tr('Missing wiki email template page "%0"', htmlspecialchars($template)));
5358					$template = '';
5359				} else {
5360					$subject_name = str_replace('tpl', 'subject tpl', $pageName);
5361					if ($tikilib->page_exists($subject_name)) {
5362						$subject = $match[0] . $subject_name;
5363					} else {
5364						$subject_name = str_replace('tpl', 'subject-tpl', $pageName);
5365						if ($tikilib->page_exists($subject_name)) {
5366							$subject = $match[0] . $subject_name;
5367						}
5368					}
5369				}
5370			}
5371		}
5372		if (empty($template)) {
5373			$template = 'mail/tracker_changed_notification.tpl';
5374		}
5375		if (empty($subject)) {
5376			$subject = 'mail/tracker_changed_notification_subject.tpl';
5377		}
5378		return [
5379			'subject' => $subject,
5380			'template' => $template,
5381		];
5382	}
5383
5384
5385	/**
5386	 * Translate the watch data and subject for each watcher
5387	 *
5388	 * @param string $the_data
5389	 * @param string $subject
5390	 * @param string $language
5391	 * @return array				translated [data, subject]
5392	 */
5393	private function translate_watch_data($the_data, $subject, $language)
5394	{
5395		// first we look for strings marked "-[...]-" to translate by watcher language
5396		$watcher_subject = $subject;
5397		$watcher_data = $the_data;
5398
5399		if (preg_match_all('/-\[([^\]]*)\]-/', $the_data, $tra_matches) > 0 && $language !== 'en') {
5400			foreach ($tra_matches[1] as $match) {
5401				// now we replace the marked strings with correct translations
5402				$tra_replace = tra($match, $language);
5403				$tra_match = "/-\[" . preg_quote($match) . "\]-/m";
5404				$watcher_subject = preg_replace($tra_match, $tra_replace, $watcher_subject);
5405				$watcher_data = preg_replace($tra_match, $tra_replace, $watcher_data);
5406			}
5407		}
5408		return [$watcher_data, $watcher_subject];
5409	}
5410
5411	private function generate_watch_data($old, $new, $trackerId, $itemId, $version, $watcher = '')
5412	{
5413		global $prefs;
5414
5415		$userslib = TikiLib::lib('user');
5416
5417		$tracker_definition = Tracker_Definition::get($trackerId);
5418		if (! $tracker_definition) {
5419			return '';
5420		}
5421
5422		$oldStatus = $old['status'];
5423		$newStatus = $new['status'];
5424		$changed = false;
5425
5426		$the_data = '';
5427		if (! empty($oldStatus) || ! empty($newStatus)) {
5428			if (! empty($itemId) && $oldStatus != $newStatus) {
5429				 $this->log($version, $itemId, -1, $oldStatus);
5430			}
5431			$the_data .= '-[Status]-: ';
5432			$statusTypes = $this->status_types('en'); // Fetch in english to translate to watcher language
5433			if (isset($oldStatus) && $oldStatus != $newStatus) {
5434				$the_data .= isset($statusTypes[$oldStatus]['label']) ? '-[' . $statusTypes[$oldStatus]['label'] . ']- -> ' : '';
5435				$changed = true;
5436			}
5437
5438			if (! empty($newStatus)) {
5439				$the_data .= '-[' . $statusTypes[$newStatus]['label'] . ']-';
5440			}
5441			$the_data .= "\n----------\n";
5442		}
5443
5444		foreach ($tracker_definition->getFields() as $field) {
5445			$fieldId = $field['fieldId'];
5446
5447			$old_value = isset($old[$fieldId]) ? $old[$fieldId] : '';
5448			$new_value = isset($new[$fieldId]) ? $new[$fieldId] : '';
5449
5450			if ($old_value == $new_value) {
5451				continue;
5452			}
5453
5454			$handler = $this->get_field_handler($field);
5455			if ($handler) {
5456				$userOk = (!$watcher || $watcher === 'admin');
5457				if (! $userOk && is_array($field['visibleBy']) && ! empty($field['visibleBy'])) {
5458					foreach ($field['visibleBy'] as $group) {
5459						$userOk = $userslib->user_is_in_group($watcher, $group);
5460						if ($userOk) {
5461							break;
5462						}
5463					}
5464				} else {
5465					$userOk = true;
5466				}
5467				if ($userOk) {
5468					$the_data .= $handler->watchCompare($old_value, $new_value);
5469				}
5470			} else {
5471				$the_data .= tr('Tracker field not enabled: fieldId=%0 type=%1', $field['fieldId'], tra($field['type'])) . "\n";
5472			}
5473			$the_data .= "\n----------\n";
5474			$changed = true;
5475		}
5476
5477		if ($changed || $prefs['tracker_always_notify'] === 'y') {
5478			return $the_data;
5479		} else {
5480			return '';
5481		}
5482	}
5483
5484	private function tracker_is_syncable($trackerId)
5485	{
5486		global $prefs;
5487		if (! empty($prefs["user_trackersync_trackers"])) {
5488			$trackersync_trackers = unserialize($prefs["user_trackersync_trackers"]);
5489			return in_array($trackerId, $trackersync_trackers);
5490		}
5491
5492		return false;
5493	}
5494
5495	private function get_tracker_item_users($trackerId, $values)
5496	{
5497		global $user, $prefs;
5498		$userlib = TikiLib::lib('user');
5499		$trackersync_users = [$user];
5500
5501		$definition = Tracker_Definition::get($trackerId);
5502
5503		if ($definition) {
5504			$fieldId = $definition->getUserField();
5505			$value = isset($values[$fieldId]) ? $values[$fieldId] : '';
5506
5507			if ($value) {
5508				$trackersync_users = $this->parse_user_field($value);
5509			}
5510		}
5511
5512		return $trackersync_users;
5513	}
5514
5515	private function get_tracker_item_coordinates($trackerId, $values)
5516	{
5517		$definition = Tracker_Definition::get($trackerId);
5518
5519		if ($definition && $fieldId = $definition->getGeolocationField()) {
5520			if (isset($values[$fieldId])) {
5521				return TikiLib::lib('geo')->parse_coordinates($values[$fieldId]);
5522			}
5523		}
5524	}
5525
5526	public function sync_user_lang($args)
5527	{
5528		global $prefs;
5529
5530		$trackerId = $args['trackerId'];
5531
5532		if ($prefs['user_trackersync_lang'] != 'y') {
5533			return;
5534		}
5535
5536		if (! $this->tracker_is_syncable($trackerId)) {
5537			return;
5538		}
5539
5540		$trackersync_users = $this->get_tracker_item_users($trackerId, $args['values']);
5541		if (empty($trackersync_users)) {
5542			return;
5543		}
5544
5545		$definition = Tracker_Definition::get($trackerId);
5546		if ($definition && $fieldId = $definition->getLanguageField()) {
5547			foreach ($trackersync_users as $trackersync_user) {
5548				TikiLib::lib('tiki')->set_user_preference($trackersync_user, 'language', $args['values'][$fieldId]);
5549			}
5550		}
5551	}
5552
5553	public function sync_user_realname($args)
5554	{
5555		global $prefs;
5556
5557		$trackerId = $args['trackerId'];
5558
5559		if (! $this->tracker_is_syncable($trackerId)) {
5560			return;
5561		}
5562
5563		$trackersync_users = $this->get_tracker_item_users($trackerId, $args['values']);
5564		if (empty($trackersync_users)) {
5565			return;
5566		}
5567
5568		if (! empty($prefs["user_trackersync_realname"])) {
5569			// Fields to concatenate are delimited by + and priority sets are delimited by ,
5570			$trackersync_realnamefields = preg_split('/\s*,\s*/', $prefs["user_trackersync_realname"]);
5571
5572			foreach ($trackersync_realnamefields as $fields) {
5573				$parts = [];
5574				$fields = preg_split('/\s*\+\s*/', $fields);
5575				foreach ($fields as $field) {
5576					$field = (int) $field;
5577					if (isset($args['values'][$field])) {
5578						$parts[] = $args['values'][$field];
5579					}
5580				}
5581
5582				$realname = implode(' ', $parts);
5583
5584				if (! empty($realname)) {
5585					foreach ($trackersync_users as $trackersync_user) {
5586						TikiLib::lib('tiki')->set_user_preference($trackersync_user, 'realName', $realname);
5587					}
5588				}
5589			}
5590		}
5591	}
5592
5593	public function sync_user_geo($args)
5594	{
5595		global $prefs;
5596
5597		$trackerId = $args['trackerId'];
5598
5599		if (! $this->tracker_is_syncable($trackerId)) {
5600			return;
5601		}
5602
5603		$trackersync_users = $this->get_tracker_item_users($trackerId, $args['values']);
5604		if (empty($trackersync_users)) {
5605			return;
5606		}
5607
5608		if ($geo = $this->get_tracker_item_coordinates($trackerId, $args['values'])) {
5609			$tikilib = TikiLib::lib('tiki');
5610
5611			foreach ($trackersync_users as $trackersync_user) {
5612				$tikilib->set_user_preference($trackersync_user, 'lon', $geo['lon']);
5613				$tikilib->set_user_preference($trackersync_user, 'lat', $geo['lat']);
5614				if (! empty($geo['zoom'])) {
5615					$tikilib->set_user_preference($trackersync_user, 'zoom', $geo['zoom']);
5616				}
5617			}
5618		}
5619	}
5620
5621	public function sync_item_geo($args)
5622	{
5623		$trackerId = $args['trackerId'];
5624		$itemId = $args['object'];
5625
5626		if ($geo = $this->get_tracker_item_coordinates($trackerId, $args['values'])) {
5627			if ($geo && $itemId) {
5628				TikiLib::lib('geo')->set_coordinates('trackeritem', $itemId, $geo);
5629			}
5630		}
5631	}
5632
5633	public function sync_user_groups($args)
5634	{
5635		global $prefs;
5636
5637		$trackerId = $args['trackerId'];
5638
5639		if (! $this->tracker_is_syncable($trackerId)) {
5640			return;
5641		}
5642
5643		$trackersync_users = $this->get_tracker_item_users($trackerId, $args['values']);
5644		if (empty($trackersync_users)) {
5645			return;
5646		}
5647
5648		if (empty($prefs["user_trackersync_groups"])) {
5649			return;
5650		}
5651
5652		$definition = Tracker_Definition::get($trackerId);
5653		$userslib = TikiLib::lib('user');
5654
5655		$trackersync_groupfields = preg_split('/\s*,\s*/', $prefs["user_trackersync_groups"]);
5656		foreach ($trackersync_groupfields as $field) {
5657			$field = (int)$field;
5658			if (! isset($args['values'][$field])) {
5659				continue;
5660			}
5661			$field = $definition->getField($field);
5662			$handler = $this->get_field_handler($field, $args['values']);
5663			$group = $handler->renderOutput();
5664			if (empty($group) || ! $userslib->group_exists($group)) {
5665				continue;
5666			}
5667			foreach ($trackersync_users as $trackersync_user) {
5668				if (! $userslib->user_exists($trackersync_user)) {
5669					continue;
5670				}
5671				if ($userslib->user_is_in_group($trackersync_user, $group)) {
5672					continue;
5673				}
5674				$userslib->assign_user_to_group($trackersync_user, $group);
5675			}
5676		}
5677	}
5678
5679	public function sync_item_auto_categories($args)
5680	{
5681		$trackerId = $args['trackerId'];
5682		$itemId = $args['object'];
5683		$definition = Tracker_Definition::get($trackerId);
5684
5685		if ($definition && $definition->isEnabled('autoCreateCategories')) {
5686			$categlib = TikiLib::lib('categ');
5687			$tracker_item_desc = $this->get_isMain_value($trackerId, $itemId);
5688
5689			// Verify that parentCat exists Or Create It
5690			$parentcategId = $categlib->get_category_id("Tracker $trackerId");
5691			if (! isset($parentcategId)) {
5692				$parentcategId = $categlib->add_category(0, "Tracker $trackerId", $definition->getConfiguration('description'));
5693			}
5694			// Verify that the sub Categ doesn't already exists
5695			$currentCategId = $categlib->get_category_id("Tracker Item $itemId");
5696			if (! isset($currentCategId) || $currentCategId == 0) {
5697				$currentCategId = $categlib->add_category($parentcategId, "Tracker Item $itemId", $tracker_item_desc);
5698			} else {
5699				$categlib->update_category($currentCategId, "Tracker Item $itemId", $tracker_item_desc, $parentcategId);
5700			}
5701			$cat_type = "trackeritem";
5702			$cat_objid = $itemId;
5703			$cat_desc = '';
5704			$cat_name = "Tracker Item $itemId";
5705			$cat_href = "tiki-view_tracker_item.php?trackerId=$trackerId&itemId=$itemId";
5706			// ?? HAS to do it ?? $categlib->uncategorize_object($cat_type, $cat_objid);
5707			$catObjectId = $categlib->is_categorized($cat_type, $cat_objid);
5708			if (! $catObjectId) {
5709				$catObjectId = $categlib->add_categorized_object($cat_type, $cat_objid, $cat_desc, $cat_name, $cat_href);
5710			}
5711			$categlib->categorize($catObjectId, $currentCategId);
5712		}
5713	}
5714
5715	private function get_viewable_category_field_cats($trackerId)
5716	{
5717		$definition = Tracker_Definition::get($trackerId);
5718		$categories = [];
5719
5720		if (! $definition) {
5721			return [];
5722		}
5723
5724		foreach ($definition->getFields() as $field) {
5725			if ($field['type'] == 'e') {
5726				$parentId = $field['options_array'][0];
5727				$descends = isset($field['options_array'][3]) && $field['options_array'][3] == 1;
5728				if (ctype_digit($parentId) && $parentId > 0) {
5729					$cats = TikiLib::lib('categ')->getCategories(['identifier' => $parentId, 'type' => $descends ? 'descendants' : 'children']);
5730				} else {
5731					$cats = [];
5732				}
5733
5734				foreach ($cats as $c) {
5735					$categories[] = $c['categId'];
5736				}
5737			}
5738		}
5739
5740		return array_unique(array_filter($categories));
5741	}
5742
5743	public function invalidate_item_cache($args)
5744	{
5745		$itemId = $args['object'];
5746
5747		$cachelib = TikiLib::lib('cache');
5748		$cachelib->invalidate('trackerItemLabel' . $itemId);
5749
5750		if (isset($args['values']) && isset($args['old_values'])) {
5751			$fields = array_merge(array_keys($args['values']), array_keys($args['old_values']));
5752			$fields = array_unique($fields);
5753		}
5754
5755		if (! empty($fields)) {
5756			foreach ($fields as $fieldId) {
5757				$old = isset($args['old_values'][$fieldId]) ? $args['old_values'][$fieldId] : null;
5758				$new = isset($args['values'][$fieldId]) ? $args['values'][$fieldId] : null;
5759
5760				if ($old !== $new) {
5761					$this->invalidate_field_cache($fieldId);
5762				}
5763			}
5764		}
5765	}
5766
5767	private function invalidate_field_cache($fieldId)
5768	{
5769		global $prefs, $user;
5770		$multi_languages = $prefs['available_languages'];
5771		if (! $multi_languages) {
5772			$multi_languages = [];
5773		}
5774
5775		$multi_languages[] = '';
5776
5777		$cachelib = TikiLib::lib('cache');
5778
5779		foreach ($multi_languages as $lang) {
5780			$cachelib->invalidate(md5('trackerfield' . $fieldId . 'o' . $user . $lang));
5781			$cachelib->invalidate(md5('trackerfield' . $fieldId . 'c' . $user . $lang));
5782			$cachelib->invalidate(md5('trackerfield' . $fieldId . 'p' . $user . $lang));
5783			$cachelib->invalidate(md5('trackerfield' . $fieldId . 'op' . $user . $lang));
5784			$cachelib->invalidate(md5('trackerfield' . $fieldId . 'oc' . $user . $lang));
5785			$cachelib->invalidate(md5('trackerfield' . $fieldId . 'pc' . $user . $lang));
5786			$cachelib->invalidate(md5('trackerfield' . $fieldId . 'opc' . $user . $lang));
5787		}
5788	}
5789
5790	public function group_tracker_create($args)
5791	{
5792		global $user, $group;
5793		$trackerId = $args['trackerId'];
5794		$itemId = $args['object'];
5795		$new_itemId = isset($args['new_itemId']) ? $args['new_itemId'] : '';
5796		$tracker_info = isset($args['tracker_info']) ? $args['tracker_info'] : '';
5797		$definition = Tracker_Definition::get($trackerId);
5798
5799		if ($definition && $definition->isEnabled('autoCreateGroup')) {
5800			$creatorGroupFieldId = $definition->getWriterGroupField();
5801
5802			if (! empty($creatorGroupFieldId) && $definition->isEnabled('autoAssignGroupItem')) {
5803				$autoCopyGroup = $definition->getConfiguration('autoCopyGroup');
5804				if ($autoCopyGroup) {
5805					$this->modify_field($new_itemId, $tracker_info['autoCopyGroup'], $group);
5806					$fil[$tracker_info['autoCopyGroup']] = $group;
5807				}
5808			}
5809			$desc = $this->get_isMain_value($trackerId, $itemId);
5810			if (empty($desc)) {
5811				$desc = $definition->getConfiguration('description');
5812			}
5813
5814			$userlib = TikiLib::lib('user');
5815			$groupName = $args['values'][$creatorGroupFieldId];
5816			if ($userlib->add_group($groupName, $desc, '', 0, $trackerId, '', 'y', 0, '', '', $creatorGroupFieldId)) {
5817				if ($groupId = $definition->getConfiguration('autoCreateGroupInc')) {
5818					$userlib->group_inclusion($groupName, $this->table('users_groups')->fetchOne('groupName', ['id' => $groupId]));
5819				}
5820			}
5821			if ($definition->isEnabled('autoAssignCreatorGroup')) {
5822				$userlib->assign_user_to_group($user, $groupName);
5823			}
5824			if ($definition->isEnabled('autoAssignCreatorGroupDefault')) {
5825				$userlib->set_default_group($user, $groupName);
5826				$_SESSION['u_info']['group'] = $groupName;
5827			}
5828		}
5829	}
5830
5831	public function update_tracker_summary($args)
5832	{
5833		$items = $this->items();
5834		$trackerId = (int) $args['trackerId'];
5835		$cant_items = $items->fetchCount(['trackerId' => $trackerId]);
5836		$this->trackers()->update(['items' => (int) $cant_items, 'lastModif' => $this->now], ['trackerId' => $trackerId]);
5837	}
5838
5839	public function sync_freetags($args)
5840	{
5841		$definition = Tracker_Definition::get($args['trackerId']);
5842
5843		if ($definition && $field = $definition->getFreetagField()) {
5844			global $user;
5845			$freetaglib = TikiLib::lib('freetag');
5846			$freetaglib->update_tags($user, $args['object'], 'trackeritem', $args['values'][$field]);
5847		}
5848	}
5849
5850	public function update_create_missing_pages($args)
5851	{
5852		global $user;
5853		$tikilib = TikiLib::lib('tiki');
5854
5855		$definition = Tracker_Definition::get($args['trackerId']);
5856		if (! $definition) {
5857			return;
5858		}
5859
5860		foreach ($definition->getFields() as $field) {
5861			$fieldId = $field['fieldId'];
5862			$value = isset($args['values'][$fieldId]) ? $args['values'][$fieldId] : '';
5863			if ($field['type'] == 'k' && $value != '' && ! empty($field['options'][2])) {
5864				if (! $this->page_exists($value)) {
5865					$IP = $this->get_ip_address();
5866					$info = $this->get_page_info($field['options'][2]);
5867					$tikilib->create_page($value, 0, $info['data'], $tikilib->now, '', $user, $IP, $info['description'], $info['lang'], $info['is_html'], [], $info['wysiwyg'], $info['wiki_authors_style']);
5868				}
5869			}
5870		}
5871	}
5872
5873	public function get_maximum_value($fieldId)
5874	{
5875		return $this->itemFields()->fetchOne($this->itemFields()->expr('MAX(CAST(`value` as UNSIGNED))'), ['fieldId' => (int) $fieldId]);
5876	}
5877
5878	public function sync_categories($args)
5879	{
5880		$definition = Tracker_Definition::get($args['trackerId']);
5881		if (! $definition) {
5882			return;
5883		}
5884
5885		$ins_categs = [];
5886		$parent_categs_only = [];
5887		$tosync = false;
5888		$managed_fields = [];
5889
5890		$categorizedFields = $definition->getCategorizedFields();
5891
5892		if (isset($args['supplied'])) {
5893			// Exclude fields that were not part of the request
5894			$categorizedFields = array_intersect($categorizedFields, $args['supplied']);
5895		}
5896
5897		foreach ($categorizedFields as $fieldId) {
5898			if (isset($args['values'][$fieldId])) {
5899				$ins_categs = array_merge($ins_categs, array_filter(explode(',', $args['values'][$fieldId])));
5900				$managed_fields[] = $fieldId;
5901				$tosync = true;
5902			}
5903		}
5904
5905		if ($tosync) {
5906			$this->categorized_item($args['trackerId'], $args['object'], "item {$args['object']}", $ins_categs, null, false, $managed_fields);
5907		}
5908	}
5909
5910
5911	/**
5912	 * Render a field value for input or output. The result depends on the fieldtype.
5913	 * Note: Each fieldtype has its own input/output handler.
5914	 * @param array $params - either a complete field array or a trackerid and a permName
5915	 * <pre>
5916	 * $param = array(
5917	 *        // required
5918	 *        'field' => array( 'fieldId' => 1, 'trackerId' => 2, 'permName' => 'myPermName', 'etc' => '...')
5919	 *        //'trackerId' => 1            // instread of 'field'
5920	 *        //'permName>' => 'myPermName' // instread of 'field'
5921	 *
5922	 *        // optional
5923	 *        'item' => array('fieldId1' => fieldValue1, 'fieldId2' => fieldValue2) // optional
5924	 *        'itemId' = 5                  // itemId
5925	 *        'process' => 'y'              // renders the value using the correct field handler
5926	 *        'oldValue' => ''              // renders the new and old values using \Tracker_Field_Abstract::renderDiff
5927	 *        'list_mode' => ''             // i.e. 'y', 'cvs' or 'text' will be used in \Tracker_Field_Abstract::renderOutput
5928	 * )
5929	 * </pre>
5930	 * @return string - rendered value (with html ?). i.e from $r = $handler->renderInput($context), renderOutput or renderDiff
5931	 * @throws Exception
5932	 */
5933	public function field_render_value($params)
5934	{
5935		// accept either a complete field definition or a trackerId/permName
5936		if (isset($params['field'])) {
5937			$field = $params['field'];
5938		} elseif (isset($params['trackerId'], $params['permName'])) {
5939			$definition = Tracker_Definition::get($params['trackerId']);
5940			$field = $definition->getFieldFromPermName($params['permName']);
5941		} elseif (isset($params['fieldId'])) {
5942			$field = $this->get_field_info($params['fieldId']);
5943		} else {
5944			return tr('Field not specified');
5945		}
5946
5947		// preset $item = array('itemId' => value). Either from param or empty
5948		$item = isset($params['item']) ? $params['item'] : [];
5949
5950		// if we have an itemId, pass it to our new item structure
5951		if (isset($params['itemId'])) {
5952			$item['itemId'] = $params['itemId'];
5953		}
5954
5955		// check wether we have a value assigned to $fields.
5956		// This might be the case if $fields was passed through $params and not from the tracker definition.
5957		// Build the $items['fieldId'] = value structure
5958		if (isset($field['fieldId'])) {
5959			if (isset($field['value'])) {
5960				$item[$field['fieldId']] = $field['value'];
5961			} elseif (isset($item['itemId'])) {
5962				$item[$field['fieldId']] = $this->get_item_value(null, $item['itemId'], $field['fieldId']);
5963			} elseif (isset($params['value'])) {
5964				$field['value'] = $params['value'];
5965				$field['ins_' . $field['fieldId']] = $field['value'];
5966				$item[$field['fieldId']] = $field['value'];
5967			}
5968		}
5969
5970		// get the handler for the specific fieldtype.
5971		$handler = $this->get_field_handler($field, $item);
5972
5973		if ($handler) {
5974			if (! isset($field['value'])) {
5975				$data = $handler->getFieldData();
5976				$field['value'] = $data['value'];
5977			}
5978
5979			if (isset($params['process']) && $params['process'] == 'y') {
5980				if ($field['type'] === 'e') {	// category
5981					if (! is_array($field['value'])) {
5982						$categIds = explode(',', $field['value']);
5983					} else {
5984						$categIds = $field['value'];
5985					}
5986					$requestData = ['ins_' . $field['fieldId'] => $categIds];
5987				} else {
5988					$requestData = $field;
5989				}
5990				$linkedField = $handler->getFieldData($requestData);
5991				$field = array_merge($field, $linkedField);
5992				$field['ins_id'] = 'ins_' . $field['fieldId'];
5993				$handler = $this->get_field_handler($field, $item);
5994			}
5995
5996			$context = $params;
5997			$fieldId = $field['fieldId'];
5998			unset($context['item']);
5999			unset($context['field']);
6000			if (empty($context['list_mode'])) {
6001				$context['list_mode'] = 'n';
6002			}
6003
6004			if (! empty($params['editable']) && $params['field']['type'] !== 'STARS') {
6005				if ($params['editable'] === true) {
6006					// Some callers pass true/false instead of an actual mode, default to block
6007					$params['editable'] = 'block';
6008				}
6009
6010				if ($params['editable'] == 'direct') {
6011					$r = $handler->renderInput($context);
6012					$params['editable'] = 'block';
6013					$fetchUrl = null;
6014				} else {
6015					$r = $handler->renderOutput($context);
6016					$fetchUrl = [
6017						'controller' => 'tracker',
6018						'action' => 'fetch_item_field',
6019						'trackerId' => $field['trackerId'],
6020						'itemId' => $item['itemId'],
6021						'fieldId' => $field['fieldId'],
6022						'listMode' => $context['list_mode']
6023					];
6024				}
6025
6026				$r = new Tiki_Render_Editable(
6027					$r,
6028					[
6029						'layout' => $params['editable'],
6030						'label' => $field['name'],
6031						'group' => ! empty($params['editgroup']) ? $params['editgroup'] : false,
6032						'object_store_url' => [
6033							'controller' => 'tracker',
6034							'action' => 'update_item',
6035							'trackerId' => $field['trackerId'],
6036							'itemId' => $item['itemId'],
6037						],
6038						'field_fetch_url' => $fetchUrl,
6039					]
6040				);
6041			} elseif (isset($params['oldValue'])) {
6042				$r = $handler->renderDiff($context);
6043			} else {
6044				$r = $handler->renderOutput($context);
6045			}
6046
6047			TikiLib::lib('smarty')->assign("f_$fieldId", $r);
6048			$fieldPermName = $field['permName'];
6049			TikiLib::lib('smarty')->assign("f_$fieldPermName", $r);
6050			return $r;
6051		}
6052	}
6053
6054	public function get_child_items($itemId)
6055	{
6056		return $this->fetchAll('SELECT permName as field, itemId FROM tiki_tracker_item_fields v INNER JOIN tiki_tracker_fields f ON v.fieldId = f.fieldId WHERE f.type = "r" AND v.value = ?', [$itemId]);
6057	}
6058
6059	public function get_field_by_perm_name($permName)
6060	{
6061		return $this->get_tracker_field($permName);
6062	}
6063
6064	public function refresh_index_on_master_update($args)
6065	{
6066		// Event handler
6067		// See pref tracker_refresh_itemlink_detail
6068
6069		$modifiedFields = [];
6070		foreach ($args['old_values'] as $key => $old) {
6071			if (! isset($args['values'][$key]) || $args['values'][$key] != $old) {
6072				$modifiedFields[] = $key;
6073			}
6074		}
6075
6076		$items = $this->findLinkedItems(
6077			$args['object'],
6078			function ($field, $handler) use ($modifiedFields, $args) {
6079				return $handler->itemsRequireRefresh($args['trackerId'], $modifiedFields);
6080			}
6081		);
6082
6083		$searchlib = TikiLib::lib('unifiedsearch');
6084		foreach ($items as $itemId) {
6085			$searchlib->invalidateObject('trackeritem', $itemId);
6086		}
6087	}
6088
6089	private function findLinkedItems($itemId, $callback)
6090	{
6091		$fields = $this->table('tiki_tracker_fields');
6092		$list = $fields->fetchAll(
6093			$fields->all(),
6094			['type' => $fields->exactly('r')]
6095		);
6096
6097		$toConsider = [];
6098
6099		foreach ($list as $field) {
6100			$handler = $this->get_field_handler($field);
6101
6102			if ($handler && $callback($field, $handler)) {
6103				$toConsider[] = $field['fieldId'];
6104			}
6105		}
6106
6107		$itemFields = $this->itemFields();
6108		$items = $itemFields->fetchColumn(
6109			'itemId',
6110			[
6111				'fieldId' => $itemFields->in($toConsider),
6112				'value' => $itemId,
6113			]
6114		);
6115
6116		return array_unique($items);
6117	}
6118
6119	public function refresh_itemslist_index($args)
6120	{
6121		// Event handler
6122		// See pref tracker_refresh_itemslist_detail
6123
6124		$modifiedFields = [];
6125		foreach ($args['old_values'] as $key => $old) {
6126			if (! isset($args['values'][$key]) || $args['values'][$key] != $old) {
6127				$modifiedFields[] = $key;
6128			}
6129		}
6130		foreach ($args['values'] as $key => $new) {
6131			if (! isset($args['old_values'][$key]) || $args['old_values'][$key] != $new) {
6132				$modifiedFields[] = $key;
6133			}
6134		}
6135		$modifiedFields = array_unique($modifiedFields);
6136
6137		$items = [];
6138
6139		$fields = $this->table('tiki_tracker_fields');
6140		$list = $fields->fetchAll(
6141			$fields->all(),
6142			['type' => $fields->exactly('l')]
6143		);
6144		foreach ($list as $field) {
6145			$handler = $this->get_field_handler($field);
6146			if ($handler && $handler->itemsRequireRefresh($args['trackerId'], $modifiedFields)) {
6147				$itemId = $args['object'];
6148
6149				$fieldIdHere = (int) $handler->getOption('fieldIdHere');
6150				$fieldIdThere = (int) $handler->getOption('fieldIdThere');
6151
6152				// quick way of getting all ItemsList items pointing to the itemId via the field we examine
6153				if (empty($fieldIdThere)) {
6154					$query = "SELECT itemId
6155					FROM tiki_tracker_item_fields ttif
6156					WHERE ttif.fieldId = ?
6157					AND ttif.`value` = ?";
6158					$bindvars = [$fieldIdHere, $itemId];
6159				} else {
6160					$query = "SELECT COALESCE(ttif2.itemId, ttif1.value) as itemId
6161					FROM tiki_tracker_item_fields ttif1
6162					LEFT JOIN tiki_tracker_item_fields ttif2 ON (ttif2.value = ttif1.value OR ttif2.value = ttif1.itemId) AND ttif2.fieldId = ?
6163					WHERE ttif1.fieldId = ?
6164					AND ttif1.itemId = ?";
6165					$bindvars = [$fieldIdHere, $fieldIdThere, $itemId];
6166				}
6167
6168				$fieldItems = $this->fetchAll($query, $bindvars);
6169				$fieldItems = array_map(
6170					function ($row) {
6171						return $row['itemId'];
6172					},
6173					$fieldItems
6174				);
6175				$items = array_merge($items, $fieldItems);
6176			}
6177		}
6178		$items = array_unique($items);
6179
6180		$searchlib = TikiLib::lib('unifiedsearch');
6181		foreach ($items as $itemId) {
6182			$searchlib->invalidateObject('trackeritem', $itemId);
6183		}
6184	}
6185
6186	public function update_user_account($args)
6187	{
6188		// Try to find if the tracker is a user tracker, flag update to associated user
6189
6190		$fields = array_keys($args['values']);
6191		if (! $fields) {
6192			return;
6193		}
6194		$table = $this->table('users_groups');
6195		$fields = array_filter($fields, 'is_numeric');
6196		$field = $table->fetchOne(
6197			'usersFieldId',
6198			[
6199				'usersFieldId' => $table->in($fields),
6200			]
6201		);
6202
6203		if ($field && ! empty($args['values'][$field])) {
6204			TikiLib::events()->trigger(
6205				'tiki.user.update',
6206				[
6207					'type' => 'user',
6208					'object' => $args['values'][$field],
6209				]
6210			);
6211		}
6212	}
6213	// connect a user to his user item on the email field / email user
6214	public function update_user_item($user, $email, $emailFieldId)
6215	{
6216		$field = $this->get_tracker_field($emailFieldId);
6217		$trackerId = $field['trackerId'];
6218		$definition = Tracker_Definition::get($trackerId);
6219		$userFieldId = $definition->getUserField();
6220		$listfields[$userFieldId] = $definition->getField($userFieldId);
6221		$filterfields[0] = $emailFieldId; // Email field in the user tracker
6222		$exactvalue[0] = $email;
6223		$items = $this->list_items($trackerId, 0, -1, 'created', $listfields, $filterfields, '', 'opc', '', $exactvalue);
6224		$found = false;
6225		foreach ($items['data'] as $item) {
6226			if (empty($item['field_values'][0]['value'])) {
6227				$found = true;
6228				$this->modify_field($item['itemId'], $userFieldId, $user);
6229			} elseif ($item['field_values'][0]['value'] == $user) {
6230				$found = true;
6231			}
6232		}
6233		return $found;
6234	}
6235
6236
6237	/**
6238	 * Called from lib/setup/events.php when object are categorized.
6239	 * This is to ensure that article and trackeritem categories stay in sync when article indexing is on
6240	 * as part of the RSS Article generator feature.
6241	 * @param $args
6242	 * @param $event
6243	 * @param $priority
6244	 * @throws Exception
6245	 */
6246	public function sync_tracker_article_categories($args, $event, $priority)
6247	{
6248		global $prefs;
6249		$catlib = TikiLib::lib('categ');
6250		if ($args['type'] == 'article') {
6251			//if it's an article, find the associated trackeritem per the relation
6252			$relationlib = TikiLib::lib('relation');
6253			$artRelation = $relationlib->get_relations_to('article', $args['object'], 'tiki.article.attach', '', '1');
6254			if (empty($artRelation)) {
6255				return;
6256			}
6257			$tracker_item_id = $artRelation[0]['itemId'];
6258			//if the tracker isn't the article tracker as per the pref, don't sync
6259			if (! $tracker_item_id || $prefs['tracker_article_trackerId'] != $this->get_tracker_for_item($tracker_item_id)) {
6260				return;
6261			}
6262			// get the trackeritem's categories and add or remove the same categories that the article had
6263			// added or removed as per the event
6264			$categories = $catlib->get_object_categories('trackeritem', $tracker_item_id);
6265			$categories_old = $categories;
6266			foreach ($args['added'] as $added) {
6267				if (! in_array($added, $categories)) {
6268					$categories[] = $added;
6269				}
6270			}
6271			foreach ($args['removed'] as $removed) {
6272				if (in_array($removed, $categories)) {
6273					$categories = array_diff($categories, [$removed]);
6274				}
6275			}
6276			//update the trackeritems categories if there were new ones added/removed
6277			if ($categories != $categories_old) {
6278				$catlib->update_object_categories($categories, $tracker_item_id, 'trackeritem');
6279			}
6280		} elseif ($args['type'] == 'trackeritem') {
6281			//if trackeritem, make sure it's the article tracker that we're dealing with
6282			$trackerId = $this->get_tracker_for_item($args['object']);
6283			if ($prefs['tracker_article_trackerId'] != $trackerId) {
6284				return;
6285			}
6286			$definition = Tracker_Definition::get($trackerId);
6287			//find the article field in this tracker and from there find the relation for the
6288			$relationlib = TikiLib::lib('relation');
6289			$artRelation = $relationlib->get_relations_from('trackeritem', $args['object'], 'tiki.article.attach', '', '1');
6290			if (empty($artRelation)) {
6291				return;
6292			}
6293			$articleId = $artRelation[0]['itemId'];
6294			// get the articles's categories and add or remove the same categories that the trackeritem had
6295			// added or removed as per the event
6296			$categories = $catlib->get_object_categories('article', $articleId);
6297			$categories_old = $categories;
6298			foreach ($args['added'] as $added) {
6299				if (! in_array($added, $categories)) {
6300					$categories[] = $added;
6301				}
6302			}
6303			foreach ($args['removed'] as $removed) {
6304				if (in_array($removed, $categories)) {
6305					$categories = array_diff($categories, [$removed]);
6306				}
6307			}
6308			//update the article's categories if there were new ones added/removed
6309			if ($categories != $categories_old) {
6310				$catlib->update_object_categories($categories, $articleId, 'article');
6311			}
6312		}
6313	}
6314
6315	/**
6316	 * Called when accessing contents of a Tracker UserSelector field.
6317	 * Purpose is to parse the csv string of usernames stored inside and format an array.
6318	 * @param $value csv-formatted string
6319	 * @return array of resulting usernames
6320	 */
6321	public function parse_user_field($value)
6322	{
6323		return array_filter(
6324			array_map(function ($user) {
6325				return trim($user);
6326			}, is_array($value) ? $value : str_getcsv($value))
6327		);
6328	}
6329
6330	/**
6331	 * Given a currency exchange rate tracker and a date,
6332	 * return all available currency rates valid for that time.
6333	 * @param $trackerId the currency tracker
6334	 * @param $date
6335	 * @return array of exchange rates
6336	 */
6337	public function exchange_rates($trackerId, $date) {
6338		static $rates = [];
6339		if (isset($rates[$date])) {
6340			return $rates[$date];
6341		}
6342		$rates[$date] = [];
6343		$currencyField = $dateField = $rateField = null;
6344		$definition = Tracker_Definition::get($trackerId);
6345		$fields = $definition->getFields();
6346		foreach ($fields as $field) {
6347			switch ($field['type']) {
6348				case 't':
6349					if (!$currencyField) {
6350						$currencyField = $field;
6351					}
6352					break;
6353				case 'f':
6354				case 'j':
6355					if (!$dateField) {
6356						$dateField = $field;
6357					}
6358					break;
6359				case 'n':
6360					if (!$rateField) {
6361						$rateField = $field;
6362					}
6363					break;
6364			}
6365		}
6366		if ($currencyField && $dateField && $rateField) {
6367			$currencies = $this->list_tracker_field_values($trackerId, $currencyField['fieldId']);
6368			foreach ($currencies as $currency) {
6369				$rates[$date][$currency] = $this->getOne('SELECT ttif3.value as rate FROM tiki_tracker_items tti
6370					LEFT JOIN tiki_tracker_item_fields ttif1 ON tti.itemId = ttif1.itemId AND ttif1.fieldId = ?
6371					LEFT JOIN tiki_tracker_item_fields ttif2 ON tti.itemId = ttif2.itemId AND ttif2.fieldId = ?
6372					LEFT JOIN tiki_tracker_item_fields ttif3 ON tti.itemId = ttif3.itemId AND ttif3.fieldId = ?
6373					WHERE tti.trackerId = ? AND ttif1.value = ? AND DATE_FORMAT(FROM_UNIXTIME(ttif2.value), "%Y-%m-%d") <= ?
6374					ORDER BY ttif2.value DESC',
6375					[$currencyField['fieldId'], $dateField['fieldId'], $rateField['fieldId'], $trackerId, $currency, $date]);
6376			}
6377		}
6378		return $rates[$date];
6379	}
6380
6381	/**
6382	 * Generate unique tracker field Permanent name
6383	 * @param $definition
6384	 * @param $permName
6385	 * @param int $maxAllowedSize
6386	 * @return string
6387	 * @throws Services_Exception_DuplicateValue
6388	 */
6389	public static function generatePermName($definition, $permName, $maxAllowedSize = Tracker_Item::PERM_NAME_MAX_ALLOWED_SIZE)
6390	{
6391		// Ensure that PermName is no longer than 50 characters, since the maximum allowed by MySQL Full
6392		// Text Search as Unified Search Index is 64, and trackers will internally prepend "tracker_field_",
6393		// which are another 14 characters (50+14=64). We could allow longer permanent names when other search
6394		// index engines are the ones being used, but this will probably only delay the problem until the admin
6395		// wants to change the search engine for some reason (some constrains in Lucene or Elastic Search,
6396		// as experience demonstrated in some production sites in real use cases over long periods of time).
6397		// And to increase chances to avoid conflict when long names only differ in the end of the long string,
6398		// where some meaningful info resides, we'll get the first (PERM_NAME_MAX_ALLOWED_SIZE - 10) chars, 1 underscore and the last 9 chars.
6399		$permName = (strlen($permName) > $maxAllowedSize) ? substr($permName, 0, ($maxAllowedSize - 10)) . '_' . substr($permName, -9) : $permName;
6400
6401		// Quick way to solve permName conflict, which is very common in languages that only use characters considered
6402		// special for this purpose (ie: hebrew). Ideally we should use fieldId, but it haven't been defined yet.
6403		$tries = 0;
6404		while ($definition->getFieldFromPermName($permName)) {
6405			$permName = substr($permName, 0, ($maxAllowedSize - 5)) . "_" . rand(1000, 9999);
6406			// Let's avoid theoretical chance of infinite loop
6407			if (++$tries > 100) {
6408				throw new Services_Exception_DuplicateValue('permName', $permName);
6409			}
6410		}
6411
6412		return $permName;
6413	}
6414}
6415