ZoomSearchController.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. <?php
  2. declare(strict_types=1);
  3. namespace PhpMyAdmin\Controllers\Table;
  4. use PhpMyAdmin\Core;
  5. use PhpMyAdmin\DatabaseInterface;
  6. use PhpMyAdmin\DbTableExists;
  7. use PhpMyAdmin\Relation;
  8. use PhpMyAdmin\Response;
  9. use PhpMyAdmin\Table\Search;
  10. use PhpMyAdmin\Template;
  11. use PhpMyAdmin\Url;
  12. use PhpMyAdmin\Util;
  13. use function array_search;
  14. use function count;
  15. use function htmlspecialchars;
  16. use function in_array;
  17. use function intval;
  18. use function is_numeric;
  19. use function json_encode;
  20. use function mb_strtolower;
  21. use function md5;
  22. use function preg_match;
  23. use function preg_replace;
  24. use function str_ireplace;
  25. use function str_replace;
  26. use function strncasecmp;
  27. use function strtoupper;
  28. /**
  29. * Handles table zoom search tab.
  30. *
  31. * Display table zoom search form, create SQL queries from form data.
  32. */
  33. class ZoomSearchController extends AbstractController
  34. {
  35. /** @var Search */
  36. private $search;
  37. /** @var Relation */
  38. private $relation;
  39. /** @var array */
  40. private $columnNames;
  41. /** @var array */
  42. private $columnTypes;
  43. /** @var array */
  44. private $originalColumnTypes;
  45. /** @var array */
  46. private $columnCollations;
  47. /** @var array */
  48. private $columnNullFlags;
  49. /** @var bool Whether a geometry column is present */
  50. private $geomColumnFlag;
  51. /** @var array Foreign keys */
  52. private $foreigners;
  53. /** @var DatabaseInterface */
  54. private $dbi;
  55. /**
  56. * @param Response $response
  57. * @param string $db Database name.
  58. * @param string $table Table name.
  59. * @param DatabaseInterface $dbi
  60. */
  61. public function __construct($response, Template $template, $db, $table, Search $search, Relation $relation, $dbi)
  62. {
  63. parent::__construct($response, $template, $db, $table);
  64. $this->search = $search;
  65. $this->relation = $relation;
  66. $this->dbi = $dbi;
  67. $this->columnNames = [];
  68. $this->columnTypes = [];
  69. $this->originalColumnTypes = [];
  70. $this->columnCollations = [];
  71. $this->columnNullFlags = [];
  72. $this->geomColumnFlag = false;
  73. $this->foreigners = [];
  74. $this->loadTableInfo();
  75. }
  76. public function index(): void
  77. {
  78. global $goto, $db, $table, $url_params, $cfg, $err_url;
  79. Util::checkParameters(['db', 'table']);
  80. $url_params = ['db' => $db, 'table' => $table];
  81. $err_url = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
  82. $err_url .= Url::getCommon($url_params, '&');
  83. DbTableExists::check();
  84. $this->addScriptFiles([
  85. 'vendor/stickyfill.min.js',
  86. 'makegrid.js',
  87. 'sql.js',
  88. 'vendor/jqplot/jquery.jqplot.js',
  89. 'vendor/jqplot/plugins/jqplot.canvasTextRenderer.js',
  90. 'vendor/jqplot/plugins/jqplot.canvasAxisLabelRenderer.js',
  91. 'vendor/jqplot/plugins/jqplot.dateAxisRenderer.js',
  92. 'vendor/jqplot/plugins/jqplot.highlighter.js',
  93. 'vendor/jqplot/plugins/jqplot.cursor.js',
  94. 'table/zoom_plot_jqplot.js',
  95. 'table/change.js',
  96. ]);
  97. /**
  98. * Handle AJAX request for data row on point select
  99. */
  100. if (isset($_POST['get_data_row'])
  101. && $_POST['get_data_row'] == true
  102. ) {
  103. $this->getDataRowAction();
  104. return;
  105. }
  106. /**
  107. * Handle AJAX request for changing field information
  108. * (value,collation,operators,field values) in input form
  109. */
  110. if (isset($_POST['change_tbl_info'])
  111. && $_POST['change_tbl_info'] == true
  112. ) {
  113. $this->changeTableInfoAction();
  114. return;
  115. }
  116. //Set default datalabel if not selected
  117. if (! isset($_POST['zoom_submit']) || $_POST['dataLabel'] == '') {
  118. $dataLabel = $this->relation->getDisplayField($this->db, $this->table);
  119. } else {
  120. $dataLabel = $_POST['dataLabel'];
  121. }
  122. // Displays the zoom search form
  123. $this->displaySelectionFormAction($dataLabel);
  124. /**
  125. * Handle the input criteria and generate the query result
  126. * Form for displaying query results
  127. */
  128. if (! isset($_POST['zoom_submit'])
  129. || $_POST['criteriaColumnNames'][0] === 'pma_null'
  130. || $_POST['criteriaColumnNames'][1] === 'pma_null'
  131. || $_POST['criteriaColumnNames'][0] == $_POST['criteriaColumnNames'][1]
  132. ) {
  133. return;
  134. }
  135. if (! isset($goto)) {
  136. $goto = Util::getScriptNameForOption(
  137. $GLOBALS['cfg']['DefaultTabTable'],
  138. 'table'
  139. );
  140. }
  141. $this->zoomSubmitAction($dataLabel, $goto);
  142. }
  143. /**
  144. * Gets all the columns of a table along with their types, collations
  145. * and whether null or not.
  146. */
  147. private function loadTableInfo(): void
  148. {
  149. // Gets the list and number of columns
  150. $columns = $this->dbi->getColumns(
  151. $this->db,
  152. $this->table,
  153. null,
  154. true
  155. );
  156. // Get details about the geometry functions
  157. $geom_types = Util::getGISDatatypes();
  158. foreach ($columns as $row) {
  159. // set column name
  160. $this->columnNames[] = $row['Field'];
  161. $type = (string) $row['Type'];
  162. // before any replacement
  163. $this->originalColumnTypes[] = mb_strtolower($type);
  164. // check whether table contains geometric columns
  165. if (in_array($type, $geom_types)) {
  166. $this->geomColumnFlag = true;
  167. }
  168. // reformat mysql query output
  169. if (strncasecmp($type, 'set', 3) == 0
  170. || strncasecmp($type, 'enum', 4) == 0
  171. ) {
  172. $type = str_replace(',', ', ', $type);
  173. } else {
  174. // strip the "BINARY" attribute, except if we find "BINARY(" because
  175. // this would be a BINARY or VARBINARY column type
  176. if (! preg_match('@BINARY[\(]@i', $type)) {
  177. $type = str_ireplace('BINARY', '', $type);
  178. }
  179. $type = str_ireplace('ZEROFILL', '', $type);
  180. $type = str_ireplace('UNSIGNED', '', $type);
  181. $type = mb_strtolower($type);
  182. }
  183. if (empty($type)) {
  184. $type = '&nbsp;';
  185. }
  186. $this->columnTypes[] = $type;
  187. $this->columnNullFlags[] = $row['Null'];
  188. $this->columnCollations[]
  189. = ! empty($row['Collation']) && $row['Collation'] !== 'NULL'
  190. ? $row['Collation']
  191. : '';
  192. }
  193. // Retrieve foreign keys
  194. $this->foreigners = $this->relation->getForeigners($this->db, $this->table);
  195. }
  196. /**
  197. * Display selection form action
  198. *
  199. * @param string $dataLabel Data label
  200. *
  201. * @return void
  202. */
  203. public function displaySelectionFormAction($dataLabel = null)
  204. {
  205. global $goto;
  206. if (! isset($goto)) {
  207. $goto = Util::getScriptNameForOption(
  208. $GLOBALS['cfg']['DefaultTabTable'],
  209. 'table'
  210. );
  211. }
  212. $column_names = $this->columnNames;
  213. $criteria_column_names = $_POST['criteriaColumnNames'] ?? null;
  214. $keys = [];
  215. for ($i = 0; $i < 4; $i++) {
  216. if (! isset($criteria_column_names[$i])) {
  217. continue;
  218. }
  219. if ($criteria_column_names[$i] === 'pma_null') {
  220. continue;
  221. }
  222. $keys[$criteria_column_names[$i]] = array_search($criteria_column_names[$i], $column_names);
  223. }
  224. $this->render('table/zoom_search/index', [
  225. 'db' => $this->db,
  226. 'table' => $this->table,
  227. 'goto' => $goto,
  228. 'self' => $this,
  229. 'geom_column_flag' => $this->geomColumnFlag,
  230. 'column_names' => $column_names,
  231. 'data_label' => $dataLabel,
  232. 'keys' => $keys,
  233. 'criteria_column_names' => $criteria_column_names,
  234. 'criteria_column_types' => $_POST['criteriaColumnTypes'] ?? null,
  235. 'max_plot_limit' => ! empty($_POST['maxPlotLimit'])
  236. ? intval($_POST['maxPlotLimit'])
  237. : intval($GLOBALS['cfg']['maxRowPlotLimit']),
  238. ]);
  239. }
  240. /**
  241. * Get data row action
  242. *
  243. * @return void
  244. */
  245. public function getDataRowAction()
  246. {
  247. if (! Core::checkSqlQuerySignature($_POST['where_clause'], $_POST['where_clause_sign'])) {
  248. return;
  249. }
  250. $extra_data = [];
  251. $row_info_query = 'SELECT * FROM ' . Util::backquote($_POST['db']) . '.'
  252. . Util::backquote($_POST['table']) . ' WHERE ' . $_POST['where_clause'];
  253. $result = $this->dbi->query(
  254. $row_info_query . ';',
  255. DatabaseInterface::CONNECT_USER,
  256. DatabaseInterface::QUERY_STORE
  257. );
  258. $fields_meta = $this->dbi->getFieldsMeta($result);
  259. while ($row = $this->dbi->fetchAssoc($result)) {
  260. // for bit fields we need to convert them to printable form
  261. $i = 0;
  262. foreach ($row as $col => $val) {
  263. if ($fields_meta[$i]->type === 'bit') {
  264. $row[$col] = Util::printableBitValue(
  265. (int) $val,
  266. (int) $fields_meta[$i]->length
  267. );
  268. }
  269. $i++;
  270. }
  271. $extra_data['row_info'] = $row;
  272. }
  273. $this->response->addJSON($extra_data);
  274. }
  275. /**
  276. * Change table info action
  277. *
  278. * @return void
  279. */
  280. public function changeTableInfoAction()
  281. {
  282. $field = $_POST['field'];
  283. if ($field === 'pma_null') {
  284. $this->response->addJSON('field_type', '');
  285. $this->response->addJSON('field_collation', '');
  286. $this->response->addJSON('field_operators', '');
  287. $this->response->addJSON('field_value', '');
  288. return;
  289. }
  290. $key = array_search($field, $this->columnNames);
  291. $search_index
  292. = (isset($_POST['it']) && is_numeric($_POST['it'])
  293. ? intval($_POST['it']) : 0);
  294. $properties = $this->getColumnProperties($search_index, $key);
  295. $this->response->addJSON(
  296. 'field_type',
  297. htmlspecialchars($properties['type'])
  298. );
  299. $this->response->addJSON('field_collation', $properties['collation']);
  300. $this->response->addJSON('field_operators', $properties['func']);
  301. $this->response->addJSON('field_value', $properties['value']);
  302. }
  303. /**
  304. * Zoom submit action
  305. *
  306. * @param string $dataLabel Data label
  307. * @param string $goto Goto
  308. *
  309. * @return void
  310. */
  311. public function zoomSubmitAction($dataLabel, $goto)
  312. {
  313. //Query generation part
  314. $sql_query = $this->search->buildSqlQuery();
  315. $sql_query .= ' LIMIT ' . $_POST['maxPlotLimit'];
  316. //Query execution part
  317. $result = $this->dbi->query(
  318. $sql_query . ';',
  319. DatabaseInterface::CONNECT_USER,
  320. DatabaseInterface::QUERY_STORE
  321. );
  322. $fields_meta = $this->dbi->getFieldsMeta($result);
  323. $data = [];
  324. while ($row = $this->dbi->fetchAssoc($result)) {
  325. //Need a row with indexes as 0,1,2 for the getUniqueCondition
  326. // hence using a temporary array
  327. $tmpRow = [];
  328. foreach ($row as $val) {
  329. $tmpRow[] = $val;
  330. }
  331. //Get unique condition on each row (will be needed for row update)
  332. $uniqueCondition = Util::getUniqueCondition(
  333. $result,
  334. count($this->columnNames),
  335. $fields_meta,
  336. $tmpRow,
  337. true
  338. );
  339. //Append it to row array as where_clause
  340. $row['where_clause'] = $uniqueCondition[0];
  341. $row['where_clause_sign'] = Core::signSqlQuery($uniqueCondition[0]);
  342. $tmpData = [
  343. $_POST['criteriaColumnNames'][0] =>
  344. $row[$_POST['criteriaColumnNames'][0]],
  345. $_POST['criteriaColumnNames'][1] =>
  346. $row[$_POST['criteriaColumnNames'][1]],
  347. 'where_clause' => $uniqueCondition[0],
  348. 'where_clause_sign' => Core::signSqlQuery($uniqueCondition[0]),
  349. ];
  350. $tmpData[$dataLabel] = $dataLabel ? $row[$dataLabel] : '';
  351. $data[] = $tmpData;
  352. }
  353. unset($tmpData);
  354. $column_names_hashes = [];
  355. foreach ($this->columnNames as $columnName) {
  356. $column_names_hashes[$columnName] = md5($columnName);
  357. }
  358. $this->render('table/zoom_search/result_form', [
  359. 'db' => $this->db,
  360. 'table' => $this->table,
  361. 'column_names' => $this->columnNames,
  362. 'column_names_hashes' => $column_names_hashes,
  363. 'foreigners' => $this->foreigners,
  364. 'column_null_flags' => $this->columnNullFlags,
  365. 'column_types' => $this->columnTypes,
  366. 'goto' => $goto,
  367. 'data' => $data,
  368. 'data_json' => json_encode($data),
  369. 'zoom_submit' => isset($_POST['zoom_submit']),
  370. 'foreign_max_limit' => $GLOBALS['cfg']['ForeignKeyMaxLimit'],
  371. ]);
  372. }
  373. /**
  374. * Provides a column's type, collation, operators list, and criteria value
  375. * to display in table search form
  376. *
  377. * @param int $search_index Row number in table search form
  378. * @param int $column_index Column index in ColumnNames array
  379. *
  380. * @return array Array containing column's properties
  381. */
  382. public function getColumnProperties($search_index, $column_index)
  383. {
  384. $selected_operator = ($_POST['criteriaColumnOperators'][$search_index] ?? '');
  385. $entered_value = ($_POST['criteriaValues'] ?? '');
  386. //Gets column's type and collation
  387. $type = $this->columnTypes[$column_index];
  388. $collation = $this->columnCollations[$column_index];
  389. $cleanType = preg_replace('@\(.*@s', '', $type);
  390. //Gets column's comparison operators depending on column type
  391. $typeOperators = $this->dbi->types->getTypeOperatorsHtml(
  392. $cleanType,
  393. $this->columnNullFlags[$column_index],
  394. $selected_operator
  395. );
  396. $func = $this->template->render('table/search/column_comparison_operators', [
  397. 'search_index' => $search_index,
  398. 'type_operators' => $typeOperators,
  399. ]);
  400. //Gets link to browse foreign data(if any) and criteria inputbox
  401. $foreignData = $this->relation->getForeignData(
  402. $this->foreigners,
  403. $this->columnNames[$column_index],
  404. false,
  405. '',
  406. ''
  407. );
  408. $htmlAttributes = '';
  409. if (in_array($cleanType, $this->dbi->types->getIntegerTypes())) {
  410. $extractedColumnspec = Util::extractColumnSpec(
  411. $this->originalColumnTypes[$column_index]
  412. );
  413. $is_unsigned = $extractedColumnspec['unsigned'];
  414. $minMaxValues = $this->dbi->types->getIntegerRange(
  415. $cleanType,
  416. ! $is_unsigned
  417. );
  418. $htmlAttributes = 'data-min="' . $minMaxValues[0] . '" '
  419. . 'data-max="' . $minMaxValues[1] . '"';
  420. }
  421. $htmlAttributes .= ' onfocus="return '
  422. . 'verifyAfterSearchFieldChange(' . $search_index . ', \'#zoom_search_form\')"';
  423. $value = $this->template->render('table/search/input_box', [
  424. 'str' => '',
  425. 'column_type' => (string) $type,
  426. 'column_data_type' => strtoupper($cleanType),
  427. 'html_attributes' => $htmlAttributes,
  428. 'column_id' => 'fieldID_',
  429. 'in_zoom_search_edit' => false,
  430. 'foreigners' => $this->foreigners,
  431. 'column_name' => $this->columnNames[$column_index],
  432. 'column_name_hash' => md5($this->columnNames[$column_index]),
  433. 'foreign_data' => $foreignData,
  434. 'table' => $this->table,
  435. 'column_index' => $search_index,
  436. 'foreign_max_limit' => $GLOBALS['cfg']['ForeignKeyMaxLimit'],
  437. 'criteria_values' => $entered_value,
  438. 'db' => $this->db,
  439. 'in_fbs' => true,
  440. ]);
  441. return [
  442. 'type' => $type,
  443. 'collation' => $collation,
  444. 'func' => $func,
  445. 'value' => $value,
  446. ];
  447. }
  448. }