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(' ', ' ', strip_tags($mail_data))); 382 } else { 383 $mail->setText(str_replace(' ', ' ', 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(' ', ' ', strip_tags($mail_data))); 2866 } else { 2867 $mail->setText(str_replace(' ', ' ', 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(' ', ' ', strip_tags($mail_data))); 5277 } else { 5278 $mail->setText(str_replace(' ', ' ', 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