model.php 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885
  1. <?php
  2. /**
  3. * The model file of search module of ZenTaoPMS.
  4. *
  5. * @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.cnezsoft.com)
  6. * @license ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
  7. * @author Chunsheng Wang <chunsheng@cnezsoft.com>
  8. * @package search
  9. * @version $Id: model.php 5082 2013-07-10 01:14:45Z wyd621@gmail.com $
  10. * @link https://www.zentao.net
  11. */
  12. ?>
  13. <?php
  14. class searchModel extends model
  15. {
  16. /**
  17. * 构造搜索表单时调用被搜索模块的方法处理搜索参数。
  18. * Call the method of the searched module to process search parameters when constructing the search form.
  19. *
  20. * @param string $module
  21. * @param bool $cacheSearchFunc // 是否缓存构造搜索参数的方法。默认不缓存以加载真实值。Wheater to cache the method of constructing search parameters. Default is not to cache to load real values.
  22. * @access public
  23. * @return array
  24. */
  25. public function processSearchParams($module, $cacheSearchFunc = false)
  26. {
  27. $cacheKey = $module . 'SearchFunc';
  28. $funcModel = $this->session->$cacheKey['funcModel'] ?? '';
  29. $funcName = $this->session->$cacheKey['funcName'] ?? '';
  30. $funcArgs = $this->session->$cacheKey['funcArgs'] ?? [];
  31. if(!$funcModel || !$funcName || !$funcArgs) return $_SESSION[$module . 'searchParams'] ?? [];
  32. $funcArgs['cacheSearchFunc'] = $cacheSearchFunc;
  33. return $this->loadModel($funcModel)->$funcName(...array_values($funcArgs)); // PHP 8.0以下只能展开索引数组。PHP 8.0 below can only unpack indexed arrays.
  34. }
  35. /**
  36. * 设置搜索参数的session。
  37. * Set search params to session.
  38. *
  39. * @param array $searchConfig
  40. * @access public
  41. * @return void
  42. */
  43. public function setSearchParams(&$searchConfig)
  44. {
  45. $module = $searchConfig['module'];
  46. if($this->config->edition != 'open') $searchConfig = $this->searchTao->processBuildinFields($module, $searchConfig);
  47. $searchParams['module'] = $module;
  48. $searchParams['actionURL'] = $searchConfig['actionURL'];
  49. $searchParams['style'] = zget($searchConfig, 'style', 'full');
  50. $searchParams['onMenuBar'] = zget($searchConfig, 'onMenuBar', 'no');
  51. $searchParams['queryID'] = isset($searchConfig['queryID']) ? $searchConfig['queryID'] : 0;
  52. if(empty($_SESSION[$module . 'SearchFunc']))
  53. {
  54. $searchParams['fields'] = $searchConfig['fields'];
  55. $searchParams['params'] = $searchConfig['params'];
  56. }
  57. $this->session->set($module . 'searchParams', $searchParams);
  58. }
  59. /**
  60. * 设置默认的搜索参数。
  61. * Set default params for selection.
  62. *
  63. * @param string $module
  64. * @param array $fields
  65. * @param array $params
  66. * @access public
  67. * @return array
  68. */
  69. public function setDefaultParams($module, $fields, $params)
  70. {
  71. $fields = array_keys($fields);
  72. list($users, $products, $executions) = $this->getParamValues($module, $fields, $params);
  73. foreach($fields as $fieldName)
  74. {
  75. if(!isset($params[$fieldName])) $params[$fieldName] = array('operator' => '=', 'control' => 'input', 'values' => '');
  76. if($params[$fieldName]['values'] == 'users') $params[$fieldName]['values'] = $users;
  77. if($params[$fieldName]['values'] == 'products') $params[$fieldName]['values'] = $products;
  78. if($params[$fieldName]['values'] == 'executions') $params[$fieldName]['values'] = $executions;
  79. /* 处理数组。*/
  80. /* Process array value. */
  81. if(is_array($params[$fieldName]['values']))
  82. {
  83. /* For build right sql when key is 0 and is not null. e.g. confirmed field. */
  84. if(isset($params[$fieldName]['values'][0]) and $params[$fieldName]['values'][0] !== '')
  85. {
  86. $params[$fieldName]['values'] = array('ZERO' => $params[$fieldName]['values'][0]) + $params[$fieldName]['values'];
  87. unset($params[$fieldName]['values'][0]);
  88. }
  89. elseif(empty($params[$fieldName]['values']))
  90. {
  91. $params[$fieldName]['values'] = array('' => '', 'null' => $this->lang->search->null);
  92. }
  93. elseif(empty($params[$fieldName]['nonull']))
  94. {
  95. $params[$fieldName]['values'] = $params[$fieldName]['values'] + array('null' => $this->lang->search->null);
  96. }
  97. }
  98. }
  99. return $params;
  100. }
  101. /**
  102. * 构建查询语句。
  103. * Build the query to execute.
  104. *
  105. * @access public
  106. * @return void
  107. */
  108. public function buildQuery()
  109. {
  110. /* Init vars. */
  111. $module = $this->post->module;
  112. $searchConfig = $this->processSearchParams($module, true);
  113. $searchFields = $searchConfig['fields'] ?? [] ;
  114. $fieldParams = $searchConfig['params'] ?? [];
  115. $groupItems = $this->config->search->groupItems;
  116. $groupAndOr = strtoupper($this->post->groupAndOr);
  117. if($groupAndOr != 'AND' && $groupAndOr != 'OR') $groupAndOr = 'AND';
  118. $queryForm = $this->searchTao->initSession($module, $searchFields, $fieldParams);
  119. $scoreNum = 0;
  120. $where = '';
  121. for($i = 1; $i <= $groupItems * 2; $i ++)
  122. {
  123. /* The and or between two groups. */
  124. $formIndex = $i - 1;
  125. if($i == 1) $where .= '(( 1 = 1 ';
  126. if($i == $groupItems + 1) $where .= " ) $groupAndOr ( 1 = 1 ";
  127. /* Set var names. */
  128. $fieldName = "field$i";
  129. $andOrName = "andOr$i";
  130. $operatorName = "operator$i";
  131. $valueName = "value$i";
  132. $field = $this->post->$fieldName;
  133. $value = $this->post->$valueName;
  134. $fieldControl = $fieldParams[$field]['control'] ?? '';
  135. if(empty($field) || $value === '' || $value === false) continue; // false means no exist this post item. '' means no search data. ignore it.
  136. if(!preg_match('/^[a-zA-Z0-9]+$/', $field)) continue; // Fix sql injection.
  137. /* 如果是输入框,并且输入框的值为'0',或者 id 的值为'0',将值设置为zero。*/
  138. if($fieldControl == 'input' && $value === '0') $this->post->set($valueName, 'ZERO');
  139. if($field == 'id' && $value === '0') $this->post->set($valueName, 'ZERO');
  140. /* set queryForm. */
  141. list($andOr, $operator, $value) = $this->searchTao->processQueryFormDatas($fieldParams, $field, $andOrName, $operatorName, $valueName);
  142. $queryForm[$formIndex] = array('field' => $field, 'andOr' => strtolower($andOr), 'operator' => $operator, 'value' => $value);
  143. /* Set where. */
  144. $where = $this->searchTao->setWhere($where, $field, $operator, $value, $andOr, $fieldControl);
  145. $scoreNum += 1;
  146. }
  147. foreach($queryForm as $index => $queryField)
  148. {
  149. if(!empty($queryForm[$index]['groupAndOr'])) $queryForm[$index]['groupAndOr'] = strtolower($groupAndOr);
  150. }
  151. $where .=" ))";
  152. $where = $this->searchTao->replaceDynamic($where);
  153. /* Save to session. */
  154. $querySessionName = $this->post->module . 'Query';
  155. $formSessionName = $this->post->module . 'Form';
  156. $this->session->set($querySessionName, $where);
  157. $this->session->set($formSessionName, $queryForm);
  158. if($scoreNum > 2 && !dao::isError()) $this->loadModel('score')->create('search', 'saveQueryAdvanced');
  159. }
  160. /**
  161. * Build the query to execute.
  162. *
  163. * @access public
  164. * @return void
  165. */
  166. public function buildOldQuery()
  167. {
  168. /* Init vars. */
  169. $where = '';
  170. $groupItems = $this->config->search->groupItems;
  171. $groupAndOr = strtoupper($this->post->groupAndOr);
  172. $module = $this->post->module;
  173. $searchConfig = $this->processSearchParams($module, true);
  174. $fieldParams = $searchConfig['params'] ?? [];
  175. $scoreNum = 0;
  176. if($groupAndOr != 'AND' and $groupAndOr != 'OR') $groupAndOr = 'AND';
  177. for($i = 1; $i <= $groupItems * 2; $i ++)
  178. {
  179. /* The and or between two groups. */
  180. if($i == 1) $where .= '(( 1 = 1 ';
  181. if($i == $groupItems + 1) $where .= " ) $groupAndOr ( 1 = 1 ";
  182. /* Set var names. */
  183. $fieldName = "field$i";
  184. $andOrName = "andOr$i";
  185. $operatorName = "operator$i";
  186. $valueName = "value$i";
  187. /* Fix bug #2704. */
  188. $field = $this->post->$fieldName;
  189. if(!preg_match('/^[a-zA-Z0-9]+$/', $field)) continue; // Fix sql injection.
  190. $fieldControl = $fieldParams[$field]['control'] ?? '';
  191. if($fieldControl == 'input' and $this->post->$valueName === '0') $this->post->set($valueName, 'ZERO');
  192. if($field == 'id' and $this->post->$valueName === '0') $this->post->set($valueName, 'ZERO');
  193. /* Skip empty values. */
  194. if($this->post->$valueName == false) continue;
  195. if($this->post->$valueName == 'ZERO') $this->post->$valueName = 0; // ZERO is special, stands to 0.
  196. if($fieldControl == 'select' and $this->post->$valueName === 'null') $this->post->set($valueName, ''); // Null is special, stands to empty if control is select. Fix bug #3279.
  197. $scoreNum += 1;
  198. /* Set and or. */
  199. $andOr = strtoupper($this->post->$andOrName);
  200. if($andOr != 'AND' and $andOr != 'OR') $andOr = 'AND';
  201. /* Set operator. */
  202. $value = addcslashes(trim($this->post->$valueName), '%');
  203. $operator = $this->post->$operatorName;
  204. if(!isset($this->lang->search->operators[$operator])) $operator = '=';
  205. /* Set condition. */
  206. $condition = '';
  207. if($operator == "include")
  208. {
  209. if($this->post->$fieldName == 'module')
  210. {
  211. $allModules = $this->loadModel('tree')->getAllChildId($value);
  212. if($allModules) $condition = helper::dbIN($allModules);
  213. }
  214. else
  215. {
  216. $condition = $fieldControl == 'select' ? " LIKE CONCAT('%,', '{$value}', ',%')" : ' LIKE ' . $this->dbh->quote("%$value%");
  217. }
  218. }
  219. elseif($operator == "notinclude")
  220. {
  221. if($this->post->$fieldName == 'module')
  222. {
  223. $allModules = $this->loadModel('tree')->getAllChildId($value);
  224. if($allModules) $condition = " NOT " . helper::dbIN($allModules);
  225. }
  226. else
  227. {
  228. $condition = $fieldControl == 'select' ? " NOT LIKE CONCAT('%,', '{$value}', ',%')" : ' NOT LIKE ' . $this->dbh->quote("%$value%");
  229. }
  230. }
  231. elseif($operator == 'belong')
  232. {
  233. if($this->post->$fieldName == 'module')
  234. {
  235. $allModules = $this->loadModel('tree')->getAllChildId($value);
  236. if($allModules) $condition = helper::dbIN($allModules);
  237. }
  238. elseif($this->post->$fieldName == 'dept')
  239. {
  240. $allDepts = $this->loadModel('dept')->getAllChildId($value);
  241. $condition = helper::dbIN($allDepts);
  242. }
  243. else
  244. {
  245. $condition = ' = ' . $this->dbh->quote($value) . ' ';
  246. }
  247. }
  248. else
  249. {
  250. if($operator == 'between' and !isset($this->config->search->dynamic[$value])) $operator = '=';
  251. $condition = $operator . ' ' . $this->dbh->quote($value) . ' ';
  252. if($operator == '=' and $this->post->$fieldName == 'id' and preg_match('/^[0-9]+(,[0-9]*)+$/', $value) and !preg_match('/[\x7f-\xff]+/', $value))
  253. {
  254. $values = array_filter(explode(',', trim($this->dbh->quote($value), "'")));
  255. foreach($values as $value) $value = "'" . $value . "'";
  256. $value = implode(',', $values);
  257. $operator = 'IN';
  258. $condition = $operator . ' (' . $value . ') ';
  259. }
  260. }
  261. /* Processing query criteria. */
  262. if($operator == '=' and preg_match('/^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$/', $value))
  263. {
  264. $condition = '`' . $this->post->$fieldName . "` >= '$value' AND `" . $this->post->$fieldName . "` <= '$value 23:59:59'";
  265. $where .= " $andOr ($condition)";
  266. }
  267. elseif($operator == '!=' and preg_match('/^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$/', $value))
  268. {
  269. $condition = '`' . $this->post->$fieldName . "` < '$value' OR `" . $this->post->$fieldName . "` > '$value 23:59:59'";
  270. $where .= " $andOr ($condition)";
  271. }
  272. elseif($operator == '<=' and preg_match('/^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$/', $value))
  273. {
  274. $where .= " $andOr " . '`' . $this->post->$fieldName . "` <= '$value 23:59:59'";
  275. }
  276. elseif($operator == '>' and preg_match('/^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$/', $value))
  277. {
  278. $where .= " $andOr " . '`' . $this->post->$fieldName . "` > '$value 23:59:59'";
  279. }
  280. elseif(in_array($operator, array('include', 'notinclude')) && $fieldControl == 'select')
  281. {
  282. $where .= " $andOr CONCAT(',', `{$this->post->$fieldName}`, ',') {$condition}";
  283. }
  284. elseif($condition)
  285. {
  286. $where .= " $andOr " . '`' . $this->post->$fieldName . '` ' . $condition;
  287. }
  288. }
  289. $where .=" ))";
  290. $where = $this->replaceDynamic($where);
  291. /* Save to session. */
  292. $querySessionName = $this->post->module . 'Query';
  293. $formSessionName = $this->post->module . 'Form';
  294. $this->session->set($querySessionName, $where);
  295. $this->session->set($formSessionName, $_POST);
  296. if($scoreNum > 2 && !dao::isError()) $this->loadModel('score')->create('search', 'saveQueryAdvanced');
  297. }
  298. /**
  299. * 转换 queryForm 格式以适应 buildQuery 的检查。
  300. * Convert queryForm to fit the buildQuery check.
  301. *
  302. * @param array $queryForm
  303. * @access public
  304. * @return array
  305. */
  306. public function convertQueryForm($queryForm)
  307. {
  308. if(isset($queryForm['field1'])) return $queryForm;
  309. $convertedForm = array();
  310. foreach($queryForm as $i => $formItem)
  311. {
  312. $i++;
  313. if(isset($formItem['groupAndOr']))
  314. {
  315. $convertedForm['groupAndOr'] = $formItem['groupAndOr'];
  316. }
  317. elseif(isset($formItem['field']))
  318. {
  319. $convertedForm['field' . $i] = $formItem['field'];
  320. $convertedForm['andOr' . $i] = $formItem['andOr'];
  321. $convertedForm['operator' . $i] = $formItem['operator'];
  322. $convertedForm['value' . $i] = $formItem['value'];
  323. }
  324. }
  325. return $convertedForm;
  326. }
  327. /**
  328. * 获取查询。
  329. * Get a query.
  330. *
  331. * @param int $queryID
  332. * @access public
  333. * @return object|bool
  334. */
  335. public function getQuery($queryID)
  336. {
  337. $queryID = (int)$queryID;
  338. $query = $this->dao->findByID($queryID)->from(TABLE_USERQUERY)->fetch();
  339. if(!$query) return false;
  340. if(in_array($query->module, $this->config->search->oldQuery)) return $this->getOldQuery($queryID);
  341. /* Decode html encode. */
  342. $query->form = htmlspecialchars_decode($query->form, ENT_QUOTES);
  343. /* 如果搜索表单中值有变量,把表单值放到post 表单,重新生成 query。*/
  344. /* If form has variable, regenerate query. */
  345. $hasDynamic = strpos($query->form, '$') !== false;
  346. $query->form = unserialize($query->form);
  347. if($hasDynamic)
  348. {
  349. $_POST = $this->convertQueryForm($query->form);
  350. $_POST['module'] = $query->module;
  351. $this->buildQuery();
  352. $querySessionName = $query->module . 'Query';
  353. $query->sql = $_SESSION[$querySessionName];
  354. }
  355. /* 将queryform[filed1] 转换为 queryform[1]['field']。*/
  356. /* Process queryform[filed1] to queryform[1]['field']. */
  357. $queryForm = array();
  358. if(isset($query->form['field1']))
  359. {
  360. foreach($query->form as $field => $value)
  361. {
  362. $index = substr($field, -1);
  363. if(is_numeric($index))
  364. {
  365. $field = substr($field, 0, strlen($field) - 1);
  366. $queryForm[$index][$field] = $value;
  367. }
  368. elseif($field == 'groupAndOr')
  369. {
  370. $queryForm[$field][$field] = $value;
  371. }
  372. }
  373. $query->form = array_values($queryForm);
  374. }
  375. return $query;
  376. }
  377. /**
  378. * Get a query.
  379. *
  380. * @param int $queryID
  381. * @access public
  382. * @return object
  383. */
  384. public function getOldQuery($queryID)
  385. {
  386. $query = $this->dao->findByID($queryID)->from(TABLE_USERQUERY)->fetch();
  387. if(!$query) return false;
  388. /* Decode html encode. */
  389. $query->form = htmlspecialchars_decode($query->form, ENT_QUOTES);
  390. $query->sql = htmlspecialchars_decode($query->sql, ENT_QUOTES);
  391. $hasDynamic = strpos($query->form, '$') !== false;
  392. $query->form = unserialize($query->form);
  393. if($hasDynamic)
  394. {
  395. $_POST = $query->form;
  396. $this->buildQuery();
  397. $querySessionName = $query->form['module'] . 'Query';
  398. $query->sql = $_SESSION[$querySessionName];
  399. }
  400. return $query;
  401. }
  402. /**
  403. * 将搜索条件保存到session中。
  404. * Save query to session.
  405. *
  406. * @param string $module
  407. * @param int $queryID
  408. * @access public
  409. * @return string
  410. */
  411. public function setQuery($module, $queryID = 0)
  412. {
  413. $querySessionName = $module . 'Query';
  414. $formSessionName = $module . 'Form';
  415. $queryID = (int)$queryID;
  416. if($queryID)
  417. {
  418. $query = $this->getQuery($queryID);
  419. if($query)
  420. {
  421. $this->session->set($querySessionName, $query->sql);
  422. $this->session->set($formSessionName, $query->form);
  423. }
  424. else
  425. {
  426. $this->session->set($querySessionName, ' 1 = 1');
  427. }
  428. }
  429. else
  430. {
  431. if($this->session->$querySessionName == false) $this->session->set($querySessionName, ' 1 = 1');
  432. }
  433. return $this->session->$querySessionName;
  434. }
  435. /**
  436. * 获取一个查询。
  437. * Get a query.
  438. *
  439. * @param int $queryID
  440. * @access public
  441. * @return object|false
  442. */
  443. public function getByID($queryID)
  444. {
  445. return $this->dao->findByID($queryID)->from(TABLE_USERQUERY)->fetch();
  446. }
  447. /**
  448. * 保存当前的查询。
  449. * Save current query to db.
  450. *
  451. * @access public
  452. * @return void
  453. */
  454. public function saveQuery()
  455. {
  456. $sqlVar = $this->post->module . 'Query';
  457. $formVar = $this->post->module . 'Form';
  458. $sql = $_SESSION[$sqlVar];
  459. if(!$sql) $sql = ' 1 = 1 ';
  460. $query = fixer::input('post')
  461. ->add('account', $this->app->user->account)
  462. ->add('form', serialize($_SESSION[$formVar]))
  463. ->add('sql', $sql)
  464. ->skipSpecial('sql,form')
  465. ->remove('onMenuBar')
  466. ->get();
  467. if($this->post->onMenuBar) $query->shortcut = '1';
  468. $this->dao->insert(TABLE_USERQUERY)->data($query)->autoCheck()->check('title', 'notempty')->exec();
  469. if(dao::isError()) return false;
  470. $queryID = $this->dao->lastInsertID();
  471. $this->loadModel('score')->create('search', 'saveQuery', $queryID);
  472. return $queryID;
  473. }
  474. /**
  475. * 删除保存的查询。
  476. * Delete current query from db.
  477. *
  478. * @param int $queryID
  479. * @access public
  480. * @return bool
  481. */
  482. public function deleteQuery($queryID)
  483. {
  484. $this->dao->delete()->from(TABLE_USERQUERY)->where('id')->eq($queryID)->andWhere('account')->eq($this->app->user->account)->exec();
  485. return !dao::isError();
  486. }
  487. /**
  488. * 获取id,title 的键值对。
  489. * Get title => id pairs of a user.
  490. *
  491. * @param string $module
  492. * @access public
  493. * @return array
  494. */
  495. public function getQueryPairs($module)
  496. {
  497. $queries = $this->dao->select('id, title')->from(TABLE_USERQUERY)->where('module')->eq($module)->andWhere('account', true)->eq($this->app->user->account)->orWhere('common')->eq(1)->markRight(1)->orderBy('id_desc')->fetchPairs();
  498. return array('' => $this->lang->search->myQuery) + $queries;
  499. }
  500. /**
  501. * 获取查询列表。
  502. * Get query list.
  503. *
  504. * @param string $module
  505. * @access public
  506. * @return array
  507. */
  508. public function getQueryList($module)
  509. {
  510. return $this->dao->select('id, account, title')
  511. ->from(TABLE_USERQUERY)
  512. ->where('module')->eq($module)
  513. ->andWhere('account', true)->eq($this->app->user->account)
  514. ->orWhere('common')->eq(1)
  515. ->markRight(1)
  516. ->orderBy('id_desc')
  517. ->fetchAll();
  518. }
  519. /**
  520. * 获取可访问的有索引的模块。
  521. * Get counts of keyword search results.
  522. *
  523. * @param string|array $type
  524. * @access public
  525. * @return array
  526. */
  527. public function getListCount($type = 'all')
  528. {
  529. $allowedObjects = $this->searchTao->getAllowedObjects($type);
  530. $filterObjects = array();
  531. foreach($allowedObjects as $index => $object)
  532. {
  533. if(strpos(',feedback,ticket,', ",$object,") === false) continue;
  534. unset($allowedObjects[$index]);
  535. $filterObjects[] = $object;
  536. }
  537. $typeCount = $this->dao->select("objectType, COUNT(1) AS objectCount")->from(TABLE_SEARCHINDEX)
  538. ->where('((vision')->eq($this->config->vision)
  539. ->andWhere('objectType')->in($allowedObjects)
  540. ->markRight(1)
  541. ->orWhere('(objectType')->in($filterObjects)
  542. ->markRight(2)
  543. ->andWhere('addedDate')->le(helper::now())
  544. ->groupBy('objectType')
  545. ->fetchPairs();
  546. arsort($typeCount);
  547. return $typeCount;
  548. }
  549. /**
  550. * 获取搜索结果。
  551. * get search results of keywords.
  552. *
  553. * @param string $keywords
  554. * @param string $type
  555. * @param object $pager
  556. * @access public
  557. * @return array
  558. */
  559. public function getList($keywords, $type, $pager = null)
  560. {
  561. $filter = $this->app->loadClass('sqlfilter');
  562. $keywords = $filter->getRecommendedFilter($keywords, 'medium');
  563. list($words, $againstCond, $likeCondition) = $this->searchTao->getSqlParams($keywords);
  564. $allowedObjects = $this->searchTao->getAllowedObjects($type);
  565. $filterObjects = array();
  566. foreach($allowedObjects as $index => $object)
  567. {
  568. if(strpos(',feedback,ticket,', ",$object,") === false) continue;
  569. unset($allowedObjects[$index]);
  570. $filterObjects[] = $object;
  571. }
  572. if(in_array($this->config->db->driver, $this->config->pgsqlDriverList))
  573. {
  574. $table = "SELECT *, ts_rank(to_tsvector('pg_catalog.english', coalesce(title, '') || ' ' || coalesce(content, '')), to_tsquery('pg_catalog.english', '{$againstCond}')) AS score FROM " . TABLE_SEARCHINDEX;
  575. $score = '0.02';
  576. }
  577. elseif($this->config->db->driver == 'dm')
  578. {
  579. $spliter = $this->app->loadClass('spliter');
  580. $labels = array('_', '、', ' ', '-', '\n', '?', '@', '&', '%', '~', '`', '+', '*', '/', '\\', '。', ',');
  581. $words = str_replace($labels, ' ', $words);
  582. $words = explode(' ', trim($words));
  583. $conditions = [];
  584. foreach($words as $word)
  585. {
  586. $trimmedWord = trim($word);
  587. $conditions[] = "title LIKE '%" . $trimmedWord . "%' OR content LIKE '%" . $trimmedWord . "%'";
  588. }
  589. $table = "SELECT *, (CASE WHEN (" . implode(' OR ', $conditions) . ") THEN 1 ELSE 0 END) AS score FROM " . TABLE_SEARCHINDEX;
  590. $score = '1';
  591. }
  592. else
  593. {
  594. $table = "SELECT *, (MATCH(title, content) AGAINST('{$againstCond}' IN BOOLEAN MODE)) AS score FROM " . TABLE_SEARCHINDEX;
  595. $score = '1';
  596. }
  597. $stmt = $this->dao->select('*')->from("({$table})")->alias('t1')
  598. ->where("(score >= {$score} {$likeCondition})")
  599. ->andWhere('((vision')->eq($this->config->vision)
  600. ->andWhere('objectType')->in($allowedObjects)
  601. ->markRight(1)
  602. ->orWhere('(objectType')->in($filterObjects)
  603. ->markRight(2)
  604. ->andWhere('addedDate')->le(helper::now())
  605. ->orderBy('score_desc, editedDate_desc')
  606. ->query();
  607. $results = array();
  608. $idListGroup = array();
  609. while($record = $stmt->fetch())
  610. {
  611. $results[$record->id] = $record;
  612. $module = $record->objectType == 'case' ? 'testcase' : $record->objectType;
  613. $idListGroup[$module][$record->objectID] = $record->objectID;
  614. }
  615. $results = $this->searchTao->checkPriv($results, $idListGroup);
  616. if(empty($results)) return $results;
  617. /* Reset pager total and get this page data. */
  618. if($pager) $results = $this->searchTao->setResultsInPage($results, $pager);
  619. $objectList = $this->searchTao->getobjectList($idListGroup);
  620. return $this->processResults($results, $objectList, $words);
  621. }
  622. /**
  623. * 保存一个索引项。
  624. * Save an index item.
  625. *
  626. * @param string $objectType article|blog|page|product|thread|reply|
  627. * @param object $object
  628. * @access public
  629. * @return bool
  630. */
  631. public function saveIndex($objectType, $object)
  632. {
  633. $fields = $this->config->search->fields->{$objectType};
  634. if(empty($fields)) return true;
  635. /* 如果是文档,将文档的内容追加上。*/
  636. /* If the objectType is doc, append the content of doc. */
  637. if($objectType == 'doc' && $this->config->edition != 'open') $object = $this->searchTao->appendFiles($object);
  638. $index = new stdclass();
  639. $index->objectType = $objectType;
  640. $index->objectID = $object->{$fields->id};
  641. $index->title = $object->{$fields->title};
  642. $index->addedDate = !empty($object->{$fields->addedDate}) ? (!helper::isZeroDate($object->{$fields->addedDate}) ? $object->{$fields->addedDate} : NULL) : NULL;
  643. $index->editedDate = !empty($object->{$fields->editedDate}) ? (!helper::isZeroDate($object->{$fields->editedDate}) ? $object->{$fields->editedDate} : NULL) : NULL;
  644. $index->vision = isset($object->vision) ? $object->vision : 'rnd';
  645. $index->content = '';
  646. $contentFields = explode(',', $fields->content . ',comment');
  647. foreach($contentFields as $field)
  648. {
  649. if(empty($field)) continue;
  650. $index->content .= $object->$field;
  651. }
  652. $spliter = $this->app->loadClass('spliter');
  653. $titleSplited = $spliter->utf8Split($index->title);
  654. $contentSplited = $spliter->utf8Split(strip_tags($index->content));
  655. $index->title = $titleSplited['words'];
  656. $index->content = $contentSplited['words'];
  657. $this->saveDict($titleSplited['dict'] + $contentSplited['dict']);
  658. $this->dao->delete()->from(TABLE_SEARCHINDEX)->where('objectType')->eq($index->objectType)->andWhere('objectID')->eq($index->objectID)->exec();
  659. $this->dao->insert(TABLE_SEARCHINDEX)->data($index)->exec();
  660. return !dao::isError();
  661. }
  662. /**
  663. * 保存搜索字典。
  664. * Save dict info.
  665. *
  666. * @param array $dict
  667. * @access public
  668. * @return bool
  669. */
  670. public function saveDict($dict)
  671. {
  672. static $savedDict;
  673. if(empty($savedDict)) $savedDict = $this->dao->select("`key`")->from(TABLE_SEARCHDICT)->fetchPairs();
  674. foreach($dict as $key => $value)
  675. {
  676. if(!is_numeric($key) || empty($value) || strlen($key) != 5 || $key < 0 || $key > 65535) continue;
  677. if(isset($savedDict[$key])) continue;
  678. $this->dao->replace(TABLE_SEARCHDICT)->data(array('key' => $key, 'value' => $value))->exec();
  679. $savedDict[$key] = $key;
  680. }
  681. return !dao::isError();
  682. }
  683. /**
  684. * 构建索引查询。
  685. * Build index query.
  686. *
  687. * @param string $type
  688. * @param bool $testDelete
  689. * @access public
  690. * @return object
  691. * @param bool $testDeleted
  692. */
  693. public function buildIndexQuery($type, $testDeleted = true)
  694. {
  695. $table = $this->config->objectTables[$type];
  696. if($type == 'story' || $type == 'requirement' || $type == 'epic')
  697. {
  698. $query = $this->dao->select('DISTINCT t1.*, t2.spec, t2.verify')->from($table)->alias('t1')
  699. ->leftJoin(TABLE_STORYSPEC)->alias('t2')->on('t1.id=t2.story')
  700. ->where('t1.deleted')->eq('0')
  701. ->andWhere('type')->eq($type)
  702. ->andWhere('t1.version=t2.version');
  703. }
  704. elseif($type == 'doc')
  705. {
  706. $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');
  707. }
  708. else
  709. {
  710. $data = '';
  711. if($testDeleted) $data = $this->dao->select('*')->from($table)->limit(1)->fetch();
  712. $query = $this->dao->select('t1.*')->from($table)->alias('t1')
  713. ->where('1=1')
  714. ->beginIF($type == 'program')->andWhere('type')->eq('program')->fi()
  715. ->beginIF($type == 'project')->andWhere('type')->eq('project')->fi()
  716. ->beginIF($type == 'execution')->andWhere('type')->in('stage,sprint,kanban')->fi()
  717. ->beginIF(isset($data->deleted))->andWhere('t1.deleted')->eq('0')->fi();
  718. }
  719. return $query;
  720. }
  721. /**
  722. * 构建索引。
  723. * Build all search index.
  724. *
  725. * @param string $type
  726. * @param int $lastID
  727. * @access public
  728. * @return array
  729. */
  730. public function buildAllIndex($type = '', $lastID = 0)
  731. {
  732. /* 如果类型是空的,获取第一个类型,从第一个类型开始创建索引。*/
  733. /* If the type is empty, get the first type and create index starting from the first type. */
  734. if(empty($type))
  735. {
  736. $this->dao->delete()->from(TABLE_SEARCHINDEX)->exec();
  737. $this->dao->delete()->from(TABLE_SEARCHDICT)->exec();
  738. try
  739. {
  740. $this->dbh->exec('ALTER TABLE ' . TABLE_SEARCHINDEX . ' auto_increment=1');
  741. }
  742. catch(Exception $e){}
  743. $type = key((array)$this->config->search->fields);
  744. }
  745. $limit = 100;
  746. $nextObject = false;
  747. /* 获取某些字段值并且将其保存到索引。*/
  748. /* Get some field value and save it to index. */
  749. foreach($this->config->search->fields as $module => $field)
  750. {
  751. if($module != $type && !$nextObject) continue;
  752. if(!isset($this->config->objectTables[$module])) continue;
  753. if($module == $type) $nextObject = true;
  754. while(true)
  755. {
  756. $query = $this->buildIndexQuery($module);
  757. $dataList = $query->beginIF($lastID)->andWhere('t1.id')->gt($lastID)->fi()->orderBy('t1.id')->limit($limit)->fetchAll('id', false);
  758. if(empty($dataList))
  759. {
  760. $lastID = 0;
  761. break;
  762. }
  763. $dataList = $this->searchTao->processDataList($module, $field, $dataList);
  764. foreach($dataList as $data) $this->saveIndex($module, $data);
  765. return array('type' => $module, 'count' => count($dataList), 'lastID' => max(array_keys($dataList)));
  766. }
  767. }
  768. return array('finished' => true);
  769. }
  770. /**
  771. * 删除索引。
  772. * Delete index of an object.
  773. *
  774. * @param string $objectType
  775. * @param int $objectID
  776. * @access public
  777. * @return bool
  778. */
  779. public function deleteIndex($objectType, $objectID)
  780. {
  781. $this->dao->delete()->from(TABLE_SEARCHINDEX)->where('objectType')->eq($objectType)->andWhere('objectID')->eq($objectID)->exec();
  782. return !dao::isError();
  783. }
  784. }