SearchController.php 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273
  1. <?php
  2. /* vim: set expandtab sw=4 ts=4 sts=4: */
  3. /**
  4. * Holds the PhpMyAdmin\Controllers\Table\SearchController
  5. *
  6. * @package PhpMyAdmin\Controllers
  7. */
  8. declare(strict_types=1);
  9. namespace PhpMyAdmin\Controllers\Table;
  10. use PhpMyAdmin\DatabaseInterface;
  11. use PhpMyAdmin\Relation;
  12. use PhpMyAdmin\Response;
  13. use PhpMyAdmin\Sql;
  14. use PhpMyAdmin\Template;
  15. use PhpMyAdmin\Util;
  16. /**
  17. * Class SearchController
  18. *
  19. * @package PhpMyAdmin\Controllers
  20. */
  21. class SearchController extends AbstractController
  22. {
  23. /**
  24. * Normal search or Zoom search
  25. *
  26. * @access private
  27. * @var string
  28. */
  29. private $_searchType;
  30. /**
  31. * Names of columns
  32. *
  33. * @access private
  34. * @var array
  35. */
  36. private $_columnNames;
  37. /**
  38. * Types of columns
  39. *
  40. * @access private
  41. * @var array
  42. */
  43. private $_columnTypes;
  44. /**
  45. * Types of columns without any replacement
  46. *
  47. * @access private
  48. * @var array
  49. */
  50. private $_originalColumnTypes;
  51. /**
  52. * Collations of columns
  53. *
  54. * @access private
  55. * @var array
  56. */
  57. private $_columnCollations;
  58. /**
  59. * Null Flags of columns
  60. *
  61. * @access private
  62. * @var array
  63. */
  64. private $_columnNullFlags;
  65. /**
  66. * Whether a geometry column is present
  67. *
  68. * @access private
  69. * @var boolean
  70. */
  71. private $_geomColumnFlag;
  72. /**
  73. * Foreign Keys
  74. *
  75. * @access private
  76. * @var array
  77. */
  78. private $_foreigners;
  79. /**
  80. * Connection charset
  81. *
  82. * @access private
  83. * @var string
  84. */
  85. private $_connectionCharSet;
  86. protected $url_query;
  87. /**
  88. * @var Relation
  89. */
  90. private $relation;
  91. /**
  92. * Constructor
  93. *
  94. * @param Response $response Response object
  95. * @param DatabaseInterface $dbi DatabaseInterface object
  96. * @param Template $template Template object
  97. * @param string $db Database name
  98. * @param string $table Table name
  99. * @param string $searchType Search type
  100. * @param string $url_query URL query
  101. * @param Relation $relation Relation instance
  102. */
  103. public function __construct(
  104. $response,
  105. $dbi,
  106. Template $template,
  107. $db,
  108. $table,
  109. $searchType,
  110. $url_query,
  111. Relation $relation
  112. ) {
  113. parent::__construct($response, $dbi, $template, $db, $table);
  114. $this->url_query = $url_query;
  115. $this->_searchType = $searchType;
  116. $this->_columnNames = [];
  117. $this->_columnNullFlags = [];
  118. $this->_columnTypes = [];
  119. $this->_originalColumnTypes = [];
  120. $this->_columnCollations = [];
  121. $this->_geomColumnFlag = false;
  122. $this->_foreigners = [];
  123. $this->relation = $relation;
  124. // Loads table's information
  125. $this->_loadTableInfo();
  126. $this->_connectionCharSet = $this->dbi->fetchValue(
  127. "SELECT @@character_set_connection"
  128. );
  129. }
  130. /**
  131. * Gets all the columns of a table along with their types, collations
  132. * and whether null or not.
  133. *
  134. * @return void
  135. */
  136. private function _loadTableInfo()
  137. {
  138. // Gets the list and number of columns
  139. $columns = $this->dbi->getColumns(
  140. $this->db,
  141. $this->table,
  142. null,
  143. true
  144. );
  145. // Get details about the geometry functions
  146. $geom_types = Util::getGISDatatypes();
  147. foreach ($columns as $row) {
  148. // set column name
  149. $this->_columnNames[] = $row['Field'];
  150. $type = $row['Type'];
  151. // before any replacement
  152. $this->_originalColumnTypes[] = mb_strtolower($type);
  153. // check whether table contains geometric columns
  154. if (in_array($type, $geom_types)) {
  155. $this->_geomColumnFlag = true;
  156. }
  157. // reformat mysql query output
  158. if (strncasecmp($type, 'set', 3) == 0
  159. || strncasecmp($type, 'enum', 4) == 0
  160. ) {
  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[]
  178. = ! empty($row['Collation']) && $row['Collation'] != 'NULL'
  179. ? $row['Collation']
  180. : '';
  181. } // end for
  182. // Retrieve foreign keys
  183. $this->_foreigners = $this->relation->getForeigners($this->db, $this->table);
  184. }
  185. /**
  186. * Index action
  187. *
  188. * @return void
  189. */
  190. public function indexAction()
  191. {
  192. global $goto;
  193. switch ($this->_searchType) {
  194. case 'replace':
  195. if (isset($_POST['find'])) {
  196. $this->findAction();
  197. return;
  198. }
  199. $this->response
  200. ->getHeader()
  201. ->getScripts()
  202. ->addFile('table/find_replace.js');
  203. if (isset($_POST['replace'])) {
  204. $this->replaceAction();
  205. }
  206. // Displays the find and replace form
  207. $this->displaySelectionFormAction();
  208. break;
  209. case 'normal':
  210. $this->response->getHeader()
  211. ->getScripts()
  212. ->addFiles(
  213. [
  214. 'makegrid.js',
  215. 'sql.js',
  216. 'table/select.js',
  217. 'table/change.js',
  218. 'vendor/jquery/jquery.uitablefilter.js',
  219. 'gis_data_editor.js',
  220. ]
  221. );
  222. if (isset($_POST['range_search'])) {
  223. $this->rangeSearchAction();
  224. return;
  225. }
  226. /**
  227. * No selection criteria received -> display the selection form
  228. */
  229. if (! isset($_POST['columnsToDisplay'])
  230. && ! isset($_POST['displayAllColumns'])
  231. ) {
  232. $this->displaySelectionFormAction();
  233. } else {
  234. $this->doSelectionAction();
  235. }
  236. break;
  237. case 'zoom':
  238. $this->response->getHeader()
  239. ->getScripts()
  240. ->addFiles(
  241. [
  242. 'makegrid.js',
  243. 'sql.js',
  244. 'vendor/jqplot/jquery.jqplot.js',
  245. 'vendor/jqplot/plugins/jqplot.canvasTextRenderer.js',
  246. 'vendor/jqplot/plugins/jqplot.canvasAxisLabelRenderer.js',
  247. 'vendor/jqplot/plugins/jqplot.dateAxisRenderer.js',
  248. 'vendor/jqplot/plugins/jqplot.highlighter.js',
  249. 'vendor/jqplot/plugins/jqplot.cursor.js',
  250. 'table/zoom_plot_jqplot.js',
  251. 'table/change.js',
  252. ]
  253. );
  254. /**
  255. * Handle AJAX request for data row on point select
  256. *
  257. * @var boolean Object containing parameters for the POST request
  258. */
  259. if (isset($_POST['get_data_row'])
  260. && $_POST['get_data_row'] == true
  261. ) {
  262. $this->getDataRowAction();
  263. return;
  264. }
  265. /**
  266. * Handle AJAX request for changing field information
  267. * (value,collation,operators,field values) in input form
  268. *
  269. * @var boolean Object containing parameters for the POST request
  270. */
  271. if (isset($_POST['change_tbl_info'])
  272. && $_POST['change_tbl_info'] == true
  273. ) {
  274. $this->changeTableInfoAction();
  275. return;
  276. }
  277. //Set default datalabel if not selected
  278. if (! isset($_POST['zoom_submit']) || $_POST['dataLabel'] == '') {
  279. $dataLabel = $this->relation->getDisplayField($this->db, $this->table);
  280. } else {
  281. $dataLabel = $_POST['dataLabel'];
  282. }
  283. // Displays the zoom search form
  284. $this->displaySelectionFormAction($dataLabel);
  285. /*
  286. * Handle the input criteria and generate the query result
  287. * Form for displaying query results
  288. */
  289. if (isset($_POST['zoom_submit'])
  290. && $_POST['criteriaColumnNames'][0] != 'pma_null'
  291. && $_POST['criteriaColumnNames'][1] != 'pma_null'
  292. && $_POST['criteriaColumnNames'][0] != $_POST['criteriaColumnNames'][1]
  293. ) {
  294. if (! isset($goto)) {
  295. $goto = Util::getScriptNameForOption(
  296. $GLOBALS['cfg']['DefaultTabTable'],
  297. 'table'
  298. );
  299. }
  300. $this->zoomSubmitAction($dataLabel, $goto);
  301. }
  302. break;
  303. }
  304. }
  305. /**
  306. * Zoom submit action
  307. *
  308. * @param string $dataLabel Data label
  309. * @param string $goto Goto
  310. *
  311. * @return void
  312. */
  313. public function zoomSubmitAction($dataLabel, $goto)
  314. {
  315. //Query generation part
  316. $sql_query = $this->_buildSqlQuery();
  317. $sql_query .= ' LIMIT ' . $_POST['maxPlotLimit'];
  318. //Query execution part
  319. $result = $this->dbi->query(
  320. $sql_query . ";",
  321. DatabaseInterface::CONNECT_USER,
  322. DatabaseInterface::QUERY_STORE
  323. );
  324. $fields_meta = $this->dbi->getFieldsMeta($result);
  325. $data = [];
  326. while ($row = $this->dbi->fetchAssoc($result)) {
  327. //Need a row with indexes as 0,1,2 for the getUniqueCondition
  328. // hence using a temporary array
  329. $tmpRow = [];
  330. foreach ($row as $val) {
  331. $tmpRow[] = $val;
  332. }
  333. //Get unique condition on each row (will be needed for row update)
  334. $uniqueCondition = Util::getUniqueCondition(
  335. $result, // handle
  336. count($this->_columnNames), // fields_cnt
  337. $fields_meta, // fields_meta
  338. $tmpRow, // row
  339. true, // force_unique
  340. false, // restrict_to_table
  341. null // analyzed_sql_results
  342. );
  343. //Append it to row array as where_clause
  344. $row['where_clause'] = $uniqueCondition[0];
  345. $tmpData = [
  346. $_POST['criteriaColumnNames'][0] =>
  347. $row[$_POST['criteriaColumnNames'][0]],
  348. $_POST['criteriaColumnNames'][1] =>
  349. $row[$_POST['criteriaColumnNames'][1]],
  350. 'where_clause' => $uniqueCondition[0],
  351. ];
  352. $tmpData[$dataLabel] = $dataLabel ? $row[$dataLabel] : '';
  353. $data[] = $tmpData;
  354. }
  355. unset($tmpData);
  356. //Displays form for point data and scatter plot
  357. $titles = [
  358. 'Browse' => Util::getIcon(
  359. 'b_browse',
  360. __('Browse foreign values')
  361. ),
  362. ];
  363. $column_names_hashes = [];
  364. foreach ($this->_columnNames as $columnName) {
  365. $column_names_hashes[$columnName] = md5($columnName);
  366. }
  367. $this->response->addHTML(
  368. $this->template->render('table/search/zoom_result_form', [
  369. 'db' => $this->db,
  370. 'table' => $this->table,
  371. 'column_names' => $this->_columnNames,
  372. 'column_names_hashes' => $column_names_hashes,
  373. 'foreigners' => $this->_foreigners,
  374. 'column_null_flags' => $this->_columnNullFlags,
  375. 'column_types' => $this->_columnTypes,
  376. 'titles' => $titles,
  377. 'goto' => $goto,
  378. 'data' => $data,
  379. 'data_json' => json_encode($data),
  380. 'zoom_submit' => isset($_POST['zoom_submit']),
  381. 'foreign_max_limit' => $GLOBALS['cfg']['ForeignKeyMaxLimit'],
  382. ])
  383. );
  384. }
  385. /**
  386. * Change table info action
  387. *
  388. * @return void
  389. */
  390. public function changeTableInfoAction()
  391. {
  392. $field = $_POST['field'];
  393. if ($field == 'pma_null') {
  394. $this->response->addJSON('field_type', '');
  395. $this->response->addJSON('field_collation', '');
  396. $this->response->addJSON('field_operators', '');
  397. $this->response->addJSON('field_value', '');
  398. return;
  399. }
  400. $key = array_search($field, $this->_columnNames);
  401. $search_index
  402. = (isset($_POST['it']) && is_numeric($_POST['it'])
  403. ? intval($_POST['it']) : 0);
  404. $properties = $this->getColumnProperties($search_index, $key);
  405. $this->response->addJSON(
  406. 'field_type',
  407. htmlspecialchars($properties['type'])
  408. );
  409. $this->response->addJSON('field_collation', $properties['collation']);
  410. $this->response->addJSON('field_operators', $properties['func']);
  411. $this->response->addJSON('field_value', $properties['value']);
  412. }
  413. /**
  414. * Get data row action
  415. *
  416. * @return void
  417. */
  418. public function getDataRowAction()
  419. {
  420. $extra_data = [];
  421. $row_info_query = 'SELECT * FROM ' . Util::backquote($_POST['db']) . '.'
  422. . Util::backquote($_POST['table']) . ' WHERE ' . $_POST['where_clause'];
  423. $result = $this->dbi->query(
  424. $row_info_query . ";",
  425. DatabaseInterface::CONNECT_USER,
  426. DatabaseInterface::QUERY_STORE
  427. );
  428. $fields_meta = $this->dbi->getFieldsMeta($result);
  429. while ($row = $this->dbi->fetchAssoc($result)) {
  430. // for bit fields we need to convert them to printable form
  431. $i = 0;
  432. foreach ($row as $col => $val) {
  433. if ($fields_meta[$i]->type == 'bit') {
  434. $row[$col] = Util::printableBitValue(
  435. (int) $val,
  436. (int) $fields_meta[$i]->length
  437. );
  438. }
  439. $i++;
  440. }
  441. $extra_data['row_info'] = $row;
  442. }
  443. $this->response->addJSON($extra_data);
  444. }
  445. /**
  446. * Do selection action
  447. *
  448. * @return void
  449. */
  450. public function doSelectionAction()
  451. {
  452. /**
  453. * Selection criteria have been submitted -> do the work
  454. */
  455. $sql_query = $this->_buildSqlQuery();
  456. /**
  457. * Add this to ensure following procedures included running correctly.
  458. */
  459. $sql = new Sql();
  460. $sql->executeQueryAndSendQueryResponse(
  461. null, // analyzed_sql_results
  462. false, // is_gotofile
  463. $this->db, // db
  464. $this->table, // table
  465. null, // find_real_end
  466. null, // sql_query_for_bookmark
  467. null, // extra_data
  468. null, // message_to_show
  469. null, // message
  470. null, // sql_data
  471. $GLOBALS['goto'], // goto
  472. $GLOBALS['pmaThemeImage'], // pmaThemeImage
  473. null, // disp_query
  474. null, // disp_message
  475. null, // query_type
  476. $sql_query, // sql_query
  477. null, // selectedTables
  478. null // complete_query
  479. );
  480. }
  481. /**
  482. * Display selection form action
  483. *
  484. * @param string $dataLabel Data label
  485. *
  486. * @return void
  487. */
  488. public function displaySelectionFormAction($dataLabel = null)
  489. {
  490. global $goto;
  491. $this->url_query .= '&amp;goto=tbl_select.php&amp;back=tbl_select.php';
  492. if (! isset($goto)) {
  493. $goto = Util::getScriptNameForOption(
  494. $GLOBALS['cfg']['DefaultTabTable'],
  495. 'table'
  496. );
  497. }
  498. // Displays the table search form
  499. $this->response->addHTML(
  500. $this->template->render('secondary_tabs', [
  501. 'url_params' => [
  502. 'db' => $this->db,
  503. 'table' => $this->table,
  504. ],
  505. 'sub_tabs' => $this->_getSubTabs(),
  506. ])
  507. );
  508. $column_names = $this->_columnNames;
  509. $column_types = $this->_columnTypes;
  510. $types = [];
  511. if ($this->_searchType == 'replace') {
  512. $num_cols = count($column_names);
  513. for ($i = 0; $i < $num_cols; $i++) {
  514. $types[$column_names[$i]] = preg_replace('@\\(.*@s', '', $column_types[$i]);
  515. }
  516. }
  517. $criteria_column_names = isset($_POST['criteriaColumnNames']) ? $_POST['criteriaColumnNames'] : null;
  518. $keys = [];
  519. for ($i = 0; $i < 4; $i++) {
  520. if (isset($criteria_column_names[$i])) {
  521. if ($criteria_column_names[$i] != 'pma_null') {
  522. $keys[$criteria_column_names[$i]] = array_search($criteria_column_names[$i], $column_names);
  523. }
  524. }
  525. }
  526. $this->response->addHTML(
  527. $this->template->render('table/search/selection_form', [
  528. 'search_type' => $this->_searchType,
  529. 'db' => $this->db,
  530. 'table' => $this->table,
  531. 'goto' => $goto,
  532. 'self' => $this,
  533. 'geom_column_flag' => $this->_geomColumnFlag,
  534. 'column_names' => $column_names,
  535. 'column_types' => $column_types,
  536. 'types' => $types,
  537. 'column_collations' => $this->_columnCollations,
  538. 'data_label' => $dataLabel,
  539. 'keys' => $keys,
  540. 'criteria_column_names' => $criteria_column_names,
  541. 'default_sliders_state' => $GLOBALS['cfg']['InitialSlidersState'],
  542. 'criteria_column_types' => isset($_POST['criteriaColumnTypes']) ? $_POST['criteriaColumnTypes'] : null,
  543. 'sql_types' => $this->dbi->types,
  544. 'max_rows' => intval($GLOBALS['cfg']['MaxRows']),
  545. 'max_plot_limit' => ! empty($_POST['maxPlotLimit'])
  546. ? intval($_POST['maxPlotLimit'])
  547. : intval($GLOBALS['cfg']['maxRowPlotLimit']),
  548. ])
  549. );
  550. }
  551. /**
  552. * Range search action
  553. *
  554. * @return void
  555. */
  556. public function rangeSearchAction()
  557. {
  558. $min_max = $this->getColumnMinMax($_POST['column']);
  559. $this->response->addJSON('column_data', $min_max);
  560. }
  561. /**
  562. * Find action
  563. *
  564. * @return void
  565. */
  566. public function findAction()
  567. {
  568. $useRegex = array_key_exists('useRegex', $_POST)
  569. && $_POST['useRegex'] == 'on';
  570. $preview = $this->getReplacePreview(
  571. $_POST['columnIndex'],
  572. $_POST['find'],
  573. $_POST['replaceWith'],
  574. $useRegex,
  575. $this->_connectionCharSet
  576. );
  577. $this->response->addJSON('preview', $preview);
  578. }
  579. /**
  580. * Replace action
  581. *
  582. * @return void
  583. */
  584. public function replaceAction()
  585. {
  586. $this->replace(
  587. $_POST['columnIndex'],
  588. $_POST['findString'],
  589. $_POST['replaceWith'],
  590. $_POST['useRegex'],
  591. $this->_connectionCharSet
  592. );
  593. $this->response->addHTML(
  594. Util::getMessage(
  595. __('Your SQL query has been executed successfully.'),
  596. null,
  597. 'success'
  598. )
  599. );
  600. }
  601. /**
  602. * Returns HTML for previewing strings found and their replacements
  603. *
  604. * @param int $columnIndex index of the column
  605. * @param string $find string to find in the column
  606. * @param string $replaceWith string to replace with
  607. * @param boolean $useRegex to use Regex replace or not
  608. * @param string $charSet character set of the connection
  609. *
  610. * @return string HTML for previewing strings found and their replacements
  611. */
  612. public function getReplacePreview(
  613. $columnIndex,
  614. $find,
  615. $replaceWith,
  616. $useRegex,
  617. $charSet
  618. ) {
  619. $column = $this->_columnNames[$columnIndex];
  620. if ($useRegex) {
  621. $result = $this->_getRegexReplaceRows(
  622. $columnIndex,
  623. $find,
  624. $replaceWith,
  625. $charSet
  626. );
  627. } else {
  628. $sql_query = "SELECT "
  629. . Util::backquote($column) . ","
  630. . " REPLACE("
  631. . Util::backquote($column) . ", '" . $find . "', '"
  632. . $replaceWith
  633. . "'),"
  634. . " COUNT(*)"
  635. . " FROM " . Util::backquote($this->db)
  636. . "." . Util::backquote($this->table)
  637. . " WHERE " . Util::backquote($column)
  638. . " LIKE '%" . $find . "%' COLLATE " . $charSet . "_bin"; // here we
  639. // change the collation of the 2nd operand to a case sensitive
  640. // binary collation to make sure that the comparison
  641. // is case sensitive
  642. $sql_query .= " GROUP BY " . Util::backquote($column)
  643. . " ORDER BY " . Util::backquote($column) . " ASC";
  644. $result = $this->dbi->fetchResult($sql_query, 0);
  645. }
  646. return $this->template->render('table/search/replace_preview', [
  647. 'db' => $this->db,
  648. 'table' => $this->table,
  649. 'column_index' => $columnIndex,
  650. 'find' => $find,
  651. 'replace_with' => $replaceWith,
  652. 'use_regex' => $useRegex,
  653. 'result' => $result,
  654. ]);
  655. }
  656. /**
  657. * Finds and returns Regex pattern and their replacements
  658. *
  659. * @param int $columnIndex index of the column
  660. * @param string $find string to find in the column
  661. * @param string $replaceWith string to replace with
  662. * @param string $charSet character set of the connection
  663. *
  664. * @return array|bool Array containing original values, replaced values and count
  665. */
  666. private function _getRegexReplaceRows(
  667. $columnIndex,
  668. $find,
  669. $replaceWith,
  670. $charSet
  671. ) {
  672. $column = $this->_columnNames[$columnIndex];
  673. $sql_query = "SELECT "
  674. . Util::backquote($column) . ","
  675. . " 1," // to add an extra column that will have replaced value
  676. . " COUNT(*)"
  677. . " FROM " . Util::backquote($this->db)
  678. . "." . Util::backquote($this->table)
  679. . " WHERE " . Util::backquote($column)
  680. . " RLIKE '" . $this->dbi->escapeString($find) . "' COLLATE "
  681. . $charSet . "_bin"; // here we
  682. // change the collation of the 2nd operand to a case sensitive
  683. // binary collation to make sure that the comparison is case sensitive
  684. $sql_query .= " GROUP BY " . Util::backquote($column)
  685. . " ORDER BY " . Util::backquote($column) . " ASC";
  686. $result = $this->dbi->fetchResult($sql_query, 0);
  687. if (is_array($result)) {
  688. /* Iterate over possible delimiters to get one */
  689. $delimiters = [
  690. '/',
  691. '@',
  692. '#',
  693. '~',
  694. '!',
  695. '$',
  696. '%',
  697. '^',
  698. '&',
  699. '_',
  700. ];
  701. $found = false;
  702. for ($i = 0, $l = count($delimiters); $i < $l; $i++) {
  703. if (strpos($find, $delimiters[$i]) === false) {
  704. $found = true;
  705. break;
  706. }
  707. }
  708. if (! $found) {
  709. return false;
  710. }
  711. $find = $delimiters[$i] . $find . $delimiters[$i];
  712. foreach ($result as $index => $row) {
  713. $result[$index][1] = preg_replace(
  714. $find,
  715. $replaceWith,
  716. $row[0]
  717. );
  718. }
  719. }
  720. return $result;
  721. }
  722. /**
  723. * Replaces a given string in a column with a give replacement
  724. *
  725. * @param int $columnIndex index of the column
  726. * @param string $find string to find in the column
  727. * @param string $replaceWith string to replace with
  728. * @param boolean $useRegex to use Regex replace or not
  729. * @param string $charSet character set of the connection
  730. *
  731. * @return void
  732. */
  733. public function replace(
  734. $columnIndex,
  735. $find,
  736. $replaceWith,
  737. $useRegex,
  738. $charSet
  739. ) {
  740. $column = $this->_columnNames[$columnIndex];
  741. if ($useRegex) {
  742. $toReplace = $this->_getRegexReplaceRows(
  743. $columnIndex,
  744. $find,
  745. $replaceWith,
  746. $charSet
  747. );
  748. $sql_query = "UPDATE " . Util::backquote($this->table)
  749. . " SET " . Util::backquote($column) . " = CASE";
  750. if (is_array($toReplace)) {
  751. foreach ($toReplace as $row) {
  752. $sql_query .= "\n WHEN " . Util::backquote($column)
  753. . " = '" . $this->dbi->escapeString($row[0])
  754. . "' THEN '" . $this->dbi->escapeString($row[1]) . "'";
  755. }
  756. }
  757. $sql_query .= " END"
  758. . " WHERE " . Util::backquote($column)
  759. . " RLIKE '" . $this->dbi->escapeString($find) . "' COLLATE "
  760. . $charSet . "_bin"; // here we
  761. // change the collation of the 2nd operand to a case sensitive
  762. // binary collation to make sure that the comparison
  763. // is case sensitive
  764. } else {
  765. $sql_query = "UPDATE " . Util::backquote($this->table)
  766. . " SET " . Util::backquote($column) . " ="
  767. . " REPLACE("
  768. . Util::backquote($column) . ", '" . $find . "', '"
  769. . $replaceWith
  770. . "')"
  771. . " WHERE " . Util::backquote($column)
  772. . " LIKE '%" . $find . "%' COLLATE " . $charSet . "_bin"; // here we
  773. // change the collation of the 2nd operand to a case sensitive
  774. // binary collation to make sure that the comparison
  775. // is case sensitive
  776. }
  777. $this->dbi->query(
  778. $sql_query,
  779. DatabaseInterface::CONNECT_USER,
  780. DatabaseInterface::QUERY_STORE
  781. );
  782. $GLOBALS['sql_query'] = $sql_query;
  783. }
  784. /**
  785. * Finds minimum and maximum value of a given column.
  786. *
  787. * @param string $column Column name
  788. *
  789. * @return array
  790. */
  791. public function getColumnMinMax($column)
  792. {
  793. $sql_query = 'SELECT MIN(' . Util::backquote($column) . ') AS `min`, '
  794. . 'MAX(' . Util::backquote($column) . ') AS `max` '
  795. . 'FROM ' . Util::backquote($this->db) . '.'
  796. . Util::backquote($this->table);
  797. return $this->dbi->fetchSingleRow($sql_query);
  798. }
  799. /**
  800. * Returns an array with necessary configurations to create
  801. * sub-tabs in the table_select page.
  802. *
  803. * @return array Array containing configuration (icon, text, link, id, args)
  804. * of sub-tabs
  805. */
  806. private function _getSubTabs()
  807. {
  808. $subtabs = [];
  809. $subtabs['search']['icon'] = 'b_search';
  810. $subtabs['search']['text'] = __('Table search');
  811. $subtabs['search']['link'] = 'tbl_select.php';
  812. $subtabs['search']['id'] = 'tbl_search_id';
  813. $subtabs['search']['args']['pos'] = 0;
  814. $subtabs['zoom']['icon'] = 'b_select';
  815. $subtabs['zoom']['link'] = 'tbl_zoom_select.php';
  816. $subtabs['zoom']['text'] = __('Zoom search');
  817. $subtabs['zoom']['id'] = 'zoom_search_id';
  818. $subtabs['replace']['icon'] = 'b_find_replace';
  819. $subtabs['replace']['link'] = 'tbl_find_replace.php';
  820. $subtabs['replace']['text'] = __('Find and replace');
  821. $subtabs['replace']['id'] = 'find_replace_id';
  822. return $subtabs;
  823. }
  824. /**
  825. * Builds the sql search query from the post parameters
  826. *
  827. * @return string the generated SQL query
  828. */
  829. private function _buildSqlQuery()
  830. {
  831. $sql_query = 'SELECT ';
  832. // If only distinct values are needed
  833. $is_distinct = isset($_POST['distinct']) ? 'true' : 'false';
  834. if ($is_distinct == 'true') {
  835. $sql_query .= 'DISTINCT ';
  836. }
  837. // if all column names were selected to display, we do a 'SELECT *'
  838. // (more efficient and this helps prevent a problem in IE
  839. // if one of the rows is edited and we come back to the Select results)
  840. if (isset($_POST['zoom_submit']) || ! empty($_POST['displayAllColumns'])) {
  841. $sql_query .= '* ';
  842. } else {
  843. $sql_query .= implode(
  844. ', ',
  845. Util::backquote($_POST['columnsToDisplay'])
  846. );
  847. } // end if
  848. $sql_query .= ' FROM '
  849. . Util::backquote($_POST['table']);
  850. $whereClause = $this->_generateWhereClause();
  851. $sql_query .= $whereClause;
  852. // if the search results are to be ordered
  853. if (isset($_POST['orderByColumn']) && $_POST['orderByColumn'] != '--nil--') {
  854. $sql_query .= ' ORDER BY '
  855. . Util::backquote($_POST['orderByColumn'])
  856. . ' ' . $_POST['order'];
  857. } // end if
  858. return $sql_query;
  859. }
  860. /**
  861. * Provides a column's type, collation, operators list, and criteria value
  862. * to display in table search form
  863. *
  864. * @param integer $search_index Row number in table search form
  865. * @param integer $column_index Column index in ColumnNames array
  866. *
  867. * @return array Array containing column's properties
  868. */
  869. public function getColumnProperties($search_index, $column_index)
  870. {
  871. $selected_operator = (isset($_POST['criteriaColumnOperators'][$search_index])
  872. ? $_POST['criteriaColumnOperators'][$search_index] : '');
  873. $entered_value = (isset($_POST['criteriaValues'])
  874. ? $_POST['criteriaValues'] : '');
  875. $titles = [
  876. 'Browse' => Util::getIcon(
  877. 'b_browse',
  878. __('Browse foreign values')
  879. ),
  880. ];
  881. //Gets column's type and collation
  882. $type = $this->_columnTypes[$column_index];
  883. $collation = $this->_columnCollations[$column_index];
  884. $cleanType = preg_replace('@\(.*@s', '', $type);
  885. //Gets column's comparison operators depending on column type
  886. $typeOperators = $this->dbi->types->getTypeOperatorsHtml(
  887. $cleanType,
  888. $this->_columnNullFlags[$column_index],
  889. $selected_operator
  890. );
  891. $func = $this->template->render('table/search/column_comparison_operators', [
  892. 'search_index' => $search_index,
  893. 'type_operators' => $typeOperators,
  894. ]);
  895. //Gets link to browse foreign data(if any) and criteria inputbox
  896. $foreignData = $this->relation->getForeignData(
  897. $this->_foreigners,
  898. $this->_columnNames[$column_index],
  899. false,
  900. '',
  901. ''
  902. );
  903. $html_attributes = '';
  904. if (in_array($cleanType, $this->dbi->types->getIntegerTypes())) {
  905. $extracted_columnspec = Util::extractColumnSpec(
  906. $this->_originalColumnTypes[$column_index]
  907. );
  908. $is_unsigned = $extracted_columnspec['unsigned'];
  909. $min_max_values = $this->dbi->types->getIntegerRange(
  910. $cleanType,
  911. ! $is_unsigned
  912. );
  913. $html_attributes = 'min="' . $min_max_values[0] . '" '
  914. . 'max="' . $min_max_values[1] . '"';
  915. $type = 'INT';
  916. }
  917. $html_attributes .= " onchange= 'return verifyAfterSearchFieldChange(" . $column_index . ")'";
  918. $value = $this->template->render('table/search/input_box', [
  919. 'str' => '',
  920. 'column_type' => (string) $type,
  921. 'html_attributes' => $html_attributes,
  922. 'column_id' => 'fieldID_',
  923. 'in_zoom_search_edit' => false,
  924. 'foreigners' => $this->_foreigners,
  925. 'column_name' => $this->_columnNames[$column_index],
  926. 'column_name_hash' => md5($this->_columnNames[$column_index]),
  927. 'foreign_data' => $foreignData,
  928. 'table' => $this->table,
  929. 'column_index' => $search_index,
  930. 'foreign_max_limit' => $GLOBALS['cfg']['ForeignKeyMaxLimit'],
  931. 'criteria_values' => $entered_value,
  932. 'db' => $this->db,
  933. 'titles' => $titles,
  934. 'in_fbs' => true,
  935. ]);
  936. return [
  937. 'type' => $type,
  938. 'collation' => $collation,
  939. 'func' => $func,
  940. 'value' => $value,
  941. ];
  942. }
  943. /**
  944. * Generates the where clause for the SQL search query to be executed
  945. *
  946. * @return string the generated where clause
  947. */
  948. private function _generateWhereClause()
  949. {
  950. if (isset($_POST['customWhereClause'])
  951. && trim($_POST['customWhereClause']) != ''
  952. ) {
  953. return ' WHERE ' . $_POST['customWhereClause'];
  954. }
  955. // If there are no search criteria set or no unary criteria operators,
  956. // return
  957. if (! isset($_POST['criteriaValues'])
  958. && ! isset($_POST['criteriaColumnOperators'])
  959. && ! isset($_POST['geom_func'])
  960. ) {
  961. return '';
  962. }
  963. // else continue to form the where clause from column criteria values
  964. $fullWhereClause = [];
  965. foreach ($_POST['criteriaColumnOperators'] as $column_index => $operator) {
  966. $unaryFlag = $this->dbi->types->isUnaryOperator($operator);
  967. $tmp_geom_func = isset($_POST['geom_func'][$column_index])
  968. ? $_POST['geom_func'][$column_index] : null;
  969. $whereClause = $this->_getWhereClause(
  970. $_POST['criteriaValues'][$column_index],
  971. $_POST['criteriaColumnNames'][$column_index],
  972. $_POST['criteriaColumnTypes'][$column_index],
  973. $operator,
  974. $unaryFlag,
  975. $tmp_geom_func
  976. );
  977. if ($whereClause) {
  978. $fullWhereClause[] = $whereClause;
  979. }
  980. } // end foreach
  981. if (! empty($fullWhereClause)) {
  982. return ' WHERE ' . implode(' AND ', $fullWhereClause);
  983. }
  984. return '';
  985. }
  986. /**
  987. * Return the where clause in case column's type is ENUM.
  988. *
  989. * @param mixed $criteriaValues Search criteria input
  990. * @param string $func_type Search function/operator
  991. *
  992. * @return string part of where clause.
  993. */
  994. private function _getEnumWhereClause($criteriaValues, $func_type)
  995. {
  996. if (! is_array($criteriaValues)) {
  997. $criteriaValues = explode(',', $criteriaValues);
  998. }
  999. $enum_selected_count = count($criteriaValues);
  1000. if ($func_type == '=' && $enum_selected_count > 1) {
  1001. $func_type = 'IN';
  1002. $parens_open = '(';
  1003. $parens_close = ')';
  1004. } elseif ($func_type == '!=' && $enum_selected_count > 1) {
  1005. $func_type = 'NOT IN';
  1006. $parens_open = '(';
  1007. $parens_close = ')';
  1008. } else {
  1009. $parens_open = '';
  1010. $parens_close = '';
  1011. }
  1012. $enum_where = '\''
  1013. . $this->dbi->escapeString($criteriaValues[0]) . '\'';
  1014. for ($e = 1; $e < $enum_selected_count; $e++) {
  1015. $enum_where .= ', \''
  1016. . $this->dbi->escapeString($criteriaValues[$e]) . '\'';
  1017. }
  1018. return ' ' . $func_type . ' ' . $parens_open
  1019. . $enum_where . $parens_close;
  1020. }
  1021. /**
  1022. * Return the where clause for a geometrical column.
  1023. *
  1024. * @param mixed $criteriaValues Search criteria input
  1025. * @param string $names Name of the column on which search is submitted
  1026. * @param string $func_type Search function/operator
  1027. * @param string $types Type of the field
  1028. * @param bool $geom_func Whether geometry functions should be applied
  1029. *
  1030. * @return string part of where clause.
  1031. */
  1032. private function _getGeomWhereClause(
  1033. $criteriaValues,
  1034. $names,
  1035. $func_type,
  1036. $types,
  1037. $geom_func = null
  1038. ) {
  1039. $geom_unary_functions = [
  1040. 'IsEmpty' => 1,
  1041. 'IsSimple' => 1,
  1042. 'IsRing' => 1,
  1043. 'IsClosed' => 1,
  1044. ];
  1045. $where = '';
  1046. // Get details about the geometry functions
  1047. $geom_funcs = Util::getGISFunctions($types, true, false);
  1048. // If the function takes multiple parameters
  1049. if (strpos($func_type, "IS NULL") !== false || strpos($func_type, "IS NOT NULL") !== false) {
  1050. return Util::backquote($names) . " " . $func_type;
  1051. } elseif ($geom_funcs[$geom_func]['params'] > 1) {
  1052. // create gis data from the criteria input
  1053. $gis_data = Util::createGISData($criteriaValues, $this->dbi->getVersion());
  1054. return $geom_func . '(' . Util::backquote($names)
  1055. . ', ' . $gis_data . ')';
  1056. }
  1057. // New output type is the output type of the function being applied
  1058. $type = $geom_funcs[$geom_func]['type'];
  1059. $geom_function_applied = $geom_func
  1060. . '(' . Util::backquote($names) . ')';
  1061. // If the where clause is something like 'IsEmpty(`spatial_col_name`)'
  1062. if (isset($geom_unary_functions[$geom_func])
  1063. && trim($criteriaValues) == ''
  1064. ) {
  1065. $where = $geom_function_applied;
  1066. } elseif (in_array($type, Util::getGISDatatypes())
  1067. && ! empty($criteriaValues)
  1068. ) {
  1069. // create gis data from the criteria input
  1070. $gis_data = Util::createGISData($criteriaValues, $this->dbi->getVersion());
  1071. $where = $geom_function_applied . " " . $func_type . " " . $gis_data;
  1072. } elseif (strlen($criteriaValues) > 0) {
  1073. $where = $geom_function_applied . " "
  1074. . $func_type . " '" . $criteriaValues . "'";
  1075. }
  1076. return $where;
  1077. }
  1078. /**
  1079. * Return the where clause for query generation based on the inputs provided.
  1080. *
  1081. * @param mixed $criteriaValues Search criteria input
  1082. * @param string $names Name of the column on which search is submitted
  1083. * @param string $types Type of the field
  1084. * @param string $func_type Search function/operator
  1085. * @param bool $unaryFlag Whether operator unary or not
  1086. * @param bool $geom_func Whether geometry functions should be applied
  1087. *
  1088. * @return string generated where clause.
  1089. */
  1090. private function _getWhereClause(
  1091. $criteriaValues,
  1092. $names,
  1093. $types,
  1094. $func_type,
  1095. $unaryFlag,
  1096. $geom_func = null
  1097. ) {
  1098. // If geometry function is set
  1099. if (! empty($geom_func)) {
  1100. return $this->_getGeomWhereClause(
  1101. $criteriaValues,
  1102. $names,
  1103. $func_type,
  1104. $types,
  1105. $geom_func
  1106. );
  1107. }
  1108. $backquoted_name = Util::backquote($names);
  1109. $where = '';
  1110. if ($unaryFlag) {
  1111. $where = $backquoted_name . ' ' . $func_type;
  1112. } elseif (strncasecmp($types, 'enum', 4) == 0 && (! empty($criteriaValues) || $criteriaValues[0] === '0')) {
  1113. $where = $backquoted_name;
  1114. $where .= $this->_getEnumWhereClause($criteriaValues, $func_type);
  1115. } elseif ($criteriaValues != '') {
  1116. // For these types we quote the value. Even if it's another type
  1117. // (like INT), for a LIKE we always quote the value. MySQL converts
  1118. // strings to numbers and numbers to strings as necessary
  1119. // during the comparison
  1120. if (preg_match('@char|binary|blob|text|set|date|time|year@i', $types)
  1121. || mb_strpos(' ' . $func_type, 'LIKE')
  1122. ) {
  1123. $quot = '\'';
  1124. } else {
  1125. $quot = '';
  1126. }
  1127. // LIKE %...%
  1128. if ($func_type == 'LIKE %...%') {
  1129. $func_type = 'LIKE';
  1130. $criteriaValues = '%' . $criteriaValues . '%';
  1131. }
  1132. if ($func_type == 'REGEXP ^...$') {
  1133. $func_type = 'REGEXP';
  1134. $criteriaValues = '^' . $criteriaValues . '$';
  1135. }
  1136. if ('IN (...)' != $func_type
  1137. && 'NOT IN (...)' != $func_type
  1138. && 'BETWEEN' != $func_type
  1139. && 'NOT BETWEEN' != $func_type
  1140. ) {
  1141. return $backquoted_name . ' ' . $func_type . ' ' . $quot
  1142. . $this->dbi->escapeString($criteriaValues) . $quot;
  1143. }
  1144. $func_type = str_replace(' (...)', '', $func_type);
  1145. //Don't explode if this is already an array
  1146. //(Case for (NOT) IN/BETWEEN.)
  1147. if (is_array($criteriaValues)) {
  1148. $values = $criteriaValues;
  1149. } else {
  1150. $values = explode(',', $criteriaValues);
  1151. }
  1152. // quote values one by one
  1153. $emptyKey = false;
  1154. foreach ($values as $key => &$value) {
  1155. if ('' === $value) {
  1156. $emptyKey = $key;
  1157. $value = 'NULL';
  1158. continue;
  1159. }
  1160. $value = $quot . $this->dbi->escapeString(trim($value))
  1161. . $quot;
  1162. }
  1163. if ('BETWEEN' == $func_type || 'NOT BETWEEN' == $func_type) {
  1164. $where = $backquoted_name . ' ' . $func_type . ' '
  1165. . (isset($values[0]) ? $values[0] : '')
  1166. . ' AND ' . (isset($values[1]) ? $values[1] : '');
  1167. } else { //[NOT] IN
  1168. if (false !== $emptyKey) {
  1169. unset($values[$emptyKey]);
  1170. }
  1171. $wheres = [];
  1172. if (! empty($values)) {
  1173. $wheres[] = $backquoted_name . ' ' . $func_type
  1174. . ' (' . implode(',', $values) . ')';
  1175. }
  1176. if (false !== $emptyKey) {
  1177. $wheres[] = $backquoted_name . ' IS NULL';
  1178. }
  1179. $where = implode(' OR ', $wheres);
  1180. if (1 < count($wheres)) {
  1181. $where = '(' . $where . ')';
  1182. }
  1183. }
  1184. } // end if
  1185. return $where;
  1186. }
  1187. }