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