Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
Nathan Nguyen committed Sep 18, 2024
1 parent d79bc95 commit 5fb3bd7
Show file tree
Hide file tree
Showing 5 changed files with 404 additions and 0 deletions.
160 changes: 160 additions & 0 deletions classes/helper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

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;
}
}
39 changes: 39 additions & 0 deletions classes/privacy/provider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

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';
}
}
149 changes: 149 additions & 0 deletions cli/find.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* 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);
27 changes: 27 additions & 0 deletions lang/en/tool_advancedreplace.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* 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.';
Loading

0 comments on commit 5fb3bd7

Please sign in to comment.