dbi = $dbi; $this->columnNames = []; $this->columnTypes = []; $this->loadTableInfo(); $this->connectionCharSet = (string) $this->dbi->fetchValue('SELECT @@character_set_connection'); } public function __invoke(): void { global $db, $table, $urlParams, $cfg, $errorUrl; Util::checkParameters(['db', 'table']); $urlParams = ['db' => $db, 'table' => $table]; $errorUrl = Util::getScriptNameForOption($cfg['DefaultTabTable'], 'table'); $errorUrl .= Url::getCommon($urlParams, '&'); DbTableExists::check(); if (isset($_POST['find'])) { $this->findAction(); return; } $this->addScriptFiles(['table/find_replace.js']); if (isset($_POST['replace'])) { $this->replaceAction(); } // Displays the find and replace form $this->displaySelectionFormAction(); } /** * Gets all the columns of a table along with their types. */ private function loadTableInfo(): void { // Gets the list and number of columns $columns = $this->dbi->getColumns($this->db, $this->table, true); foreach ($columns as $row) { // set column name $this->columnNames[] = $row['Field']; $type = (string) $row['Type']; // reformat mysql query output if (strncasecmp($type, 'set', 3) == 0 || strncasecmp($type, 'enum', 4) == 0) { $type = str_replace(',', ', ', $type); } else { // strip the "BINARY" attribute, except if we find "BINARY(" because // this would be a BINARY or VARBINARY column type if (! preg_match('@BINARY[\(]@i', $type)) { $type = str_ireplace('BINARY', '', $type); } $type = str_ireplace('ZEROFILL', '', $type); $type = str_ireplace('UNSIGNED', '', $type); $type = mb_strtolower($type); } if (empty($type)) { $type = ' '; } $this->columnTypes[] = $type; } } /** * Display selection form action */ public function displaySelectionFormAction(): void { global $goto; if (! isset($goto)) { $goto = Util::getScriptNameForOption($GLOBALS['cfg']['DefaultTabTable'], 'table'); } $column_names = $this->columnNames; $column_types = $this->columnTypes; $types = []; $num_cols = count($column_names); for ($i = 0; $i < $num_cols; $i++) { $types[$column_names[$i]] = preg_replace('@\\(.*@s', '', $column_types[$i]); } $this->render('table/find_replace/index', [ 'db' => $this->db, 'table' => $this->table, 'goto' => $goto, 'column_names' => $column_names, 'types' => $types, 'sql_types' => $this->dbi->types, ]); } public function findAction(): void { $useRegex = array_key_exists('useRegex', $_POST) && $_POST['useRegex'] === 'on'; $preview = $this->getReplacePreview( $_POST['columnIndex'], $_POST['find'], $_POST['replaceWith'], $useRegex, $this->connectionCharSet ); $this->response->addJSON('preview', $preview); } public function replaceAction(): void { $this->replace( $_POST['columnIndex'], $_POST['findString'], $_POST['replaceWith'], $_POST['useRegex'], $this->connectionCharSet ); $this->response->addHTML( Generator::getMessage( __('Your SQL query has been executed successfully.'), null, 'success' ) ); } /** * Returns HTML for previewing strings found and their replacements * * @param int $columnIndex index of the column * @param string $find string to find in the column * @param string $replaceWith string to replace with * @param bool $useRegex to use Regex replace or not * @param string $charSet character set of the connection * * @return string HTML for previewing strings found and their replacements */ public function getReplacePreview( $columnIndex, $find, $replaceWith, $useRegex, $charSet ) { $column = $this->columnNames[$columnIndex]; if ($useRegex) { $result = $this->getRegexReplaceRows($columnIndex, $find, $replaceWith, $charSet); } else { $sql_query = 'SELECT ' . Util::backquote($column) . ',' . ' REPLACE(' . Util::backquote($column) . ", '" . $find . "', '" . $replaceWith . "')," . ' COUNT(*)' . ' FROM ' . Util::backquote($this->db) . '.' . Util::backquote($this->table) . ' WHERE ' . Util::backquote($column) . " LIKE '%" . $find . "%' COLLATE " . $charSet . '_bin'; // here we // change the collation of the 2nd operand to a case sensitive // binary collation to make sure that the comparison // is case sensitive $sql_query .= ' GROUP BY ' . Util::backquote($column) . ' ORDER BY ' . Util::backquote($column) . ' ASC'; $result = $this->dbi->fetchResult($sql_query, 0); } return $this->template->render('table/find_replace/replace_preview', [ 'db' => $this->db, 'table' => $this->table, 'column_index' => $columnIndex, 'find' => $find, 'replace_with' => $replaceWith, 'use_regex' => $useRegex, 'result' => $result, ]); } /** * Finds and returns Regex pattern and their replacements * * @param int $columnIndex index of the column * @param string $find string to find in the column * @param string $replaceWith string to replace with * @param string $charSet character set of the connection * * @return array|bool Array containing original values, replaced values and count */ private function getRegexReplaceRows( $columnIndex, $find, $replaceWith, $charSet ) { $column = $this->columnNames[$columnIndex]; $sql_query = 'SELECT ' . Util::backquote($column) . ',' . ' 1,' // to add an extra column that will have replaced value . ' COUNT(*)' . ' FROM ' . Util::backquote($this->db) . '.' . Util::backquote($this->table) . ' WHERE ' . Util::backquote($column) . " RLIKE '" . $this->dbi->escapeString($find) . "' COLLATE " . $charSet . '_bin'; // here we // change the collation of the 2nd operand to a case sensitive // binary collation to make sure that the comparison is case sensitive $sql_query .= ' GROUP BY ' . Util::backquote($column) . ' ORDER BY ' . Util::backquote($column) . ' ASC'; $result = $this->dbi->fetchResult($sql_query, 0); /* Iterate over possible delimiters to get one */ $delimiters = [ '/', '@', '#', '~', '!', '$', '%', '^', '&', '_', ]; $found = false; for ($i = 0, $l = count($delimiters); $i < $l; $i++) { if (! str_contains($find, $delimiters[$i])) { $found = true; break; } } if (! $found) { return false; } $find = $delimiters[$i] . $find . $delimiters[$i]; foreach ($result as $index => $row) { $result[$index][1] = preg_replace($find, $replaceWith, $row[0]); } return $result; } /** * Replaces a given string in a column with a give replacement * * @param int $columnIndex index of the column * @param string $find string to find in the column * @param string $replaceWith string to replace with * @param bool $useRegex to use Regex replace or not * @param string $charSet character set of the connection */ public function replace( $columnIndex, $find, $replaceWith, $useRegex, $charSet ): void { $column = $this->columnNames[$columnIndex]; if ($useRegex) { $toReplace = $this->getRegexReplaceRows($columnIndex, $find, $replaceWith, $charSet); $sql_query = 'UPDATE ' . Util::backquote($this->table) . ' SET ' . Util::backquote($column) . ' = CASE'; if (is_array($toReplace)) { foreach ($toReplace as $row) { $sql_query .= "\n WHEN " . Util::backquote($column) . " = '" . $this->dbi->escapeString($row[0]) . "' THEN '" . $this->dbi->escapeString($row[1]) . "'"; } } $sql_query .= ' END' . ' WHERE ' . Util::backquote($column) . " RLIKE '" . $this->dbi->escapeString($find) . "' COLLATE " . $charSet . '_bin'; // here we // change the collation of the 2nd operand to a case sensitive // binary collation to make sure that the comparison // is case sensitive } else { $sql_query = 'UPDATE ' . Util::backquote($this->table) . ' SET ' . Util::backquote($column) . ' =' . ' REPLACE(' . Util::backquote($column) . ", '" . $find . "', '" . $replaceWith . "')" . ' WHERE ' . Util::backquote($column) . " LIKE '%" . $find . "%' COLLATE " . $charSet . '_bin'; // here we // change the collation of the 2nd operand to a case sensitive // binary collation to make sure that the comparison // is case sensitive } $this->dbi->query($sql_query); $GLOBALS['sql_query'] = $sql_query; } }