Search.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. <?php
  2. /* vim: set expandtab sw=4 ts=4 sts=4: */
  3. /**
  4. * Handles Database Search
  5. *
  6. * @package PhpMyAdmin
  7. */
  8. declare(strict_types=1);
  9. namespace PhpMyAdmin\Database;
  10. use PhpMyAdmin\DatabaseInterface;
  11. use PhpMyAdmin\Template;
  12. use PhpMyAdmin\Util;
  13. /**
  14. * Class to handle database search
  15. *
  16. * @package PhpMyAdmin
  17. */
  18. class Search
  19. {
  20. /**
  21. * Database name
  22. *
  23. * @access private
  24. * @var string
  25. */
  26. private $db;
  27. /**
  28. * Table Names
  29. *
  30. * @access private
  31. * @var array
  32. */
  33. private $tablesNamesOnly;
  34. /**
  35. * Type of search
  36. *
  37. * @access private
  38. * @var array
  39. */
  40. private $searchTypes;
  41. /**
  42. * Already set search type
  43. *
  44. * @access private
  45. * @var integer
  46. */
  47. private $criteriaSearchType;
  48. /**
  49. * Already set search type's description
  50. *
  51. * @access private
  52. * @var string
  53. */
  54. private $searchTypeDescription;
  55. /**
  56. * Search string/regexp
  57. *
  58. * @access private
  59. * @var string
  60. */
  61. private $criteriaSearchString;
  62. /**
  63. * Criteria Tables to search in
  64. *
  65. * @access private
  66. * @var array
  67. */
  68. private $criteriaTables;
  69. /**
  70. * Restrict the search to this column
  71. *
  72. * @access private
  73. * @var string
  74. */
  75. private $criteriaColumnName;
  76. /**
  77. * @var DatabaseInterface
  78. */
  79. private $dbi;
  80. /**
  81. * @var Template
  82. */
  83. public $template;
  84. /**
  85. * Public Constructor
  86. *
  87. * @param DatabaseInterface $dbi DatabaseInterface object
  88. * @param string $db Database name
  89. * @param Template $template Template object
  90. */
  91. public function __construct(DatabaseInterface $dbi, $db, Template $template)
  92. {
  93. $this->db = $db;
  94. $this->dbi = $dbi;
  95. $this->searchTypes = [
  96. '1' => __('at least one of the words'),
  97. '2' => __('all of the words'),
  98. '3' => __('the exact phrase as substring'),
  99. '4' => __('the exact phrase as whole field'),
  100. '5' => __('as regular expression'),
  101. ];
  102. $this->template = $template;
  103. // Sets criteria parameters
  104. $this->setSearchParams();
  105. }
  106. /**
  107. * Sets search parameters
  108. *
  109. * @return void
  110. */
  111. private function setSearchParams()
  112. {
  113. $this->tablesNamesOnly = $this->dbi->getTables($this->db);
  114. if (empty($_POST['criteriaSearchType'])
  115. || ! is_string($_POST['criteriaSearchType'])
  116. || ! array_key_exists(
  117. $_POST['criteriaSearchType'],
  118. $this->searchTypes
  119. )
  120. ) {
  121. $this->criteriaSearchType = 1;
  122. unset($_POST['submit_search']);
  123. } else {
  124. $this->criteriaSearchType = (int) $_POST['criteriaSearchType'];
  125. $this->searchTypeDescription
  126. = $this->searchTypes[$_POST['criteriaSearchType']];
  127. }
  128. if (empty($_POST['criteriaSearchString'])
  129. || ! is_string($_POST['criteriaSearchString'])
  130. ) {
  131. $this->criteriaSearchString = '';
  132. unset($_POST['submit_search']);
  133. } else {
  134. $this->criteriaSearchString = $_POST['criteriaSearchString'];
  135. }
  136. $this->criteriaTables = [];
  137. if (empty($_POST['criteriaTables'])
  138. || ! is_array($_POST['criteriaTables'])
  139. ) {
  140. unset($_POST['submit_search']);
  141. } else {
  142. $this->criteriaTables = array_intersect(
  143. $_POST['criteriaTables'],
  144. $this->tablesNamesOnly
  145. );
  146. }
  147. if (empty($_POST['criteriaColumnName'])
  148. || ! is_string($_POST['criteriaColumnName'])
  149. ) {
  150. unset($this->criteriaColumnName);
  151. } else {
  152. $this->criteriaColumnName = $this->dbi->escapeString(
  153. $_POST['criteriaColumnName']
  154. );
  155. }
  156. }
  157. /**
  158. * Builds the SQL search query
  159. *
  160. * @param string $table The table name
  161. *
  162. * @return array 3 SQL queries (for count, display and delete results)
  163. *
  164. * @todo can we make use of fulltextsearch IN BOOLEAN MODE for this?
  165. * PMA_backquote
  166. * DatabaseInterface::freeResult
  167. * DatabaseInterface::fetchAssoc
  168. * $GLOBALS['db']
  169. * explode
  170. * count
  171. * strlen
  172. */
  173. private function getSearchSqls($table)
  174. {
  175. // Statement types
  176. $sqlstr_select = 'SELECT';
  177. $sqlstr_delete = 'DELETE';
  178. // Table to use
  179. $sqlstr_from = ' FROM '
  180. . Util::backquote($GLOBALS['db']) . '.'
  181. . Util::backquote($table);
  182. // Gets where clause for the query
  183. $where_clause = $this->getWhereClause($table);
  184. // Builds complete queries
  185. $sql = [];
  186. $sql['select_columns'] = $sqlstr_select . ' * ' . $sqlstr_from
  187. . $where_clause;
  188. // here, I think we need to still use the COUNT clause, even for
  189. // VIEWs, anyway we have a WHERE clause that should limit results
  190. $sql['select_count'] = $sqlstr_select . ' COUNT(*) AS `count`'
  191. . $sqlstr_from . $where_clause;
  192. $sql['delete'] = $sqlstr_delete . $sqlstr_from . $where_clause;
  193. return $sql;
  194. }
  195. /**
  196. * Provides where clause for building SQL query
  197. *
  198. * @param string $table The table name
  199. *
  200. * @return string The generated where clause
  201. */
  202. private function getWhereClause($table)
  203. {
  204. // Columns to select
  205. $allColumns = $this->dbi->getColumns($GLOBALS['db'], $table);
  206. $likeClauses = [];
  207. // Based on search type, decide like/regex & '%'/''
  208. $like_or_regex = (($this->criteriaSearchType == 5) ? 'REGEXP' : 'LIKE');
  209. $automatic_wildcard = (($this->criteriaSearchType < 4) ? '%' : '');
  210. // For "as regular expression" (search option 5), LIKE won't be used
  211. // Usage example: If user is searching for a literal $ in a regexp search,
  212. // he should enter \$ as the value.
  213. $criteriaSearchStringEscaped = $this->dbi->escapeString(
  214. $this->criteriaSearchString
  215. );
  216. // Extract search words or pattern
  217. $search_words = (($this->criteriaSearchType > 2)
  218. ? [$criteriaSearchStringEscaped]
  219. : explode(' ', $criteriaSearchStringEscaped));
  220. foreach ($search_words as $search_word) {
  221. // Eliminates empty values
  222. if (strlen($search_word) === 0) {
  223. continue;
  224. }
  225. $likeClausesPerColumn = [];
  226. // for each column in the table
  227. foreach ($allColumns as $column) {
  228. if (! isset($this->criteriaColumnName)
  229. || strlen($this->criteriaColumnName) === 0
  230. || $column['Field'] == $this->criteriaColumnName
  231. ) {
  232. $column = 'CONVERT(' . Util::backquote($column['Field'])
  233. . ' USING utf8)';
  234. $likeClausesPerColumn[] = $column . ' ' . $like_or_regex . ' '
  235. . "'"
  236. . $automatic_wildcard . $search_word . $automatic_wildcard
  237. . "'";
  238. }
  239. } // end for
  240. if (count($likeClausesPerColumn) > 0) {
  241. $likeClauses[] = implode(' OR ', $likeClausesPerColumn);
  242. }
  243. } // end for
  244. // Use 'OR' if 'at least one word' is to be searched, else use 'AND'
  245. $implode_str = ($this->criteriaSearchType == 1 ? ' OR ' : ' AND ');
  246. if (empty($likeClauses)) {
  247. // this could happen when the "inside column" does not exist
  248. // in any selected tables
  249. $where_clause = ' WHERE FALSE';
  250. } else {
  251. $where_clause = ' WHERE ('
  252. . implode(') ' . $implode_str . ' (', $likeClauses)
  253. . ')';
  254. }
  255. return $where_clause;
  256. }
  257. /**
  258. * Displays database search results
  259. *
  260. * @return string HTML for search results
  261. */
  262. public function getSearchResults()
  263. {
  264. $resultTotal = 0;
  265. $rows = [];
  266. // For each table selected as search criteria
  267. foreach ($this->criteriaTables as $eachTable) {
  268. // Gets the SQL statements
  269. $newSearchSqls = $this->getSearchSqls($eachTable);
  270. // Executes the "COUNT" statement
  271. $resultCount = intval($this->dbi->fetchValue(
  272. $newSearchSqls['select_count']
  273. ));
  274. $resultTotal += $resultCount;
  275. // Gets the result row's HTML for a table
  276. $rows[] = [
  277. 'table' => htmlspecialchars($eachTable),
  278. 'new_search_sqls' => $newSearchSqls,
  279. 'result_count' => $resultCount,
  280. ];
  281. }
  282. return $this->template->render('database/search/results', [
  283. 'db' => $this->db,
  284. 'rows' => $rows,
  285. 'result_total' => $resultTotal,
  286. 'criteria_tables' => $this->criteriaTables,
  287. 'criteria_search_string' => htmlspecialchars($this->criteriaSearchString),
  288. 'search_type_description' => $this->searchTypeDescription,
  289. ]);
  290. }
  291. /**
  292. * Provides the main search form's html
  293. *
  294. * @return string HTML for selection form
  295. */
  296. public function getMainHtml()
  297. {
  298. $choices = [
  299. '1' => $this->searchTypes[1] . ' '
  300. . Util::showHint(
  301. __('Words are separated by a space character (" ").')
  302. ),
  303. '2' => $this->searchTypes[2] . ' '
  304. . Util::showHint(
  305. __('Words are separated by a space character (" ").')
  306. ),
  307. '3' => $this->searchTypes[3],
  308. '4' => $this->searchTypes[4],
  309. '5' => $this->searchTypes[5] . ' ' . Util::showMySQLDocu('Regexp'),
  310. ];
  311. return $this->template->render('database/search/main', [
  312. 'db' => $this->db,
  313. 'choices' => $choices,
  314. 'criteria_search_string' => $this->criteriaSearchString,
  315. 'criteria_search_type' => $this->criteriaSearchType,
  316. 'criteria_tables' => $this->criteriaTables,
  317. 'tables_names_only' => $this->tablesNamesOnly,
  318. 'criteria_column_name' => isset($this->criteriaColumnName)
  319. ? $this->criteriaColumnName : null,
  320. ]);
  321. }
  322. }