Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Produce summary data to feed back into --table option and clean up #13

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 30 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
<a href="https://github.com/catalyst/moodle-tool_advancedreplace/actions/workflows/ci.yml?query=branch%3AMOODLE_401_STABLE">
<img src="https://github.com/catalyst/moodle-tool_advancedreplace/workflows/ci/badge.svg?branch=MOODLE_401_STABLE">
</a>


# moodle-tool_advancedreplace

This is a Moodle plugin that allows administrators to search and replace strings in the Moodle database.
Expand All @@ -8,18 +13,38 @@ They can use simple text search or regular expressions.
## GDPR
The plugin does not store any personal data.

## Branches

| Moodle version | Branch | PHP |
|-------------------|--------------------|-----------|
| Moodle 4.1+ | `main` | 7.4+ |

## Installation

1. Install the plugin the same as any standard Moodle plugin, you can use
git to clone it into your source:

```sh
git clone [email protected]:catalyst/moodle-tool_advancedreplace.git admin/tool/advancedreplace

## Examples
- Find all occurrences of "http://example.com/" followed by any number of digits on tables:

`php admin/tool/advancedreplace/cli/find.php --regex-match="http://example.com/\d+"`
`php admin/tool/advancedreplace/cli/find.php --regex-match="http://example.com/\d+" --output=/tmp/result.csv`
- Find all occurrences of "http://example.com/" on a table:

`php admin/tool/advancedreplace/cli/find.php --regex-match="http://example.com/" --tables=page`
`php admin/tool/advancedreplace/cli/find.php --regex-match="http://example.com/\d+" --tables=page --output=/tmp/result.csv`

- Find all occurrences of "http://example.com/" on multiple tables:

`php admin/tool/advancedreplace/cli/find.php --regex-match="http://example.com/" --tables=page,forum`
`php admin/tool/advancedreplace/cli/find.php --regex-match="http://example.com/\d+" --tables=page,forum --output=/tmp/result.csv`

- Find all occurrences of "http://example.com/" on different tables and columns:

`php admin/tool/advancedreplace/cli/find.php --regex-match="http://example.com/\d+" --tables=page:content,forum:message --output=/tmp/result.csv`
- Find all occurrences of "http://example.com/" on all tables except the ones specified:

- Replace all occurrences of "http://example.com/" on different tables and columns:
`php admin/tool/advancedreplace/cli/find.php --regex-match="http://example.com/\d+" --skip-tables=page,forum --output=/tmp/result.csv`
- Find all occurrences of "http://example.com/" on all columns except the ones specified:

`php admin/tool/advancedreplace/cli/find.php --regex-match="http://example.com/" --tables=page:content,forum:message`
`php admin/tool/advancedreplace/cli/find.php --regex-match="http://example.com/\d+" --tables=page --skip-columns=intro,display --output=/tmp/result.csv`
15 changes: 9 additions & 6 deletions classes/helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@

namespace tool_advancedreplace;

defined('MOODLE_INTERNAL') || die();

require_once($CFG->libdir . '/adminlib.php');

use core\exception\moodle_exception;
use database_column_info;

Expand All @@ -41,7 +45,7 @@ class helper {
* @param string $searchstring The string to search for.
* @return array The columns to search.
*/
public static function get_columns(string $table, array $searchingcolumns = [],
private static function get_columns(string $table, array $searchingcolumns = [],
array $skiptables = [], array $skipcolumns = [], string $searchstring = ''): array {
global $DB;

Expand Down Expand Up @@ -117,7 +121,7 @@ public static function get_columns(string $table, array $searchingcolumns = [],
* @param string $skipcolumns A comma separated list of columns to skip.
* @param string $searchstring The string to search for, used to exclude columns having max length less than this.
*
* @return array
* @return array the number of columns to search and the actual columns to search.
*/
public static function build_searching_list(string $tables = '', string $skiptables = '', string $skipcolumns = '',
string $searchstring = ''): array {
Expand Down Expand Up @@ -173,7 +177,7 @@ public static function build_searching_list(string $tables = '', string $skiptab
foreach ($searchlist as $table => $columns) {
$actualcolumns = self::get_columns($table, $columns, $skiptables, $skipcolumns, $searchstring);
sort($actualcolumns);
$count += sizeof($actualcolumns);
$count += count($actualcolumns);
if (!empty($actualcolumns)) {
$actualsearchlist[$table] = $actualcolumns;
}
Expand All @@ -188,7 +192,7 @@ public static function build_searching_list(string $tables = '', string $skiptab
* @param string $table The table to search.
* @return string The course field name.
*/
public static function find_course_field(string $table): string {
private static function find_course_field(string $table): string {
global $DB;

// Potential course field names.
Expand Down Expand Up @@ -330,8 +334,7 @@ public static function regular_expression_search(string $search, string $table,
if (!empty($stream)) {
if ($summary) {
fputcsv($stream, [
$table,
$column->name,
"$table:$column->name",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did you combine these?

]);

// Return empty array to skip the rest of the function.
Expand Down
17 changes: 15 additions & 2 deletions cli/find.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,6 @@
// Show header.
if (!$options['summary']) {
fputcsv($fp, ['table', 'column', 'courseid', 'shortname', 'id', 'match']);
} else {
fputcsv($fp, ['table', 'column']);
}

// Perform the search.
Expand Down Expand Up @@ -146,5 +144,20 @@
}

$progress->update_full(100, "Finished searching into $output");

fclose($fp);

if ($summary) {
// Read the content of the output file.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this all looks weird to me, whats the reason for this? we really don't want to touch the file a second time and we definitely don't want to load it all into memory at once

$filecontent = file_get_contents($output);
// Replace new line with comma.
$filecontent = str_replace("\n", ',', $filecontent);
// Remove the last comma.
$filecontent = substr($filecontent, 0, -1);
// Add a new line at the end.
$filecontent .= "\n";
// Write the content back to the file.
file_put_contents($output, $filecontent);
}

exit(0);
2 changes: 1 addition & 1 deletion lang/en/tool_advancedreplace.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

$string['errorinvalidparam'] = 'Invalid parameter.';
$string['errorregexnotsupported'] = 'Regular expression searches are not supported by this database.';
$string['errorsearchmethod'] = 'Please choose one of the search methods: plain text or regular expression.';
$string['errorinvalidparam'] = 'Invalid parameter.';
$string['pluginname'] = 'Advanced DB search and replace';
$string['privacy:metadata'] = 'The plugin does not store any personal data.';
238 changes: 238 additions & 0 deletions tests/helper_test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
<?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 test.
*
* @package tool_advancedreplace
* @copyright 2024 Catalyst IT Australia Pty Ltd
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class helper_test extends \advanced_testcase {
/**
* Data provider for test_build_searching_list.
*
* @return array
*/
public static function build_searching_list_provider(): array {
return [
[
'', '', '', '',
// Should include these tables/columns.
[
'page' => 'content, intro',
'assign' => 'intro, name',
],
// Should not include these tables/columns.
[
'page' => 'id, introformat, timecreated, timemodified, timelimit',
'assign' => 'id, introformat, course',
'config' => '',
'logstore_standard_log' => '',
],
],
[
'page', '', '', '',
[
'page' => 'content, intro',
],
[
'assign' => '',
],
],
[
'page:content,assign:intro', '', '', '',
[
'page' => 'content',
'assign' => 'intro',
],
[
'page' => 'intro',
'assign' => 'name',
],
],
[
'', 'assign', '', '',
[
'page' => '',
],
[
'assign' => '',
],
],
[
'', 'assign', 'content', '',
[
'page' => '',
],
[
'assign' => '',
'page:content' => '',
],
],

];
}

/**
* Test build_searching_list.
*
* @dataProvider build_searching_list_provider
* @covers \tool_advancedreplace\helper::build_searching_list
*
* @param string $tables the tables to search
* @param string $skiptables the tables to skip
* @param string $skipcolumns the columns to skip
* @param string $searchstring the search string
* @param array $expectedlist the tables/columns which should be in the result
* @param array $unexpectedlist the tables/columns which should not be in the result
*
* return void
*/
public function test_build_searching_list(string $tables, string $skiptables, string $skipcolumns , string $searchstring,
array $expectedlist, array $unexpectedlist): void {
$this->resetAfterTest();
[$count, $searchlist] = helper::build_searching_list($tables, $skiptables, $skipcolumns, $searchstring);

// Columns should be in the result.
foreach ($expectedlist as $table => $columns) {
// Make sure the table is in the result.
$this->assertArrayHasKey($table, $searchlist);

// Get the name of the columns that we are going to search.
$searchcolumns = array_map(function ($column) {
return $column->name;
}, $searchlist[$table]);

if (empty($columns)) {
continue;
}

// Each column should be in the search list.
$columns = explode(',', $columns);
foreach ($columns as $column) {
// Get all columns in the table.
$this->assertContains(trim($column), $searchcolumns);
}
}

// Columns should not be in the result.
foreach ($unexpectedlist as $table => $columns) {
if (!empty($columns)) {
// Specific columns of this table should not be in the result.
$this->assertArrayHasKey($table, $searchlist);
$columns = explode(',', $columns);
foreach ($columns as $column) {
$this->assertNotContains(trim($column), $searchlist[$table]);
}
} else {
// The table should not be in the result.
$this->assertArrayNotHasKey($table, $searchlist);
}

}
}

/**
* Plain text search.
*
* @covers \tool_advancedreplace\helper::plain_text_search
*/
public function test_plain_text_search(): void {
$this->resetAfterTest();

$searchstring = 'https://example.com.au';

// Create a course.
$course = $this->getDataGenerator()->create_course();

// Create a page content.
$this->getDataGenerator()->create_module('page', (object) [
'course' => $course,
'content' => 'This is a page content with a link to https://example.com.au',
'contentformat' => FORMAT_HTML,
]);

// Create an assignment.
$this->getDataGenerator()->create_module('assign', (object)[
'course' => $course->id,
'name' => 'Test!',
'intro' => 'This is an assignment with a link to https://example.com.au/5678',
'introformat' => FORMAT_HTML,
]);

[$count, $searchlist] = helper::build_searching_list('page,assign');
$result = [];
foreach ($searchlist as $table => $columns) {
foreach ($columns as $column) {
$result = array_merge($result, helper::plain_text_search($searchstring, $table, $column));
}
}
$this->assertNotNull($result['page']['content']);
$this->assertNotNull($result['assign']['intro']);
}

/**
* Regular expression search.
*
* @covers \tool_advancedreplace\helper::regular_expression_search
*/
public function test_regular_expression_search(): void {
$this->resetAfterTest();

$searchstring = "https://example.com.au/\d+";

// Create a course.
$course = $this->getDataGenerator()->create_course();
// Create a page content.
$this->getDataGenerator()->create_module('page', (object)[
'course' => $course,
'content' => 'This is a page content with a link to https://example.com.au/1234',
'contentformat' => FORMAT_HTML,
]);
// Create an assignment.
$this->getDataGenerator()->create_module('assign', (object)[
'course' => $course->id,
'name' => 'Test!',
'intro' => 'This is an assignment with a link to https://example.com.au/5678',
'introformat' => FORMAT_HTML,
]);

[$count, $searchlist] = helper::build_searching_list('page,assign');
$result = [];
foreach ($searchlist as $table => $columns) {
foreach ($columns as $column) {
$result = array_merge($result, helper::regular_expression_search($searchstring, $table, $column));
}
}

// Replace "/" with "\/", as it is used as delimiters.
$searchstring = str_replace('/', '\\/', $searchstring);

// Add delimiters to the search string.
$searchstring = '/' . $searchstring . '/';

// Check if page content matches the search string.
$pagecontent = $result['page']['content'];
$this->assertMatchesRegularExpression($searchstring, $pagecontent->current()->content);

// Check if assignment intro matches the search string.
$assignintro = $result['assign']['intro'];
$this->assertMatchesRegularExpression($searchstring, $assignintro->current()->intro);
}
}
Loading