PageRenderTime 56ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/libraries/classes/Controllers/Table/ZoomSearchController.php

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