From 9f5a63fb57cccb42f0d830a46a82022d07f0fd65 Mon Sep 17 00:00:00 2001 From: Nathan Nguyen Date: Tue, 24 Sep 2024 15:02:12 +1000 Subject: [PATCH] Replace script --- classes/helper.php | 96 ++++++++++++++++++++ cli/find.php | 2 +- cli/replace.php | 147 +++++++++++++++++++++++++++++++ lang/en/tool_advancedreplace.php | 6 ++ 4 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 cli/replace.php diff --git a/classes/helper.php b/classes/helper.php index 17fc93f..cbe0776 100644 --- a/classes/helper.php +++ b/classes/helper.php @@ -21,6 +21,7 @@ require_once($CFG->libdir . '/adminlib.php'); use core\exception\moodle_exception; +use core_text; use database_column_info; /** @@ -272,6 +273,7 @@ public static function plain_text_search(string $search, string $table, $record->courseshortname ?? '', $record->id, $record->$columnname, + '', ]); } } else { @@ -360,6 +362,7 @@ public static function regular_expression_search(string $search, string $table, $record->courseshortname ?? '', $record->id, $match, + '', ]); } } @@ -371,4 +374,97 @@ public static function regular_expression_search(string $search, string $table, } return $results; } + + /** + * Get column info from column name. + * + * @param string $table The table name. + * @param string $columnname The column name. + */ + private static function get_column_info(string $table, string $columnname): database_column_info { + global $DB; + + $columns = $DB->get_columns($table); + $column = null; + foreach ($columns as $col) { + if ($col->name == $columnname) { + $column = $col; + break; + } + } + + if (is_null($column)) { + throw new moodle_exception(get_string('errorcolumnnotfound', 'tool_advancedreplace', $columnname)); + } + + return $column; + } + + /** + * Replace all text in a table and column. + * + * @param string $table The table to search. + * @param string $columnname The column to search. + * @param string $search The text to search for. + * @param string $replace The text to replace with. + * @param int $id The id of the record to restrict the search. + */ + public static function replace_text_in_a_record(string $table, string $columnname, + string $search, string $replace, int $id) { + + $column = self::get_column_info($table, $columnname); + + self::replace_all_text($table, $column, $search, $replace, ' AND id = ?', [$id]); + } + + /** + * A clone of the core function replace_all_text. + * We have optional id parameter to restrict the search. + * + * @since Moodle 2.6.1 + * @param string $table name of the table + * @param database_column_info $column + * @param string $search text to search for + * @param string $replace text to replace with + * @param string $wheresql additional where clause + * @param array $whereparams parameters for the where clause + */ + private static function replace_all_text($table, database_column_info $column, string $search, string $replace, + string $wheresql = '', array $whereparams = []) { + global $DB; + + if (!$DB->replace_all_text_supported()) { + throw new moodle_exception(get_string('errorreplacetextnotsupported', 'tool_advancedreplace')); + } + + // Enclose the column name by the proper quotes if it's a reserved word. + $columnname = $DB->get_manager()->generator->getEncQuoted($column->name); + + $searchsql = $DB->sql_like($columnname, '?'); + $searchparam = '%'.$DB->sql_like_escape($search).'%'; + + // Additional where clause. + $searchsql .= $wheresql; + $params = [$search, $replace, $searchparam] + $whereparams; + + switch ($column->meta_type) { + case 'C': + if (core_text::strlen($search) < core_text::strlen($replace)) { + $colsize = $column->max_length; + $sql = "UPDATE {".$table."} + SET $columnname = " . $DB->sql_substr("REPLACE(" . $columnname . ", ?, ?)", 1, $colsize) . " + WHERE $searchsql"; + break; + } + // Otherwise, do not break and use the same query as in the 'X' case. + case 'X': + $sql = "UPDATE {".$table."} + SET $columnname = REPLACE($columnname, ?, ?) + WHERE $searchsql"; + break; + default: + throw new moodle_exception(get_string('errorcolumntypenotsupported', 'tool_advancedreplace')); + } + $DB->execute($sql, $params); + } } diff --git a/cli/find.php b/cli/find.php index 5c64df9..892e48b 100644 --- a/cli/find.php +++ b/cli/find.php @@ -115,7 +115,7 @@ // Show header. if (!$options['summary']) { - fputcsv($fp, ['table', 'column', 'courseid', 'shortname', 'id', 'match']); + fputcsv($fp, ['table', 'column', 'courseid', 'shortname', 'id', 'match', 'replace']); } else { fputcsv($fp, ['table', 'column']); } diff --git a/cli/replace.php b/cli/replace.php new file mode 100644 index 0000000..efc3ff2 --- /dev/null +++ b/cli/replace.php @@ -0,0 +1,147 @@ +. + +/** + * Replace strings using uploaded CSV file. + * + * @package tool_advancedreplace + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use tool_advancedreplace\helper; + +define('CLI_SCRIPT', true); + +require(__DIR__.'/../../../../config.php'); +require_once($CFG->libdir.'/clilib.php'); +require_once($CFG->libdir.'/adminlib.php'); +require_once($CFG->dirroot . '/lib/csvlib.class.php'); +$help = + "Replace strings using uploaded CSV file.. + +Options: +--input=FILE Required. Input CSV file produced by find.php in detail mode. +-h, --help Print out this help. + +Example: +\$ sudo -u www-data /usr/bin/php admin/tool/advancedreplace/cli/replace.php --input=/tmp/result.csv +"; + +list($options, $unrecognized) = cli_get_params( + [ + 'input' => null, + 'help' => false, + ], + [ + 'h' => 'help', + ] +); +core_php_time_limit::raise(); + +if ($unrecognized) { + $unrecognized = implode("\n ", $unrecognized); + cli_error(get_string('cliunknowoption', 'admin', $unrecognized)); +} + +// Ensure that we have required parameters. +if ($options['help'] || empty($options['input'])) { + echo $help; + exit(0); +} + +try { + $file = validate_param($options['input'], PARAM_PATH); +} catch (invalid_parameter_exception $e) { + cli_error(get_string('errorinvalidparam', 'tool_advancedreplace')); +} + +if (!file_exists($file)) { + cli_error(get_string('errorfilenotfound', 'tool_advancedreplace')); +} + +// Open the file for reading. +$fp = fopen($file, 'r'); +$data = fread($fp, filesize($file)); +fclose($fp); + +// Load the CSV content. +$iid = csv_import_reader::get_new_iid('tool_advancedreplace'); +$csvimport = new csv_import_reader($iid, 'tool_advancedreplace'); +$contentcount = $csvimport->load_csv_content($data, 'utf-8', 'comma'); + +if ($contentcount === false) { + cli_error(get_string('errorinvalidfile', 'tool_advancedreplace')); +} + +// Read the header. +$header = $csvimport->get_columns(); +if (empty($header)) { + cli_error(get_string('errorinvalidfile', 'tool_advancedreplace')); +} + +// Check if all required columns are present, and show which ones are missing. +$requiredcolumns = ['table', 'column', 'id', 'match', 'replace']; +$missingcolumns = array_diff($requiredcolumns, $header); + +if (!empty($missingcolumns)) { + cli_error(get_string('errormissingfields', 'tool_advancedreplace', implode(', ', $missingcolumns))); +} + +// Column indexes. +$tableindex = array_search('table', $header); +$columnindex = array_search('column', $header); +$idindex = array_search('id', $header); +$matchindex = array_search('match', $header); +$replaceindex = array_search('replace', $header); + +// Progress bar. +$progress = new progress_bar(); +$progress->create(); + +// Read the data and replace the strings. +$dataset = []; +$csvimport->init(); +$rowcount = 0; +$rowskip = 0; +while ($record = $csvimport->next()) { + $dataset[] = $record; + $table = $record[$tableindex]; + $columnname = $record[$columnindex]; + $id = $record[$idindex]; + $match = $record[$matchindex]; + $replace = $record[$replaceindex]; + + if (empty($replace)) { + // Skip if 'replace' is empty. + $rowskip++; + } else { + // Replace the string. + helper::replace_text_in_a_record($table, $columnname, $match, $replace, $id); + } + + // Update the progress bar. + $rowcount++; + $progress->update_full(100 * $rowcount / $contentcount, "Processed $rowcount records. Skipped $rowskip records."); +} + +// Show progress. +$progress->update_full('100', "Processed $rowcount records. Skipped $rowskip records."); + +$csvimport->cleanup(); +$csvimport->close(); + +exit(0); diff --git a/lang/en/tool_advancedreplace.php b/lang/en/tool_advancedreplace.php index 0f37377..9b9416f 100644 --- a/lang/en/tool_advancedreplace.php +++ b/lang/en/tool_advancedreplace.php @@ -22,8 +22,14 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['errorcolumnnotfound'] = 'Column not found.'; +$string['errorcolumntypenotsupported'] = 'Column type is not supported.'; +$string['errorfilenotfound'] = 'File not found.'; +$string['errorinvalidfile'] = 'The file is not valid.'; $string['errorinvalidparam'] = 'Invalid parameter.'; +$string['errormissingfields'] = 'The following fields are missing: {$a}'; $string['errorregexnotsupported'] = 'Regular expression searches are not supported by this database.'; +$string['errorreplacetextnotsupported'] = 'Replace all text is not supported by this database.'; $string['errorsearchmethod'] = 'Please choose one of the search methods: plain text or regular expression.'; $string['pluginname'] = 'Advanced DB search and replace'; $string['privacy:metadata'] = 'The plugin does not store any personal data.';