Skip to content

Commit

Permalink
Improve replace error output handling #113
Browse files Browse the repository at this point in the history
  • Loading branch information
bwalkerl authored and brendanheywood committed Jan 22, 2025
1 parent ac02aea commit cb7e112
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 31 deletions.
64 changes: 41 additions & 23 deletions classes/helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -650,16 +650,17 @@ public static function find_link_function($table, $column) {
/**
* Replace all text in a table and column.
*
* @param int $rownum Row number in CSV.
* @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.
* @param array $rowcounts The count/outcome for each row.
*
* @param replace_error_handler $errorhandler
*/
public static function replace_text_in_a_record(string $table, string $columnname,
string $search, string $replace, int $id, &$rowcounts) {
public static function replace_text_in_a_record(int $rownum, string $table, string $columnname, string $search,
string $replace, int $id, &$rowcounts, replace_error_handler $errorhandler) {
global $DB;

$column = self::get_column_info($table, $columnname);
Expand All @@ -670,9 +671,9 @@ public static function replace_text_in_a_record(string $table, string $columnnam
$record = $DB->get_record($table, array('id' => $id), $columnname);

if (!$record) {
mtrace(get_string('errorreplacingstringnorecord', 'tool_advancedreplace',
['id' => $id, 'table' => $table, 'column' => $columnname]));
$rowcounts['error']++;
$errorhandler->add([$rownum, $table, $columnname, $id,
get_string('errorreplacingstringnorecord', 'tool_advancedreplace')]);
return;
}

Expand All @@ -687,9 +688,9 @@ public static function replace_text_in_a_record(string $table, string $columnnam
} else if (str_contains($record->$columnname, $replace)) {
$rowcounts['replacematch']++;
} else {
mtrace(get_string('errorreplacingstring', 'tool_advancedreplace',
['id' => $id, 'table' => $table, 'column' => $columnname]));
$rowcounts['error']++;
$errorhandler->add([$rownum, $table, $columnname, $id,
get_string('errorreplacingstring', 'tool_advancedreplace')]);
}
}

Expand Down Expand Up @@ -784,6 +785,10 @@ public static function handle_replace_csv(string $data, string $type = 'db') {
$progress = new progress_bar();
$progress->create();

// Error handler, which will output a table of errors.
$errorhandler = new replace_error_handler();
$errorhandler->set_type($type);

// Read the data and replace the strings.
$csvimport->init();
$rowcounts = [
Expand All @@ -792,15 +797,18 @@ public static function handle_replace_csv(string $data, string $type = 'db') {
'error' => 0,
'replacematch' => 0,
];
$totalrows = 0;

// Start at 1 since we've already read the header.
$rownum = 1;
while ($record = $csvimport->next()) {
$rownum++;
if (empty($record[$replaceindex])) {
// Skip if 'replace' is empty.
$rowcounts['skipped']++;
} else if ($type == 'db') {
// Replace the string.
self::replace_text_in_a_record($record[$tableindex], $record[$columnindex],
$record[$matchindex], $record[$replaceindex], $record[$idindex], $rowcounts);
self::replace_text_in_a_record($rownum, $record[$tableindex], $record[$columnindex],
$record[$matchindex], $record[$replaceindex], $record[$idindex], $rowcounts, $errorhandler);
} else if ($type == 'files') {
$filerecord = [
'contextid' => $record[$contextidindex],
Expand All @@ -812,18 +820,18 @@ public static function handle_replace_csv(string $data, string $type = 'db') {
'mimetype' => $record[$mimeindex],
];

self::replace_text_in_file($filerecord, $record[$matchindex], $record[$replaceindex],
$record[$internalindex], $rowcounts);
self::replace_text_in_file($rownum, $filerecord, $record[$matchindex], $record[$replaceindex],
$record[$internalindex], $rowcounts, $errorhandler);
}
$totalrows++;
// Update the progress bar.
$progress->update_full(
100 * $totalrows / ($contentcount - 1), $rowcounts['success']. " Replaced, ".$rowcounts['skipped']." Skipped, "
100 * $rownum / $contentcount, $rowcounts['success']. " Replaced, ".$rowcounts['skipped']." Skipped, "
.$rowcounts['replacematch']." Already replaced, ".$rowcounts['error']." Errors."
);
}
$csvimport->cleanup();
$csvimport->close();
$errorhandler->finish();
}

/**
Expand All @@ -847,14 +855,16 @@ public static function get_replace_csv_content(int $draftid): string {
/**
* Replace a string in a file stored in Moodle's file storage. Supports both normal files and files inside zip archives.
*
* @param int $rownum Row number in CSV.
* @param array $filerecord File record
* @param string $match The string to search for in the file's contents.
* @param string $replace The string to replace the matched string with.
* @param string $internal The name of the internal file to modify (only used for zip files).
* @param array $rowcounts The count/outcome for each row.
*
* @param replace_error_handler $errorhandler
*/
public static function replace_text_in_file(array $filerecord, string $match, string $replace, string $internal, array &$rowcounts) {
public static function replace_text_in_file(int $rownum, array $filerecord, string $match, string $replace, string $internal,
array &$rowcounts, replace_error_handler $errorhandler) {
$fs = get_file_storage();
$file = $fs->get_file(
$filerecord['contextid'],
Expand All @@ -866,17 +876,17 @@ public static function replace_text_in_file(array $filerecord, string $match, st
);

if (!$file) {
mtrace(get_string('errorreplacingfilenotfound', 'tool_advancedreplace',
['filename' => $filerecord['filename']]));
$rowcounts['error']++;
$errorhandler->add([$rownum, $filerecord['component'], $filerecord['filearea'], $filerecord['filename'],
get_string('errorreplacingfilenotfound', 'tool_advancedreplace')]);
return;
}

// Specify tmp filename to avoid unique constraint conflict.
$filerecord['filename'] = time();

if ($filerecord['mimetype'] == 'application/zip' || $filerecord['mimetype'] == 'application/zip.h5p') {
if ($newzip = self::replace_text_in_zip($file, $match, $replace, $internal, $rowcounts)) {
if ($newzip = self::replace_text_in_zip($rownum, $file, $match, $replace, $internal, $rowcounts, $errorhandler)) {
$newfile = $fs->create_file_from_pathname($filerecord, $newzip);
} else {
return;
Expand All @@ -900,23 +910,25 @@ public static function replace_text_in_file(array $filerecord, string $match, st
$newfile->delete();
$rowcounts['success']++;
} else {
mtrace(get_string('errorreplacingfile', 'tool_advancedreplace',
['replace' => $match, 'filename' => $filerecord['filepath']]));
$rowcounts['error']++;
$errorhandler->add([$rownum, $filerecord['component'], $filerecord['filearea'], $file->get_filename(),
get_string('errorreplacingfile', 'tool_advancedreplace')]);
}
}

/**
* Extracts a file by name from a zip archive, replaces a string, and updates the zip file.
*
* @param int $rownum Row number in CSV.
* @param \stored_file $zipfile Name of the file to extract and modify inside the zip.
* @param string $searchstring The string to search for in the file's contents.
* @param string $replacestring The string to replace the search string with.
* @param string $internalfilename The file name of the internal file to be modified.
* @param array $rowcounts The count/outcome for each row.
* @param replace_error_handler $errorhandler
*/
public static function replace_text_in_zip(\stored_file $zipfile, string $searchstring,
string $replacestring, string $internalfilename, array &$rowcounts) {
public static function replace_text_in_zip(int $rownum, \stored_file $zipfile, string $searchstring, string $replacestring,
string $internalfilename, array &$rowcounts, replace_error_handler $errorhandler) {

// Create a temporary file path for working with the ZIP file.
$tempzip = make_request_directory() . '/' . $zipfile->get_filename();
Expand All @@ -926,6 +938,8 @@ public static function replace_text_in_zip(\stored_file $zipfile, string $search
$zip = new \ZipArchive();
if ($zip->open($tempzip) !== true) {
$rowcounts['error']++;
$errorhandler->add([$rownum, $zipfile->get_component(), $zipfile->get_filearea(), $internalfilename,
get_string('errorreplacingopenzip', 'tool_advancedreplace')]);
return false;
}

Expand All @@ -934,6 +948,8 @@ public static function replace_text_in_zip(\stored_file $zipfile, string $search
if ($fileindex === false) {
$zip->close();
$rowcounts['error']++;
$errorhandler->add([$rownum, $zipfile->get_component(), $zipfile->get_filearea(), $internalfilename,
get_string('errorreplacingfilenotfoundzip', 'tool_advancedreplace')]);
return false;
}

Expand All @@ -942,6 +958,8 @@ public static function replace_text_in_zip(\stored_file $zipfile, string $search
if ($filecontent === false) {
$zip->close();
$rowcounts['error']++;
$errorhandler->add([$rownum, $zipfile->get_component(), $zipfile->get_filearea(), $internalfilename,
get_string('errorreplacingcontentzip', 'tool_advancedreplace')]);
return false;
}

Expand Down
169 changes: 169 additions & 0 deletions classes/replace_error_handler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<?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;

/**
* Replace error handler.
*
* @package tool_advancedreplace
* @copyright 2025 Catalyst IT Australia Pty Ltd
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class replace_error_handler {

/** @var \flexible_table table */
protected $table = null;

/** @var array row data */
protected $rowdata = [];

/** @var string type of replace */
protected $type = '';

/** @var array db columns */
protected const DB_COLUMNS = [
'row',
'table',
'column',
'id',
'error',
];

/** @var array file columns */
protected const FILE_COLUMNS = [
'row',
'component',
'filearea',
'filename',
'error',
];

/** @var array cli character length mapping */
protected const CLI_CHAR_MAPPING = [
'row' => 10,
'id' => 10,
'table' => 32,
'column' => 32,
'component' => 24,
'filearea' => 24,
'filename' => 32,
'error' => 50,
];

/**
* Sets up error output.
* @return void
*/
protected function init(): void {
global $PAGE;

if (!CLI_SCRIPT) {
$table = new \flexible_table('error-table');
$table->baseurl = $PAGE->url;
$table->define_columns($this->get_columns());
$table->define_headers($this->get_headers());
$table->set_attribute('class', 'admintable generaltable');
$table->setup();
$this->table = $table;
}
}

/**
* Add a row of error output
* @param array $row output data
* @return void
*/
public function add(array $row): void {
if (!CLI_SCRIPT) {
if (!isset($this->table)) {
$this->init();
}
$this->table->add_data($row);
} else {
// Otherwise store data to print later.
$this->rowdata[] = $row;
}
}

/**
* Finish error output
* @return void
*/
public function finish(): void {
if (!CLI_SCRIPT) {
if (isset($this->table)) {
$this->table->finish_output();
}
} else if (!empty($this->rowdata)) {
// Print CLI error output at end.
$format = $this->get_cli_format();
$headers = $this->get_headers();
mtrace(sprintf($format, ...$headers));
foreach ($this->rowdata as $row) {
mtrace(sprintf($format, ...$row));
}
}
}

/**
* Sets the type of replace
* @param string $type
* @return void
*/
public function set_type(string $type): void {
$this->type = $type;
}

/**
* Gets columns for the error table
* @return array columns
*/
protected function get_columns(): array {
if ($this->type === 'db') {
return self::DB_COLUMNS;
} else if ($this->type === 'files') {
return self::FILE_COLUMNS;
}
return [];
}

/**
* Gets headers for the error table
* @return array headers
*/
protected function get_headers(): array {
$headers = [];
$columns = $this->get_columns();
foreach ($columns as $column) {
$headers[] = get_string('field_' . $column, 'tool_advancedreplace');
}
return $headers;
}

/**
* Gets the formatting of sprintf for CLI output
* @return string format
*/
protected function get_cli_format(): string {
$format = '';
$columns = $this->get_columns();
foreach ($columns as $column) {
$format .= '%-' . self::CLI_CHAR_MAPPING[$column] . 's ';
}
return trim($format);
}
}
Loading

0 comments on commit cb7e112

Please sign in to comment.