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/**
10 * Foundation of all trackerfields. Each trackerfield defines its own class that derives from this one and also
11 * hast to implement Tracker_Field_Interface, Tracker_Field_Indexable.
12 *
13 */
14abstract class Tracker_Field_Abstract implements Tracker_Field_Interface, Tracker_Field_Indexable
15{
16	/**
17	 * @var string - ???
18	 */
19	private $baseKeyPrefix = '';
20
21	/**
22	 * @var array - the field definition
23	 */
24	private $definition;
25
26	/**
27	 * @var handle ??? -
28	 */
29	private $options;
30
31	/**
32	 * @var array - complex data about an item. including itemId, trackerId and values of fields by fieldId=>value pairs
33	 *
34	 */
35	private $itemData;
36
37	/**
38	 * @var array - trackerdefinition
39	 *
40	 */
41	private $trackerDefinition;
42
43
44	/**
45	 * Initialize the instance with field- and trackerdefinition and item value(s)
46	 * @param array $fieldInfo - the field definition
47	 * @param array $itemData - itemId/value pair(s)
48	 * @param array $trackerDefinition - the tracker definition.
49	 *
50	 */
51	function __construct($fieldInfo, $itemData, $trackerDefinition)
52	{
53		$this->options = Tracker_Options::fromSerialized($fieldInfo['options'], $fieldInfo);
54
55		if (! isset($fieldInfo['options_array'])) {
56			$fieldInfo['options_array'] = $this->options->buildOptionsArray();
57		}
58
59		$this->definition = $fieldInfo;
60		$this->itemData = $itemData;
61		$this->trackerDefinition = $trackerDefinition;
62	}
63
64
65	/**
66	 * Not implemented here. Its upto to the extending class.
67	 * @param array $context - ???
68	 * @return string $renderedContent depending on the $context
69	 */
70	public function renderInput($context = [])
71	{
72		return 'Not implemented';
73	}
74
75
76	/**
77	 * Render output for this field.
78	 * IMPORTANT: This method uses the following $_GET args directly: 'page'
79	 * @TODO fixit so it does not directly access the $_GET array. Better pass it as a param.
80	 * @param array $context -keys:
81	 * <pre>
82	 * $context = array(
83	 * 		// required
84	 * 		// optional
85	 * 		'url' => 'sefurl', // other values 'offset', 'tr_offset'
86	 *  	'reloff' => true, // checked only if set
87	 *  	'showpopup' => 'y', // wether to show that value in a mouseover popup
88	 *  	'showlinks' => 'n' // NO check for 'y' but 'n'
89	 *  	'list_mode' => 'csv' //
90	 * );
91	 * </pre>
92	 *
93	 * @return string $renderedContent depending on the $context
94	 * @throws Exception
95	 */
96	public function renderOutput($context = [])
97	{
98		// only if this field is marked as link and the is no request for a csv export
99		// create the link and if required the mouseover popup
100		if ($this->isLink($context)) {
101			$itemId = $this->getItemId();
102			$query = [];
103			if (isset($_GET['page'])) {
104				$query['from'] = $_GET['page'];
105			}
106
107			$classList = ['tablename'];
108			$metadata = TikiLib::lib('object')->get_metadata('trackeritem', $itemId, $classList);
109
110			require_once('lib/smarty_tiki/modifier.sefurl.php');
111			$href = smarty_modifier_sefurl($itemId, 'trackeritem');
112			$href .= (strpos($href, '?') === false) ? '?' : '&';
113			$href .= http_build_query($query, '', '&');
114			$href = rtrim($href, '?&');
115
116			$arguments = [
117				'class' => implode(' ', $classList),
118				'href' => $href,
119			];
120			if (! empty($context['url'])) {
121				if ($context['url'] == 'sefurl') {
122					$context['url'] = 'item' . $itemId;
123				} elseif (strpos($context['url'], 'itemId') !== false) {
124					$context['url'] = preg_replace('/([&|\?])itemId=?[^&]*/', '\\1itemId=' . $itemId, $context['url']);
125				} elseif (isset($context['reloff']) && strpos($context['url'], 'offset') !== false) {
126					$smarty = TikiLib::lib('smarty');
127					$context['url'] = preg_replace('/([&|\?])tr_offset=?[^&]*/', '\\1tr_offset' . $smarty->tpl_vars['iTRACKERLIST']
128						. '=' . $context['reloff'], $context['url']);
129				}
130				$arguments['href'] = $context['url'];
131			}
132
133			$pre = '<a';
134			foreach ($arguments as $key => $value) {
135				$pre .= ' ' . $key . '="' . htmlentities($value, ENT_QUOTES, 'UTF-8') . '"';
136			}
137
138			// add the html / js for the mouseover popup
139			if (isset($context['showpopup']) && $context['showpopup'] == 'y') {
140				// check if trackerplugin has set popup fields using the popup parameter
141				$pluginPopupFields = isset($context['popupfields']) ? $context['popupfields'] : null;
142				$popup = $this->renderPopup($pluginPopupFields);
143
144				if ($popup) {
145					$popup = preg_replace('/<\!--.*?-->/', '', $popup);	// remove comments added by log_tpl
146					$popup = preg_replace('/\s+/', ' ', $popup);
147					$pre .= " $popup";
148				}
149			}
150
151			$pre .= $metadata;
152			$pre .= '>';
153			$post = '</a>';
154
155			return $pre . $this->renderInnerOutput($context) . $post;
156		} else {
157			// no link, no mouseover popup. Note: can also be a part of a csv request
158			return $this->renderInnerOutput($context);
159		}
160	}
161
162	/**
163	 * Render a diff of two values for a tracker field.
164	 * Will use the item value if $context['oldValue'] is not supplied
165	 *
166	 * @param array $context [value, oldValue, etc as for renderOutput]
167	 * @return string|string[] html, usually a table
168	 */
169	public function renderDiff($context = [])
170	{
171		if ($context['oldValue']) {
172			$old = $context['oldValue'];
173		} else {
174			$old = '';
175		}
176		if ($context['value']) {
177			$new = $context['value'];
178		} else {
179			$new = $this->getValue('');
180		}
181		if (isset($context['renderedOldValue'])) {
182			$old = $context['renderedOldValue'];
183		} else {
184			$old_definition = $this->definition;
185			$key = 'ins_' . $this->getConfiguration('fieldId');
186			if ($this->getConfiguration('type') === 'e' && is_string($old)) {	// category fields need an array input
187				$old = explode(',', $old);
188			}
189			$this->definition = array_merge($this->definition, $this->getFieldData([$key => $old]));
190			$this->itemData[$this->getConfiguration('fieldId')] = $old;
191			$old = $this->renderInnerOutput(['list_mode' => 'csv']);
192			$old = str_replace(['%%%', '<br>', '<br/>'], ["\n", ' ', ' '], $old);
193			$this->definition = $old_definition;
194			$this->itemData[$this->getConfiguration('fieldId')] = $new;
195		}
196		$new = $this->renderInnerOutput(['list_mode' => 'csv']);
197		$new = str_replace(['%%%', '<br>', '<br/>'], ["\n", ' ', ' '], $new);
198		if (empty($context['diff_style'])) {
199			$context['diff_style'] = 'inlinediff';
200		}
201		require_once('lib/diff/difflib.php');
202		$diff = diff2($old, $new, $context['diff_style']);
203		$result = '';
204
205		if (is_array($diff)) {
206			// unidiff mode
207			foreach ($diff as $part) {
208				if ($part["type"] == "diffdeleted") {
209					foreach ($part["data"] as $chunk) {
210						$result .= "<blockquote>- $chunk</blockquote>";
211					}
212				}
213				if ($part["type"] == "diffadded") {
214					foreach ($part["data"] as $chunk) {
215						$result .= "<blockquote>+ $chunk</blockquote>";
216					}
217				}
218			}
219		} else {
220			$result = strpos($diff, '<tr') === 0 ? '<table>' . $diff . '</table>' : $diff;
221			$result = preg_replace('/<tr class="diffheader">.*?<\/tr>/', '', $result);
222			$result = str_replace('<table>', '<table class="table">', $result);
223		}
224		return $result;
225	}
226
227	function watchCompare($old, $new)
228	{
229		$name = $this->getConfiguration('name');
230		$is_visible = $this->getConfiguration('isHidden', 'n') == 'n';
231
232		if (! $is_visible) {
233			return '';
234		}
235
236		if ($old) {
237			// split old value by lines
238			$lines = explode("\n", $old);
239			// mark every old value line with standard email reply character
240			$old_value_lines = '';
241			foreach ($lines as $line) {
242				$old_value_lines .= '> ' . $line . "\n";
243			}
244			return "[-[$name]-]:\n--[Old]--:\n$old_value_lines\n\n*-[New]-*:\n$new";
245		} else {
246			return "[-[$name]-]:\n$new";
247		}
248	}
249
250
251	private function isLink($context = [])
252	{
253		$type = $this->getConfiguration('type');
254		if ($type == 'x') {
255			return false;
256		}
257
258		if ($this->getConfiguration('showlinks', 'y') == 'n') {
259			return false;
260		}
261
262		if (isset($context['showlinks']) && $context['showlinks'] == 'n') {
263			return false;
264		}
265
266		if (isset($context['list_mode']) && $context['list_mode'] == 'csv') {
267			return false;
268		}
269
270		$itemId = $this->getItemId();
271		if (empty($itemId)) {
272			return false;
273		}
274		$itemObject = Tracker_Item::fromInfo($this->itemData);
275
276		$status = $this->getData('status');
277
278		if ($this->getConfiguration('isMain', 'n') == 'y'
279			&& ($itemObject->canView()	|| $itemObject->getPerm('comment_tracker_items'))
280			) {
281			return (bool) $this->getItemId();
282		}
283
284		return false;
285	}
286
287
288	/**
289	 * Create the html/js to show a popupwindow on mouseover when the trackeritem has a field with link enabled.
290	 * The formatting is done via smarty based on 'trackeroutput/popup.tpl'
291	 * @param array $pluginPopupFields - array with fieldids set by trackerlist plugin. if not set the tracker defaults will be used.
292	 * @return NULL|string $popupHtml
293	 */
294	private function renderPopup($pluginPopupFields = null)
295	{
296		// support of trackerlist plugin popup field - if popup is set and has fields - show the fields as defined and in their order
297		// if parameter popup is set but without fields show no popup
298		// note: the popup template code in wikiplugin_trackerlist.tpl does not seem to be used at all - only the flag $showpopup
299		if ($pluginPopupFields && is_array($pluginPopupFields)) {
300			$fields = $pluginPopupFields;
301		} else {
302			// plugin trackerlist not involved
303			$fields = $this->trackerDefinition->getPopupFields();
304		}
305
306		if (empty($fields)) {
307			return null;
308		}
309
310		$factory = $this->trackerDefinition->getFieldFactory();
311
312		// let's honor doNotShowEmptyField
313		$tracker_info = $this->trackerDefinition->getInformation();
314		$doNotShowEmptyField = $tracker_info['doNotShowEmptyField'];
315
316		$popupFields = [];
317		$item = Tracker_Item::fromInfo($this->itemData);
318
319		foreach ($fields as $id) {
320			if (! $item->canViewField($id)) {
321				continue;
322			}
323			$field = $this->trackerDefinition->getField($id);
324
325			if (! isset($this->itemData[$field['fieldId']])) {
326				if (! empty($this->itemData['field_values'])) {
327					foreach ($this->itemData['field_values'] as $fieldVal) {
328						if ($fieldVal['fieldId'] == $id) {
329							if (isset($fieldVal['value'])) {
330								$this->itemData[$field['fieldId']] = $fieldVal['value'];
331							}
332						}
333					}
334				} else {
335					$this->itemData[$field['fieldId']] = TikiLib::lib('trk')->get_item_value(
336						$this->trackerDefinition->getConfiguration('trackerId'),
337						$this->itemData['itemId'],
338						$id
339					);
340				}
341			}
342			$handler = $factory->getHandler($field, $this->itemData);
343
344			if ($handler && ($doNotShowEmptyField !== 'y' || ! empty($this->itemData[$field['fieldId']]))) {
345				$field = array_merge($field, $handler->getFieldData());
346				$popupFields[] = $field;
347			}
348		}
349
350		$smarty = TikiLib::lib('smarty');
351		$smarty->assign('popupFields', $popupFields);
352		$smarty->assign('popupItem', $this->itemData);
353		return trim($smarty->fetch('trackeroutput/popup.tpl'));
354	}
355
356	/**
357	 * return the html for the output of a field without link, prepend...
358	 * @param array $context - key 'list_mode' defines wether to output for a list or a simple value
359	 * @return string $html
360	 */
361	protected function renderInnerOutput($context = [])
362	{
363		$value = $this->getConfiguration('value');
364		$pvalue = $this->getConfiguration('pvalue', $value);
365
366		if (isset($context['list_mode']) && $context['list_mode'] === 'csv') {
367			$default = ['CR' => '%%%', 'delimitorL' => '"', 'delimitorR' => '"'];
368			$context = array_merge($default, $context);
369			$value = str_replace(["\r\n", "\n", '<br />', $context['delimitorL'], $context['delimitorR']], [$context['CR'], $context['CR'], $context['CR'], $context['delimitorL'] . $context['delimitorL'], $context['delimitorR'] . $context['delimitorR']], $value);
370			return $value;
371		} else {
372			return $pvalue;
373		}
374	}
375
376	/**
377	 * Return the HTML id/name of input tag for this
378	 * field in the item form
379	 *
380	 * @return string
381	 */
382	protected function getInsertId()
383	{
384		return 'ins_' . $this->definition['fieldId'];
385	}
386
387	protected function getFilterId()
388	{
389		return 'filter_' . $this->definition['fieldId'];
390	}
391
392	protected function getFieldId()
393	{
394		return $this->definition['fieldId'];
395	}
396
397	/**
398	 * Gets data from the field's configuration
399	 *
400	 * i.e. from the field definition in the database plus what is returned by the field's getFieldData() function
401	 *
402	 * @param string $key
403	 * @param mixed $default
404	 * @return mixed
405	 */
406	protected function getConfiguration($key, $default = false)
407	{
408		return isset($this->definition[$key]) ? $this->definition[$key] : $default;
409	}
410
411	/**
412	 * Return the value for this item field. Depending on fieldtype that could be the itemId of a linked field.
413	 * Value is looked for in:
414	 * $this->itemData[fieldNumber]
415	 * $this->definition['value']
416	 * $this->itemData['fields'][permName]
417	 * @param mixed $default the field value used if none is set
418	 * @return mixed field value
419	 */
420	protected function getValue($default = '')
421	{
422		$key = $this->getConfiguration('fieldId');
423
424		if (isset($this->itemData[$key])) {
425			$value = $this->itemData[$key];
426		} elseif (isset($this->definition['value'])) {
427			$value = $this->definition['value'];
428		} elseif (isset($this->itemData['fields'][$this->getConfiguration('permName')])) {
429			$value = $this->itemData['fields'][$this->getConfiguration('permName')];
430		} else {
431			$value = null;
432		}
433
434		return $value === null ? $default : $value;
435	}
436
437	protected function getItemId()
438	{
439		return $this->getData('itemId');
440	}
441
442	protected function getData($key, $default = false)
443	{
444		return isset($this->itemData[$key]) ? $this->itemData[$key] : $default;
445	}
446
447	protected function getItemField($permName)
448	{
449		$field = $this->trackerDefinition->getFieldFromPermName($permName);
450
451		if ($field) {
452			$id = $field['fieldId'];
453
454			return $this->getData($id);
455		}
456	}
457
458	/**
459	 * Return option from the options array.
460	 * For the list of options for a particular field check its getTypes() method.
461	 * Note: This function should be public, as long as certain low-level trackerlib functions need to be accessed directly.
462	 * Otherwise one would be forced to get the options from fields like this: $myField['options_array'][0] ...
463	 * @param int $number | string $key.  depending on type: based on the numeric array position, or by name.
464	 * @param mixed $default - defaultValue to return if nothing found
465	 * @return mixed
466	 */
467	public function getOption($key, $default = false)
468	{
469		if (is_numeric($key)) {
470			return $this->options->getParamFromIndex($key, $default);
471		} else {
472			return $this->options->getParam($key, $default);
473		}
474	}
475
476	/**
477	 * Get the tracker definition object
478	 *
479	 * @return \Tracker_Definition
480	 */
481	protected function getTrackerDefinition()
482	{
483		return $this->trackerDefinition;
484	}
485
486	/**
487	 * Get the item's data
488	 *
489	 * @return array
490	 */
491	protected function getItemData()
492	{
493		return $this->itemData;
494	}
495
496	protected function renderTemplate($file, $context = [], $data = [])
497	{
498		$smarty = TikiLib::lib('smarty');
499
500		//ensure value is set, because it may not always come from definition
501		if (! isset($this->definition['value'])) {
502			$this->definition['value'] = $this->getValue();
503		}
504
505		$smarty->assign('field', $this->definition);
506		$smarty->assign('context', $context);
507		$smarty->assign('item', $this->getItemData());
508		$smarty->assign('data', $data);
509
510		return $smarty->fetch($file, $file);
511	}
512
513	function getDocumentPart(Search_Type_Factory_Interface $typeFactory)
514	{
515		$baseKey = $this->getBaseKey();
516		return [
517			$baseKey => $typeFactory->sortable($this->getValue()),
518		];
519	}
520
521	function getProvidedFields()
522	{
523		$baseKey = $this->getBaseKey();
524		return [$baseKey];
525	}
526
527	function getGlobalFields()
528	{
529		$baseKey = $this->getBaseKey();
530		return [$baseKey => true];
531	}
532
533	function getBaseKey()
534	{
535		global $prefs;
536		$indexKey = $prefs['unified_trackerfield_keys'];
537		return 'tracker_field_' . $this->baseKeyPrefix . $this->getConfiguration($indexKey);
538	}
539
540	function setBaseKeyPrefix($prefix)
541	{
542		$this->baseKeyPrefix = $prefix;
543	}
544
545	/**
546	 * Default implementation is to replace the value
547	 */
548	public function addValue($value) {
549		return $value;
550	}
551
552	/**
553	 * Default implementation is to remove the value
554	 */
555	public function removeValue($value) {
556		return '';
557	}
558}
559