diff --git a/classes/helper.php b/classes/helper.php new file mode 100644 index 0000000..47d7f45 --- /dev/null +++ b/classes/helper.php @@ -0,0 +1,160 @@ +. + +namespace tool_advancedreplace; + +/** + * Helper class to search and replace text throughout the whole database. + * + * @package tool_advancedreplace + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class helper { + + // Flag to indicate we search all columns in a table. + const ALL_COLUMNS = 'all columns'; + + private static function plain_text_search(string $search, string $table, + string $column = self::ALL_COLUMNS, $limit = 0): array { + global $DB; + + $results = []; + + $columns = $DB->get_columns($table); + + if ($column !== self::ALL_COLUMNS) { + // Only search the specified column. + $columns = array_filter($columns, function($col) use ($column) { + return $col->name == $column; + }); + } + + foreach ($columns as $column) { + $columnname = $DB->get_manager()->generator->getEncQuoted($column->name); + + $searchsql = $DB->sql_like($columnname, '?', false); + $searchparam = '%'.$DB->sql_like_escape($search).'%'; + + $sql = "SELECT id, $columnname + FROM {".$table."} + WHERE $searchsql"; + + if ($column->meta_type === 'X' || $column->meta_type === 'C') { + $records = $DB->get_records_sql($sql, array($searchparam), 0, $limit); + if ($records) { + $results[$table][$column->name] = $records; + } + } + } + + return $results; + } + + private static function regular_expression_search(string $search, string $table, + string $column = self::ALL_COLUMNS, $limit = 0): array { + global $DB; + + $results = []; + + $columns = $DB->get_columns($table); + + if ($column !== self::ALL_COLUMNS) { + // Only search the specified column. + $columns = array_filter($columns, function($col) use ($column) { + return $col->name == $column; + }); + } + + foreach ($columns as $column) { + $columnname = $DB->get_manager()->generator->getEncQuoted($column->name); + + if ($DB->sql_regex_supported()) { + $select = $columnname . ' ' . $DB->sql_regex() . ' :pattern '; + $params = ['pattern' => $search]; + + if ($column->meta_type === 'X' || $column->meta_type === 'C') { + $records = $DB->get_records_select($table, $select, $params, '', '*', 0, $limit); + + if ($records) { + $results[$table][$column->name] = $records; + } + } + } + } + + return $results; + } + + public static function search(string $search, bool $regex = false, string $tables = '', $limit = 0): array { + global $DB; + + // Build a list of tables and columns to search. + $tablelist = explode(',', $tables); + $searchlist = []; + foreach ($tablelist as $table) { + $tableandcols = explode(':', $table); + $tablename = $tableandcols[0]; + $columnname = $tableandcols[1] ?? ''; + + // Check if the table already exists in the list. + if (array_key_exists($tablename, $searchlist)) { + // Skip if the table has already been flagged to search all columns. + if (in_array(self::ALL_COLUMNS, $searchlist[$tablename])) { + continue; + } + + // Skip if the column already exists in the list for that table. + if (!in_array($columnname, $searchlist[$tablename])) { + continue; + } + } + + // Add the table to the list. + if ($columnname == '') { + // If the column is not specified, search all columns in the table. + $searchlist[$tablename][] = self::ALL_COLUMNS; + } else { + // Add the column to the list. + $searchlist[$tablename][] = $columnname; + } + } + + // If no tables are specified, search all tables and columns. + if (empty($tables)) { + $tables = $DB->get_tables(); + // Mark all columns in each table to be searched. + foreach ($tables as $table) { + $searchlist[$table] = [self::ALL_COLUMNS]; + } + } + + // Perform the search for each table and column. + $results = []; + foreach ($searchlist as $table => $columns) { + foreach ($columns as $column) { + // Perform the search on this column. + if ($regex) { + $results = array_merge($results, self::regular_expression_search($search, $table, $column, $limit)); + } else { + $results = array_merge($results, self::plain_text_search($search, $table, $column, $limit)); + } + } + } + + return $results; + } +} diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php new file mode 100644 index 0000000..8583554 --- /dev/null +++ b/classes/privacy/provider.php @@ -0,0 +1,39 @@ +. + +namespace tool_advancedreplace\privacy; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem implementation for tool_advancedreplace. + * + * @package tool_advancedreplace + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\null_provider { + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function get_reason(): string { + return 'privacy:metadata'; + } +} \ No newline at end of file diff --git a/cli/find.php b/cli/find.php new file mode 100644 index 0000000..4a044eb --- /dev/null +++ b/cli/find.php @@ -0,0 +1,149 @@ +. + +/** + * Search strings throughout all texts in the whole database. + * + * @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'); + +$help = + "Search text throughout the whole database. + +Options: +--search=STRING String to search for. +--regex-match=STRING Use regular expression to match the search string. +--tables=tablename:columnname Tables and columns to search. Separate multiple tables/columns with a comma. + If not specified, search all tables and columns. + If specify table only, search all columns in the table. + Example: + --tables=user:username,user:email + --tables=user,assign_submission:submission + --tables=user,assign_submission +--summary Summary mode, only shows column/table where the text is found. + If not specified, run in detail mode, which shows the full text where the search string is found. +-h, --help Print out this help. + +Example: +\$ sudo -u www-data /usr/bin/php admin/tool/advancedreplace/cli/find.php --search=thelostsoul --summary +\$ sudo -u www-data /usr/bin/php admin/tool/advancedreplace/cli/find.php --regex-match=thelostsoul\\d+ --summary +"; + +list($options, $unrecognized) = cli_get_params( + array( + 'search' => null, + 'regex-match' => null, + 'tables' => '', + 'summary' => false, + 'help' => false, + ), + array( + 'h' => 'help', + ) +); + +if ($unrecognized) { + $unrecognized = implode("\n ", $unrecognized); + cli_error(get_string('cliunknowoption', 'admin', $unrecognized)); +} + +// Ensure that we have required parameters. +if ($options['help'] || (!is_string($options['search']) && empty($options['regex-match']))) { + echo $help; + exit(0); +} + +// Ensure we only have one search method. +if (!empty($options['regex-match']) && !empty($options['search'])) { + cli_error(get_string('errorsearchmethod', 'tool_advancedreplace')); +} + +try { + if (!empty($options['search'])) { + $search = validate_param($options['search'], PARAM_RAW); + } else { + $search = validate_param($options['regex-match'], PARAM_RAW); + } + $tables = validate_param($options['tables'], PARAM_RAW); +} catch (invalid_parameter_exception $e) { + cli_error(get_string('invalidcharacter', 'tool_advancedreplace')); +} + +// Perform the search. +$result = helper::search($search, !empty($options['regex-match']), $tables, $options['regex-match'] ? 1 : 0); + +// Notifying the user if no results were found. +if (empty($result)) { + echo "No results found.\n"; + exit(0); +} + +// Show header +if (!$options['summary']) { + echo "Table, Column, ID, Match \n"; +} else { + echo "Table, Column\n"; +} + +// Output the result. +foreach ($result as $table => $columns) { + foreach ($columns as $column => $rows) { + if ($options['summary']) { + echo "$table, $column\n"; + } else { + foreach ($rows as $row) { + // Fields to show. + $id = $row->id; + $data = $row->$column; + + if (!empty($options['regex-match'])) { + // If the search string is a regular expression, show each matching instance. + + // Replace "/" with "\/", as it is used as delimiters. + $search = str_replace('/', '\\/', $search); + + // Replace "\\" with "\". + $search = str_replace('\\\\', '\\', $search); + + // Perform the regular expression search. + preg_match_all( "/" . $search . "/", $data, $matches); + + if (!empty($matches[0])) { + // Show the result foreach match. + foreach ($matches[0] as $match) { + echo "$table, $column, $id, \"$match\"\n"; + } + } + } else { + // Show the result for simple plain text search. + echo "$table, $column, $id, \"$data\"\n"; + } + } + } + } +} + +exit(0); diff --git a/lang/en/tool_advancedreplace.php b/lang/en/tool_advancedreplace.php new file mode 100644 index 0000000..7260db4 --- /dev/null +++ b/lang/en/tool_advancedreplace.php @@ -0,0 +1,27 @@ +. + +/** + * Strings for component 'tool_advancedreplace', language 'en', branch 'MOODLE_22_STABLE' + * + * @package tool_advancedreplace + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$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.'; diff --git a/version.php b/version.php new file mode 100644 index 0000000..3928937 --- /dev/null +++ b/version.php @@ -0,0 +1,29 @@ +. + +/** + * Version details. + * + * @package tool_advancedreplace + * @copyright 2024 Catalyst IT Australia Pty Ltd + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2024091700; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2024041600; // Requires this Moodle version. +$plugin->component = 'tool_advancedreplace'; // Full name of the plugin (used for diagnostics)