* @package search * @version $Id: model.php 5082 2013-07-10 01:14:45Z wyd621@gmail.com $ * @link http://www.zentao.net */ ?> config->edition != 'open') { $flowModule = $module; if($module == 'projectStory' || $module == 'executionStory') $flowModule = 'story'; if($module == 'projectBuild' || $module == 'executionBuild') $flowModule = 'build'; if($module == 'projectBug') $flowModule = 'bug'; $buildin = false; $this->app->loadLang('workflow'); $this->app->loadConfig('workflow'); if(!empty($this->config->workflow->buildin)) { foreach($this->config->workflow->buildin->modules as $appName => $appModules) { if(isset($appModules->$flowModule)) { $buildin = true; break; } } } if($buildin) { $fields = $this->loadModel('workflowfield')->getList($flowModule, 'searchOrder, `order`, id'); $maxCount = $this->config->maxCount; $this->config->maxCount = 0; $fieldValues = array(); $formName = $module . 'Form'; if($this->session->$formName) { foreach($this->session->$formName as $formKey => $formField) { if(strpos($formKey, 'field') !== false) { $fieldNO = substr($formKey, 5); $fieldNO = "value" . $fieldNO; $formNameList = $this->session->$formName; $fieldValue = zget($formNameList, $fieldNO, ''); if($fieldValue) $fieldValues[$formField][$fieldValue] = $fieldValue; } } } foreach($fields as $field) { if($field->canSearch == 0 || $field->buildin) continue; if(in_array($field->control, $this->config->workflowfield->optionControls)) { $field->options = $this->workflowfield->getFieldOptions($field, true, zget($fieldValues, $field->field, ''), '', $this->config->flowLimit); } $searchConfig['fields'][$field->field] = $field->name; $searchConfig['params'][$field->field] = $this->loadModel('flow', 'sys')->processSearchParams($field->control, $field->options); } $this->config->maxCount = $maxCount; } } $searchParams['module'] = $searchConfig['module']; $searchParams['searchFields'] = json_encode($searchConfig['fields']); $searchParams['fieldParams'] = json_encode($searchConfig['params']); $searchParams['actionURL'] = $searchConfig['actionURL']; $searchParams['style'] = zget($searchConfig, 'style', 'full'); $searchParams['onMenuBar'] = zget($searchConfig, 'onMenuBar', 'no'); $searchParams['queryID'] = isset($searchConfig['queryID']) ? $searchConfig['queryID'] : 0; $this->session->set($module . 'searchParams', $searchParams); } /** * Build the query to execute. * * @access public * @return void */ public function buildQuery() { /* Init vars. */ $where = ''; $groupItems = $this->config->search->groupItems; $groupAndOr = strtoupper($this->post->groupAndOr); $module = $this->session->searchParams['module']; $searchParams = $module . 'searchParams'; $fieldParams = json_decode($_SESSION[$searchParams]['fieldParams']); $scoreNum = 0; if($groupAndOr != 'AND' and $groupAndOr != 'OR') $groupAndOr = 'AND'; for($i = 1; $i <= $groupItems * 2; $i ++) { /* The and or between two groups. */ if($i == 1) $where .= '(( 1 '; if($i == $groupItems + 1) $where .= " ) $groupAndOr ( 1 "; /* Set var names. */ $fieldName = "field$i"; $andOrName = "andOr$i"; $operatorName = "operator$i"; $valueName = "value$i"; /* Fix bug #2704. */ $field = $this->post->$fieldName; if(isset($fieldParams->$field) and $fieldParams->$field->control == 'input' and $this->post->$valueName === '0') $this->post->$valueName = 'ZERO'; if($field == 'id' and $this->post->$valueName === '0') $this->post->$valueName = 'ZERO'; /* Skip empty values. */ if($this->post->$valueName == false) continue; if($this->post->$valueName == 'ZERO') $this->post->$valueName = 0; // ZERO is special, stands to 0. if(isset($fieldParams->$field) and $fieldParams->$field->control == 'select' and $this->post->$valueName === 'null') $this->post->$valueName = ''; // Null is special, stands to empty if control is select. Fix bug #3279. $scoreNum += 1; /* Set and or. */ $andOr = strtoupper($this->post->$andOrName); if($andOr != 'AND' and $andOr != 'OR') $andOr = 'AND'; /* Set operator. */ $value = addcslashes(trim($this->post->$valueName), '%'); $operator = $this->post->$operatorName; if(!isset($this->lang->search->operators[$operator])) $operator = '='; /* Set condition. */ $condition = ''; if($operator == "include") { if($this->post->$fieldName == 'module') { $allModules = $this->loadModel('tree')->getAllChildId($value); if($allModules) $condition = helper::dbIN($allModules); } else { $condition = ' LIKE ' . $this->dbh->quote("%$value%"); } } elseif($operator == "notinclude") { if($this->post->$fieldName == 'module') { $allModules = $this->loadModel('tree')->getAllChildId($value); if($allModules) $condition = " NOT " . helper::dbIN($allModules); } else { $condition = ' NOT LIKE ' . $this->dbh->quote("%$value%"); } } elseif($operator == 'belong') { if($this->post->$fieldName == 'module') { $allModules = $this->loadModel('tree')->getAllChildId($value); if($allModules) $condition = helper::dbIN($allModules); } elseif($this->post->$fieldName == 'dept') { $allDepts = $this->loadModel('dept')->getAllChildId($value); $condition = helper::dbIN($allDepts); } else { $condition = ' = ' . $this->dbh->quote($value) . ' '; } } else { if($operator == 'between' and !isset($this->config->search->dynamic[$value])) $operator = '='; $condition = $operator . ' ' . $this->dbh->quote($value) . ' '; if($operator == '=' and $this->post->$fieldName == 'id' and preg_match('/^[0-9]+(,[0-9]*)+$/', $value) and !preg_match('/[\x7f-\xff]+/', $value)) { $values = array_filter(explode(',', trim($this->dbh->quote($value), "'"))); foreach($values as $value) $value = "'" . $value . "'"; $value = implode(',', $values); $operator = 'IN'; $condition = $operator . ' (' . $value . ') '; } } /* Processing query criteria. */ if($operator == '=' and preg_match('/^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$/', $value)) { $condition = '`' . $this->post->$fieldName . "` >= '$value' AND `" . $this->post->$fieldName . "` <= '$value 23:59:59'"; $where .= " $andOr ($condition)"; } elseif($operator == '!=' and preg_match('/^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$/', $value)) { $condition = '`' . $this->post->$fieldName . "` < '$value' OR `" . $this->post->$fieldName . "` > '$value 23:59:59'"; $where .= " $andOr ($condition)"; } elseif($operator == '<=' and preg_match('/^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$/', $value)) { $where .= " $andOr " . '`' . $this->post->$fieldName . "` <= '$value 23:59:59'"; } elseif($operator == '>' and preg_match('/^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$/', $value)) { $where .= " $andOr " . '`' . $this->post->$fieldName . "` > '$value 23:59:59'"; } elseif($condition) { $where .= " $andOr " . '`' . $this->post->$fieldName . '` ' . $condition; } } $where .=" ))"; $where = $this->replaceDynamic($where); /* Save to session. */ $querySessionName = $this->post->module . 'Query'; $formSessionName = $this->post->module . 'Form'; $this->session->set($querySessionName, $where); $this->session->set($formSessionName, $_POST); if($scoreNum > 2 && !dao::isError()) $this->loadModel('score')->create('search', 'saveQueryAdvanced'); } /** * Init the search session for the first time search. * * @param string $module * @param array $fields * @param array $fieldParams * @access public * @return void */ public function initSession($module, $fields, $fieldParams) { $formSessionName = $module . 'Form'; if(isset($_SESSION[$formSessionName]) and $_SESSION[$formSessionName] != false) return; for($i = 1; $i <= $this->config->search->groupItems * 2; $i ++) { /* Var names. */ $fieldName = "field$i"; $andOrName = "andOr$i"; $operatorName = "operator$i"; $valueName = "value$i"; $currentField = key($fields); $operator = isset($fieldParams[$currentField]['operator']) ? $fieldParams[$currentField]['operator'] : '='; $queryForm[$fieldName] = key($fields); $queryForm[$andOrName] = 'and'; $queryForm[$operatorName] = $operator; $queryForm[$valueName] = ''; if(!next($fields)) reset($fields); } $queryForm['groupAndOr'] = 'and'; $this->session->set($formSessionName, $queryForm); } /** * Set default params for selection. * * @param array $fields * @param array $params * @access public * @return array */ public function setDefaultParams($fields, $params) { $hasProduct = false; $hasExecution = false; $hasUser = false; $appendUsers = array(); $module = $_SESSION['searchParams']['module']; $formSessionName = $module . 'Form'; if(isset($_SESSION[$formSessionName])) { for($i = 1; $i <= $this->config->search->groupItems; $i ++) { $fieldName = 'field' . $i; $valueName = 'value' . $i; $fieldName = $_SESSION[$formSessionName][$fieldName]; if(isset($params[$fieldName]) and $params[$fieldName]['values'] == 'users') { if($_SESSION[$formSessionName][$valueName]) $appendUsers[] = $_SESSION[$formSessionName][$valueName]; } } } $fields = array_keys($fields); foreach($fields as $fieldName) { if(empty($params[$fieldName])) continue; if($params[$fieldName]['values'] == 'products') $hasProduct = true; if($params[$fieldName]['values'] == 'users') $hasUser = true; if($params[$fieldName]['values'] == 'executions') $hasExecution = true; } if($hasUser) { $users = $this->loadModel('user')->getPairs('realname|noclosed', $appendUsers, $this->config->maxCount); $users['$@me'] = $this->lang->search->me; } if($hasProduct) $products = array('' => '') + $this->loadModel('product')->getPairs('', $this->session->project); if($hasExecution) $executions = array('' => '') + $this->loadModel('execution')->getPairs($this->session->project); foreach($fields as $fieldName) { if(!isset($params[$fieldName])) $params[$fieldName] = array('operator' => '=', 'control' => 'input', 'values' => ''); if($params[$fieldName]['values'] == 'users') { if(!empty($this->config->user->moreLink)) $this->config->moreLinks["field{$fieldName}"] = $this->config->user->moreLink; $params[$fieldName]['values'] = $users; } if($params[$fieldName]['values'] == 'products') $params[$fieldName]['values'] = $products; if($params[$fieldName]['values'] == 'executions') $params[$fieldName]['values'] = $executions; if(is_array($params[$fieldName]['values'])) { /* For build right sql when key is 0 and is not null. e.g. confirmed field. */ if(isset($params[$fieldName]['values'][0]) and $params[$fieldName]['values'][0] !== '') { $params[$fieldName]['values'] = array('ZERO' => $params[$fieldName]['values'][0]) + $params[$fieldName]['values']; unset($params[$fieldName]['values'][0]); } elseif(empty($params[$fieldName]['values'])) { $params[$fieldName]['values'] = array('' => '', 'null' => $this->lang->search->null); } elseif(empty($params[$fieldName]['nonull'])) { $params[$fieldName]['values'] = $params[$fieldName]['values'] + array('null' => $this->lang->search->null); } } } return $params; } /** * Get a query. * * @param int $queryID * @access public * @return object */ public function getQuery($queryID) { $query = $this->dao->findByID($queryID)->from(TABLE_USERQUERY)->fetch(); if(!$query) return false; /* Decode html encode. */ $query->form = htmlspecialchars_decode($query->form, ENT_QUOTES); $query->sql = htmlspecialchars_decode($query->sql, ENT_QUOTES); $hasDynamic = strpos($query->form, '$') !== false; $query->form = unserialize($query->form); if($hasDynamic) { $_POST = $query->form; $this->buildQuery(); $querySessionName = $query->form['module'] . 'Query'; $query->sql = $_SESSION[$querySessionName]; } return $query; } /** * Get a query. * * @param int $queryID * @access public * @return object */ public function getByID($queryID) { $query = $this->dao->findByID($queryID)->from(TABLE_USERQUERY)->fetch(); if(!$query) return false; return $query; } /** * Save current query to db. * * @access public * @return void */ public function saveQuery() { $sqlVar = $this->post->module . 'Query'; $formVar = $this->post->module . 'Form'; $sql = $_SESSION[$sqlVar]; if(!$sql) $sql = ' 1 = 1 '; $query = fixer::input('post') ->add('account', $this->app->user->account) ->add('form', serialize($_SESSION[$formVar])) ->add('sql', $sql) ->skipSpecial('sql,form') ->remove('onMenuBar') ->get(); if($this->post->onMenuBar) $query->shortcut = 1; $this->dao->insert(TABLE_USERQUERY)->data($query)->autoCheck()->check('title', 'notempty')->exec(); if(!dao::isError()) { $queryID = $this->dao->lastInsertID(); if(!dao::isError()) $this->loadModel('score')->create('search', 'saveQuery', $queryID); return $queryID; } return false; } /** * Delete current query from db. * * @param int $queryID * @access public * @return void */ public function deleteQuery($queryID) { $this->dao->delete()->from(TABLE_USERQUERY)->where('id')->eq($queryID)->andWhere('account')->eq($this->app->user->account)->exec(); } /** * Get title => id pairs of a user. * * @param string $module * @access public * @return array */ public function getQueryPairs($module) { $queries = $this->dao->select('id, title') ->from(TABLE_USERQUERY) ->where() ->markLeft(1) ->where('account')->eq($this->app->user->account) ->orWhere('common')->eq(1) ->markRight(1) ->andWhere('module')->eq($module) ->orderBy('id_desc') ->fetchPairs(); if(!$queries) return array('' => $this->lang->search->myQuery); $queries = array('' => $this->lang->search->myQuery) + $queries; return $queries; } /** * Get query list. * * @param string $module * @access public * @return array */ public function getQueryList($module) { $queries = $this->dao->select('id, account, title') ->from(TABLE_USERQUERY) ->where() ->markLeft(1) ->where('account')->eq($this->app->user->account) ->orWhere('common')->eq(1) ->markRight(1) ->andWhere('module')->eq($module) ->orderBy('id_desc') ->fetchAll(); if(!$queries) return array('' => $this->lang->search->myQuery); $queries = array('' => $this->lang->search->myQuery) + $queries; return $queries; } /** * Get records by the condition. * * @param string $module * @param string $moduleIdList * @param string $conditions * @access public * @return array */ public function getBySelect($module, $moduleIdList, $conditions) { if($module == 'story') { $pairs = 'id,title'; $table = TABLE_STORY; } else if($module == 'task') { $pairs = 'id,name'; $table = TABLE_TASK; } $query = '`' . $conditions['field1'] . '`'; $operator = $conditions['operator1']; $value = $conditions['value1']; if(!isset($this->lang->search->operators[$operator])) $operator = '='; if($operator == "include") { $query .= ' LIKE ' . $this->dbh->quote("%$value%"); } elseif($operator == "notinclude") { $where .= ' NOT LIKE ' . $this->dbh->quote("%$value%"); } else { $query .= $operator . ' ' . $this->dbh->quote($value) . ' '; } foreach($moduleIdList as $id) { if(!$id) continue; $title = $this->dao->select($pairs) ->from($table) ->where('id')->eq((int)$id) ->andWhere($query) ->fetch(); if($title) $results[$id] = $title; } if(!isset($results)) return array(); return $this->formatResults($results, $module); } /** * Format the results. * * @param array $results * @param string $module * @access public * @return array */ public function formatResults($results, $module) { /* Get title field. */ $title = ($module == 'story') ? 'title' : 'name'; $resultPairs = array('' => ''); foreach($results as $result) $resultPairs[$result->id] = $result->id . ':' . $result->$title; return $resultPairs; } /** Replace dynamic account and date. * * @param string $query * @access public * @return string */ public function replaceDynamic($query) { $this->app->loadClass('date'); $lastWeek = date::getLastWeek(); $thisWeek = date::getThisWeek(); $lastMonth = date::getLastMonth(); $thisMonth = date::getThisMonth(); $yesterday = date::yesterday(); $today = date(DT_DATE1); if(strpos($query, '$') !== false) { $query = str_replace('$@me', $this->app->user->account, $query); $query = str_replace("'\$lastMonth'", "'" . $lastMonth['begin'] . "' and '" . $lastMonth['end'] . "'", $query); $query = str_replace("'\$thisMonth'", "'" . $thisMonth['begin'] . "' and '" . $thisMonth['end'] . "'", $query); $query = str_replace("'\$lastWeek'", "'" . $lastWeek['begin'] . "' and '" . $lastWeek['end'] . "'", $query); $query = str_replace("'\$thisWeek'", "'" . $thisWeek['begin'] . "' and '" . $thisWeek['end'] . "'", $query); $query = str_replace("'\$yesterday'", "'" . $yesterday . ' 00:00:00' . "' and '" . $yesterday . ' 23:59:59' . "'", $query); $query = str_replace("'\$today'", "'" . $today . ' 00:00:00' . "' and '" . $today . ' 23:59:59' . "'", $query); } return $query; } /** * Get list sql params. * * @param string $keywords * @param string $type * @access protected * @return array */ protected function getSqlParams($keywords, $type) { $spliter = $this->app->loadClass('spliter'); $words = explode(' ', self::unify($keywords, ' ')); $against = ''; $againstCond = ''; foreach($words as $word) { $splitedWords = $spliter->utf8Split($word); $trimedWord = trim($splitedWords['words']); $against .= '"' . $trimedWord . '" '; $againstCond .= '(+"' . $trimedWord . '") '; if(is_numeric($word) and strpos($word, '.') === false and strlen($word) == 5) $againstCond .= "(-\" $word \") "; } $likeCondition = ''; /* Assisted lookup by like condition when only one word. */ if(count($words) == 1 and strpos($words[0], ' ') === false and !is_numeric($words[0])) $likeCondition = "OR title like '%{$trimedWord}%' OR content like '%{$trimedWord}%'"; $words = str_replace('"', '', $against); $words = str_pad($words, 5, '_'); $allowedObject = array(); if($type != 'all') { foreach($type as $module) $allowedObject[] = $module; } else { if($this->config->systemMode == 'light') unset($this->config->search->fields->program); foreach($this->config->search->fields as $objectType => $fields) { $module = $objectType; if($module == 'case') $module = 'testcase'; if(common::hasPriv($module, 'view')) $allowedObject[] = $objectType; if($module == 'caselib' and common::hasPriv('caselib', 'view')) $allowedObject[] = $objectType; if($module == 'deploystep' and common::haspriv('deploy', 'viewstep')) $allowedobject[] = $objectType; } } return array($words, $againstCond, $likeCondition, $allowedObject); } /** * Get counts of keyword search results. * * @param string $keywords * @param string $type * @access public * @return array */ public function getListCount($keywords = '', $type = 'all') { list($words, $againstCond, $likeCondition, $allowedObject) = $this->getSqlParams($keywords, $type); $filterObject = array(); foreach($allowedObject as $index => $object) { if(strpos(',feedback,ticket,', ",$object,") !== false) { unset($allowedObject[$index]); $filterObject[] = $object; } } $typeCount = $this->dao->select("objectType, count(*) as objectCount") ->from(TABLE_SEARCHINDEX) ->where('((vision')->eq($this->config->vision) ->andWhere('objectType')->in($allowedObject) ->markRight(1) ->orWhere('(objectType')->in($filterObject) ->markRight(2) ->andWhere('addedDate')->le(helper::now()) ->groupBy('objectType') ->fetchPairs('objectType', 'objectCount'); arsort($typeCount); return $typeCount; } /** * get search results of keywords. * * @param string $keywords * @param string $type * @param object $pager * @access public * @return array */ public function getList($keywords, $type, $pager = null) { list($words, $againstCond, $likeCondition, $allowedObject) = $this->getSqlParams($keywords, $type); $filterObject = array(); foreach($allowedObject as $index => $object) { if(strpos(',feedback,ticket,', ",$object,") !== false) { unset($allowedObject[$index]); $filterObject[] = $object; } } $scoreColumn = "(MATCH(title, content) AGAINST('{$againstCond}' IN BOOLEAN MODE))"; $stmt = $this->dao->select("*, {$scoreColumn} as score") ->from(TABLE_SEARCHINDEX) ->where("(MATCH(title,content) AGAINST('{$againstCond}' IN BOOLEAN MODE) >= 1 {$likeCondition})") ->andWhere('((vision')->eq($this->config->vision) ->andWhere('objectType')->in($allowedObject) ->markRight(1) ->orWhere('(objectType')->in($filterObject) ->markRight(2) ->andWhere('addedDate')->le(helper::now()) ->orderBy('score_desc, editedDate_desc') ->query(); $idListGroup = array(); $results = array(); while($record = $stmt->fetch()) { $module = $record->objectType == 'case' ? 'testcase' : $record->objectType; $idListGroup[$module][$record->objectID] = $record->objectID; $results[$record->id] = $record; } $results = $this->checkPriv($results, $idListGroup); if(empty($results)) return $results; /* Reset pager total and get this page data. */ $pager->setRecTotal(count($results)); $pager->setPageTotal(); $pager->setPageID($pager->pageID); $results = array_chunk($results, $pager->recPerPage, true); $results = $results[$pager->pageID - 1]; $idListGroup = array(); foreach($results as $record) { $module = $record->objectType == 'case' ? 'testcase' : $record->objectType; $idListGroup[$module][$record->objectID] = $record->objectID; } $objectList = array(); $linkProjectModules = ',task,bug,testcase,build,release,testtask,testsuite,testreport,trainplan,'; foreach($idListGroup as $module => $idList) { if(!isset($this->config->objectTables[$module])) continue; $table = $this->config->objectTables[$module]; $fields = ''; if($module == 'issue') $fields = $this->config->edition == 'max' ? 'id,project,owner,lib' : 'id,project,owner'; if($module == 'project') $fields = 'id,model'; if($module == 'execution')$fields = 'id,type,project'; if($module == 'story' or $module == 'requirement') $fields = $this->config->edition == 'max' ? 'id,type,lib' : 'id,type'; if(($module == 'risk' or $module == 'opportunity') and $this->config->edition == 'max') $fields = 'id,lib'; if($module == 'doc' and $this->config->edition == 'max') $fields = 'id,assetLib,assetLibType'; if(empty($fields)) continue; $objectList[$module] = $this->dao->select($fields)->from($table)->where('id')->in($idList)->fetchAll('id'); } foreach($results as $record) { $record->title = str_replace(' ', '', $this->decode($this->markKeywords($record->title, $words))); $record->title = str_replace('_', '', $record->title); $record->summary = str_replace(' ', '', $this->getSummary($record->content, $words)); $record->summary = str_replace('_', '', $record->summary); $module = $record->objectType == 'case' ? 'testcase' : $record->objectType; $method = 'view'; if($module == 'deploystep') { $module = 'deploy'; $method = 'viewstep'; } if(strpos($linkProjectModules, ",$module,") !== false) { if(!isset($this->config->objectTables[$record->objectType])) continue; $record->url = helper::createLink($module, $method, "id={$record->objectID}"); } elseif($module == 'issue') { $issue = $objectList['issue'][$record->objectID]; if(!empty($issue->lib)) { $module = 'assetlib'; $method = 'issueView'; } $record->url = helper::createLink($module, $method, "id={$record->objectID}", '', false, $issue->project); $record->extraType = empty($issue->owner) ? 'commonIssue' : 'stakeholderIssue'; } elseif($module == 'project') { $projectModel = $objectList['project'][$record->objectID]->model; $method = $projectModel == 'kanban' ? 'index' : 'view'; $record->url = helper::createLink('project', $method, "id={$record->objectID}"); } elseif($module == 'execution') { $execution = $objectList['execution'][$record->objectID]; $method = $execution->type == 'kanban' ? 'kanban' : $method; $record->url = helper::createLink('execution', $method, "id={$record->objectID}"); $record->extraType = empty($execution->type) ? '' : $execution->type; } elseif($module == 'story' or $module == 'requirement') { $story = $objectList[$module][$record->objectID]; $module = 'story'; if(!empty($story->lib)) { $module = 'assetlib'; $method = 'storyView'; } $record->url = helper::createLink($module, $method, "id={$record->objectID}", '', false, 0, true); if($this->config->vision == 'lite') $record->url = helper::createLink('projectstory', $method, "storyID={$record->objectID}", '', false, 0, true); $record->extraType = isset($story->type) ? $story->type : ''; } elseif(($module == 'risk' or $module == 'opportunity') and $this->config->edition == 'max') { $object = $objectList[$module][$record->objectID]; if(!empty($object->lib)) { $method = $module == 'risk' ? 'riskView' : 'opportunityView'; $module = 'assetlib'; } $record->url = helper::createLink($module, $method, "id={$record->objectID}", '', false, 0, true); } elseif($module == 'doc' and $this->config->edition == 'max') { $doc = $objectList['doc'][$record->objectID]; if(!empty($doc->assetLib)) { $module = 'assetlib'; $method = $doc->assetLibType == 'practice' ? 'practiceView' : 'componentView'; } $record->url = helper::createLink($module, $method, "id={$record->objectID}", '', false, 0, true); } else { $record->url = helper::createLink($module, $method, "id={$record->objectID}"); } } return $results; } /** * Save an index item. * * @param string $objectType article|blog|page|product|thread|reply| * @param int $objectID * @access public * @return void */ public function saveIndex($objectType, $object) { $fields = $this->config->search->fields->{$objectType}; if(empty($fields)) return true; if($objectType == 'doc' && $this->config->edition != 'open') $object = $this->appendFiles($object); $index = new stdclass(); $index->objectID = $object->{$fields->id}; $index->objectType = $objectType; $index->title = $object->{$fields->title}; $index->addedDate = isset($object->{$fields->addedDate}) ? $object->{$fields->addedDate} : '0000-00-00 00:00:00'; $index->editedDate = isset($object->{$fields->editedDate}) ? $object->{$fields->editedDate} : '0000-00-00 00:00:00'; $index->vision = isset($object->vision) ? $object->vision : 'rnd'; $index->content = ''; $contentFields = explode(',', $fields->content . ',comment'); foreach($contentFields as $field) { if(empty($field)) continue; $index->content .= $object->$field; } $spliter = $this->app->loadClass('spliter'); $titleSplited = $spliter->utf8Split($index->title); $index->title = $titleSplited['words']; $contentSplited = $spliter->utf8Split(strip_tags($index->content)); $index->content = $contentSplited['words']; $this->saveDict($titleSplited['dict'] + $contentSplited['dict']); $this->dao->replace(TABLE_SEARCHINDEX)->data($index)->exec(); return true; } /** * Save dict info. * * @param array $words * @access public * @return void */ public function saveDict($dict) { static $savedDict; if(empty($savedDict)) $savedDict = $this->dao->select("`key`")->from(TABLE_SEARCHDICT)->fetchPairs('key', 'key'); foreach($dict as $key => $value) { if(!is_numeric($key) or empty($value) or strlen($key) != 5 or $key < 0 or $key > 65535) continue; if(isset($savedDict[$key])) continue; $this->dao->insert(TABLE_SEARCHDICT)->data(array('key' => $key, 'value' => $value))->exec(); $savedDict[$key] = $key; } } /** * Transfer unicode to words. * * @param string $string * @access public * @return void */ public function decode($string) { static $dict; if(empty($dict)) { $dict = $this->dao->select("concat(`key`, ' ') as `key`, value")->from(TABLE_SEARCHDICT)->fetchPairs(); $dict['|'] = ''; } if(strpos($string, ' ') === false) return zget($dict, $string . ' '); return trim(str_replace(array_keys($dict), array_values($dict), $string . ' ')); } /** * Get summary of results. * * @param string $content * @param string $words * @access public * @return string */ public function getSummary($content, $words) { $length = $this->config->search->summaryLength; if(strlen($content) <= $length) return $this->decode($this->markKeywords($content, $words)); $content = $this->markKeywords($content, $words); preg_match_all("/\.*\<\/span\>/U", $content, $matches); if(empty($matches[0])) return $this->decode($this->markKeywords(substr($content, 0, $length), $words)); $matches = $matches[0]; $score = 0; $needle = ''; foreach($matches as $matched) { if(strlen($matched) > $score) { $content = str_replace($needle, strip_tags($needle), $content); $needle = $matched; $score = strlen($matched); } } $content = str_replace('decode($summary); } /** * Check product and project priv. * * @param array $results * @param array $objectPairs * @access public * @return array */ public function checkPriv($results, $objectPairs = array()) { if($this->app->user->admin) return $results; $this->loadModel('doc'); $products = $this->app->user->view->products; $shadowProducts = $this->dao->select('id')->from(TABLE_PRODUCT)->where('shadow')->eq(1)->fetchPairs('id'); $programs = $this->app->user->view->programs; $projects = $this->app->user->view->projects; $executions = $this->app->user->view->sprints; $objectPairs = array(); $total = count($results); if(empty($objectPairs)) { foreach($results as $record) $objectPairs[$record->objectType][$record->objectID] = $record->id; } foreach($objectPairs as $objectType => $objectIdList) { $objectProducts = array(); $objectExecutions = array(); if(!isset($this->config->objectTables[$objectType])) continue; $table = $this->config->objectTables[$objectType]; if(strpos(',bug,case,testcase,productplan,release,story,testtask,', ",$objectType,") !== false) { $objectProducts = $this->dao->select('id,product')->from($table)->where('id')->in(array_keys($objectIdList))->fetchGroup('product', 'id'); } elseif(strpos(',build,task,testreport,', ",$objectType,") !== false) { $objectExecutions = $this->dao->select('id,execution')->from($table)->where('id')->in(array_keys($objectIdList))->fetchGroup('execution', 'id'); } elseif($objectType == 'effort') { $efforts = $this->dao->select('id,product,execution')->from($table)->where('id')->in(array_keys($objectIdList))->fetchAll(); foreach($efforts as $effort) { $objectExecutions[$effort->execution][$effort->id] = $effort; $effortProducts = explode(',', trim($effort->product, ',')); foreach($effortProducts as $effortProduct) $objectProducts[$effortProduct][$effort->id] = $effort; } } elseif($objectType == 'product') { foreach($objectIdList as $productID => $recordID) { if(strpos(",$products,", ",$productID,") === false) unset($results[$recordID]); if(in_array($productID, $shadowProducts)) unset($results[$recordID]); } } elseif($objectType == 'program') { foreach($objectIdList as $programID => $recordID) { if(strpos(",$programs,", ",$programID,") === false) unset($results[$recordID]); } } elseif($objectType == 'project') { foreach($objectIdList as $projectID => $recordID) { if(strpos(",$projects,", ",$projectID,") === false) unset($results[$recordID]); } } elseif($objectType == 'execution') { foreach($objectIdList as $executionID => $recordID) { if(strpos(",$executions,", ",$executionID,") === false) unset($results[$recordID]); } } elseif($objectType == 'doc') { $objectDocs = $this->dao->select('*')->from($table)->where('id')->in(array_keys($objectIdList)) ->andWhere('deleted')->eq(0) ->fetchAll('id'); $privLibs = array(); foreach($objectIdList as $docID => $recordID) { if(!isset($objectDocs[$docID]) or !$this->doc->checkPrivDoc($objectDocs[$docID])) { unset($results[$recordID]); continue; } $objectDoc = $objectDocs[$docID]; $privLibs[$objectDoc->lib] = $objectDoc->lib; } $libs = $this->doc->getLibs('all'); $objectDocLibs = $this->dao->select('id')->from(TABLE_DOCLIB)->where('id')->in($privLibs) ->andWhere('id')->in(array_keys($libs)) ->andWhere('deleted')->eq(0) ->fetchPairs('id', 'id'); foreach($objectDocs as $docID => $doc) { $libID = $doc->lib; if(!isset($objectDocLibs[$libID])) { $recordID = $objectIdList[$docID]; unset($results[$recordID]); } } } elseif($objectType == 'todo') { $objectTodos = $this->dao->select('id')->from($table)->where('id')->in(array_keys($objectIdList))->andWhere("private")->eq(1)->andWhere('account')->ne($this->app->user->account)->fetchPairs('id', 'id'); foreach($objectTodos as $todoID) { if(isset($objectIdList[$todoID])) { $recordID = $objectIdList[$todoID]; unset($results[$recordID]); } } } elseif($objectType == 'testsuite') { $objectSuites = $this->dao->select('id')->from($table)->where('id')->in(array_keys($objectIdList)) ->andWhere("type")->eq('private') ->andWhere('deleted')->eq(0) ->fetchPairs('id', 'id'); foreach($objectSuites as $suiteID) { if(isset($objectIdList[$suiteID])) { $recordID = $objectIdList[$suiteID]; unset($results[$recordID]); } } } elseif(strpos(',feedback,ticket,', ",$objectType,") !== false) { $grantProducts = $this->loadModel('feedback')->getGrantProducts(); $objects = $this->dao->select('*')->from($table)->where('id')->in(array_keys($objectIdList))->fetchAll('id'); foreach($objects as $objectID => $object) { if($objectType == 'feedback' and $object->openedBy == $this->app->user->account) continue; if(isset($grantProducts[$object->product])) continue; if(isset($objectIdList[$objectID])) { $recordID = $objectIdList[$objectID]; unset($results[$recordID]); } } } foreach($objectProducts as $productID => $idList) { if(empty($productID)) continue; if(strpos(",$products,", ",$productID,") === false) { foreach($idList as $object) { $recordID = $objectIdList[$object->id]; unset($results[$recordID]); } } } foreach($objectExecutions as $executionID => $idList) { if(empty($executionID)) continue; if(strpos(",$executions,", ",$executionID,") === false) { foreach($idList as $object) { $recordID = $objectIdList[$object->id]; unset($results[$recordID]); } } } } return $results; } /** * Mark keywords in content. * * @param string $content * @param string $keywords * @access public * @return void */ public function markKeywords($content, $keywords) { $words = explode(' ', trim($keywords, ' ')); $markedWords = array(); $leftMark = '|0000'; $rightMark = '0000|'; foreach($words as $key => $word) { if(preg_match('/^\|[0-9]+\|$/', $word)) { $words[$key] = trim($word, '|'); } elseif(is_numeric($word)) { $words[$key] = $word . ' '; } else { $words[$key] = strlen($word) == 5 ? str_replace('_', '', $word) : $word; } $markedWords[] = $leftMark . $this->decode($word) . $rightMark; } $content = str_replace($words, $markedWords, $content . ' '); $content = str_replace(array($leftMark, $rightMark), array("", " "), $content); $content = str_replace(" ", '', $content); $content = str_replace("", '', $content); return $content; } /** * Build index query. * * @param string $type * @param bool $testDelete * @access public * @return string */ public function buildIndexQuery($type, $testDeleted = true) { $table = $this->config->objectTables[$type]; if($type == 'story' or $type == 'requirement') { $query = $this->dao->select('DISTINCT t1.*,t2.spec,t2.verify')->from($table)->alias('t1') ->leftJoin(TABLE_STORYSPEC)->alias('t2')->on('t1.id=t2.story') ->where('t1.deleted')->eq(0) ->andWhere('type')->eq($type) ->andWhere('t1.version=t2.version'); } elseif($type == 'doc') { $query = $this->dao->select('DISTINCT t1.*,t2.content,t2.digest')->from($table)->alias('t1')->leftJoin(TABLE_DOCCONTENT)->alias('t2')->on('t1.id=t2.doc')->where('t1.deleted')->eq(0)->andWhere('t1.version=t2.version'); } else { $data = ''; if($testDeleted) $data = $this->dao->select('*')->from($table)->limit(1)->fetch(); $query = $this->dao->select('t1.*')->from($table)->alias('t1') ->where('1') ->beginIF($type == 'program')->andWhere('type')->eq('program')->fi() ->beginIF($type == 'project')->andWhere('type')->eq('project')->fi() ->beginIF($type == 'execution')->andWhere('type')->in('stage,sprint,kanban')->fi() ->beginIF(isset($data->deleted))->andWhere('t1.deleted')->eq(0)->fi(); } return $query; } /** * Build all search index. * * @param string $type * @param int $lastID * @access public * @return bool */ public function buildAllIndex($type = '', $lastID = 0) { $limit = 100; $nextObject = false; if(empty($type)) { $this->dao->delete()->from(TABLE_SEARCHINDEX)->exec(); $this->dao->delete()->from(TABLE_SEARCHDICT)->exec(); try { $this->dbh->exec('ALTER TABLE ' . TABLE_SEARCHINDEX . ' auto_increment=1'); } catch(Exception $e){} $type = key($this->config->search->fields); } foreach($this->config->search->fields as $module => $field) { if($module != $type and !$nextObject) continue; if($module == $type) $nextObject = true; if(!isset($this->config->objectTables[$module])) continue; while(true) { $query = $this->buildIndexQuery($module); $dataList = $query->beginIF($lastID)->andWhere('t1.id')->gt($lastID)->fi()->orderBy('t1.id')->limit($limit)->fetchAll('id'); if(empty($dataList)) { $lastID = 0; break; } if($module == 'case') $caseStep = $this->dao->select('*')->from(TABLE_CASESTEP)->where('`case`')->in(array_keys($dataList))->fetchGroup('case', 'id'); $actions = $this->dao->select('*')->from(TABLE_ACTION) ->where('objectType')->eq($module) ->andWhere('objectID')->in(array_keys($dataList)) ->orderBy('date asc') ->fetchGroup('objectID', 'id'); $files = $this->dao->select('id,objectID,title,extension')->from(TABLE_FILE) ->where('objectType')->eq($module) ->andWhere('objectID')->in(array_keys($dataList)) ->orderBy('id asc') ->fetchGroup('objectID', 'id'); foreach($dataList as $id => $data) { $data->comment = ''; if(isset($actions[$id])) { foreach($actions[$id] as $action) { if($action->action == 'opened')$data->{$field->addedDate} = $action->date; $data->{$field->editedDate} = $action->date; if(!empty($action->comment)) $data->comment .= $action->comment . "\n"; } } if(isset($files[$id])) { foreach($files[$id] as $file) { if(!empty($file->title)) $data->comment .= $file->title . '.' . $file->extension . "\n"; } } if($module == 'case') { $data->desc = ''; $data->expect = ''; if(isset($caseStep[$id])) { foreach($caseStep[$id] as $step) { if($step->version != $data->version) continue; $data->desc .= $step->desc . "\n"; $data->expect .= $step->expect . "\n"; } } } } foreach($dataList as $data) $this->saveIndex($module, $data); return array('type' => $module, 'count' => count($dataList), 'lastID' => max(array_keys($dataList))); } } return array('finished' => true); } /** * Delete index of an object. * * @param string $objectType * @param int $objectID * @access public * @return void */ public function deleteIndex($objectType, $objectID) { $this->dao->delete()->from(TABLE_SEARCHINDEX)->where('objectType')->eq($objectType)->andWhere('objectID')->eq($objectID)->exec(); return !dao::isError(); } /** * Unified processing of search keywords. * * @param string $string * @param string $to * @access public * @return void */ public static function unify($string, $to = ',') { $labels = array('_', '、', ' ', '-', '\n', '?', '@', '&', '%', '~', '`', '+', '*', '/', '\\', '。', ','); $string = str_replace($labels, $to, $string); return preg_replace("/[{$to}]+/", $to, trim($string, $to)); } /** * Append document content to the document. * * @param string $object * @access public * @return void */ public function appendFiles($object) { $docFiles = $this->dao->select('files')->from(TABLE_DOCCONTENT)->where('doc')->eq($object->id)->orderBy('version')->limit(1)->fetch('files'); if(empty($docFiles)) return $object; $allDocFiles = $this->loadModel('file')->getByObject('doc', $object->id); if(!isset($object->comment)) $object->comment = ''; foreach($allDocFiles as $file) { if(strpos(",$docFiles,", ",{$file->id},") === false) continue; if(strpos('docx|doc', $file->extension) !== false) { $convertedFile = $this->file->convertOffice($file, 'txt'); if($convertedFile) $object->comment .= substr(file_get_contents($convertedFile), 0, $this->config->search->maxFileSize); } if($file->extension == 'txt') { $object->comment .= substr(file_get_contents($file->realPath), 0, $this->config->search->maxFileSize); } } return $object; } }