FindReplaceController.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. <?php
  2. declare(strict_types=1);
  3. namespace PhpMyAdmin\Controllers\Table;
  4. use PhpMyAdmin\DatabaseInterface;
  5. use PhpMyAdmin\DbTableExists;
  6. use PhpMyAdmin\Html\Generator;
  7. use PhpMyAdmin\Response;
  8. use PhpMyAdmin\Template;
  9. use PhpMyAdmin\Url;
  10. use PhpMyAdmin\Util;
  11. use function array_key_exists;
  12. use function count;
  13. use function is_array;
  14. use function mb_strtolower;
  15. use function preg_match;
  16. use function preg_replace;
  17. use function str_ireplace;
  18. use function str_replace;
  19. use function strncasecmp;
  20. use function strpos;
  21. /**
  22. * Handles find and replace tab.
  23. *
  24. * Displays find and replace form, allows previewing and do the replacing.
  25. */
  26. class FindReplaceController extends AbstractController
  27. {
  28. /** @var array */
  29. private $columnNames;
  30. /** @var array */
  31. private $columnTypes;
  32. /** @var string */
  33. private $connectionCharSet;
  34. /** @var DatabaseInterface */
  35. private $dbi;
  36. /**
  37. * @param Response $response
  38. * @param string $db Database name
  39. * @param string $table Table name
  40. * @param DatabaseInterface $dbi
  41. */
  42. public function __construct($response, Template $template, $db, $table, $dbi)
  43. {
  44. parent::__construct($response, $template, $db, $table);
  45. $this->dbi = $dbi;
  46. $this->columnNames = [];
  47. $this->columnTypes = [];
  48. $this->loadTableInfo();
  49. $this->connectionCharSet = $this->dbi->fetchValue(
  50. 'SELECT @@character_set_connection'
  51. );
  52. }
  53. public function index(): void
  54. {
  55. global $db, $table, $url_params, $cfg, $err_url;
  56. Util::checkParameters(['db', 'table']);
  57. $url_params = ['db' => $db, 'table' => $table];
  58. $err_url = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table');
  59. $err_url .= Url::getCommon($url_params, '&');
  60. DbTableExists::check();
  61. if (isset($_POST['find'])) {
  62. $this->findAction();
  63. return;
  64. }
  65. $this->addScriptFiles(['table/find_replace.js']);
  66. if (isset($_POST['replace'])) {
  67. $this->replaceAction();
  68. }
  69. // Displays the find and replace form
  70. $this->displaySelectionFormAction();
  71. }
  72. /**
  73. * Gets all the columns of a table along with their types.
  74. */
  75. private function loadTableInfo(): void
  76. {
  77. // Gets the list and number of columns
  78. $columns = $this->dbi->getColumns(
  79. $this->db,
  80. $this->table,
  81. null,
  82. true
  83. );
  84. foreach ($columns as $row) {
  85. // set column name
  86. $this->columnNames[] = $row['Field'];
  87. $type = (string) $row['Type'];
  88. // reformat mysql query output
  89. if (strncasecmp($type, 'set', 3) == 0
  90. || strncasecmp($type, 'enum', 4) == 0
  91. ) {
  92. $type = str_replace(',', ', ', $type);
  93. } else {
  94. // strip the "BINARY" attribute, except if we find "BINARY(" because
  95. // this would be a BINARY or VARBINARY column type
  96. if (! preg_match('@BINARY[\(]@i', $type)) {
  97. $type = str_ireplace('BINARY', '', $type);
  98. }
  99. $type = str_ireplace('ZEROFILL', '', $type);
  100. $type = str_ireplace('UNSIGNED', '', $type);
  101. $type = mb_strtolower($type);
  102. }
  103. if (empty($type)) {
  104. $type = '&nbsp;';
  105. }
  106. $this->columnTypes[] = $type;
  107. }
  108. }
  109. /**
  110. * Display selection form action
  111. */
  112. public function displaySelectionFormAction(): void
  113. {
  114. global $goto;
  115. if (! isset($goto)) {
  116. $goto = Util::getScriptNameForOption(
  117. $GLOBALS['cfg']['DefaultTabTable'],
  118. 'table'
  119. );
  120. }
  121. $column_names = $this->columnNames;
  122. $column_types = $this->columnTypes;
  123. $types = [];
  124. $num_cols = count($column_names);
  125. for ($i = 0; $i < $num_cols; $i++) {
  126. $types[$column_names[$i]] = preg_replace(
  127. '@\\(.*@s',
  128. '',
  129. $column_types[$i]
  130. );
  131. }
  132. $this->render('table/find_replace/index', [
  133. 'db' => $this->db,
  134. 'table' => $this->table,
  135. 'goto' => $goto,
  136. 'column_names' => $column_names,
  137. 'types' => $types,
  138. 'sql_types' => $this->dbi->types,
  139. ]);
  140. }
  141. public function findAction(): void
  142. {
  143. $useRegex = array_key_exists('useRegex', $_POST)
  144. && $_POST['useRegex'] === 'on';
  145. $preview = $this->getReplacePreview(
  146. $_POST['columnIndex'],
  147. $_POST['find'],
  148. $_POST['replaceWith'],
  149. $useRegex,
  150. $this->connectionCharSet
  151. );
  152. $this->response->addJSON('preview', $preview);
  153. }
  154. public function replaceAction(): void
  155. {
  156. $this->replace(
  157. $_POST['columnIndex'],
  158. $_POST['findString'],
  159. $_POST['replaceWith'],
  160. $_POST['useRegex'],
  161. $this->connectionCharSet
  162. );
  163. $this->response->addHTML(
  164. Generator::getMessage(
  165. __('Your SQL query has been executed successfully.'),
  166. null,
  167. 'success'
  168. )
  169. );
  170. }
  171. /**
  172. * Returns HTML for previewing strings found and their replacements
  173. *
  174. * @param int $columnIndex index of the column
  175. * @param string $find string to find in the column
  176. * @param string $replaceWith string to replace with
  177. * @param bool $useRegex to use Regex replace or not
  178. * @param string $charSet character set of the connection
  179. *
  180. * @return string HTML for previewing strings found and their replacements
  181. */
  182. public function getReplacePreview(
  183. $columnIndex,
  184. $find,
  185. $replaceWith,
  186. $useRegex,
  187. $charSet
  188. ) {
  189. $column = $this->columnNames[$columnIndex];
  190. if ($useRegex) {
  191. $result = $this->getRegexReplaceRows(
  192. $columnIndex,
  193. $find,
  194. $replaceWith,
  195. $charSet
  196. );
  197. } else {
  198. $sql_query = 'SELECT '
  199. . Util::backquote($column) . ','
  200. . ' REPLACE('
  201. . Util::backquote($column) . ", '" . $find . "', '"
  202. . $replaceWith
  203. . "'),"
  204. . ' COUNT(*)'
  205. . ' FROM ' . Util::backquote($this->db)
  206. . '.' . Util::backquote($this->table)
  207. . ' WHERE ' . Util::backquote($column)
  208. . " LIKE '%" . $find . "%' COLLATE " . $charSet . '_bin'; // here we
  209. // change the collation of the 2nd operand to a case sensitive
  210. // binary collation to make sure that the comparison
  211. // is case sensitive
  212. $sql_query .= ' GROUP BY ' . Util::backquote($column)
  213. . ' ORDER BY ' . Util::backquote($column) . ' ASC';
  214. $result = $this->dbi->fetchResult($sql_query, 0);
  215. }
  216. return $this->template->render('table/find_replace/replace_preview', [
  217. 'db' => $this->db,
  218. 'table' => $this->table,
  219. 'column_index' => $columnIndex,
  220. 'find' => $find,
  221. 'replace_with' => $replaceWith,
  222. 'use_regex' => $useRegex,
  223. 'result' => $result,
  224. ]);
  225. }
  226. /**
  227. * Finds and returns Regex pattern and their replacements
  228. *
  229. * @param int $columnIndex index of the column
  230. * @param string $find string to find in the column
  231. * @param string $replaceWith string to replace with
  232. * @param string $charSet character set of the connection
  233. *
  234. * @return array|bool Array containing original values, replaced values and count
  235. */
  236. private function getRegexReplaceRows(
  237. $columnIndex,
  238. $find,
  239. $replaceWith,
  240. $charSet
  241. ) {
  242. $column = $this->columnNames[$columnIndex];
  243. $sql_query = 'SELECT '
  244. . Util::backquote($column) . ','
  245. . ' 1,' // to add an extra column that will have replaced value
  246. . ' COUNT(*)'
  247. . ' FROM ' . Util::backquote($this->db)
  248. . '.' . Util::backquote($this->table)
  249. . ' WHERE ' . Util::backquote($column)
  250. . " RLIKE '" . $this->dbi->escapeString($find) . "' COLLATE "
  251. . $charSet . '_bin'; // here we
  252. // change the collation of the 2nd operand to a case sensitive
  253. // binary collation to make sure that the comparison is case sensitive
  254. $sql_query .= ' GROUP BY ' . Util::backquote($column)
  255. . ' ORDER BY ' . Util::backquote($column) . ' ASC';
  256. $result = $this->dbi->fetchResult($sql_query, 0);
  257. if (is_array($result)) {
  258. /* Iterate over possible delimiters to get one */
  259. $delimiters = [
  260. '/',
  261. '@',
  262. '#',
  263. '~',
  264. '!',
  265. '$',
  266. '%',
  267. '^',
  268. '&',
  269. '_',
  270. ];
  271. $found = false;
  272. for ($i = 0, $l = count($delimiters); $i < $l; $i++) {
  273. if (strpos($find, $delimiters[$i]) === false) {
  274. $found = true;
  275. break;
  276. }
  277. }
  278. if (! $found) {
  279. return false;
  280. }
  281. $find = $delimiters[$i] . $find . $delimiters[$i];
  282. foreach ($result as $index => $row) {
  283. $result[$index][1] = preg_replace(
  284. $find,
  285. $replaceWith,
  286. $row[0]
  287. );
  288. }
  289. }
  290. return $result;
  291. }
  292. /**
  293. * Replaces a given string in a column with a give replacement
  294. *
  295. * @param int $columnIndex index of the column
  296. * @param string $find string to find in the column
  297. * @param string $replaceWith string to replace with
  298. * @param bool $useRegex to use Regex replace or not
  299. * @param string $charSet character set of the connection
  300. *
  301. * @return void
  302. */
  303. public function replace(
  304. $columnIndex,
  305. $find,
  306. $replaceWith,
  307. $useRegex,
  308. $charSet
  309. ) {
  310. $column = $this->columnNames[$columnIndex];
  311. if ($useRegex) {
  312. $toReplace = $this->getRegexReplaceRows(
  313. $columnIndex,
  314. $find,
  315. $replaceWith,
  316. $charSet
  317. );
  318. $sql_query = 'UPDATE ' . Util::backquote($this->table)
  319. . ' SET ' . Util::backquote($column) . ' = CASE';
  320. if (is_array($toReplace)) {
  321. foreach ($toReplace as $row) {
  322. $sql_query .= "\n WHEN " . Util::backquote($column)
  323. . " = '" . $this->dbi->escapeString($row[0])
  324. . "' THEN '" . $this->dbi->escapeString($row[1]) . "'";
  325. }
  326. }
  327. $sql_query .= ' END'
  328. . ' WHERE ' . Util::backquote($column)
  329. . " RLIKE '" . $this->dbi->escapeString($find) . "' COLLATE "
  330. . $charSet . '_bin'; // here we
  331. // change the collation of the 2nd operand to a case sensitive
  332. // binary collation to make sure that the comparison
  333. // is case sensitive
  334. } else {
  335. $sql_query = 'UPDATE ' . Util::backquote($this->table)
  336. . ' SET ' . Util::backquote($column) . ' ='
  337. . ' REPLACE('
  338. . Util::backquote($column) . ", '" . $find . "', '"
  339. . $replaceWith
  340. . "')"
  341. . ' WHERE ' . Util::backquote($column)
  342. . " LIKE '%" . $find . "%' COLLATE " . $charSet . '_bin'; // here we
  343. // change the collation of the 2nd operand to a case sensitive
  344. // binary collation to make sure that the comparison
  345. // is case sensitive
  346. }
  347. $this->dbi->query(
  348. $sql_query,
  349. DatabaseInterface::CONNECT_USER,
  350. DatabaseInterface::QUERY_STORE
  351. );
  352. $GLOBALS['sql_query'] = $sql_query;
  353. }
  354. }