now = time(); $this->registerSectionFormat('flat', 'view', 'trackeroutput/layout_flat.tpl', tr('Flat')); $this->registerSectionFormat('flat', 'edit', 'trackerinput/layout_flat.tpl', tr('Flat')); $this->registerSectionFormat('tab', 'view', 'trackeroutput/layout_tab.tpl', tr('Tabs')); $this->registerSectionFormat('tab', 'edit', 'trackerinput/layout_tab.tpl', tr('Tabs')); } public function registerSectionFormat($layout, $mode, $template, $label) { if ($template) { $this->sectionFormats[$layout][$mode] = [ 'template' => $template, 'label' => $label, ]; } } public function unregisterSectionFormat($layout) { unset($this->sectionFormats[$layout]); } public function getSectionFormatTemplate($layout, $mode) { if (isset($this->sectionFormats[$layout][$mode])) { return $this->sectionFormats[$layout][$mode]['template']; } elseif ($layout == 'config' || $layout === 'n') { // Special handling for config, fallback to default flat (also for when sectionFormat gets saved as "n" in legacy trackers) return $this->getSectionFormatTemplate('flat', $mode); } else { throw new Exception(tr('No template available for %0 - %1', $layout, $mode)); } } public function getGlobalSectionFormats() { $out = []; foreach ($this->sectionFormats as $layout => $modes) { if (count($modes) == 2) { $first = reset($modes); $out[$layout] = $first['label']; } } $out['config'] = tr('Configured'); return $out; } private function attachments() { return $this->table('tiki_tracker_item_attachments'); } private function comments() { return $this->table('tiki_comments'); } private function itemFields() { return $this->table('tiki_tracker_item_fields', false); } private function trackers() { return $this->table('tiki_trackers'); } private function items() { return $this->table('tiki_tracker_items'); } private function fields() { return $this->table('tiki_tracker_fields'); } private function options() { return $this->table('tiki_tracker_options'); } private function logs() { return $this->table('tiki_tracker_item_field_logs', false); } private function groupWatches() { return $this->table('tiki_group_watches'); } private function userWatches() { return $this->table('tiki_user_watches'); } public function remove_field_images($fieldId) { $itemFields = $this->itemFields(); $values = $itemFields->fetchColumn('value', ['fieldId' => (int) $fieldId]); foreach ($values as $file) { if (file_exists($file)) { unlink($file); } } } public function add_item_attachment_hit($id) { global $prefs, $user; if (StatsLib::is_stats_hit()) { $attachments = $this->attachments(); $attachments->update(['hits' => $attachments->increment(1)], ['attId' => (int) $id]); } return true; } public function get_item_attachment_owner($attId) { return $this->attachments()->fetchOne('user', ['attId' => (int) $attId]); } public function list_item_attachments($itemId, $offset = 0, $maxRecords = -1, $sort_mode = 'attId_asc', $find = '') { $attachments = $this->attachments(); $order = $attachments->sortMode($sort_mode); $fields = ['user', 'attId', 'itemId', 'filename', 'filesize', 'filetype', 'hits', 'created', 'comment', 'longdesc', 'version']; $conditions = [ 'itemId' => (int) $itemId, ]; if ($find) { $conditions['filename'] = $attachments->like("%$find%"); } return [ 'data' => $attachments->fetchAll($fields, $conditions, $maxRecords, $offset, $order), 'cant' => $attachments->fetchCount($conditions), ]; } public function get_item_nb_attachments($itemId) { $attachments = $this->attachments(); $ret = $attachments->fetchRow( ['hits' => $attachments->sum('hits'), 'attachments' => $attachments->count()], ['itemId' => $itemId] ); return $ret ? $ret : []; } public function get_item_nb_comments($itemId) { return $this->comments()->fetchCount(['object' => (int) $itemId, 'objectType' => 'trackeritem']); } public function list_all_attachments($offset = 0, $maxRecords = -1, $sort_mode = 'created_desc', $find = '') { $attachments = $this->attachments(); $fields = ['user', 'attId', 'itemId', 'filename', 'filesize', 'filetype', 'hits', 'created', 'comment', 'path']; $order = $attachments->sortMode($sort_mode); $conditions = []; if ($find) { $conditions['filename'] = $attachments->like("%$find%"); } return [ 'data' => $attachments->fetchAll($fields, $conditions, $maxRecords, $offset, $order), 'cant' => $attachments->fetchCount($conditions), ]; } public function file_to_db($path, $attId) { if (is_readable($path)) { $updateResult = $this->attachments()->update( ['data' => file_get_contents($path), 'path' => ''], ['attId' => (int) $attId] ); if ($updateResult) { unlink($path); } } } public function db_to_file($path, $attId) { $attachments = $this->attachments(); $data = $attachments->fetchOne('data', ['attId' => (int) $attId]); if (false !== file_put_contents($path, $data)) { $attachments->update(['data' => '', 'path' => basename($path)], ['attId' => (int) $attId]); } } public function get_item_attachment($attId) { return $this->attachments()->fetchFullRow(['attId' => (int) $attId]); } public function remove_item_attachment($attId = 0, $itemId = 0) { global $prefs; $attachments = $this->attachments(); $paths = []; if (empty($attId) && ! empty($itemId)) { if ($prefs['t_use_db'] === 'n') { $paths = $attachments->fetchColumn('path', ['itemId' => $itemId]); } $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']); $attachments->deleteMultiple(['itemId' => $itemId]); } elseif (! empty($attId)) { if ($prefs['t_use_db'] === 'n') { $paths = $attachments->fetchColumn('path', ['attId' => (int) $attId]); } $this->query('update `tiki_tracker_item_fields` ttif left join `tiki_tracker_fields` ttf using (`fieldId`) set `value`=? where ttif.`value`=? and ttf.`type`=?', ['', (string) $attId, 'A']); $attachments->delete(['attId' => (int) $attId]); } foreach (array_filter($paths) as $path) { @unlink($prefs['t_use_dir'] . $path); } } public function replace_item_attachment($attId, $filename, $type, $size, $data, $comment, $user, $fhash, $version, $longdesc, $trackerId = 0, $itemId = 0, $options = '', $notif = true) { global $prefs; $attachments = $this->attachments(); $comment = strip_tags($comment); $now = $this->now; if (empty($attId)) { $attId = $attachments->insert( [ 'itemId' => (int) $itemId, 'filename' => $filename, 'filesize' => $size, 'filetype' => $type, 'data' => $data, 'created' => $now, 'hits' => 0, 'user' => $user, 'comment' => $comment, 'path' => $fhash, 'version' => $version, 'longdesc' => $longdesc, ] ); } elseif (empty($filename)) { $attachments->update( [ 'user' => $user, 'comment' => $comment, 'version' => $version, 'longdesc' => $longdesc, ], ['attId' => $attId] ); } else { $path = $attachments->fetchOne('path', ['attId' => (int) $attId]); if ($path) { @unlink($prefs['t_use_dir'] . $path); } $attachments->update( [ 'filename' => $filename, 'filesize' => $size, 'filetype' => $type, 'data' => $data, 'user' => $user, 'comment' => $comment, 'path' => $fhash, 'version' => $version, 'longdesc' => $longdesc, ], ['attId' => (int) $attId] ); } if (! $notif) { return $attId; } $options["attachment"] = ["attId" => $attId, "filename" => $filename, "comment" => $comment]; $watchers = $this->get_notification_emails($trackerId, $itemId, $options); if (count($watchers > 0)) { $smarty = TikiLib::lib('smarty'); $trackerName = $this->trackers()->fetchOne('name', ['trackerId' => (int) $trackerId]); $smarty->assign('mail_date', $this->now); $smarty->assign('mail_user', $user); $smarty->assign('mail_action', 'New File Attached to Item:' . $itemId . ' at tracker ' . $trackerName); $smarty->assign('mail_itemId', $itemId); $smarty->assign('mail_trackerId', $trackerId); $smarty->assign('mail_trackerName', $trackerName); $smarty->assign('mail_attId', $attId); $smarty->assign('mail_data', $filename . "\n" . $comment . "\n" . $version . "\n" . $longdesc); $foo = parse_url($_SERVER["REQUEST_URI"]); $machine = $this->httpPrefix(true) . $foo["path"]; $smarty->assign('mail_machine', $machine); $parts = explode('/', $foo['path']); if (count($parts) > 1) { unset($parts[count($parts) - 1]); } $smarty->assign('mail_machine_raw', $this->httpPrefix(true) . implode('/', $parts)); if (! isset($_SERVER["SERVER_NAME"])) { $_SERVER["SERVER_NAME"] = $_SERVER["HTTP_HOST"]; } include_once('lib/webmail/tikimaillib.php'); $smarty->assign('server_name', $_SERVER['SERVER_NAME']); $desc = $this->get_isMain_value($trackerId, $itemId); $smarty->assign('mail_item_desc', $desc); foreach ($watchers as $w) { $mail = new TikiMail($w['user']); if (! isset($w['template'])) { $w['template'] = ''; } $content = $this->parse_notification_template($w['template']); $mail->setSubject($smarty->fetchLang($w['language'], $content['subject'])); $mail_data = $smarty->fetchLang($w['language'], $content['template']); if (isset($w['templateFormat']) && $w['templateFormat'] == 'html') { $mail->setHtml($mail_data, str_replace(' ', ' ', strip_tags($mail_data))); } else { $mail->setText(str_replace(' ', ' ', strip_tags($mail_data))); } $mail->send([$w['email']]); } } return $attId; } public function list_last_comments($trackerId = 0, $itemId = 0, $offset = -1, $maxRecords = -1) { global $user; $mid = "1=1"; $bindvars = []; if ($itemId != 0) { $mid .= " and `itemId`=?"; $bindvars[] = (int) $itemId; } if ($trackerId != 0) { $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"; $bindvars[] = $trackerId; $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"; } else { $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"; $query_cant = "select count(*) from `tiki_comments` where $mid AND `objectType` = 'trackeritem'"; } $ret = $this->fetchAll($query, $bindvars, $maxRecords, $offset); $cant = $this->getOne($query_cant, $bindvars); foreach ($ret as $key => &$res) { $itemObject = Tracker_Item::fromId($res['itemId']); if (! $itemObject->canView()) { --$cant; unset($ret[$key]); continue; } $res["parsed"] = $this->parse_comment($res["data"]); } return [ 'data' => array_values($ret), 'cant' => $cant, ]; } public function get_last_position($trackerId) { $fields = $this->fields(); return $fields->fetchOne($fields->max('position'), ['trackerId' => (int) $trackerId]); } public function get_tracker_item($itemId) { $res = $this->items()->fetchFullRow(['itemId' => (int) $itemId]); if (! $res) { return false; } $itemFields = $this->itemFields(); $fields = $itemFields->fetchMap('fieldId', 'value', ['itemId' => (int) $itemId]); foreach ($fields as $id => $value) { $res[$id] = $value; } return $res; } public function get_all_item_id($trackerId, $fieldId, $value) { $query = "select distinct ttif.`itemId` from `tiki_tracker_items` tti, `tiki_tracker_item_fields` ttif "; $query .= " where tti.`itemId`=ttif.`itemId` and tti.`trackerId`=? and ttif.`fieldId`=? "; $value = "%$value%"; $query .= " and ttif.`value` LIKE ?"; $result = $this->fetchAll($query, [(int) $trackerId, (int)$fieldId, $value]); $itemIds = []; foreach ($result as $row) { $itemIds[] = $row['itemId']; } return $itemIds; } public function get_item_id($trackerId, $fieldId, $value, $partial = false) { $query = "select ttif.`itemId` from `tiki_tracker_items` tti, `tiki_tracker_fields` ttf, `tiki_tracker_item_fields` ttif "; $query .= " where tti.`trackerId`=ttf.`trackerId` and ttif.`fieldId`=ttf.`fieldId` and tti.`itemId`=ttif.`itemId` and ttf.`trackerId`=? and ttf.`fieldId`=? "; if ($partial) { $value = "%$value%"; $query .= " and ttif.`value` LIKE ?"; } else { $query .= " and ttif.`value`=?"; } return $this->getOne($query, [(int) $trackerId, (int) $fieldId, $value]); } public function get_item($trackerId, $fieldId, $value) { $itemId = $this->get_item_id($trackerId, $fieldId, $value); return $this->get_tracker_item($itemId); } /* experimental shared */ /* trackerId is useless */ public function get_item_value($trackerId, $itemId, $fieldId) { global $prefs; static $cache = []; $cacheKey = "$fieldId.$itemId"; if (isset($cache[$cacheKey])) { return $cache[$cacheKey]; } $value = $this->itemFields()->fetchOne('value', ['fieldId' => (int) $fieldId, 'itemId' => (int) $itemId]); if ($this->is_multilingual($fieldId) == 'y') { $list = json_decode($value, true); if (isset($list[$prefs['language']])) { return $list[$prefs['language']]; } } if (TikiLib::lib('tiki')->get_memory_avail() < 1048576 * 10) { $cache = []; } $cache[$cacheKey] = $value; return $value; } public function get_item_status($itemId) { $status = $this->items()->fetchOne('status', ['itemId' => (int) $itemId]); return $status; } /*shared*/ public function list_tracker_items($trackerId, $offset, $maxRecords, $sort_mode, $fields, $status = '', $initial = '') { $filters = []; if ($fields) { $temp_max = count($fields["data"]); for ($i = 0; $i < $temp_max; $i++) { $fieldId = $fields["data"][$i]["fieldId"]; $filters[$fieldId] = $fields["data"][$i]; } } $csort_mode = ''; if (substr($sort_mode, 0, 2) == "f_") { list($a,$csort_mode,$corder) = explode('_', $sort_mode, 3); } $trackerId = (int) $trackerId; if ($trackerId == -1) { $mid = " where 1=1 "; $bindvars = []; } else { $mid = " where tti.`trackerId`=? "; $bindvars = [$trackerId]; } if ($status) { $mid .= " and tti.`status`=? "; $bindvars[] = $status; } if ($initial) { $mid .= "and ttif.`value` like ?"; $bindvars[] = $initial . '%'; } if (! $sort_mode) { $temp_max = count($fields["data"]); for ($i = 0; $i < $temp_max; $i++) { if ($fields['data'][$i]['isMain'] == 'y') { $csort_mode = $fields['data'][$i]['name']; break; } } } if ($csort_mode) { $sort_mode = $csort_mode . "_desc"; $bindvars[] = $csort_mode; $query = "select tti.*, ttif.`value` from `tiki_tracker_items` tti, `tiki_tracker_item_fields` ttif, `tiki_tracker_fields` ttf "; $query .= " $mid and tti.`itemId`=ttif.`itemId` and ttf.`fieldId`=ttif.`fieldId` and ttf.`name`=? order by ttif.`value`"; $query_cant = "select count(*) from `tiki_tracker_items` tti, `tiki_tracker_item_fields` ttif, `tiki_tracker_fields` ttf "; $query_cant .= " $mid and tti.`itemId`=ttif.`itemId` and ttf.`fieldId`=ttif.`fieldId` and ttf.`name`=? "; } else { if (! $sort_mode) { $sort_mode = "lastModif_desc"; } $query = "select * from `tiki_tracker_items` tti $mid order by " . $this->convertSortMode($sort_mode); $query_cant = "select count(*) from `tiki_tracker_items` tti $mid "; } $result = $this->fetchAll($query, $bindvars, $maxRecords, $offset); $cant = $this->getOne($query_cant, $bindvars); $ret = []; foreach ($result as $res) { $fields = []; $itid = $res["itemId"]; $query2 = "select ttif.`fieldId`,`name`,`value`,`type`,`isTblVisible`,`isMain`,`position` from `tiki_tracker_item_fields` ttif, `tiki_tracker_fields` ttf where ttif.`fieldId`=ttf.`fieldId` and `itemId`=? order by `position` asc"; $result2 = $this->fetchAll($query2, [(int) $res["itemId"]]); $pass = true; $kx = ""; foreach ($result2 as $res2) { // Check if the field is visible! $fieldId = $res2["fieldId"]; if (count($filters) > 0) { if (isset($filters[$fieldId]["value"]) and $filters[$fieldId]["value"]) { if (in_array($filters[$fieldId]["type"], ['a', 't'])) { if (! stristr($res2["value"], $filters[$fieldId]["value"])) { $pass = false; } } else { if (strtolower($res2["value"]) != strtolower($filters[$fieldId]["value"])) { $pass = false; } } } if (preg_replace("/[^a-zA-Z0-9]/", "", $res2["name"]) == $csort_mode) { $kx = $res2["value"] . $itid; } } $fields[] = $res2; } $res["field_values"] = $fields; $res["comments"] = $this->table('tiki_comments')->fetchCount(['object' => (int) $itid, 'objectType' => 'trackeritem']); if ($pass) { $kl = $kx . $itid; $ret["$kl"] = $res; } } ksort($ret); //$ret=$this->sort_items_by_condition($ret,$sort_mode); $retval = []; $retval["data"] = array_values($ret); $retval["cant"] = $cant; return $retval; } /*shared*/ public function get_user_items($auser, $with_groups = true) { global $user; $items = []; $query = "select ttf.`trackerId`, tti.`itemId` from `tiki_tracker_fields` ttf, `tiki_tracker_items` tti, `tiki_tracker_item_fields` ttif"; $query .= " where ttf.`fieldId`=ttif.`fieldId` and ttif.`itemId`=tti.`itemId` and `type`=? and tti.`status`=? and `value`=?"; $result = $this->fetchAll($query, ['u','o',$auser]); $ret = []; $trackers = $this->table('tiki_trackers'); $trackerFields = $this->table('tiki_tracker_fields'); $trackerItemFields = $this->table('tiki_tracker_item_fields'); //FIXME Perm:filter ? foreach ($result as $res) { $itemObject = Tracker_Item::fromId($res['itemId']); if (! $itemObject->canView()) { continue; } $itemId = $res["itemId"]; $trackerId = $res["trackerId"]; // Now get the isMain field for this tracker $fieldId = $trackerFields->fetchOne('fieldId', ['isMain' => 'y', 'trackerId' => (int) $trackerId]); // Now get the field value $value = $trackerItemFields->fetchOne('value', ['fieldId' => (int) $fieldId, 'itemId' => (int) $itemId]); $tracker = $trackers->fetchOne('name', ['trackerId' => (int) $trackerId]); $aux["trackerId"] = $trackerId; $aux["itemId"] = $itemId; $aux["value"] = $value; $aux["name"] = $tracker; if (! in_array($itemId, $items)) { $ret[] = $aux; $items[] = $itemId; } } if ($with_groups) { $groups = $this->get_user_groups($auser); foreach ($groups as $group) { $query = "select ttf.`trackerId`, tti.`itemId` from `tiki_tracker_fields` ttf, `tiki_tracker_items` tti, `tiki_tracker_item_fields` ttif "; $query .= " where ttf.`fieldId`=ttif.`fieldId` and ttif.`itemId`=tti.`itemId` and `type`=? and tti.`status`=? and `value`=?"; $result = $this->fetchAll($query, ['g', 'o', $group]); foreach ($result as $res) { $itemId = $res["itemId"]; $trackerId = $res["trackerId"]; // Now get the isMain field for this tracker $fieldId = $trackerFields->fetchOne('fieldId', ['isMain' => 'y', 'trackerId' => (int)$trackerId]); // Now get the field value $value = $trackerItemFields->fetchOne('value', ['fieldId' => (int)$fieldId, 'itemId' => (int)$itemId]); $tracker = $trackers->fetchOne('name', ['trackerId' => (int)$trackerId]); $aux["trackerId"] = $trackerId; $aux["itemId"] = $itemId; $aux["value"] = $value; $aux["name"] = $tracker; if (! in_array($itemId, $items)) { $ret[] = $aux; $items[] = $itemId; } } } } return $ret; } /* experimental shared */ public function get_items_list($trackerId, $fieldId, $value, $status = 'o', $multiple = false, $sortFieldIds = null) { static $cache = []; $cacheKey = implode('.', [ $trackerId, $fieldId, $value, $status, $multiple, is_array($sortFieldIds) ? implode($sortFieldIds) : $sortFieldIds ]); if (isset($cache[$cacheKey])) { return $cache[$cacheKey]; } $query = "select distinct tti.`itemId`, tti.`itemId` i from `tiki_tracker_items` tti, `tiki_tracker_item_fields` ttif "; $bindvars = []; if (is_string($sortFieldIds)) { $sortFieldIds = preg_split('/\|/', $sortFieldIds, -1, PREG_SPLIT_NO_EMPTY); } if (! empty($sortFieldIds)) { foreach ($sortFieldIds as $i => $sortFieldId) { $query .= " left join `tiki_tracker_item_fields` ttif$i on ttif.`itemId` = ttif$i.`itemId` and ttif$i.`fieldId` = ?"; $bindvars[] = (int)$sortFieldId; } } $query .= " where tti.`itemId`=ttif.`itemId` and ttif.`fieldId`=?"; $bindvars[] = (int)$fieldId; if ($multiple) { $query .= " and FIND_IN_SET(?, ttif.`value`)"; } else { $query .= " and ttif.`value`=?"; } $bindvars[] = $value; if (! empty($status)) { $query .= ' and ' . $this->in('tti.status', str_split($status, 1), $bindvars); } if (! empty($sortFieldIds)) { $query .= " order by " . implode( ',', array_map( function ($i) { return "ttif$i.value"; }, array_keys($sortFieldIds) ) ); } $items = $this->fetchAll($query, $bindvars); $items = array_map( function ($row) { return $row['itemId']; }, $items ); if (TikiLib::lib('tiki')->get_memory_avail() < 1048576 * 10) { $cache = []; } $cache[$cacheKey] = $items; return $items; } public function get_tracker($trackerId) { return $this->table('tiki_trackers')->fetchFullRow(['trackerId' => (int) $trackerId]); } public function get_field_info($fieldId) { return $this->table('tiki_tracker_fields')->fetchFullRow(['fieldId' => (int) $fieldId]); } /** * Marks fields as empty * @param array $fields * @return array */ public function mark_fields_as_empty($fields) { $lastHeader = -1; $elemSinceLastHeader = 0; foreach ($fields as $key => $trac) { if ( ! (empty($trac['value']) && empty($trac['cat']) && empty($trac['links']) && $trac['type'] != 's' && $trac['type'] != 'STARS' && $trac['type'] != 'h' && $trac['type'] != 'l' && $trac['type'] != 'W') && ! ($trac['options_array'][0] == 'password' && $trac['type'] == 'p') ) { if ($trac['type'] == 'h') { if ($lastHeader > 0 && $elemSinceLastHeader == 0) { $fields[$lastHeader]['field_is_empty'] = true; } $lastHeader = $key; $elemSinceLastHeader = 0; } else { $elemSinceLastHeader++; } // this has a value continue; } $fields[$key]['field_is_empty'] = true; } if ($lastHeader > 0 && $elemSinceLastHeader == 0) { $fields[$lastHeader]['field_is_empty'] = true; } return $fields; } // includePermissions: Include the permissions of each tracker in its element's "permissions" subelement public function list_trackers($offset = 0, $maxRecords = -1, $sort_mode = 'name_asc', $find = '', $includePermissions = false) { $categlib = TikiLib::lib('categ'); $join = ''; $where = ''; $bindvars = []; if ($jail = $categlib->get_jail()) { $categlib->getSqlJoin($jail, 'tracker', '`tiki_trackers`.`trackerId`', $join, $where, $bindvars); } if ($find) { $findesc = '%' . $find . '%'; $where .= ' and (`tiki_trackers`.`name` like ? or `tiki_trackers`.`description` like ?)'; $bindvars = array_merge($bindvars, [$findesc, $findesc]); } $query = "select * from `tiki_trackers` $join where 1=1 $where order by `tiki_trackers`." . $this->convertSortMode($sort_mode); $query_cant = "select count(*) from `tiki_trackers` $join where 1=1 $where"; $result = $this->fetchAll($query, $bindvars, $maxRecords, $offset); $cant = $this->getOne($query_cant, $bindvars); $ret = []; $list = []; //FIXME Perm:filter ? foreach ($result as $res) { global $user; $add = $this->user_has_perm_on_object($user, $res['trackerId'], 'tracker', 'tiki_p_view_trackers'); if ($add) { if ($includePermissions) { $res['permissions'] = Perms::get('tracker', $res['trackerId']); } $ret[] = $res; $list[$res['trackerId']] = $res['name']; } } $retval = []; $retval["list"] = $list; $retval["data"] = $ret; $retval["cant"] = $cant; return $retval; } /** * Get trackers by ID * @param array $columns * @param $trackerIds * @return array|bool */ public function getTrackersByIds($trackerIds, $columns = []) { return $this->trackers()->fetchAll($columns, [ 'trackerId' => $this->trackers()->in($trackerIds) ]); } // This function gets the prefix alias page name e.g. Org:230 for the pretty tracker // wiki page corresponding to a tracker item (230 in the example) using prefix aliases // Returns false if no such page is found. public function get_trackeritem_pagealias($itemId) { global $prefs; $trackerId = $this->table('tiki_tracker_items')->fetchOne('trackerId', ['itemId' => $itemId]); $semanticlib = TikiLib::lib('semantic'); $t_links = $semanticlib->getLinksUsing('trackerid', ['toPage' => $trackerId]); if (count($t_links)) { if ($prefs['feature_multilingual'] == 'y' && count($t_links) > 1) { foreach ($t_links as $t) { if ($prefs['language'] == TikiLib::lib('multilingual')->getLangOfPage($t['fromPage'])) { $target = $t['fromPage']; break; } } } else { $target = $t_links[0]['fromPage']; } $p_links = $semanticlib->getLinksUsing('prefixalias', ['fromPage' => $target]); if (count($p_links)) { $ret = $p_links[0]['toPage'] . $itemId; return $ret; } else { return false; } } else { return false; } } public function concat_item_from_fieldslist($trackerId, $itemId, $fieldsId, $status = 'o', $separator = ' ', $list_mode = '', $strip_tags = false, $format = '', $item = []) { $res = ''; $values = []; if (is_string($fieldsId)) { $fieldsId = preg_split('/\|/', $fieldsId, -1, PREG_SPLIT_NO_EMPTY); } $definition = Tracker_Definition::get($trackerId); if ($definition) { foreach ($fieldsId as $k => $field) { $myfield = $definition->getField($field); $myfield['value'] = $this->get_item_value( $trackerId, $itemId, $field ); if (! isset($item['itemId'])) { $item['itemId'] = $itemId; } $value = trim($this->field_render_value( ['field' => $myfield, 'process' => 'y', 'list_mode' => $list_mode, 'item' => $item] )); if ($format) { $values[] = $value; } else { if ($k > 0) { $res .= $separator; } $res .= $value; } } if ($format) { if ($list_mode !== 'csv') { // preserve spaces in the format string $format = str_replace(' ', ' ', $format); } // use the underlying translation function to replace the %0 etc placeholders (and translate if necessary) $res = tra($format, '', false, $values); } if ($strip_tags) { $res = strip_tags($res); } } else { Feedback::error(tr('Tracker %0 not found for Field %1', $trackerId, implode(',', $fieldsId))); } return $res; } public function concat_all_items_from_fieldslist($trackerId, $fieldsId, $status = 'o', $separator = ' ', $strip_tags = false) { if (is_string($fieldsId)) { $fieldsId = preg_split('/\|/', $fieldsId, -1, PREG_SPLIT_NO_EMPTY); } $res = []; $definition = Tracker_Definition::get($trackerId); foreach ($fieldsId as $field) { if ($myfield = $definition->getField($field)) { $is_date = ($myfield['type'] == 'f'); $is_trackerlink = ($myfield['type'] == 'r'); $tmp = $this->get_all_items($trackerId, $field, $status); $options = $myfield['options_map']; foreach ($tmp as $key => $value) { if ($is_date) { $value = $this->date_format("%e/%m/%y", $value); } if ($is_trackerlink && $options['displayFieldsList'] && ! empty($options['displayFieldsList'][0])) { $item = $this->get_tracker_item($key); $itemId = $item[$field]; $value = $this->concat_item_from_fieldslist($options['trackerId'], $itemId, $options['displayFieldsList'], $status, $separator, '', $strip_tags); } if (! empty($res[$key])) { $res[$key] .= $separator . $value; } else { $res[$key] = $value; } } } } return $res; } public function get_fields_from_fieldslist($trackerId, $fieldsId) { if (is_string($fieldsId)) { $fieldsId = preg_split('/\|/', $fieldsId, -1, PREG_SPLIT_NO_EMPTY); } $res = []; $definition = Tracker_Definition::get($trackerId); foreach ($fieldsId as $field) { if ($myfield = $definition->getField($field)) { $res[$field] = $myfield['permName']; } } return $res; } public function valid_status($status) { return in_array($status, ['o', 'c', 'p', 'op', 'oc', 'pc', 'opc']); } /** * Gets an array of itemId => rendered value for a certain field for use in ItemLinks (mainly) * * @param int $trackerId * @param int $fieldId * @param string $status * @return array */ public function get_all_items($trackerId, $fieldId, $status = 'o') { global $prefs, $user; $cachelib = TikiLib::lib('cache'); if (! $trackerId) { return [tr('*** ERROR: Tracker ID not set ***', $fieldId)]; } if (! $fieldId) { return [tr('*** ERROR: Field ID not set ***', $fieldId)]; } $definition = Tracker_Definition::get($trackerId); if (! $definition) { // could be a deleted field referred to by a list type field return [tr('*** ERROR: Tracker %0 not found ***', $trackerId)]; } $field = $definition->getField($fieldId); if (! $field) { // could be a deleted field referred to by a list type field return [tr('*** ERROR: Field %0 not found ***', $fieldId)]; } $jail = ''; if ($prefs['feature_categories'] == 'y') { $categlib = TikiLib::lib('categ'); $jail = $categlib->get_jail(); } $sort_mode = "value_asc"; $cacheKey = 'trackerfield' . $fieldId . $status . $user; if ($this->is_multilingual($fieldId) == 'y') { $cacheKey .= $prefs['language']; } if (! empty($jail)) { $cacheKey .= serialize($jail); } $cacheKey = md5($cacheKey); if (( ! $ret = $cachelib->getSerialized($cacheKey) ) || ! $this->valid_status($status)) { $sts = preg_split('//', $status, -1, PREG_SPLIT_NO_EMPTY); $mid = " (" . implode('=? or ', array_fill(0, count($sts), 'tti.`status`')) . "=?) "; $fieldIdArray = preg_split('/\|/', $fieldId, -1, PREG_SPLIT_NO_EMPTY); $mid .= " and (" . implode('=? or ', array_fill(0, count($fieldIdArray), 'ttif.`fieldId`')) . "=?) "; $bindvars = array_merge($sts, $fieldIdArray); $join = ''; if (! empty($jail)) { $categlib->getSqlJoin($jail, 'trackeritem', 'tti.`itemId`', $join, $mid, $bindvars); } $query = "select ttif.`itemId` , ttif.`value` FROM `tiki_tracker_items` tti,`tiki_tracker_item_fields` ttif $join "; $query .= " WHERE $mid and tti.`itemId` = ttif.`itemId` order by " . $this->convertSortMode($sort_mode); $items = $this->fetchAll($query, $bindvars); Perms::bulk(['type' => 'trackeritem', 'parentId' => $trackerId], 'object', array_map(function ($res) { return $res['itemId']; }, $items)); $ret = []; foreach ($items as $res) { $itemId = $res['itemId']; $itemObject = Tracker_Item::fromId($itemId); if (! $itemObject) { Feedback::error(tr('TrackerLib::get_all_items: No item for itemId %0', $itemId)); } elseif ($itemObject->canView()) { $ret[] = $res; } } $cachelib->cacheItem($cacheKey, serialize($ret)); } $ret2 = []; foreach ($ret as $res) { $itemId = $res['itemId']; $field['value'] = $res['value']; $rendered = $this->field_render_value([ 'field' => $field, 'process' => 'y', 'smarty_assign' => 'n', ]); $ret2[$itemId] = trim(strip_tags($rendered), " \t\n\r\0\x0B\xC2\xA0"); } return $ret2; } public function need_to_check_categ_perms($allfields = '') { global $prefs; if ($allfields === false) { // use for itemlink field - otherwise will be too slow return false; } $needToCheckCategPerms = false; if ($prefs['feature_categories'] == 'y') { $categlib = TikiLib::lib('categ'); if (empty($allfields['data'])) { $needToCheckCategPerms = true; } else { foreach ($allfields['data'] as $f) { if ($f['type'] == 'e') { $needToCheckCategPerms = true; break; } } } } return $needToCheckCategPerms; } public function get_all_tracker_items($trackerId) { return $this->items()->fetchColumn('itemId', ['trackerId' => (int) $trackerId]); } public function getSqlStatus($status, &$mid, &$bindvars, $trackerId, $skip_status_perm_check = false) { global $user; if (is_array($status)) { $status = implode('', $status); } // Check perms 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')) { $status = str_replace('p', '', $status); } 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')) { $status = str_replace('c', '', $status); } if (! $status) { return false; } elseif ($status == 'opc') { return true; } elseif (strlen($status) > 1) { $sts = preg_split('//', $status, -1, PREG_SPLIT_NO_EMPTY); if (count($sts)) { $mid .= " and (" . implode('=? or ', array_fill(0, count($sts), '`status`')) . "=?) "; $bindvars = array_merge($bindvars, $sts); } } else { $mid .= " and tti.`status`=? "; $bindvars[] = $status; } return true; } public function group_creator_has_perm($trackerId, $perm) { global $prefs; $definition = Tracker_Definition::get($trackerId); if ($definition && $groupCreatorFieldId = $definition->getWriterGroupField()) { $tracker_info = $definition->getInformation(); $perms = $this->get_special_group_tracker_perm($tracker_info); return empty($perms[$perm]) ? false : true; } else { return false; } } /* group creator perms can only add perms,they can not take away perm 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 must always be combined with a filter on the groups */ public function get_special_group_tracker_perm($tracker_info, $global = false) { global $prefs; $userlib = TikiLib::lib('user'); $smarty = TikiLib::lib('smarty'); $ret = []; $perms = $userlib->get_object_permissions($tracker_info['trackerId'], 'tracker', $prefs['trackerCreatorGroupName']); foreach ($perms as $perm) { $ret[$perm['permName']] = 'y'; if ($global) { $p = $perm['permName']; global $$p; $$p = 'y'; $smarty->assign("$p", 'y'); } } if ($tracker_info['writerGroupCanModify'] == 'y') { // old configuration $ret['tiki_p_modify_tracker_items'] = 'y'; if ($global) { $tiki_p_modify_tracker_items = 'y'; $smarty->assign('tiki_p_modify_tracker_items', 'y'); } } return $ret; } /* to filter filterfield is an array of fieldIds * and the value of each field is either filtervalue or exactvalue * ex: filterfield=array('1','2', 'sqlsearch'=>array('3', '4'), '5') * ex: filtervalue=array(array('this', '*that'), '') * ex: exactvalue= array('', array('there', 'those'), 'these', array('>'=>10)) * 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 * listfields = array(fieldId=>array('type'=>, 'name'=>...), ...) * allfields is only for performance issue - check if one field is a category */ 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) { //echo '
FILTERFIELD:'; print_r($filterfield); echo ''; global $prefs; $cat_table = ''; $sort_tables = ''; $sort_join_clauses = ''; $csort_mode = ''; $corder = ''; $trackerId = (int) $trackerId; $numsort = false; $prefSort = false; $mid = ' WHERE tti.`trackerId` = ? '; $bindvars = [$trackerId]; $join = ''; if (! empty($filter)) { $mid2 = []; if (! empty($filter['comment'])) { $cat_table .= ' LEFT JOIN `tiki_comments` tc ON tc.`object` = tti.`itemId` AND tc.`objectType` = "trackeritem"'; $mid2[] = '(tc.`title` LIKE ? OR tc.`data` LIKE ?)'; $bindvars[] = '%' . $filter['comment'] . '%'; $bindvars[] = '%' . $filter['comment'] . '%'; unset($filter['comment']); } $this->parse_filter($filter, $mid2, $bindvars); if (! empty($mid2)) { $mid .= ' AND ' . implode(' AND ', $mid2); } } if (! $this->getSqlStatus($status, $mid, $bindvars, $trackerId, $skip_status_perm_check) && ! $skip_status_perm_check && $status) { return ['cant' => 0, 'data' => '']; } if (substr($sort_mode, 0, 2) == 'f_') { list($a, $asort_mode, $corder) = preg_split('/_/', $sort_mode); } if ($initial) { $mid .= ' AND ttif.`value` LIKE ?'; $bindvars[] = $initial . '%'; if (isset($asort_mode)) { $mid .= ' AND ttif.`fieldId` = ?'; $bindvars[] = $asort_mode; } } if (! $sort_mode) { $sort_mode = 'lastModif_desc'; } if (substr($sort_mode, 0, 2) == 'f_' or ! empty($filterfield)) { if (substr($sort_mode, 0, 2) == 'f_') { $csort_mode = 'sttif.`value` '; $sort_tables = ' LEFT JOIN (`tiki_tracker_item_fields` sttif)' . ' ON (tti.`itemId` = sttif.`itemId`' . (! empty($asort_mode) ? " AND sttif.`fieldId` = $asort_mode" : '') . ')'; // Do we need a numerical sort on the field ? $field = $this->get_tracker_field($asort_mode); if ($field) { switch ($field['type']) { case 'C': case '*': case 'q': case 'n': case 'f': // DateTime case 'j': // JsCalendar case 'CAL': // CalendarItem $numsort = true; break; case 'DUR': $csort_mode = Tracker_Field_Duration::getSortModeSql(); break; case 'l': // Do nothing, value is dynamic and thus cannot be sorted on $csort_mode = 1; $csort_tables = ''; break; case 'r': $link_field = (int)$field['fieldId']; $remote_field = (int)$field['options_array'][1]; $sort_tables = ' LEFT JOIN `tiki_tracker_item_fields` itemlink ON tti.itemId = itemlink.itemId AND itemlink.fieldId = ' . $link_field . ' LEFT JOIN `tiki_tracker_item_fields` sttif ON itemlink.value = sttif.itemId AND sttif.fieldId = ' . $remote_field . ' '; break; case 's': // 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 $numsort = true; // } break; case 'p': $prefSort = true; break; case 'e': $csort_mode = "sttif.name"; $sort_tables = ' LEFT JOIN `tiki_tracker_item_fields` scttif ON scttif.`itemId` = tti.`itemId` AND scttif.`fieldId` = ' . (int) $field['fieldId'] . ' LEFT JOIN `tiki_categories` sttif ON substring_index(trim(both "," from scttif.value), ",", 1) = sttif.categId'; break; case 'math': if (strpos($field['options'], 'numeric_sort') == true) { if ($corder == 'asc') { $corder = 'nasc'; } else { $corder = 'ndesc'; } } break; } } else { // don't sort of the field doesn't exist $csort_mode = 1; $corder = 'asc'; $sort_tables = ''; } } else { list($csort_mode, $corder) = preg_split('/_/', $sort_mode); $csort_mode = 'tti.`' . $csort_mode . '` '; } if (empty($filterfield)) { $nb_filtered_fields = 0; } elseif (! is_array($filterfield)) { $fv = $filtervalue; $ev = $exactvalue; $ff = (int) $filterfield; $nb_filtered_fields = 1; } else { $nb_filtered_fields = count($filterfield); } $last = 0; for ($i = 0; $i < $nb_filtered_fields; $i++) { if (is_array($filterfield)) { //multiple filter on an exact value or a like value - each value can be simple or an array $ff = (int) $filterfield[$i]; $ff_array = $filterfield[$i]; // Need value as array used below $ev = ! empty($exactvalue[$i]) ? $exactvalue[$i] : null; $fv = ! empty($filtervalue[$i]) ? $filtervalue[$i] : null; } $filter = $this->get_tracker_field($ff); // Determine if field is an item list field and postpone filtering till later if so if ($filter["type"] == 'l' && isset($filter['options_array'][2]) && isset($filter['options_array'][2]) && isset($filter['options_array'][3])) { $linkfilter[] = ['filterfield' => $ff, 'exactvalue' => $ev, 'filtervalue' => $fv]; continue; } $value = empty($fv) ? $ev : $fv; $search_for_blank = ( is_null($ev) && is_null($fv) ) || ( is_array($value) && count($value) == 1 && ( empty($value[0]) || ( is_array($value[0]) && count($value[0]) == 1 && empty($value[0][0]) ) ) ); $cat_table .= ' ' . ( $search_for_blank ? 'LEFT' : 'INNER' ) . " JOIN `tiki_tracker_item_fields` ttif$i ON ttif$i.`itemId` = tti.`itemId`"; $last++; if (isset($ff_array['sqlsearch']) && is_array($ff_array['sqlsearch'])) { $mid .= " AND ttif$i.`fieldId` in (" . implode(',', array_fill(0, count($ff_array['sqlsearch']), '?')) . ')'; $bindvars = array_merge($bindvars, $ff_array['sqlsearch']); } elseif (isset($ff_array['usersearch']) && is_array($ff_array['usersearch'])) { $mid .= " AND ttif$i.`fieldId` in (" . implode(',', array_fill(0, count($ff_array['usersearch']), '?')) . ')'; $bindvars = array_merge($bindvars, $ff_array['usersearch']); } elseif ($ff) { if ($search_for_blank) { $cat_table .= " AND ttif$i.`fieldId` = " . (int)$ff; } else { $mid .= " AND ttif$i.`fieldId`=? "; $bindvars[] = $ff; } } if ($filter['type'] == 'p' && (! empty($fv) || ! empty($ev))) { $definition = Tracker_Definition::get($trackerId); $userFieldId = $definition->getUserField(); $prefName = ''; $trackerFieldOptions = $this->getOne('SELECT `options` FROM `tiki_tracker_fields` WHERE fieldId = ?', $ff); if ($trackerFieldOptions && $trackerFieldOptions = json_decode($trackerFieldOptions)) { $prefName = $trackerFieldOptions->type ?? ''; } if ($userFieldId && $prefName) { $cat_table .= " INNER JOIN `tiki_tracker_item_fields` uttif$i ON (uttif$i.`itemId` = tti.`itemId` AND uttif$i.`fieldId` = $userFieldId)"; $cat_table .= " INNER JOIN `tiki_user_preferences` tup$i ON (tup$i.user = uttif$i.`value`)"; $mid .= " AND tup$i.prefName = ? AND tup$i.value like ?"; $bindvars[] = $prefName; $bindvars[] = $ev ?: "%$fv%"; } } elseif ($filter['type'] == 'e' && $prefs['feature_categories'] == 'y' && (! empty($ev) || ! empty($fv))) { //category $value = empty($fv) ? $ev : $fv; if (! is_array($value) && $value != '') { $value = [$value]; $not = ''; } elseif (is_array($value) && array_key_exists('not', $value)) { $value = [$value['not']]; $not = 'not'; } if (empty($not) && count($value) == 1 && ( empty($value[0]) || ( is_array($value[0]) && count($value[0]) == 1 && empty($value[0][0]) ) )) { $cat_table .= " left JOIN `tiki_objects` tob$ff ON (tob$ff.`itemId` = tti.`itemId` AND tob$ff.`type` = 'trackeritem')" . " left JOIN `tiki_category_objects` tco$ff ON (tob$ff.`objectId` = tco$ff.`catObjectId`)"; $mid .= " AND tco$ff.`categId` IS NULL "; continue; } if (empty($not)) { $cat_table .= " INNER JOIN `tiki_objects` tob$ff ON (tob$ff.`itemId` = tti.`itemId`)" . " INNER JOIN `tiki_category_objects` tco$ff ON (tob$ff.`objectId` = tco$ff.`catObjectId`)"; $mid .= " AND tob$ff.`type` = 'trackeritem' AND tco$ff.`categId` IN ( "; } else { $cat_table .= " left JOIN `tiki_objects` tob$ff ON (tob$ff.`itemId` = tti.`itemId`)" . " left JOIN `tiki_category_objects` tco$ff ON (tob$ff.`objectId` = tco$ff.`catObjectId`)"; $mid .= " AND tob$ff.`type` = 'trackeritem' AND tco$ff.`categId` NOT IN ( "; } $first = true; foreach ($value as $k => $catId) { if (is_array($catId)) { // this is a grouped AND logic for optimization indicated by the value being array $innerfirst = true; foreach ($catId as $c) { if (is_array($c)) { $innerfirst = true; foreach ($c as $d) { $bindvars[] = $d; if ($innerfirst) { $innerfirst = false; } else { $mid .= ','; } $mid .= '?'; } } else { $bindvars[] = $c; $mid .= '?'; } } if ($k < count($value) - 1) { $mid .= " ) AND "; if (empty($not)) { $ff2 = $ff . '_' . $k; $cat_table .= " INNER JOIN `tiki_category_objects` tco$ff2 ON (tob$ff.`objectId` = tco$ff2.`catObjectId`)"; $mid .= "tco$ff2.`categId` IN ( "; } else { $ff2 = $ff . '_' . $k; $cat_table .= " left JOIN `tiki_category_objects` tco$ff2 ON (tob$ff.`objectId` = tco$ff2.`catObjectId`)"; $mid .= "tco$ff2.`categId` NOT IN ( "; } } } else { $bindvars[] = $catId; if ($first) { $first = false; } else { $mid .= ','; } $mid .= '?'; } } $mid .= " ) "; if (! empty($not)) { $mid .= " OR tco$ff.`categId` IS NULL "; } } elseif ($filter['type'] == 'usergroups') { $definition = Tracker_Definition::get($trackerId); $userFieldId = $definition->getUserField(); $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`)"; $mid .= ' AND ttifu.`fieldId`=? AND uug.`groupName`=? '; $bindvars[] = $userFieldId; $bindvars[] = empty($ev) ? $fv : $ev; } elseif ($filter['type'] == 'u' && $ev > '') { // user selector and exact value if (is_array($ev)) { $keys = array_keys($ev); if ($keys[0] === 'not') { $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 )"; } else { $mid .= " AND ( ttif$i.`value` REGEXP " . implode(' OR ttif$i.`value` REGEXP ', array_fill(0, count($ev), '?')) . " )"; } $bindvars = array_merge( $bindvars, array_values(array_map(function ($ev) { return "[[:<:]]{$ev}[[:>:]]"; }, $ev)) ); } else { $mid .= " AND ttif$i.`value` REGEXP ? "; $bindvars[] = "[[:<:]]{$ev}[[:>:]]"; } } elseif ($filter['type'] == '*') { // star $mid .= " AND ttif$i.`value`*1>=? "; $bindvars[] = $ev; if (($j = array_search($ev, $filter['options_array'])) !== false && $j + 1 < count($filter['options_array'])) { $mid .= " AND ttif$i.`value`*1 "; $bindvars[] = $filter['options_array'][$j + 1]; } } elseif ($filter['type'] == 'r' && ($fv || $ev)) { $cv = $fv ? $fv : $ev; $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] . ' '; if (is_numeric($cv)) { $mid .= " AND ( ttif{$i}_remote.`value` LIKE ? OR ttif$i.`value` = ? ) "; $bindvars[] = $ev ? $ev : "%$fv%"; $bindvars[] = $cv; } else { $mid .= " AND ttif{$i}_remote.`value` LIKE ? "; $bindvars[] = $ev ? $ev : "%$fv%"; } } elseif ($filter['type'] == 'REL' && ($fv || $ev)) { $rv = $ev ?: $fv; $options = explode("\n", $rv); foreach ($options as $option) { $mid .= " AND (ttif$i.`value` LIKE ? OR ttif$i.`value` LIKE ?)"; $option = trim($option); $bindvars[] = "%$option"; $bindvars[] = "%$option\n%"; } } elseif ($ev > '') { if (is_array($ev)) { $keys = array_keys($ev); if (in_array((string) $keys[0], ['<', '>'])) { $mid .= " AND ttif$i.`value`" . $keys[0] . "? + 0"; $bindvars[] = $ev[$keys[0]]; } elseif (in_array((string) $keys[0], ['<=', '>='])) { $mid .= " AND (ttif$i.`value`" . $keys[0] . "? + 0 OR ttif$i.`value` = ?)"; $bindvars[] = $ev[$keys[0]]; $bindvars[] = $ev[$keys[0]]; } elseif ($keys[0] === 'not') { $mid .= " AND ( ttif$i.`value` not in (" . implode(',', array_fill(0, count($ev), '?')) . ") OR ttif$i.`value` IS NULL )"; $bindvars = array_merge($bindvars, array_values($ev)); } else { $mid .= " AND ttif$i.`value` in (" . implode(',', array_fill(0, count($ev), '?')) . ")"; $bindvars = array_merge($bindvars, array_values($ev)); } } elseif (isset($ff_array['sqlsearch']) && is_array($ff_array['sqlsearch'])) { $mid .= " AND MATCH(ttif$i.`value`) AGAINST(? IN BOOLEAN MODE)"; $bindvars[] = $ev; } elseif (isset($ff_array['usersearch']) && is_array($ff_array['usersearch'])) { $mid .= " AND ttif$i.`value` REGEXP ? "; $bindvars[] = "[[:<:]]{$ev}[[:>:]]"; } else { $mid .= " AND ttif$i.`value`=? "; $bindvars[] = $ev == '' ? $fv : $ev; } } elseif ($fv > '') { if (! is_array($fv)) { $value = [$fv]; } else { $value = $fv; } $mid .= ' AND('; $cpt = 0; foreach ($value as $v) { if ($cpt++) { $mid .= ' OR '; } $mid .= " upper(ttif$i.`value`) like upper(?) "; if (substr($v, 0, 1) == '*' || substr($v, 0, 1) == '%') { $bindvars[] = '%' . substr($v, 1); } elseif (substr($v, -1, 1) == '*' || substr($v, -1, 1) == '%') { $bindvars[] = substr($v, 0, strlen($v) - 1) . '%'; } else { $bindvars[] = '%' . $v . '%'; } } $mid .= ')'; } elseif ($filter['type'] == 'r' && is_null($ev) && is_null($fv)) { $mid .= " AND ( ttif$i.`value`=? OR ttif$i.`value`=? OR ttif$i.`value` IS NULL )"; $bindvars[] = ''; $bindvars[] = '0'; } elseif (is_null($ev) && is_null($fv)) { // test null value $mid .= " AND ( ttif$i.`value`=? OR ttif$i.`value` IS NULL )"; $bindvars[] = ''; } } } else { if (strpos($sort_mode, '_') !== false) { list($csort_mode, $corder) = preg_split('/_/', $sort_mode); } else { $csort_mode = $sort_mode; $corder = 'asc'; } $csort_mode = "`" . $csort_mode . "`"; if ($csort_mode == '`itemId`') { $csort_mode = 'tti.`itemId`'; $numsort = true; } $sort_tables = ''; $cat_tables = ''; } $categlib = TikiLib::lib('categ'); if ($jail = $categlib->get_jail()) { $categlib->getSqlJoin($jail, 'trackeritem', 'tti.`itemId`', $join, $mid, $bindvars); } $base_tables = '(' . ' `tiki_tracker_items` tti' . ' INNER JOIN `tiki_tracker_item_fields` ttif ON tti.`itemId` = ttif.`itemId`' . ' INNER JOIN `tiki_tracker_fields` ttf ON ttf.`fieldId` = ttif.`fieldId`' . ')' . $join; $fieldIds = []; if (! empty($listfields)) { foreach ($listfields as $k => $f) { if (isset($f['fieldId'])) { $fieldIds[] = $f['fieldId']; } else { $fieldIds[] = $k; // sometimes filterfields are provided with the fieldId only on the array keys } } } if (! empty($filterfield)) { // fix: could be that there is just one field. in this case it might be a scalar, // not an array due to not handle $filterfield proper somewhere else in the code if (! is_array($filterfield)) { $filterfield = [$filterfield]; } foreach ($filterfield as $f) { if (! empty($f['sqlsearch'])) { foreach ($f['sqlsearch'] as $subf) { if (! in_array($subf, $fieldIds)) { $fieldIds[] = $subf; } } } elseif (! empty($f['usersearch'])) { foreach ($f['usersearch'] as $subf) { if (! in_array($subf, $fieldIds)) { $fieldIds[] = $subf; } } } else { if (! in_array($f, $fieldIds)) { $fieldIds[] = $f; } } } } if (! empty($fieldIds)) { $mid .= ' AND ' . $this->in('ttif.fieldId', $fieldIds, $bindvars); } if ($csort_mode == '`created`') { $csort_mode = 'tti.created'; } if ($corder == 'nasc' || $corder == 'ndesc') { $numsort = true; $corder = substr($corder, 1); } $query = 'SELECT tti.*' . ', ' . ( ($numsort) ? "cast(max($csort_mode) as decimal)" : "max($csort_mode)") . ' as `sortvalue`' . ' FROM ' . $base_tables . $sort_tables . $cat_table . $mid . ' GROUP BY tti.`itemId`, tti.`trackerId`, tti.`created`, tti.`createdBy`, tti.`status`, tti.`lastModif`, tti.`lastModifBy`, ' . $csort_mode . ' ORDER BY ' . $this->convertSortMode('sortvalue_' . $corder); if ($numsort) { $query .= ',' . $this->convertSortMode($csort_mode); } //echo htmlentities($query); print_r($bindvars); $query_cant = 'SELECT count(DISTINCT ttif.`itemId`) FROM ' . $base_tables . $sort_tables . $cat_table . $mid; // save the result $ret = []; // Start loop to get the required number of items if permissions / filters are in use. // The problem: If $maxItems and $offset are given, // but the sql query returns items the user has no permissions or the filter criteria does not match, // then only a subset of what is available would be returned. // original requested number of items $maxRecordsRequested = $maxRecords; // original page (from pagination) $offsetRequested = $offset; // offset calculated on $offsetRequested $currentOffset = 0; // set to true when we have enough records or no records left. $finished = false; // used internaly - one time query that returns the total number of records without taking into account filter or permissions $cant = $this->getOne($query_cant, $bindvars); // $cant will be modified bc its used otherwise. so save the totalCount value $totalCount = $cant; // total number of records read so far $currentCount = 0; // number of records in the result set $resultCount = 0; // outer loop - grab more records bc it might be we must filter out records. // 300 seems to be ok, bc paganination offers this as well as the size of the resultset // NOTE: This value is important with respect to memory usage and performance - especially when lots of items (like 10k+) are in use. $maxRecords = 300; // offset used for sql query $offset = 0; // optimize permission check - preload ownership fields to be able to quickly enforce canSeeOwn or wrtier group can modify permissions $definition = Tracker_Definition::get($trackerId); $ownershipFields = $definition->getItemOwnerFields(); $groupOwnershipFields = $definition->getItemGroupOwnerFields(); if ($groupField = $definition->getWriterGroupField()) { $groupOwnershipFields[] = $groupField; } while (! $finished) { $ret1 = $this->fetchAll($query, $bindvars, $maxRecords, $offset); // add. security - should not be necessary bc of check at the end. no records left - end outer loop if (count($ret1) == 0) { $finished = true; } if (! $skip_permission_check) { // preload permissions for all items to be checked Perms::bulk(['type' => 'trackeritem', 'parentId' => $trackerId], 'object', $ret1, 'itemId'); // preload ownership field values for all items to be checked $ownershipData = []; $table = $this->itemFields(); $rows = $table->fetchAll(['itemId', 'fieldId', 'value'], [ 'itemId' => $table->in(array_map(function ($row) { return $row['itemId']; }, $ret1)), 'fieldId' => $table->in($ownershipFields) ]); foreach ($rows as $row) { $ownershipData[$row['itemId']][$row['fieldId']] = $this->parse_user_field($row['value']); } $rows = $table->fetchAll(['itemId', 'fieldId', 'value'], [ 'itemId' => $table->in(array_map(function ($row) { return $row['itemId']; }, $ret1)), 'fieldId' => $table->in($groupOwnershipFields) ]); foreach ($rows as $row) { $ownershipData[$row['itemId']][$row['fieldId']] = $row['value']; } } foreach ($ret1 as $res) { $mem = TikiLib::lib('tiki')->get_memory_avail(); if ($mem > 0 && $mem < 1048576 * 10) { // Less than 10MB left? // post an error even though it doesn't get displayed when using export as the output goes into the output file Feedback::error(tr('Tracker list_items ran out of memory after %0 items.', count($ret))); break; } if (! $skip_permission_check) { // this is needed by permission checking inside tracker item $res += $ownershipData[$res['itemId']] ?? []; $itemObject = Tracker_Item::fromInfo($res); if (! $itemObject->canView()) { $cant--; // skipped record bc of permissions - need to count for outer loop $currentCount++; continue; } } $res['itemUsers'] = []; if ($listfields !== null && ! empty($listfields)) { $res['field_values'] = $this->get_item_fields($trackerId, $res['itemId'], $listfields, $res['itemUsers']); } if (! empty($asort_mode)) { foreach ($res['field_values'] as $i => $field) { if ($field['fieldId'] == $asort_mode) { $kx = $field['value'] . '.' . $res['itemId']; } } } if (isset($linkfilter) && $linkfilter) { $filterout = false; // NOTE: This implies filterfield if is link field has to be in fields set foreach ($res['field_values'] as $i => $field) { foreach ($linkfilter as $lf) { if ($field['fieldId'] == $lf["filterfield"]) { // extra comma at the front and back of filtervalue to avoid ambiguity in partial match if ($lf["filtervalue"] && strpos(',' . implode(',', $field['items']) . ',', $lf["filtervalue"]) === false) { $filterout = true; break 2; } elseif ($lf["exactvalue"] && ! in_array($lf['exactvalue'], $field['items'])) { $filterout = true; break 2; } } } } if ($filterout) { $cant--; // skipped record bc of filter criteria - need to count for outer loop $currentCount++; continue; } } $res['geolocation'] = TikiLib::lib('geo')->get_coordinates('trackeritem', $res['itemId']); // have a field, adjust counter and check if we have enough items $currentCount++; $currentOffset++; // field is stored in $res. See wether we can add it to the resultset, based on the requested offset if (! $finished && $currentOffset > $offsetRequested) { $resultCount++; if (empty($kx)) { // ex: if the sort field is non visible, $kx is null $ret[] = $res; } else { $ret[$kx] = $res; } } if ($resultCount == $maxRecordsRequested) { // have enough items to return but keep filtering out the remainder to test for ownership or status perms $finished = true; } } // foreach // are items left? if ($currentCount == $totalCount) { $finished = true; } else { $offset += $maxRecords; } } // while // End loop to get the required number of items if permissions / filters are in use if ($prefSort) { $corder === 'desc' ? krsort($ret) : ksort($ret); } $retval = []; $retval['data'] = array_values($ret); $retval['cant'] = $cant; return $retval; } /* listfields fieldId=>fielddefinition */ public function get_item_fields($trackerId, $itemId, $listfields, &$itemUsers, $alllang = false) { global $prefs, $user, $tiki_p_admin_trackers; $definition = Tracker_Definition::get($trackerId); $info = $this->get_tracker_item((int) $itemId); $factory = $definition->getFieldFactory(); $itemUsers = array_map(function ($userField) use ($info) { return isset($info[$userField]) ? $this->parse_user_field($info[$userField]) : []; }, $definition->getItemOwnerFields()); if ($itemUsers) { $itemUsers = call_user_func_array('array_merge', $itemUsers); } $fields = []; foreach ($listfields as $fieldId => $fopt) { if (empty($fopt['fieldId'])) { // to accept listfield as a simple table $fopt['fieldId'] = $fieldId; } $fopt['trackerId'] = $trackerId; $fopt['itemId'] = (int)$itemId; $handler = $factory->getHandler($fopt, $info); if ($handler) { $get = $this->extend_GET($fopt); // extend context $fopt = array_merge($fopt, $handler->getFieldData()); $fields[] = $fopt; $this->restore_GET($get); // restore context } } return($fields); } /** * Make sure $_GET is extended with the $fopt (in get_item_fields) before calling $handler->getFieldData() * Some trackers use tiki syntax replacement, that uses $_GET in ParserLib::parse_wiki_argvariable, extending * with $fopt makes sure that that the wiki syntax parser gets the right context variables * * @param Array $array Values to add to $_GET * @return Array a copy of the original $_GET array */ protected function extend_GET($array) { $get = $_GET; foreach ($array as $key => $value) { $_GET[$key] = $value; } return $get; } /** * Use to restore the $_GET context with the copy of $_GET returned by self::extend_GET * * @param Array $get the array to restore as $_GET */ protected function restore_GET($get) { $_GET = $get; } public function replace_item($trackerId, $itemId, $ins_fields, $status = '', $ins_categs = 0, $bulk_import = false, $skip_sync = false) { global $user, $prefs, $tiki_p_admin_trackers, $tiki_p_admin_users; $final_event = 'tiki.trackeritem.update'; if (! $bulk_import) { $transaction = $this->begin(); } $categlib = TikiLib::lib('categ'); $cachelib = TikiLib::lib('cache'); $smarty = TikiLib::lib('smarty'); $logslib = TikiLib::lib('logs'); $userlib = TikiLib::lib('user'); $tikilib = TikiLib::lib('tiki'); $notificationlib = TikiLib::lib('notification'); $items = $this->items(); $itemFields = $this->itemFields(); $fields = $this->fields(); if (! empty($itemId)) { // check the item really exists $itemId = (int) $this->items()->fetchOne('itemId', [ 'itemId' => $itemId]); } $fil = []; if (! empty($itemId)) { $fil = $itemFields->fetchMap('fieldId', 'value', ['itemId' => $itemId]); } $old_values = $fil; $tracker_definition = Tracker_Definition::get($trackerId); if (method_exists($tracker_definition, 'getInformation') == false) { return -1; } $tracker_info = $tracker_definition->getInformation(); if (! empty($itemId)) { $new_itemId = 0; $oldStatus = $this->items()->fetchOne('status', ['itemId' => $itemId]); $status = $status ? $status : $oldStatus; $fil['status'] = $status; $old_values['status'] = $oldStatus; if ($status != $oldStatus) { $this->change_status([$itemId], $status); } else { $this->update_items( [$itemId], [ 'lastModif' => $tikilib->now, 'lastModifBy' => $user, ], false ); } $version = $this->last_log_version($itemId) + 1; } else { if (empty($status) && isset($tracker_info['newItemStatus'])) { // set status based on tracker setting of status not explicitly requested $status = $tracker_info['newItemStatus']; } if (empty($status)) { $status = 'o'; } $fil['status'] = $status; $old_values['status'] = ''; $oldStatus = ''; $new_itemId = $items->insert( [ 'trackerId' => (int) $trackerId, 'created' => $this->now, 'createdBy' => $user, 'lastModif' => $this->now, 'lastModifBy' => $user, 'status' => $status, ] ); $logslib->add_action('Created', $new_itemId, 'trackeritem'); $version = 0; $final_event = 'tiki.trackeritem.create'; } $currentItemId = $itemId ? $itemId : $new_itemId; $item_info = $this->get_item_info($currentItemId); if (! empty($oldStatus) || ! empty($status)) { if (! empty($itemId) && $oldStatus != $status) { $this->log($version, $itemId, -1, $oldStatus); } } // If this is a user tracker it needs to be detected right here before actual looping of fields happen $trackersync_user = $user; foreach ($ins_fields["data"] as $i => $array) { if (isset($array['type']) && $array['type'] == 'u' && isset($array['options_array'][0]) && $array['options_array'][0] == '1') { if ($prefs['user_selector_realnames_tracker'] == 'y' && $array['type'] == 'u') { if (! $userlib->user_exists($array['value'])) { $finalusers = $userlib->find_best_user([$array['value']], '', 'login'); if (! empty($finalusers[0]) && ! (isset($_REQUEST['register']) && isset($_REQUEST['name']) && $_REQUEST['name'] == $array['value'])) { // It could be in fact that a new user is required (when no match is found or during registration even if match is found) $ins_fields['data'][$i]['value'] = $finalusers[0]; } } } $trackersync_user = $array['value']; } } $final = []; $postSave = []; $suppliedFields = []; foreach ($ins_fields["data"] as $i => $array) { // Old values were prefilled at the begining of the function and only replaced at the end of the iteration $fieldId = $array['fieldId']; $suppliedFields[] = $fieldId; $old_value = isset($fil[$fieldId]) ? $fil[$fieldId] : null; $handler = $this->get_field_handler($array, array_merge($item_info, $fil)); if (method_exists($handler, 'postSaveHook')) { // postSaveHook will be called with final value saved // after saving all item fields $postSave[] = [ 'fieldId' => $fieldId, 'handler' => $handler, ]; } if (method_exists($handler, 'handleFinalSave')) { // handleFinalSave will be called after all other fields are saved, and // will get as parameter all other field data (other than ones that also // use finalSave). $final[] = [ 'field' => $array, 'handler' => $handler, ]; continue; } if (method_exists($handler, 'handleSave')) { $array = array_merge($array, $handler->handleSave(! isset($array['value']) ? null : $array['value'], $old_value)); $value = ! isset($array['value']) ? null : $array['value']; if ($value !== false) { $this->modify_field($currentItemId, $array['fieldId'], $value); if ($itemId && $old_value != $value) { // On update, save old value $this->log($version, $itemId, $array['fieldId'], $old_value); } $fil[$fieldId] = $value; } continue; } $value = isset($array["value"]) ? $array["value"] : null; if (isset($array['type']) && $array['type'] == 'p' && ($user == $trackersync_user || $tiki_p_admin_users == 'y')) { if ($array['options_array'][0] == 'password') { if (! empty($array['value']) && $prefs['change_password'] == 'y' && ($e = $userlib->check_password_policy($array['value'])) == '') { $userlib->change_user_password($trackersync_user, $array['value']); } if (! empty($itemId)) { $this->log($version, $itemId, $array['fieldId'], '?'); } } elseif ($array['options_array'][0] == 'email') { if (! empty($array['value']) && validate_email($array['value']) && ($prefs['user_unique_email'] != 'y' || ! $userlib->other_user_has_email($trackersync_user, $array['value']))) { $old_value = $userlib->get_user_email($trackersync_user); $userlib->change_user_email($trackersync_user, $array['value']); } if (! empty($itemId) && $old_value != $array['value']) { $this->log($version, $itemId, $array['fieldId'], $old_value); } } else { $old_value = $tikilib->get_user_preference($trackersync_user, $array['options_array'][0]); $tikilib->set_user_preference($trackersync_user, $array['options_array'][0], $array['value']); if (! empty($itemId) && $old_value != $array['value']) { $this->log($version, $itemId, $array['fieldId'], $array['value']); } } // Should not store value in tracker database as it won't be reliable (what if pref is changed afterwards?) $value = ''; $fil[$fieldId] = $value; $this->modify_field($currentItemId, $array['fieldId'], $value); } elseif (isset($array['type']) && $array['type'] == 'k') { //page selector if ($array['value'] != '') { $this->modify_field($currentItemId, $array['fieldId'], $value); if ($itemId) { // On update, save old value $this->log($version, $itemId, $array['fieldId'], $old_value); } $fil[$fieldId] = $value; if (! $this->page_exists($array['value'])) { $opts = $array['options_array']; if (! empty($opts[2])) { $IP = $this->get_ip_address(); $info = $this->get_page_info($opts[2]); $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']); } } } } else { $is_date = isset($array['type']) ? in_array($array["type"], ['f', 'j']) : false; if ($currentItemId || ( isset($array['type']) && $array['type'] !== 'q')) { // autoincrement $this->modify_field($currentItemId, $fieldId, $value); if ($old_value != $value) { if ($is_date) { $dformat = $prefs['short_date_format'] . ' ' . $prefs['short_time_format']; if (! empty($old_value)) { $old_value = $this->date_format($dformat, (int) $old_value); } if (! empty($new_value)) { $new_value = $this->date_format($dformat, (int) $value); } else { $new_value = $value; } } else { $new_value = $value; } if ( $old_value != $new_value && ! empty($itemId) && $array['type'] !== 'W' // not for webservices ) { $this->log($version, $itemId, $array['fieldId'], $old_value); } } } $fil[$fieldId] = $value; } } // delete empty actionlog version to prevent history date overlap if ($version > 0 && $this->last_log_version($itemId) + 1 == $version) { $logslib->delete_action('Updated', $itemId, 'trackeritem', $version); } // get permnames $permNames = []; foreach ($fil as $fieldId => $value) { $field = $tracker_definition->getField($fieldId); if (! empty($field)) { if ($field['type'] !== 'W') { // not for webservices $permNames[$fieldId] = $field['permName']; } else { unset($fil[$fieldId], $old_values[$fieldId]); // webservice values are just a cache and not useful for diffs etc } } } if (count($final)) { $data = []; foreach ($fil as $fieldId => $value) { if (isset($permNames[$fieldId])) { $data[$permNames[$fieldId]] = $value; } } foreach ($final as $job) { if (isset($job['field']['value'])) { $data[$job['field']['permName']] = $job['field']['value']; } $value = $job['handler']->handleFinalSave($data); $data[$job['field']['permName']] = $value; $this->modify_field($currentItemId, $job['field']['fieldId'], $value); $fil[$job['field']['fieldId']] = $value; $permNames[$job['field']['fieldId']] = $job['field']['permName']; } } foreach ($postSave as $job) { $value = $fil[$job['fieldId']]; $job['handler']->postSaveHook($value); } $values_by_permname = []; $old_values_by_permname = []; foreach ($fil as $fieldId => $value) { if (! empty($permNames[$fieldId])) { $values_by_permname[$permNames[$fieldId]] = $value; } } foreach ($old_values as $fieldId => $value) { if (! empty($permNames[$fieldId])) { $old_values_by_permname[$permNames[$fieldId]] = $value; } } $arguments = [ 'type' => 'trackeritem', 'object' => $currentItemId, 'user' => $GLOBALS['user'], 'version' => $version, 'trackerId' => $trackerId, 'supplied' => $suppliedFields, 'values' => $fil, 'old_values' => $old_values, 'values_by_permname' => $values_by_permname, 'old_values_by_permname' => $old_values_by_permname, 'bulk_import' => $bulk_import, 'skip_sync' => $skip_sync, 'aggregate' => sha1("trackeritem/$currentItemId"), ]; // this needs to trigger no matter of the size as trackeritem categorization depends on this and other event types as well TikiLib::events()->trigger( $final_event, $arguments ); if (! $bulk_import) { $transaction->commit(); } return $currentItemId; } public function modify_field($itemId, $fieldId, $value) { $field = $this->get_tracker_field($fieldId); if (! empty($field['encryptionKeyId'])) { try { $key = new Tiki\Encryption\Key($field['encryptionKeyId']); $value = $key->encryptData($value); } catch (Tiki\Encryption\NotFoundException $e) { Feedback::error(tr('Field "%0" is encrypted with a key that no longer exists!', $field['name'])); } catch (Tiki\Encryption\Exception $e) { Feedback::error(tr('Field "%0" is encrypted using key "%1" but where was an error enrypting the data: %2', $field['name'], $key->get('name'), $e->getMessage())); } $info = '
FILTERVALUE:';print_r($filtervalue); echo '
EXACTVALUE:'; print_r($exactvalue); echo '
STATUS:'; print_r($status); echo '
FILTER:'; print_r($filter); /*echo '
LISTFIELDS'; print_r($listfields);*/ echo '
$function: {$info['description']}
"; if (count($info['params'])) { $text .= ''; print_r($cell); echo ''; } public function get_tracker_by_name($name) { return $this->trackers()->fetchOne('trackerId', ['name' => $name]); } public function get_field_by_name($trackerId, $fieldName) { return $this->fields()->fetchOne('fieldId', ['trackerId' => $trackerId, 'name' => $fieldName]); } public function get_field_by_names($trackerName, $fieldName) { $trackerId = $this->trackers()->fetchOne('trackerId', ['name' => $trackerName]); return $fieldId = $this->fields()->fetchOne('fieldId', ['trackerId' => $trackerId, 'name' => $fieldName]); } public function get_fields_by_names($trackerName, $fieldNames) { $fields = []; foreach ($fieldNames as $fieldName) { $fields[$fieldName] = $this->get_field_by_names($trackerName, $fieldName); } return $fields; } public function get_fields_by_type($type) { $fields = $this->fields(); return $fields->fetchAll( $fields->all(), ['type' => $fields->exactly($type)] ); } /** * Get a field handler for a specific fieldtype. The handler comes initialized with the field / item data passed. * @param array $field. *
* $field = array(
* // required
* 'trackerId' => 1 // trackerId
* );
* value1, 'itemid2' => value2)
* @return Tracker_Field_Abstract $tracker_field_handler - i.e. Tracker_Field_Text
*/
public function get_field_handler($field, $item = [])
{
if (! isset($field['trackerId'])) {
return false;
}
$trackerId = (int) $field['trackerId'];
$definition = Tracker_Definition::get($trackerId);
if (! $definition) {
return false;
}
return $definition->getFieldFactory()->getHandler($field, $item);
}
public function get_field_value($field, $item)
{
$handler = $this->get_field_handler($field, $item);
$values = $handler->getFieldData();
return isset($values['value']) ? $values['value'] : null;
}
private function parse_comment($data)
{
return nl2br(htmlspecialchars($data));
}
public function send_replace_item_notifications($args)
{
global $prefs, $user;
// Don't send a notification if this operation is part of a bulk import
if ($args['bulk_import']) {
return;
}
$trackerId = $args['trackerId'];
$itemId = $args['object'];
$new_values = $args['values'];
$old_values = $args['old_values'];
$tracker_definition = Tracker_Definition::get($trackerId);
if (! $tracker_definition) {
return;
}
$tracker_info = $tracker_definition->getInformation();
$watchers = $this->get_notification_emails($trackerId, $itemId, $tracker_info, $new_values['status'], $old_values['status']);
// not a great test for a new item but we don't get the event type here
$created = empty($old_values) || $old_values === ['status' => ''];
$notifyOn = isset($tracker_info['notifyOn']) ? $tracker_info['notifyOn'] : 'both';
if ($created && ($notifyOn != 'both' && $notifyOn != 'creation')) {
return;
} else if (! $created && ($notifyOn != 'both' && $notifyOn != 'update')) {
return;
}
if (count($watchers) > 0) {
$simpleEmail = isset($tracker_info['simpleEmail']) ? $tracker_info['simpleEmail'] : "n";
$trackerName = $tracker_info['name'];
if (! isset($_SERVER["SERVER_NAME"])) {
$_SERVER["SERVER_NAME"] = $_SERVER["HTTP_HOST"];
}
include_once('lib/webmail/tikimaillib.php');
if ($simpleEmail == "n") {
$mail_main_value_fieldId = $this->get_main_field($trackerId);
$mail_main_value_field = $tracker_definition->getField($mail_main_value_fieldId);
if (in_array($mail_main_value_field['type'], ['r', 'q'])) {
// Item Link & auto-inc are special cases as field value is not the displayed text. There might be other such field types.
$handler = $this->get_field_handler($mail_main_value_field);
$desc = $handler->renderOutput(['list_mode' => 'csv']);
} else {
$desc = $this->get_item_value($trackerId, $itemId, $mail_main_value_fieldId);
}
$smarty = TikiLib::lib('smarty');
$smarty->assign('mail_date', $this->now);
$smarty->assign('mail_user', $user);
$smarty->assign('mail_itemId', $itemId);
$smarty->assign('mail_item_desc', $desc);
$smarty->assign('mail_trackerId', $trackerId);
$smarty->assign('mail_trackerName', $trackerName);
$smarty->assign('server_name', $_SERVER['SERVER_NAME']);
$foo = parse_url($_SERVER["REQUEST_URI"]);
$machine = $this->httpPrefix(true) . $foo["path"];
$smarty->assign('mail_machine', $machine);
$parts = explode('/', $foo['path']);
if (count($parts) > 1) {
unset($parts[count($parts) - 1]);
}
$smarty->assign('mail_machine_raw', $this->httpPrefix(true) . implode('/', $parts));
foreach ($watchers as $watcher) {
// assign these variables inside the loop as this->tracker_render_values overrides them in case trackeroutput or similar is used
$smarty->assign_by_ref('status', $new_values['status']);
$smarty->assign_by_ref('status_old', $old_values['status']);
// expose the pretty tracker fields to the email tpls
foreach ($tracker_definition->getFields() as $field) {
$fieldId = $field['fieldId'];
$old_value = isset($old_values[$fieldId]) ? $old_values[$fieldId] : '';
$new_value = isset($new_values[$fieldId]) ? $new_values[$fieldId] : '';
$smarty->assign('f_' . $fieldId, $new_value);
$smarty->assign('f_' . $field['permName'], $new_value);
$smarty->assign('f_old_' . $fieldId, $old_value);
$smarty->assign('f_old_' . $field['permName'], $old_value);
$smarty->assign('f_name_' . $fieldId, $field['name']);
$smarty->assign('f_name_' . $field['permName'], $field['name']);
}
$watcher['language'] = $this->get_user_preference($watcher['user'], 'language', $prefs['site_language']);
if ($created) {
$label = tra('Item Creation', $watcher['language']);
} else {
$label = tra('Item Modification', $watcher['language']);
}
$mail_action = "\r\n$label\r\n\r\n";
$mail_action .= tra('Tracker', $watcher['language']) . ":\n " . tra($trackerName, $watcher['language']) . "\r\n";
$mail_action .= tra('Item', $watcher['language']) . ":\n $itemId $desc";
$smarty->assign('mail_action', $mail_action);
if (! isset($watcher['template'])) {
$watcher['template'] = '';
}
$content = $this->parse_notification_template($watcher['template']);
$subject = $smarty->fetchLang($watcher['language'], $content['subject']);
// get the diff for changes for this watcher
$the_data = $this->generate_watch_data($old_values, $new_values, $trackerId, $itemId, $args['version'], $watcher['user']);
if (empty($the_data) && $prefs['tracker_always_notify'] !== 'y') {
continue;
}
if ($tracker_info['doNotShowEmptyField'] === 'y') {
// remove empty fields if tracker says so
$the_data = preg_replace('/\[-\[.*?\]-\] -\[\(.*?\)\]-:\n\n----------\n/', '', $the_data);
}
list($watcher_data, $watcher_subject) = $this->translate_watch_data($the_data, $subject, $watcher['language']);
$smarty->assign('mail_data', $watcher_data);
if (isset($watcher['action'])) {
$smarty->assign('mail_action', $watcher['action']);
}
$smarty->assign('mail_to_user', $watcher['user']);
$mail_data = $smarty->fetchLang($watcher['language'], $content['template']);
// if the tpl returns nothing then don't send the mail
if (! empty($mail_data)) {
$mail = new TikiMail($watcher['user']);
$mail->setSubject($watcher_subject);
if (isset($watcher['templateFormat']) && $watcher['templateFormat'] == 'html') {
$mail->setHtml($mail_data, str_replace(' ', ' ', strip_tags($mail_data)));
} else {
$mail->setText(str_replace(' ', ' ', strip_tags($mail_data)));
}
$mail->send([$watcher['email']]);
}
}
} else {
// Use simple email
$foo = parse_url($_SERVER["REQUEST_URI"]);
$machine = $this->httpPrefix(true) . $foo["path"];
$parts = explode('/', $foo['path']);
if (count($parts) > 1) {
unset($parts[count($parts) - 1]);
}
$machine = $this->httpPrefix(true) . implode('/', $parts);
$userlib = TikiLib::lib('user');
if (! empty($user)) {
$my_sender = $userlib->get_user_email($user);
} else {
// look if a email field exists
$fieldId = $this->get_field_id_from_type($trackerId, 'm');
if (! empty($fieldId)) {
$my_sender = $this->get_item_value($trackerId, $itemId, $fieldId);
}
}
$the_data = $this->generate_watch_data($old_values, $new_values, $trackerId, $itemId, $args['version']);
if (empty($the_data) && $prefs['tracker_always_notify'] !== 'y') {
return;
}
// Try to find a Subject in $the_data looking for strings marked "-[Subject]-" TODO: remove the tra (language translation by submitter)
$the_string = '/^\[-\[' . tra('Subject') . '\]-\] -\[[^\]]*\]-:\n(.*)/m';
$subject_test_unchanged = preg_match($the_string, $the_data, $unchanged_matches);
$the_string = '/^\[-\[' . tra('Subject') . '\]-\]:\n(.*)\n(.*)\n\n(.*)\n(.*)/m';
$subject_test_changed = preg_match($the_string, $the_data, $matches);
$subject = '';
if ($subject_test_unchanged == 1) {
$subject = $unchanged_matches[1];
}
if ($subject_test_changed == 1) {
$subject = $matches[1] . ' ' . $matches[2] . ' ' . $matches[3] . ' ' . $matches[4];
}
$i = 0;
foreach ($watchers as $watcher) {
$watcher['language'] = $this->get_user_preference($watcher['user'], 'language', $prefs['site_language']);
$mail = new TikiMail($watcher['user']);
list($watcher_data, $watcher_subject) = $this->translate_watch_data($the_data, $subject, $watcher['language']);
$mail->setSubject('[' . $trackerName . '] ' . str_replace('> ', '', $watcher_subject) . ' (' . tra('Tracker was modified at %0 by %1', $watcher['language'], false, [$_SERVER["SERVER_NAME"], $user]) . ')');
$mail->setText(tra('View the tracker item at:', $watcher['language']) . " $machine/tiki-view_tracker_item.php?itemId=$itemId\n\n" . $watcher_data);
if (! empty($my_sender)) {
$mail->setReplyTo($my_sender);
}
$mail->send([$watcher['email']]);
$i++;
}
}
}
}
private function parse_notification_template($template)
{
$tikilib = TikiLib::lib('tiki');
$subject = "";
if (! empty($template)) { //tpl
if (! preg_match('/^(:?tpl)?wiki\:/', $template, $match)) {
if (! preg_match('/\.tpl$/', $template)) { // template file
$template .= '.tpl';
}
$template = 'mail/' . $template;
$subject = str_replace('.tpl', '_subject.tpl', $template);
} else { // wiki template
$pageName = substr($template, strlen($match[0]));
if (! $tikilib->page_exists($pageName)) {
Feedback::error(tr('Missing wiki email template page "%0"', htmlspecialchars($template)));
$template = '';
} else {
$subject_name = str_replace('tpl', 'subject tpl', $pageName);
if ($tikilib->page_exists($subject_name)) {
$subject = $match[0] . $subject_name;
} else {
$subject_name = str_replace('tpl', 'subject-tpl', $pageName);
if ($tikilib->page_exists($subject_name)) {
$subject = $match[0] . $subject_name;
}
}
}
}
}
if (empty($template)) {
$template = 'mail/tracker_changed_notification.tpl';
}
if (empty($subject)) {
$subject = 'mail/tracker_changed_notification_subject.tpl';
}
return [
'subject' => $subject,
'template' => $template,
];
}
/**
* Translate the watch data and subject for each watcher
*
* @param string $the_data
* @param string $subject
* @param string $language
* @return array translated [data, subject]
*/
private function translate_watch_data($the_data, $subject, $language)
{
// first we look for strings marked "-[...]-" to translate by watcher language
$watcher_subject = $subject;
$watcher_data = $the_data;
if (preg_match_all('/-\[([^\]]*)\]-/', $the_data, $tra_matches) > 0 && $language !== 'en') {
foreach ($tra_matches[1] as $match) {
// now we replace the marked strings with correct translations
$tra_replace = tra($match, $language);
$tra_match = "/-\[" . preg_quote($match) . "\]-/m";
$watcher_subject = preg_replace($tra_match, $tra_replace, $watcher_subject);
$watcher_data = preg_replace($tra_match, $tra_replace, $watcher_data);
}
}
return [$watcher_data, $watcher_subject];
}
private function generate_watch_data($old, $new, $trackerId, $itemId, $version, $watcher = '')
{
global $prefs;
$userslib = TikiLib::lib('user');
$tracker_definition = Tracker_Definition::get($trackerId);
if (! $tracker_definition) {
return '';
}
$oldStatus = $old['status'];
$newStatus = $new['status'];
$changed = false;
$the_data = '';
if (! empty($oldStatus) || ! empty($newStatus)) {
if (! empty($itemId) && $oldStatus != $newStatus) {
$this->log($version, $itemId, -1, $oldStatus);
}
$the_data .= '-[Status]-: ';
$statusTypes = $this->status_types('en'); // Fetch in english to translate to watcher language
if (isset($oldStatus) && $oldStatus != $newStatus) {
$the_data .= isset($statusTypes[$oldStatus]['label']) ? '-[' . $statusTypes[$oldStatus]['label'] . ']- -> ' : '';
$changed = true;
}
if (! empty($newStatus)) {
$the_data .= '-[' . $statusTypes[$newStatus]['label'] . ']-';
}
$the_data .= "\n----------\n";
}
foreach ($tracker_definition->getFields() as $field) {
$fieldId = $field['fieldId'];
$old_value = isset($old[$fieldId]) ? $old[$fieldId] : '';
$new_value = isset($new[$fieldId]) ? $new[$fieldId] : '';
if ($old_value == $new_value) {
continue;
}
$handler = $this->get_field_handler($field);
if ($handler) {
$userOk = (! $watcher || $watcher === 'admin');
if (! $userOk && is_array($field['visibleBy']) && ! empty($field['visibleBy'])) {
foreach ($field['visibleBy'] as $group) {
$userOk = $userslib->user_is_in_group($watcher, $group);
if ($userOk) {
break;
}
}
} else {
$userOk = true;
}
if ($userOk) {
$the_data .= $handler->watchCompare($old_value, $new_value);
}
} else {
$the_data .= tr('Tracker field not enabled: fieldId=%0 type=%1', $field['fieldId'], tra($field['type'])) . "\n";
}
$the_data .= "\n----------\n";
$changed = true;
}
if ($changed || $prefs['tracker_always_notify'] === 'y') {
return $the_data;
} else {
return '';
}
}
private function tracker_is_syncable($trackerId)
{
global $prefs;
if (! empty($prefs["user_trackersync_trackers"])) {
$trackersync_trackers = unserialize($prefs["user_trackersync_trackers"]);
return in_array($trackerId, $trackersync_trackers);
}
return false;
}
private function get_tracker_item_users($trackerId, $values)
{
global $user, $prefs;
$userlib = TikiLib::lib('user');
$trackersync_users = [$user];
$definition = Tracker_Definition::get($trackerId);
if ($definition) {
$fieldId = $definition->getUserField();
$value = isset($values[$fieldId]) ? $values[$fieldId] : '';
if ($value) {
$trackersync_users = $this->parse_user_field($value);
}
}
return $trackersync_users;
}
private function get_tracker_item_coordinates($trackerId, $values)
{
$definition = Tracker_Definition::get($trackerId);
if ($definition && $fieldId = $definition->getGeolocationField()) {
if (isset($values[$fieldId])) {
return TikiLib::lib('geo')->parse_coordinates($values[$fieldId]);
}
}
}
public function sync_user_lang($args)
{
global $prefs;
$trackerId = $args['trackerId'];
if ($prefs['user_trackersync_lang'] != 'y') {
return;
}
if (! $this->tracker_is_syncable($trackerId)) {
return;
}
$trackersync_users = $this->get_tracker_item_users($trackerId, $args['values']);
if (empty($trackersync_users)) {
return;
}
$definition = Tracker_Definition::get($trackerId);
if ($definition && $fieldId = $definition->getLanguageField()) {
foreach ($trackersync_users as $trackersync_user) {
TikiLib::lib('tiki')->set_user_preference($trackersync_user, 'language', $args['values'][$fieldId]);
}
}
}
public function sync_user_realname($args)
{
global $prefs;
$trackerId = $args['trackerId'];
if (! $this->tracker_is_syncable($trackerId)) {
return;
}
$trackersync_users = $this->get_tracker_item_users($trackerId, $args['values']);
if (empty($trackersync_users)) {
return;
}
if (! empty($prefs["user_trackersync_realname"])) {
// Fields to concatenate are delimited by + and priority sets are delimited by ,
$trackersync_realnamefields = preg_split('/\s*,\s*/', $prefs["user_trackersync_realname"]);
foreach ($trackersync_realnamefields as $fields) {
$parts = [];
$fields = preg_split('/\s*\+\s*/', $fields);
foreach ($fields as $field) {
$field = (int) $field;
if (isset($args['values'][$field])) {
$parts[] = $args['values'][$field];
}
}
$realname = implode(' ', $parts);
if (! empty($realname)) {
foreach ($trackersync_users as $trackersync_user) {
TikiLib::lib('tiki')->set_user_preference($trackersync_user, 'realName', $realname);
}
}
}
}
}
public function sync_user_geo($args)
{
global $prefs;
$trackerId = $args['trackerId'];
if (! $this->tracker_is_syncable($trackerId)) {
return;
}
$trackersync_users = $this->get_tracker_item_users($trackerId, $args['values']);
if (empty($trackersync_users)) {
return;
}
if ($geo = $this->get_tracker_item_coordinates($trackerId, $args['values'])) {
$tikilib = TikiLib::lib('tiki');
foreach ($trackersync_users as $trackersync_user) {
$tikilib->set_user_preference($trackersync_user, 'lon', $geo['lon']);
$tikilib->set_user_preference($trackersync_user, 'lat', $geo['lat']);
if (! empty($geo['zoom'])) {
$tikilib->set_user_preference($trackersync_user, 'zoom', $geo['zoom']);
}
}
}
}
public function sync_item_geo($args)
{
$trackerId = $args['trackerId'];
$itemId = $args['object'];
if ($geo = $this->get_tracker_item_coordinates($trackerId, $args['values'])) {
if ($geo && $itemId) {
TikiLib::lib('geo')->set_coordinates('trackeritem', $itemId, $geo);
}
}
}
public function sync_user_groups($args)
{
global $prefs;
$trackerId = $args['trackerId'];
if (! $this->tracker_is_syncable($trackerId)) {
return;
}
$trackersync_users = $this->get_tracker_item_users($trackerId, $args['values']);
if (empty($trackersync_users)) {
return;
}
if (empty($prefs["user_trackersync_groups"])) {
return;
}
$definition = Tracker_Definition::get($trackerId);
$userslib = TikiLib::lib('user');
$trackersync_groupfields = preg_split('/\s*,\s*/', $prefs["user_trackersync_groups"]);
foreach ($trackersync_groupfields as $field) {
$field = (int)$field;
if (! isset($args['values'][$field])) {
continue;
}
$field = $definition->getField($field);
$handler = $this->get_field_handler($field, $args['values']);
$group = $handler->renderOutput();
if (empty($group) || ! $userslib->group_exists($group)) {
continue;
}
foreach ($trackersync_users as $trackersync_user) {
if (! $userslib->user_exists($trackersync_user)) {
continue;
}
if ($userslib->user_is_in_group($trackersync_user, $group)) {
continue;
}
$userslib->assign_user_to_group($trackersync_user, $group);
}
}
}
public function sync_item_auto_categories($args)
{
$trackerId = $args['trackerId'];
$itemId = $args['object'];
$definition = Tracker_Definition::get($trackerId);
if ($definition && $definition->isEnabled('autoCreateCategories')) {
$categlib = TikiLib::lib('categ');
$tracker_item_desc = $this->get_isMain_value($trackerId, $itemId);
// Verify that parentCat exists Or Create It
$parentcategId = $categlib->get_category_id("Tracker $trackerId");
if (! isset($parentcategId)) {
$parentcategId = $categlib->add_category(0, "Tracker $trackerId", $definition->getConfiguration('description'));
}
// Verify that the sub Categ doesn't already exists
$currentCategId = $categlib->get_category_id("Tracker Item $itemId");
if (! isset($currentCategId) || $currentCategId == 0) {
$currentCategId = $categlib->add_category($parentcategId, "Tracker Item $itemId", $tracker_item_desc);
} else {
$categlib->update_category($currentCategId, "Tracker Item $itemId", $tracker_item_desc, $parentcategId);
}
$cat_type = "trackeritem";
$cat_objid = $itemId;
$cat_desc = '';
$cat_name = "Tracker Item $itemId";
$cat_href = "tiki-view_tracker_item.php?trackerId=$trackerId&itemId=$itemId";
// ?? HAS to do it ?? $categlib->uncategorize_object($cat_type, $cat_objid);
$catObjectId = $categlib->is_categorized($cat_type, $cat_objid);
if (! $catObjectId) {
$catObjectId = $categlib->add_categorized_object($cat_type, $cat_objid, $cat_desc, $cat_name, $cat_href);
}
$categlib->categorize($catObjectId, $currentCategId);
}
}
private function get_viewable_category_field_cats($trackerId)
{
$definition = Tracker_Definition::get($trackerId);
$categories = [];
if (! $definition) {
return [];
}
foreach ($definition->getFields() as $field) {
if ($field['type'] == 'e') {
$parentId = $field['options_array'][0];
$descends = isset($field['options_array'][3]) && $field['options_array'][3] == 1;
if (ctype_digit($parentId) && $parentId > 0) {
$cats = TikiLib::lib('categ')->getCategories(['identifier' => $parentId, 'type' => $descends ? 'descendants' : 'children']);
} else {
$cats = [];
}
foreach ($cats as $c) {
$categories[] = $c['categId'];
}
}
}
return array_unique(array_filter($categories));
}
public function invalidate_item_cache($args)
{
$itemId = $args['object'];
$cachelib = TikiLib::lib('cache');
$cachelib->invalidate('trackerItemLabel' . $itemId);
if (isset($args['values']) && isset($args['old_values'])) {
$fields = array_merge(array_keys($args['values']), array_keys($args['old_values']));
$fields = array_unique($fields);
}
if (! empty($fields)) {
foreach ($fields as $fieldId) {
$old = isset($args['old_values'][$fieldId]) ? $args['old_values'][$fieldId] : null;
$new = isset($args['values'][$fieldId]) ? $args['values'][$fieldId] : null;
if ($old !== $new) {
$this->invalidate_field_cache($fieldId);
}
}
}
}
private function invalidate_field_cache($fieldId)
{
global $prefs, $user;
$multi_languages = $prefs['available_languages'];
if (! $multi_languages) {
$multi_languages = [];
}
$multi_languages[] = '';
$cachelib = TikiLib::lib('cache');
foreach ($multi_languages as $lang) {
$cachelib->invalidate(md5('trackerfield' . $fieldId . 'o' . $user . $lang));
$cachelib->invalidate(md5('trackerfield' . $fieldId . 'c' . $user . $lang));
$cachelib->invalidate(md5('trackerfield' . $fieldId . 'p' . $user . $lang));
$cachelib->invalidate(md5('trackerfield' . $fieldId . 'op' . $user . $lang));
$cachelib->invalidate(md5('trackerfield' . $fieldId . 'oc' . $user . $lang));
$cachelib->invalidate(md5('trackerfield' . $fieldId . 'pc' . $user . $lang));
$cachelib->invalidate(md5('trackerfield' . $fieldId . 'opc' . $user . $lang));
}
}
public function group_tracker_create($args)
{
global $user, $group;
$trackerId = $args['trackerId'];
$itemId = $args['object'];
$new_itemId = isset($args['new_itemId']) ? $args['new_itemId'] : '';
$tracker_info = isset($args['tracker_info']) ? $args['tracker_info'] : '';
$definition = Tracker_Definition::get($trackerId);
if ($definition && $definition->isEnabled('autoCreateGroup')) {
$creatorGroupFieldId = $definition->getWriterGroupField();
if (! empty($creatorGroupFieldId) && $definition->isEnabled('autoAssignGroupItem')) {
$autoCopyGroup = $definition->getConfiguration('autoCopyGroup');
if ($autoCopyGroup) {
$this->modify_field($new_itemId, $tracker_info['autoCopyGroup'], $group);
$fil[$tracker_info['autoCopyGroup']] = $group;
}
}
$desc = $this->get_isMain_value($trackerId, $itemId);
if (empty($desc)) {
$desc = $definition->getConfiguration('description');
}
$userlib = TikiLib::lib('user');
$groupName = $args['values'][$creatorGroupFieldId];
if ($userlib->add_group($groupName, $desc, '', 0, $trackerId, '', 'y', 0, '', '', $creatorGroupFieldId)) {
if ($groupId = $definition->getConfiguration('autoCreateGroupInc')) {
$userlib->group_inclusion($groupName, $this->table('users_groups')->fetchOne('groupName', ['id' => $groupId]));
}
}
if ($definition->isEnabled('autoAssignCreatorGroup')) {
$userlib->assign_user_to_group($user, $groupName);
}
if ($definition->isEnabled('autoAssignCreatorGroupDefault')) {
$userlib->set_default_group($user, $groupName);
$_SESSION['u_info']['group'] = $groupName;
}
}
}
public function update_tracker_summary($args)
{
$items = $this->items();
$trackerId = (int) $args['trackerId'];
$cant_items = $items->fetchCount(['trackerId' => $trackerId]);
$this->trackers()->update(['items' => (int) $cant_items, 'lastModif' => $this->now], ['trackerId' => $trackerId]);
}
public function sync_freetags($args)
{
$definition = Tracker_Definition::get($args['trackerId']);
if ($definition && $field = $definition->getFreetagField()) {
global $user;
$freetaglib = TikiLib::lib('freetag');
$freetaglib->update_tags($user, $args['object'], 'trackeritem', $args['values'][$field]);
}
}
public function update_create_missing_pages($args)
{
global $user;
$tikilib = TikiLib::lib('tiki');
$definition = Tracker_Definition::get($args['trackerId']);
if (! $definition) {
return;
}
foreach ($definition->getFields() as $field) {
$fieldId = $field['fieldId'];
$value = isset($args['values'][$fieldId]) ? $args['values'][$fieldId] : '';
if ($field['type'] == 'k' && $value != '' && ! empty($field['options'][2])) {
if (! $this->page_exists($value)) {
$IP = $this->get_ip_address();
$info = $this->get_page_info($field['options'][2]);
$tikilib->create_page($value, 0, $info['data'], $tikilib->now, '', $user, $IP, $info['description'], $info['lang'], $info['is_html'], [], $info['wysiwyg'], $info['wiki_authors_style']);
}
}
}
}
public function get_maximum_value($fieldId)
{
return $this->itemFields()->fetchOne($this->itemFields()->expr('MAX(CAST(`value` as UNSIGNED))'), ['fieldId' => (int) $fieldId]);
}
public function sync_categories($args)
{
$definition = Tracker_Definition::get($args['trackerId']);
if (! $definition) {
return;
}
$ins_categs = [];
$parent_categs_only = [];
$tosync = false;
$managed_fields = [];
$categorizedFields = $definition->getCategorizedFields();
if (isset($args['supplied'])) {
// Exclude fields that were not part of the request
$categorizedFields = array_intersect($categorizedFields, $args['supplied']);
}
foreach ($categorizedFields as $fieldId) {
if (isset($args['values'][$fieldId])) {
$ins_categs = array_merge($ins_categs, array_filter(explode(',', $args['values'][$fieldId])));
$managed_fields[] = $fieldId;
$tosync = true;
}
}
if ($tosync) {
$this->categorized_item($args['trackerId'], $args['object'], "item {$args['object']}", $ins_categs, null, false, $managed_fields);
}
}
/**
* Render a field value for input or output. The result depends on the fieldtype.
* Note: Each fieldtype has its own input/output handler.
* @param array $params - either a complete field array or a trackerid and a permName
*
* $param = array(
* // required
* 'field' => array( 'fieldId' => 1, 'trackerId' => 2, 'permName' => 'myPermName', 'etc' => '...')
* //'trackerId' => 1 // instread of 'field'
* //'permName>' => 'myPermName' // instread of 'field'
*
* // optional
* 'item' => array('fieldId1' => fieldValue1, 'fieldId2' => fieldValue2) // optional
* 'itemId' = 5 // itemId
* 'process' => 'y' // renders the value using the correct field handler
* 'oldValue' => '' // renders the new and old values using \Tracker_Field_Abstract::renderDiff
* 'list_mode' => '' // i.e. 'y', 'cvs' or 'text' will be used in \Tracker_Field_Abstract::renderOutput
* 'smarty_assign' => 'y' // set to n to not assign the value to the $f_fieldId smarty value for pretty trackers
* )
*
* @return string - rendered value (with html ?). i.e from $r = $handler->renderInput($context), renderOutput or renderDiff
* @throws Exception
*/
public function field_render_value($params)
{
// accept either a complete field definition or a trackerId/permName
if (isset($params['field'])) {
$field = $params['field'];
} elseif (isset($params['trackerId'], $params['permName'])) {
$definition = Tracker_Definition::get($params['trackerId']);
$field = $definition->getFieldFromPermName($params['permName']);
} elseif (isset($params['fieldId'])) {
$field = $this->get_field_info($params['fieldId']);
} else {
return tr('Field not specified');
}
// preset $item = array('itemId' => value). Either from param or empty
$item = isset($params['item']) ? $params['item'] : [];
// if we have an itemId, pass it to our new item structure
if (isset($params['itemId'])) {
$item['itemId'] = $params['itemId'];
}
// check wether we have a value assigned to $fields.
// This might be the case if $fields was passed through $params and not from the tracker definition.
// Build the $items['fieldId'] = value structure
if (isset($field['fieldId'])) {
if (isset($field['value'])) {
$item[$field['fieldId']] = $field['value'];
} elseif (isset($item['itemId'])) {
$item[$field['fieldId']] = $this->get_item_value(null, $item['itemId'], $field['fieldId']);
} elseif (isset($params['value'])) {
$field['value'] = $params['value'];
$field['ins_' . $field['fieldId']] = $field['value'];
$item[$field['fieldId']] = $field['value'];
}
}
// get the handler for the specific fieldtype.
$handler = $this->get_field_handler($field, $item);
if ($handler) {
if (! isset($field['value'])) {
$data = $handler->getFieldData();
$field['value'] = $data['value'];
}
$r = null;
if (! empty($field['encryptionKeyId'])) {
try {
$key = new Tiki\Encryption\Key($field['encryptionKeyId']);
$field['value'] = $item[$field['fieldId']] = $key->decryptData($field['value']);
} catch (Tiki\Encryption\NotFoundException $e) {
$r = tr('Field is encrypted with a key that no longer exists!');
} catch (Tiki\Encryption\Exception $e) {
$field['value'] = $item[$field['fieldId']] = '';
$r = tr('Field data is encrypted using key "%0" but where was an error decrypting the data: %1', $key->get('name'), $e->getMessage());
$r .= ' ' . $key->manualEntry();
}
$handler = $this->get_field_handler($field, $item);
$field = array_merge($field, $handler->getFieldData());
$handler = $this->get_field_handler($field, $item);
}
if (isset($params['process']) && $params['process'] == 'y') {
if ($field['type'] === 'e') { // category
if (! is_array($field['value'])) {
$categIds = explode(',', $field['value']);
} else {
$categIds = $field['value'];
}
$requestData = ['ins_' . $field['fieldId'] => $categIds];
} else {
$requestData = $field;
}
$linkedField = $handler->getFieldData($requestData);
$field = array_merge($field, $linkedField);
$field['ins_id'] = 'ins_' . $field['fieldId'];
$handler = $this->get_field_handler($field, $item);
}
$context = $params;
$fieldId = $field['fieldId'];
unset($context['item']);
unset($context['field']);
if (empty($context['list_mode'])) {
$context['list_mode'] = 'n';
}
if (! is_null($r)) {
// already rendered (decryption error)
} elseif (! empty($params['editable']) && $params['field']['type'] !== 'STARS') {
if ($params['editable'] === true) {
// Some callers pass true/false instead of an actual mode, default to block
$params['editable'] = 'block';
}
if ($params['editable'] == 'direct') {
$r = $handler->renderInput($context);
$params['editable'] = 'block';
$fetchUrl = null;
} else {
$r = $handler->renderOutput($context);
$fetchUrl = [
'controller' => 'tracker',
'action' => 'fetch_item_field',
'trackerId' => $field['trackerId'],
'itemId' => $item['itemId'],
'fieldId' => $field['fieldId'],
'listMode' => $context['list_mode']
];
}
$r = new Tiki_Render_Editable(
$r,
[
'layout' => $params['editable'],
'label' => $field['name'],
'group' => ! empty($params['editgroup']) ? $params['editgroup'] : false,
'field' => [
'id' => "{$field['fieldId']}{$field['trackerId']}{$item['itemId']}",
'type' => $field['type']
],
'object_store_url' => [
'controller' => 'tracker',
'action' => 'update_item',
'trackerId' => $field['trackerId'],
'itemId' => $item['itemId'],
],
'field_fetch_url' => $fetchUrl,
]
);
} elseif (isset($params['oldValue'])) {
$r = $handler->renderDiff($context);
} else {
$r = $handler->renderOutput($context);
}
if (empty($params['smarty_assign']) || $params['smarty_assign'] !== 'n') {
TikiLib::lib('smarty')->assign("f_$fieldId", $r);
$fieldPermName = $field['permName'];
TikiLib::lib('smarty')->assign("f_$fieldPermName", $r);
}
return $r;
}
}
public function get_child_items($itemId)
{
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]);
}
public function get_field_by_perm_name($permName)
{
return $this->get_tracker_field($permName);
}
public function refresh_index_on_master_update($args)
{
// Event handler
// See pref tracker_refresh_itemlink_detail
$modifiedFields = [];
foreach ($args['old_values'] as $key => $old) {
if (! isset($args['values'][$key]) || $args['values'][$key] != $old) {
$modifiedFields[] = $key;
}
}
$items = $this->findLinkedItems(
$args['object'],
function ($field, $handler) use ($modifiedFields, $args) {
return $handler->itemsRequireRefresh($args['trackerId'], $modifiedFields);
}
);
$searchlib = TikiLib::lib('unifiedsearch');
foreach ($items as $itemId) {
$searchlib->invalidateObject('trackeritem', $itemId);
}
}
private function findLinkedItems($itemId, $callback)
{
$fields = $this->table('tiki_tracker_fields');
$list = $fields->fetchAll(
$fields->all(),
['type' => $fields->exactly('r')]
);
$toConsider = [];
foreach ($list as $field) {
$handler = $this->get_field_handler($field);
if ($handler && $callback($field, $handler)) {
$toConsider[] = $field['fieldId'];
}
}
$itemFields = $this->itemFields();
$items = $itemFields->fetchColumn(
'itemId',
[
'fieldId' => $itemFields->in($toConsider),
'value' => $itemId,
]
);
return array_unique($items);
}
public function refresh_itemslist_index($args)
{
// Event handler
// See pref tracker_refresh_itemslist_detail
$modifiedFields = [];
foreach ($args['old_values'] as $key => $old) {
if (! isset($args['values'][$key]) || $args['values'][$key] != $old) {
$modifiedFields[] = $key;
}
}
foreach ($args['values'] as $key => $new) {
if (! isset($args['old_values'][$key]) || $args['old_values'][$key] != $new) {
$modifiedFields[] = $key;
}
}
$modifiedFields = array_unique($modifiedFields);
$items = [];
$fields = $this->table('tiki_tracker_fields');
$list = $fields->fetchAll(
$fields->all(),
['type' => $fields->exactly('l')]
);
foreach ($list as $field) {
$handler = $this->get_field_handler($field);
if ($handler && $handler->itemsRequireRefresh($args['trackerId'], $modifiedFields)) {
$itemId = $args['object'];
$fieldIdHere = (int) $handler->getOption('fieldIdHere');
$fieldIdThere = (int) $handler->getOption('fieldIdThere');
// quick way of getting all ItemsList items pointing to the itemId via the field we examine
if (empty($fieldIdThere)) {
$query = "SELECT itemId
FROM tiki_tracker_item_fields ttif
WHERE ttif.fieldId = ?
AND ttif.`value` = ?";
$bindvars = [$fieldIdHere, $itemId];
} else {
$query = "SELECT COALESCE(ttif2.itemId, ttif1.value) as itemId
FROM tiki_tracker_item_fields ttif1
LEFT JOIN tiki_tracker_item_fields ttif2 ON (ttif2.value = ttif1.value OR ttif2.value = ttif1.itemId) AND ttif2.fieldId = ?
WHERE ttif1.fieldId = ?
AND ttif1.itemId = ?";
$bindvars = [$fieldIdHere, $fieldIdThere, $itemId];
}
$fieldItems = $this->fetchAll($query, $bindvars);
$fieldItems = array_map(
function ($row) {
return $row['itemId'];
},
$fieldItems
);
$items = array_merge($items, $fieldItems);
}
}
$items = array_unique($items);
$searchlib = TikiLib::lib('unifiedsearch');
foreach ($items as $itemId) {
$searchlib->invalidateObject('trackeritem', $itemId);
}
}
public function update_user_account($args)
{
// Try to find if the tracker is a user tracker, flag update to associated user
$fields = array_keys($args['values']);
if (! $fields) {
return;
}
$table = $this->table('users_groups');
$fields = array_filter($fields, 'is_numeric');
$field = $table->fetchOne(
'usersFieldId',
[
'usersFieldId' => $table->in($fields),
]
);
if ($field && ! empty($args['values'][$field])) {
TikiLib::events()->trigger(
'tiki.user.update',
[
'type' => 'user',
'object' => $args['values'][$field],
]
);
}
}
// connect a user to his user item on the email field / email user
public function update_user_item($user, $email, $emailFieldId)
{
$field = $this->get_tracker_field($emailFieldId);
$trackerId = $field['trackerId'];
$definition = Tracker_Definition::get($trackerId);
$userFieldId = $definition->getUserField();
$listfields[$userFieldId] = $definition->getField($userFieldId);
$filterfields[0] = $emailFieldId; // Email field in the user tracker
$exactvalue[0] = $email;
$items = $this->list_items($trackerId, 0, -1, 'created', $listfields, $filterfields, '', 'opc', '', $exactvalue);
$found = false;
foreach ($items['data'] as $item) {
if (empty($item['field_values'][0]['value'])) {
$found = true;
$this->modify_field($item['itemId'], $userFieldId, $user);
} elseif ($item['field_values'][0]['value'] == $user) {
$found = true;
}
}
return $found;
}
/**
* Called from lib/setup/events.php when object are categorized.
* This is to ensure that article and trackeritem categories stay in sync when article indexing is on
* as part of the RSS Article generator feature.
* @param $args
* @param $event
* @param $priority
* @throws Exception
*/
public function sync_tracker_article_categories($args, $event, $priority)
{
global $prefs;
$catlib = TikiLib::lib('categ');
if ($args['type'] == 'article') {
//if it's an article, find the associated trackeritem per the relation
$relationlib = TikiLib::lib('relation');
$artRelation = $relationlib->get_relations_to('article', $args['object'], 'tiki.article.attach', '', '1');
if (empty($artRelation)) {
return;
}
$tracker_item_id = $artRelation[0]['itemId'];
//if the tracker isn't the article tracker as per the pref, don't sync
if (! $tracker_item_id || $prefs['tracker_article_trackerId'] != $this->get_tracker_for_item($tracker_item_id)) {
return;
}
// get the trackeritem's categories and add or remove the same categories that the article had
// added or removed as per the event
$categories = $catlib->get_object_categories('trackeritem', $tracker_item_id);
$categories_old = $categories;
foreach ($args['added'] as $added) {
if (! in_array($added, $categories)) {
$categories[] = $added;
}
}
foreach ($args['removed'] as $removed) {
if (in_array($removed, $categories)) {
$categories = array_diff($categories, [$removed]);
}
}
//update the trackeritems categories if there were new ones added/removed
if ($categories != $categories_old) {
$catlib->update_object_categories($categories, $tracker_item_id, 'trackeritem');
}
} elseif ($args['type'] == 'trackeritem') {
//if trackeritem, make sure it's the article tracker that we're dealing with
$trackerId = $this->get_tracker_for_item($args['object']);
if ($prefs['tracker_article_trackerId'] != $trackerId) {
return;
}
$definition = Tracker_Definition::get($trackerId);
//find the article field in this tracker and from there find the relation for the
$relationlib = TikiLib::lib('relation');
$artRelation = $relationlib->get_relations_from('trackeritem', $args['object'], 'tiki.article.attach', '', '1');
if (empty($artRelation)) {
return;
}
$articleId = $artRelation[0]['itemId'];
// get the articles's categories and add or remove the same categories that the trackeritem had
// added or removed as per the event
$categories = $catlib->get_object_categories('article', $articleId);
$categories_old = $categories;
foreach ($args['added'] as $added) {
if (! in_array($added, $categories)) {
$categories[] = $added;
}
}
foreach ($args['removed'] as $removed) {
if (in_array($removed, $categories)) {
$categories = array_diff($categories, [$removed]);
}
}
//update the article's categories if there were new ones added/removed
if ($categories != $categories_old) {
$catlib->update_object_categories($categories, $articleId, 'article');
}
}
}
/**
* Called when accessing contents of a Tracker UserSelector field.
* Purpose is to parse the csv string of usernames stored inside and format an array.
* @param $value csv-formatted string
* @return array of resulting usernames
*/
public function parse_user_field($value)
{
return array_filter(
array_map(function ($user) {
return trim($user ?? '');
}, is_array($value) ? $value : str_getcsv($value ?? ''))
);
}
/**
* Given a configured system tracker with currency exchange rates and a date,
* return all available currency rates valid for that time.
* @param $date
* @return array of exchange rates
*/
public function exchange_rates($date)
{
global $prefs;
if ($prefs['tracker_system_currency'] != 'y') {
return [];
}
if (is_numeric($date)) {
$date = date('Y-m-d', $date);
} elseif (! empty($date)) {
$date = date('Y-m-d', strtotime($date));
} else {
$date = date('Y-m-d');
}
static $rates = [];
if (isset($rates[$date])) {
return $rates[$date];
}
$rates[$date] = [];
$trackerId = $prefs['tracker_system_currency_tracker'];
$currencyField = $prefs['tracker_system_currency_currency'];
$dateField = $prefs['tracker_system_currency_date'];
$rateField = $prefs['tracker_system_currency_rate'];
if ($trackerId && $currencyField && $dateField && $rateField) {
$currencies = $this->list_tracker_field_values($trackerId, $currencyField);
foreach ($currencies as $currency) {
$rates[$date][$currency] = $this->getOne(
'SELECT ttif3.value as rate FROM tiki_tracker_items tti
LEFT JOIN tiki_tracker_item_fields ttif1 ON tti.itemId = ttif1.itemId AND ttif1.fieldId = ?
LEFT JOIN tiki_tracker_item_fields ttif2 ON tti.itemId = ttif2.itemId AND ttif2.fieldId = ?
LEFT JOIN tiki_tracker_item_fields ttif3 ON tti.itemId = ttif3.itemId AND ttif3.fieldId = ?
WHERE tti.trackerId = ? AND ttif1.value = ? AND DATE_FORMAT(FROM_UNIXTIME(ttif2.value), \'%Y-%m-%d\') <= ?
ORDER BY ttif2.value DESC',
[$currencyField, $dateField, $rateField, $trackerId, $currency, $date]
);
if ($prefs['tracker_system_currency_direction'] == 'reverse' && $rates[$date][$currency]) {
$rates[$date][$currency] = 1 / $rates[$date][$currency];
}
}
}
return $rates[$date];
}
/**
* Generate unique tracker field Permanent name
* @param $definition
* @param $permName
* @param int $maxAllowedSize
* @return string
* @throws Services_Exception_DuplicateValue
*/
public static function generatePermName($definition, $permName, $maxAllowedSize = Tracker_Item::PERM_NAME_MAX_ALLOWED_SIZE)
{
// Ensure that PermName is no longer than 50 characters, since the maximum allowed by MySQL Full
// Text Search as Unified Search Index is 64, and trackers will internally prepend "tracker_field_",
// which are another 14 characters (50+14=64). We could allow longer permanent names when other search
// index engines are the ones being used, but this will probably only delay the problem until the admin
// wants to change the search engine for some reason (some constrains in Lucene or Elasticsearch,
// as experience demonstrated in some production sites in real use cases over long periods of time).
// And to increase chances to avoid conflict when long names only differ in the end of the long string,
// where some meaningful info resides, we'll get the first (PERM_NAME_MAX_ALLOWED_SIZE - 10) chars, 1 underscore and the last 9 chars.
$permName = (strlen($permName) > $maxAllowedSize) ? substr($permName, 0, ($maxAllowedSize - 10)) . '_' . substr($permName, -9) : $permName;
// Quick way to solve permName conflict, which is very common in languages that only use characters considered
// special for this purpose (ie: hebrew). Ideally we should use fieldId, but it haven't been defined yet.
$tries = 0;
while ($definition->getFieldFromPermName($permName)) {
$permName = substr($permName, 0, ($maxAllowedSize - 5)) . "_" . rand(1000, 9999);
// Let's avoid theoretical chance of infinite loop
if (++$tries > 100) {
throw new Services_Exception_DuplicateValue('permName', $permName);
}
}
return $permName;
}
// Add backlinks from selected page by 'k' (see page selector trackerfield)
public function add_page_selector_backlink($itemId, $fieldId, $value)
{
$tikilib = TikiLib::lib('tiki');
$pageFrom = 'objectlink:trackeritemfield:' . $itemId . ':' . $fieldId;
$tikilib->clear_links($pageFrom);
$pageTo = $value;
$tikilib->replace_link($pageFrom, $pageTo);
}
// Update and stores relations with wiki page
public function update_page_selector_relations($value, $itemId)
{
$wikilib = TikiLib::lib('wiki');
$objectType = 'trackeritemfield';
$wikilib->update_wikicontent_relations($value, $objectType, $itemId);
}
}