* chainable became popular in jQuery $(this)->fn()->fn() and is more and more popular in php. Tracker_Query uses them * to make Trackers, which are somewhat complex, easy. * * * Examples of usage (fake tracker called 'Event Tracker': * //using id * $results = Tracker_Query(1) * ->byName() * ->itemId(100) * ->query(); * * //using by name * $results = Tracker_Query('Event Tracker') * ->byName() * ->limit(1) * ->query(); * * *

* The Output of tracker query is built as tracker(items(fields())) with the keys being the id (or name for fields if byName is called). * Standard output example ($this->byName() not called): * Array * ( * [367] => Array //item repeats, key = itemId * ( * [19] => 369 //item field values, key = fieldId * [20] => 366 * [status5] => c * [trackerId] => 5 * [itemId] => 367 * [11] => internal * [162] => Array //items list associated to filedId 162 * ( * [0] => 176 // itemId * [1] => Event Name // static name of an itemId * ) * ) * ) * ) * * ByName output example ($this->byName() called): * Array * ( * [367] => Array //item repeats, key = itemId * ( * [Minute End] => 369 //item field values, key = fieldId * [Minute Start] => 366 * [status5] => c * [trackerId] => 5 * [itemId] => 367 * [Use Case] => internal * [Events] => Array //items list associated to filedId 162 * ( * [0] => 176 // itemId * [1] => Event Name // static name of an itemId * ) * ) * ) * ) * * @package Tiki * @subpackage Trackers * @author Robert Plummer * @link http://dev.tiki.org/Tracker_Query * @since TIki 8 */ class Tracker_Query { private $tracker; private $start = 0; private $end = 0; private $itemId = 0; private $equals = []; private $search = []; private $fields = []; private $status = "opc"; private $sort = null; private $limit = 100; //added limit so default wouldn't crash system private $offset = 0; private $byName = false; private $desc = false; private $render = true; private $excludeDetails = false; private $lastModif = true; private $delimiter = "[{|!|}]"; private $debug = false; private $concat = true; private $filterType = []; private $inputDefaults = []; public $itemsRaw = []; public $permissionsChecks = true; public $limitReached = false; /** * Instantiates a tracker query * * @access public static * @param mixed $tracker id, (or name if called $this->byName() before query) * @return new self */ public static function tracker($tracker) { return (new Tracker_Query($tracker)); } /** * Overrides permissions * * @access public * @param bool $permissionsChecks, default true * @return new self */ public function permissionsChecks($permissionsChecks = true) { $this->permissionsChecks = $permissionsChecks; return $this; } /** * change the start date unit, needs called before $this->query() * * @access public * @param mixed $start unix time stamp, int or string * @return $this for chainability */ public function start($start) { $this->start = $start; return $this; } /** * change the end date unit, needs called before $this->query() * * @access public * @param mixed $end unix time stamp, int or string * @return $this for chainability */ public function end($end) { $this->end = $end; return $this; } /** * change the itemid, needs called before $this->query() * * @access public * @param int $itemId to limit output to 1 item with this id * @return $this for chainability */ public function itemId($itemId) { $this->itemId = (int)$itemId; return $this; } /** * add a filter for refining results, needs called before $this->query() * * @access public * @param array $filter an array with keys type (like, and, or), field (id or name), and value (value needed * from tracker file item to be returned as a result) * @return $this for chainability */ public function filter($filter = []) { $filter = array_merge( [ 'field' => '', 'type' => 'and', 'value' => '' ], $filter ); $this->fields[] = $filter['field']; $this->filterType[] = $filter['type']; //really only things that should be accepted are "and" and "or", woops, and "like" if ($filter['type'] == 'like') { $this->search[] = $filter['value']; } else { $this->equals[] = $filter['value']; } return $this; } /** * filter results on a mysql level using 'and' type, needs called before $this->query() * * @access public * @param mixed $field either id or name when $this->byName() is called * @param string $value * @return $this for chainability */ public function filterFieldByValue($field, $value) { return $this->filter(['field' => $field, 'value' => $value, 'type' => 'and']); } /** * filter results on a mysql level using 'like' + 'and' type, needs called before $this->query() * * @access public * @param mixed $field either id or name when $this->byName() is called * @param string $value * @return $this for chainability */ public function filterFieldByValueLike($field, $value) { return $this->filter(['field' => $field, 'value' => $value, 'type' => 'like']); } /** * filter results on a mysql level using 'or' type, needs called before $this->query() * * @access public * @param mixed $field either id or name when $this->byName() is called * @param string $value * @return $this for chainability */ public function filterFieldByValueOr($field, $value) { return $this->filter(['field' => $field, 'value' => $value, 'type' => 'or']); } /** * deprecated, filter results on a mysql level on field value, needs called before $this->query() * * @access public * @param array $equals * @return $this for chainability */ public function equals($equals = []) { trigger_error("Deprecated, use filter method instead"); $this->equals = $equals; return $this; } /** * deprecated, filter results on a mysql level on field value, needs called before $this->query() * * @access public * @param array $search either id or name when $this->byName() is called * @return $this for chainability */ public function search($search) { trigger_error("Deprecated, use filter method instead"); $this->search = $search; return $this; } /** * deprecated, filter results on a mysql level on field value, needs called before $this->query() * * @access public * @param array $fields either id or name when $this->byName() is called * @return $this for chainability */ public function fields($fields = []) { trigger_error("Deprecated, use filter method instead"); $this->fields = $fields; return $this; } /** * Filter tracker items on status, needs called before $this->query() * * @access public * @param string $status any of or any combination of the 3 characters 'opc' * @return $this for chainability */ public function status($status) { $this->status = $status; return $this; } /** * Not yet implemented * * @access public * @param string $sort any of or any combination of the 3 characters 'opc' * @return $this for chainability */ public function sort($sort) { $this->sort = $sort; return $this; } /** * Change limit of items, danger with large numbers, needs called before $this->query() * * @access public * @param int $limit amount of items to return, maximum * @return $this for chainability */ public function limit($limit) { $this->limit = $limit; return $this; } /** * Change offset, needs called before $this->query() * * @access public * @param int $offset amount of items to offset * @return $this for chainability */ public function offset($offset) { $this->offset = $offset; return $this; } /** * Change tracker to use all, in tracker and fields, needs called before $this->query() * * @access public * @param bool $byName default to true, optional * @return $this for chainability */ public function byName($byName = true) { $this->byName = $byName; return $this; } /** * order by lastModified, needs called before $this->query(), default to true, needs called before $this->query() * * @access public * @return $this for chainability */ public function lastModif() { $this->lastModif = true; return $this; } /** * order by created, needs called before $this->query(), default to false, needs called before $this->query() * * @access public * @return $this for chainability */ public function created() { $this->lastModif = false; return $this; } /** * Remove details that come with each tracker item, status, itemId, trackerId, needs called before $this->query() * Default is to include details * * @access public * @param bool $excludeDetails default to true, optional * @return $this for chainability */ public function excludeDetails($excludeDetails = true) { $this->excludeDetails = $excludeDetails; return $this; } /** * Sort descending, default false, needs called before $this->query() * * @access public * @param bool $desc default to true, optional * @return $this for chainability */ public function desc($desc = true) { $this->desc = $desc; return $this; } /** * Turn rendering for tracker item fields off, effective to make tracker interactions much MUCH faster, needs called before $this->query() * * @access public * @param bool $render * @return $this for chainability */ public function render($render) { $this->render = $render; return $this; } /** * sets limit to 1 and calls $this->query() * * @access public * @return query */ public function getOne() { return $this ->limit(1) ->query(); } /** * calls desc, sets limit to 1 and calls $this->query() * * @access public * @return query */ public function getLast() { return $this ->desc(true) ->limit(1) ->query(); } /** * calls getOne, and returns only the itemId * * @access public * @return int $key itemId */ public function getItemId() { $query = $this->getOne(); $key = (int)end(array_keys($query)); $key = ($key > 0 ? $key : 0); return $key; } /** * turn debug on, if having problems, outputs the built mysql query and result set of the query, kills php * * @access public * @param bool $debug, default = true * @param bool $concat, default = true * @return $this for chainability */ public function debug($debug = true, $concat = true) { $this->debug = $debug; $this->concat = $concat; return $this; } /** * permission check on view * * @access public * @return bool view */ public function canView() { if ($this->permissionsChecks == false) { return true; } return Perms::get([ 'type' => 'tracker', 'object' => $this->trackerId() ])->view; } /** * permission check on edit * * @access public * @return bool edit */ public function canEdit() { if ($this->permissionsChecks == false) { return true; } return Perms::get([ 'type' => 'tracker', 'object' => $this->trackerId() ])->edit; } /** * permission check on delete * * @access public * @return bool delete */ public function canDelete() { if ($this->permissionsChecks == false) { return true; } return Perms::get([ 'type' => 'tracker', 'object' => $this->trackerId() ])->delete; } /** * Setup temporary table for joining trackers together * * @access public * @param mixed $tracker, id or tracker name if $this->byName() called */ public function __construct($tracker = '') { global $tikilib; $this->tracker = $tracker; $tikilib->query( "DROP TABLE IF EXISTS temp_tracker_field_options;" ); $tikilib->query( "CREATE TEMPORARY TABLE temp_tracker_field_options ( trackerIdHere INT, trackerIdThere INT, fieldIdThere INT, fieldIdHere INT, displayFieldIdThere INT, displayFieldIdHere INT, linkToItems INT, type VARCHAR(1), options VARCHAR(50) );" ); $tikilib->query( "INSERT INTO temp_tracker_field_options SELECT tiki_tracker_fields.trackerId, REPLACE(SUBSTRING( SUBSTRING_INDEX(tiki_tracker_fields.options, ',', 1), LENGTH(SUBSTRING_INDEX(tiki_tracker_fields.options, ',', 1 -1)) + 1 ), ',', ''), REPLACE(SUBSTRING( SUBSTRING_INDEX(tiki_tracker_fields.options, ',', 2), LENGTH(SUBSTRING_INDEX(tiki_tracker_fields.options, ',', 2 -1)) + 1 ), ',', ''), REPLACE(SUBSTRING( SUBSTRING_INDEX(tiki_tracker_fields.options, ',', 3), LENGTH(SUBSTRING_INDEX(tiki_tracker_fields.options, ',', 3 -1)) + 1 ), ',', ''), REPLACE(SUBSTRING( SUBSTRING_INDEX(tiki_tracker_fields.options, ',', 4), LENGTH(SUBSTRING_INDEX(tiki_tracker_fields.options, ',', 4 -1)) + 1 ), ',', ''), tiki_tracker_fields.fieldId, REPLACE(SUBSTRING( SUBSTRING_INDEX(tiki_tracker_fields.options, ',', 5), LENGTH(SUBSTRING_INDEX(tiki_tracker_fields.options, ',', 5 -1)) + 1 ), ',', ''), tiki_tracker_fields.type, tiki_tracker_fields.options FROM tiki_tracker_fields WHERE tiki_tracker_fields.type = 'l';" ); $tikilib->query( "SET group_concat_max_len = 4294967295;" ); /*For any fields that have multi items, we use php to parse those out, there shouldn't be too many */ foreach ($tikilib->fetchAll("SELECT * FROM temp_tracker_field_options WHERE options LIKE '%|%'") as $row) { $option = explode(",", $row["options"]); $displayFieldIdsThere = explode("|", $option["3"]); foreach ($displayFieldIdsThere as $key => $displayFieldIdThere) { if ($key > 0) { $tikilib->query( "INSERT INTO temp_tracker_field_options VALUES (?,?,?,?,?,?,?,?,?)", [ $row["trackerIdHere"], $row["trackerIdThere"], $row["fieldIdThere"], $row["fieldIdHere"], $displayFieldIdThere, $row["displayFieldIdHere"], $row["linkToItems"], $row["type"], $row["options"] ] ); } } } } /** * Adds the field names to the beginning of the array of tracker items * */ public static function prepend_field_header(&$trackerPrimary = [], $nameOrder = []) { global $tikilib; $result = $tikilib->fetchAll("SELECT fieldId, trackerId, name FROM tiki_tracker_fields"); $header = []; foreach ($result as $row) { $header[$row['fieldId']] = $row['name']; } $joinedTrackerHeader = []; foreach ($trackerPrimary as $item) { foreach ($item as $key => $field) { $joinedTrackerHeader[$key] = $header[$key]; } } if (! empty($nameOrder)) { $sortedHeader = []; $unsortedHeader = []; foreach ($nameOrder as $name) { foreach ($joinedTrackerHeader as $key => $field) { if ($field == $name) { $sortedHeader[$key] = $field; } else { $unsortedHeader[$key] = $field; } } } $joinedTrackerHeader = $sortedHeader + $unsortedHeader; } $joinedTrackerHeader = ["HEADER" => $joinedTrackerHeader]; return $joinedTrackerHeader + $trackerPrimary; } /** * Simple direction parsing from string to type * */ private static function sortDirection($dir) { switch ($dir) { case "asc": $dir = SORT_ASC; break; case "desc": $dir = SORT_DESC; break; case "regular": $dir = SORT_REGULAR; break; case "numeric": $dir = SORT_NUMERIC; break; case "string": $dir = SORT_STRING; break; default: $dir = SORT_ASC; } return $dir; } public static function arfsort(&$array, $fieldList) { if (! is_array($fieldList)) { $fieldList = explode('|', $fieldList); $fieldList = [[$fieldList[0], self::sortDirection($fieldList[1])]]; } else { for ($i = 0, $count_fieldList = count($fieldList); $i < $count_fieldList; ++$i) { $fieldList[$i] = explode('|', $fieldList[$i]); $fieldList[$i] = [$fieldList[$i][0], self::sortDirection($fieldList[$i][1])]; } } $GLOBALS['__ARFSORT_LIST__'] = $fieldList; usort($array, 'arfsortFunc'); } public function arfsortFunc($a, $b) { foreach ($GLOBALS['__ARFSORT_LIST__'] as $f) { switch ($f[1]) { case SORT_NUMERIC: $strc = ((float)$b[$f[0]] > (float)$a[$f[0]] ? -1 : 1); return $strc; break; default: $strc = strcasecmp($b[$f[0]], $a[$f[0]]); if ($strc != 0) { return $strc * (! empty($f[1]) && $f[1] == SORT_DESC ? 1 : -1); } } } return 0; } private function concatStr($field) { if ($this->concat == false) { return $field; } else { return "GROUP_CONCAT(" . $field . " SEPARATOR '" . $this->delimiter . "')"; } } /** * Get current tracker id * * @access public * @return int $trackerId */ public function trackerId() { if ($this->byName == true) { $trackerId = TikiLib::lib('trk')->get_tracker_by_name($this->tracker); } else { $trackerId = $this->tracker; } if (! empty($trackerId) && ! is_numeric($trackerId)) { throw new Exception("Opps, looks like you need to call ->byName();"); } return $trackerId; } /** * Query, where the mysql command is built and executed, filtered, and rendered * Orders results in a way that is human understandable and can be manipulated easily * The end result is a very simple array setup as follows: * array( //tracker(s) * array( //items * [itemId] => array ( * [fieldId or FieldName] => value, * [fieldId or FieldName] => array( //items list * [0] => '', * [1] => '' * ) * ) * ) * ) */ public function query() { $trklib = TikiLib::lib('trk'); $tikilib = TikiLib::lib('tiki'); $params = []; $fields_safe = ""; $status_safe = ""; $isSearch = false; $trackerId = $this->trackerId(); if (empty($trackerId) || $this->canView() == false) {//if we can't find a tracker, then return return []; } $trackerDefinition = Tracker_Definition::get($trackerId); $trackerFieldDefinition = $trackerDefinition->getFieldsIdKeys(); $params[] = $trackerId; if (! empty($this->start) && empty($this->search)) { $params[] = $this->start; } if (! empty($this->end) && empty($this->search)) { $params[] = $this->end; } if (! empty($this->itemId) && empty($this->search)) { $params[] = $this->itemId; } /*Get field ids from names*/ if ($this->byName == true && ! empty($this->fields)) { $fieldIds = []; foreach ($this->fields as $field) { $fieldIds[] = $tikilib->getOne( "SELECT fieldId FROM tiki_tracker_fields" . " LEFT JOIN tiki_trackers ON (tiki_trackers.trackerId = tiki_tracker_fields.trackerId)" . " WHERE" . " tiki_trackers.name = ? AND tiki_tracker_fields.name = ?", [$this->tracker, $field] ); } $this->fields = $fieldIds; } if (count($this->fields) > 0 && (count($this->equals) > 0 || count($this->search) > 0)) { for ($i = 0, $count_fields = count($this->fields); $i < $count_fields; $i++) { if (strlen($this->fields[$i]) > 0) { if ($i > 0) { switch ($this->filterType[$i]) { case "or": $fields_safe .= " OR "; break; case "and": $fields_safe .= " OR "; //Even though this is OR, we do a check later to limit more values, so initially we may have more results than are given later on, simply because of how trackers are stored and how group_concat allows us to manipulate trackers break; case "like": $fields_safe .= " AND "; break; } } $fields_safe .= " ( search_item_fields.fieldId = ? "; $params[] = $this->fields[$i]; if (isset($this->equals[$i])) { $fields_safe .= " AND search_item_fields.value = ? "; $params[] = $this->equals[$i]; } if (isset($this->search[$i]) && strlen($this->search[$i]) > 0 && $this->filterType[$i] == "like") { $fields_safe .= " AND search_item_fields.value LIKE ? "; $params[] = '%' . $this->search[$i] . '%'; } $fields_safe .= " ) "; } } if (strlen($fields_safe) > 0) { $fields_safe = " AND ( $fields_safe ) "; $isSearch = true; } } if (strlen($this->status) > 0) { for ($i = 0, $strlen_status = strlen($this->status); $i < $strlen_status; $i++) { if (strlen($this->status[$i]) > 0) { $status_safe .= " tiki_tracker_items.status = ? "; if ($i + 1 < strlen($this->status) && strlen($this->status) > 1) { $status_safe .= " OR "; } $params[] = $this->status[$i]; } } if (strlen($status_safe) > 0) { $status_safe = " AND ( $status_safe ) "; } } if (! empty($this->limit) && is_numeric($this->limit) == false) { unset($this->limit); } if (isset($this->offset) && ! empty($this->offset) && is_numeric($this->offset) == false) { unset($this->offset); } $dateUnit = ($this->lastModif ? 'lastModif' : 'created'); $query = "SELECT tiki_tracker_items.status, tiki_tracker_item_fields.itemId, tiki_tracker_fields.trackerId, " . $this->concatStr("tiki_tracker_fields.name") . " AS fieldNames, " . $this->concatStr("tiki_tracker_item_fields.fieldId") . " AS fieldIds, " . $this->concatStr("IFNULL(items_right.value, tiki_tracker_item_fields.value)") . " AS item_values FROM tiki_tracker_item_fields " . ($isSearch == true ? " AS search_item_fields " : "") . " " . ($isSearch == true ? " LEFT JOIN tiki_tracker_item_fields ON search_item_fields.itemId = tiki_tracker_item_fields.itemId " : "" ) . " LEFT JOIN tiki_tracker_fields ON tiki_tracker_fields.fieldId = tiki_tracker_item_fields.fieldId LEFT JOIN tiki_trackers ON tiki_trackers.trackerId = tiki_tracker_fields.trackerId LEFT JOIN tiki_tracker_items ON tiki_tracker_items.itemId = tiki_tracker_item_fields.itemId LEFT JOIN temp_tracker_field_options items_left_display ON items_left_display.displayFieldIdHere = tiki_tracker_item_fields.fieldId LEFT JOIN tiki_tracker_item_fields items_left ON ( items_left.fieldId = items_left_display.fieldIdHere AND items_left.itemId = tiki_tracker_item_fields.itemId ) LEFT JOIN tiki_tracker_item_fields items_middle ON ( items_middle.value = items_left.value AND items_left_display.fieldIdThere = items_middle.fieldId ) LEFT JOIN tiki_tracker_item_fields items_right ON ( items_right.itemId = items_middle.itemId AND items_right.fieldId = items_left_display.displayFieldIdThere ) WHERE tiki_trackers.trackerId = ? " . (! empty($this->start) ? " AND tiki_tracker_items." . $dateUnit . " > ? " : "") . " " . (! empty($this->end) ? " AND tiki_tracker_items." . $dateUnit . " < ? " : "") . " " . (! empty($this->itemId) ? " AND tiki_tracker_item_fields.itemId = ? " : "") . " " . (! empty($fields_safe) ? $fields_safe : "") . " " . (! empty($status_safe) ? $status_safe : "") . " GROUP BY tiki_tracker_item_fields.itemId " . ($isSearch == true ? ", search_item_fields.fieldId, search_item_fields.itemId " : "" ) . " ORDER BY tiki_tracker_items." . $dateUnit . " " . ($this->desc == true ? 'DESC' : 'ASC') . " " . (! empty($this->limit) ? " LIMIT " . $this->limit : "") . " " . (! empty($this->offset) ? " OFFSET " . $this->offset : ""); if ($this->debug == true) { $result = [$query, $params]; print_r($result); print_r($tikilib->fetchAll($query, $params)); die; } else { $result = $tikilib->fetchAll($query, $params); } $newResult = []; $neededMatches = count($this->fields); foreach ($this->fields as $i => $field) { if ($this->filterType[$i] != 'and') { $neededMatches--; } } foreach ($result as $key => $row) { if (isset($newResult[$row['itemId']])) { continue; } $newRow = []; $fieldNames = explode($this->delimiter, $row['fieldNames']); $fieldIds = explode($this->delimiter, $row['fieldIds']); $itemValues = explode($this->delimiter, $row['item_values']); $matchCount = 0; foreach ($fieldIds as $key => $fieldId) { $field = ($this->byName == true ? $fieldNames[$key] : $fieldId); $value = ''; //This script attempts to narrow the results further by using an "AND" style checking of the returned result since it cannot be made at this time in mysql if ($neededMatches > 0) { $i = array_search($fieldId, $this->fields, true); if ($i !== false) { if ($this->equals[$i] == $itemValues[$key] && $this->filterType[$i] == 'and') { $matchCount++; } } } //End "AND" style checking of results if ($this->render == true) { $value = $this->renderFieldValue($trackerFieldDefinition[$fieldId], $itemValues[$key]); } else { $value = $itemValues[$key]; } if (! isset($this->itemsRaw[$row['itemId']])) { $this->itemsRaw[$row['itemId']] = []; } if (isset($newRow[$field])) { if (is_array($newRow[$field]) == false) { $newRow[$field] = [$newRow[$field]]; $this->itemsRaw[$row['itemId']][$field] = [$itemValues[$key]]; //raw values } $newRow[$field][] = $value; $this->itemsRaw[$row['itemId']][$field][] = $itemValues[$key]; //raw values } else { $newRow[$field] = $value; $this->itemsRaw[$row['itemId']][$field] = $itemValues[$key]; //raw values } } if ($this->excludeDetails == false) { $newRow['status' . $trackerId] = $row['status']; $newRow['trackerId'] = $row['trackerId']; $newRow['itemId'] = $row['itemId']; } if ($neededMatches == 0 || $neededMatches == $matchCount) { $newResult[$row['itemId']] = $newRow; } } unset($result); $this->limitReached = (count($newResult) > $this->limit ? true : false); return $newResult; } /** * renders the field value * * @access private * @param array $fieldDefinition * @param string $value * @return mixed $value rendered field value */ private function renderFieldValue($fieldDefinition, $value) { $trklib = TikiLib::lib('trk'); $fieldDefinition['value'] = $value; //if type is text, no need to render value switch ($fieldDefinition['type']) { case 't'://text case 'S'://static text return $value; } return $trklib->field_render_value( [ 'field' => $fieldDefinition, 'process' => 'y', 'list_mode' => 'y' ] ); } /** * Removed fields from result * * @access private * @param array $fieldDefinition * @param string $value * @return mixed $value rendered field value */ public static function filter_fields_from_tracker_query($tracker, $fieldIdsToRemove = [], $fieldIdsToShow = []) { if (empty($fieldIdsToShow) == false) { $newTracker = []; foreach ($tracker as $key => $item) { $newTracker[$key] = []; foreach ($fieldIdsToShow as $fieldIdToShow) { $newTracker[$key][$fieldIdToShow] = $tracker[$key][$fieldIdToShow]; } } return $newTracker; } if (empty($fieldIdsToRemove) == false) { foreach ($tracker as $key => $item) { foreach ($fieldIdsToRemove as $fieldIdToRemove) { unset($tracker[$key][$fieldIdToRemove]); } } } return $tracker; } /** * Joins tracker arrays together. * */ public static function join_trackers($trackerLeft, $trackerRight, $fieldLeftId, $joinType) { $joinedTracker = []; switch ($joinType) { case "outer": foreach ($trackerRight as $key => $itemRight) { $match = false; foreach ($trackerLeft as $itemLeft) { if ($key == $itemLeft[$fieldLeftId]) { $match = true; $joinedTracker[$key] = $itemLeft + $itemRight; } else { $joinedTracker[$key] = $itemLeft; } } if ($match == false) { $joinedTracker[$key] = $itemRight; } } break; default: foreach ($trackerLeft as $key => $itemLeft) { if (isset($trackerRight[$itemLeft[$fieldLeftId]]) == true) { $joinedTracker[$key] = $itemLeft + $trackerRight[$itemLeft[$fieldLeftId]]; } else { $joinedTracker[$key] = $itemLeft; } } } return $joinedTracker; } public static function to_csv($array, $header = false, $col_sep = ",", $row_sep = "\n", $qut = '"', $fileName = 'file.csv') { header("Content-type: application/csv"); header("Content-Disposition: attachment; filename=" . $fileName); header("Pragma: no-cache"); header("Expires: 0"); if (! is_array($array)) { return false; } $output = ''; //Header row. if ($header == true) { foreach ($array[0] as $key => $val) { //Escaping quotes. $key = str_replace($qut, "$qut$qut", $key); $output .= "$col_sep$qut$key$qut"; } $output = substr($output, 1) . "\n"; } $cellKeys = []; $cellKeysSet = false; foreach ($array as $key => $val) { $tmp = ''; if ($cellKeysSet == false) { foreach ($val as $cell_key => $cell_val) { $cellKeys[] = $cell_key; } $cellKeysSet = true; } foreach ($cellKeys as $cellKey) { //Escaping quotes. if (is_array($val[$cellKey]) == true) { $val[$cellKey] = implode(" ", $val[$cellKey]); } $cell_val = str_replace("\n", " ", $val[$cellKey]); $cell_val = str_replace($qut, "$qut$qut", $cell_val); $tmp .= "$col_sep$qut$cell_val$qut"; } $output .= substr($tmp, 1) . $row_sep; } return $output; } /** * Programmatic and simplified way of replacing or updating a tracker item, meant for api ease and accessibility * Does not check permissions * * @param array $data example array(fieldId=>'value', fieldId=>'value') or array('fieldName'=>'value', 'fieldName'=>'value') * @return int $itemId */ public function replaceItem($data = []) { $itemData = []; $fields = TikiLib::lib("trk")->list_tracker_fields($this->trackerId()); for ($i = 0, $fieldCount = count($fields['data']); $i < $fieldCount; $i++) { if ($this->byName == true) { $fields['data'][$i]['value'] = $data[$fields['data'][$i]['name']]; } else { $fields['data'][$i]['value'] = $data[$fields['data'][$i]['fieldId']]; } } $itemId = TikiLib::lib("trk")->replace_item($this->trackerId(), $this->itemId, $fields); return $itemId; } /** * Get inputs for tracker item, useful for building interface for interacting with trackers * * @param int $itemId, 0 for new item * @param bool $includeJs injects header js for item into field value * @return array $fields array of fields just like that found in query, but the value of each field being the input */ private function getInputsForItem($itemId = 0, $includeJs = true) { $headerlib = TikiLib::lib("header"); $itemId = (int)$itemId; if ($includeJs == true) { $headerlibClone = clone $headerlib; } $trackerId = $this->trackerId(); if ($trackerId < 1) { return []; } $trackerDefinition = Tracker_Definition::get($trackerId); $fields = []; $fieldFactory = new Tracker_Field_Factory($trackerDefinition); $itemData = TikiLib::lib("trk")->get_tracker_item($itemId); foreach ($trackerDefinition->getFields() as $field) { $fieldKey = ($this->byName == true ? $field['name'] : $field['fieldId']); if ($includeJs == true) { $headerlib->clear_js(); } $field['ins_id'] = "ins_" . $field['fieldId']; if ($itemId == 0 && isset($this->inputDefaults)) { $field['value'] = $this->inputDefaults[$fieldKey]; } $fieldHandler = $fieldFactory->getHandler($field, $itemData); $fieldInput = $fieldHandler->renderInput(); if ($includeJs == true) { $fieldInput = $fieldInput . $headerlib->output_js(); } $fields[$fieldKey] = $fieldInput; } if ($includeJs == true) { //restore the header to the way it was originally $headerlib = $headerlibClone; } return $fields; } /** * Set input defaults, useful when inserting a new item and you want to set the default values * * @param array $defaults, array of defaults, array(array(fieldKey=>defaultValue),array(fieldKey=>defaultValue)) * @return $this for chainability */ public function inputDefaults($defaults = []) { $this->inputDefaults = $defaults; return $this; } /** * A set of tracker items with inputs * * @param bool $includeJs, default = false * @return $this for chainability */ public function queryInputs($includeJs = false) { if ($this->canEdit() == false) { return []; } $query = $this->query(); $items = []; foreach ($query as $itemId => $item) { $items[] = $this->getInputsForItem($itemId, $includeJs); } return $items; } /** * A single tracker item with inputs * * @param bool $includeJs, default = false * @return $this for chainability */ public function queryInput($includeJs = false) { return $this->getInputsForItem($this->itemId, $includeJs); } /** * Delete a tracker item * * @param bool $bulkMode, default = false * @return $this for chainability */ public function delete($bulkMode = false) { $trklib = TikiLib::lib('trk'); if ($this->canDelete()) { $results = $this->query(); foreach ($results as $itemId => $result) { $trklib->remove_tracker_item($itemId, $bulkMode); } } } }