Search.php 9.8 KB

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