Skip to content

Commit

Permalink
MDL-31625 fix multiple global search-replace issues
Browse files Browse the repository at this point in the history
Includes following fixes:
* support for MS SQL Server
* optional trimming of of oversized VARCHAR fields
* conversion to forms library
* full localisation
* other cleanup
  • Loading branch information
skodak committed Dec 6, 2013
1 parent c36a240 commit 6c3ae51
Show file tree
Hide file tree
Showing 11 changed files with 241 additions and 45 deletions.
69 changes: 69 additions & 0 deletions admin/tool/replace/classes/form.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?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/>.

/**
* Site wide search-replace form.
*
* @package tool_replace
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

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

require_once("$CFG->libdir/formslib.php");

/**
* Site wide search-replace form.
*/
class tool_replace_form extends moodleform {
function definition() {
global $CFG, $DB;

$mform = $this->_form;

$mform->addElement('header', 'searchhdr', get_string('pluginname', 'tool_replace'));
$mform->setExpanded('searchhdr', true);

$mform->addElement('text', 'search', get_string('searchwholedb', 'tool_replace'), 'size="50"');
$mform->setType('search', PARAM_RAW);
$mform->addElement('static', 'searchst', '', get_string('searchwholedbhelp', 'tool_replace'));
$mform->addRule('search', get_string('required'), 'required', null, 'client');

$mform->addElement('text', 'replace', get_string('replacewith', 'tool_replace'), 'size="50"', PARAM_RAW);
$mform->addElement('static', 'replacest', '', get_string('replacewithhelp', 'tool_replace'));
$mform->setType('replace', PARAM_RAW);
$mform->addElement('checkbox', 'shorten', get_string('shortenoversized', 'tool_replace'));
$mform->addRule('replace', get_string('required'), 'required', null, 'client');

$mform->addElement('header', 'confirmhdr', get_string('confirm'));
$mform->setExpanded('confirmhdr', true);
$mform->addElement('checkbox', 'sure', get_string('disclaimer', 'tool_replace'));
$mform->addRule('sure', get_string('required'), 'required', null, 'client');

$this->add_action_buttons(false, get_string('doit', 'tool_replace'));
}

function validation($data, $files) {
$errors = parent::validation($data, $files);

if (empty($data['shorten']) and core_text::strlen($data['search']) < core_text::strlen($data['replace'])) {
$errors['shorten'] = get_string('required');
}

return $errors;
}
}
51 changes: 16 additions & 35 deletions admin/tool/replace/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
/**
* Search and replace strings throughout all texts in the whole database
*
* @package tool
* @subpackage replace
* @package tool_replace
* @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
Expand All @@ -31,56 +30,38 @@

admin_externalpage_setup('toolreplace');

$search = optional_param('search', '', PARAM_RAW);
$replace = optional_param('replace', '', PARAM_RAW);
$sure = optional_param('sure', 0, PARAM_BOOL);

###################################################################
echo $OUTPUT->header();

echo $OUTPUT->heading(get_string('pageheader', 'tool_replace'));

if ($DB->get_dbfamily() !== 'mysql' and $DB->get_dbfamily() !== 'postgres') {
//TODO: add $DB->text_replace() to DML drivers
if (!$DB->replace_all_text_supported()) {
echo $OUTPUT->notification(get_string('notimplemented', 'tool_replace'));
echo $OUTPUT->footer();
die;
}

if (!data_submitted() or !$search or !$replace or !confirm_sesskey() or !$sure) { /// Print a form
echo $OUTPUT->notification(get_string('notsupported', 'tool_replace'));
echo $OUTPUT->notification(get_string('excludedtables', 'tool_replace'));
echo $OUTPUT->box_start();
echo $OUTPUT->notification(get_string('notsupported', 'tool_replace'));
echo $OUTPUT->notification(get_string('excludedtables', 'tool_replace'));
echo $OUTPUT->box_end();

$form = new tool_replace_form();

echo $OUTPUT->box_start();
echo '<div class="mdl-align">';
echo '<form action="index.php" method="post"><div>';
echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
echo '<div><label for="search">'.get_string('searchwholedb', 'tool_replace').
' </label><input id="search" type="text" name="search" size="40" /> ('.
get_string('searchwholedbhelp', 'tool_replace').')</div>';
echo '<div><label for="replace">'.get_string('replacewith', 'tool_replace').
' </label><input type="text" id="replace" name="replace" size="40" /> ('.
get_string('replacewithhelp', 'tool_replace').')</div>';
echo '<div><label for="sure">'.get_string('disclaimer', 'tool_replace').' </label><input type="checkbox" id="sure" name="sure" value="1" /></div>';
echo '<div class="buttons"><input type="submit" class="singlebutton" value="Yes, do it now" /></div>';
echo '</div></form>';
echo '</div>';
echo $OUTPUT->box_end();
if (!$data = $form->get_data()) {
$form->display();
echo $OUTPUT->footer();
die;
die();
}

// Scroll to the end when finished.
$PAGE->requires->js_init_code("window.scrollTo(0, 5000000);");

echo $OUTPUT->box_start();
db_replace($search, $replace);
db_replace($data->search, $data->replace);
echo $OUTPUT->box_end();

/// Rebuild course cache which might be incorrect now
echo $OUTPUT->notification(get_string('notifyrebuilding', 'tool_replace'), 'notifysuccess');
rebuild_course_cache();
echo $OUTPUT->notification(get_string('notifyfinished', 'tool_replace'), 'notifysuccess');
// Course caches are now rebuilt on the fly.

echo $OUTPUT->continue_button(new moodle_url('/admin/index.php'));

echo $OUTPUT->footer();


11 changes: 7 additions & 4 deletions admin/tool/replace/lang/en/tool_replace.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,18 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

$string['disclaimer'] = 'I understand the risks of this operation:';
$string['cannotfit'] = 'The replacement is longer than original and shortening is not allow, cannot continue.';
$string['disclaimer'] = 'I understand the risks of this operation';
$string['doit'] = 'Yes, do it!';
$string['excludedtables'] = 'Several tables are not updated as part of the text replacement. This include configuration, log, events, and session tables.';
$string['pageheader'] = 'Search and replace text throughout the whole database';
$string['notifyfinished'] = '...finished';
$string['notifyrebuilding'] = 'Rebuilding course cache...';
$string['notimplemented'] = 'Sorry, this feature is implemented only for MySQL and PostgreSQL databases.';
$string['notimplemented'] = 'Sorry, this feature is not implemented in your database driver.';
$string['notsupported'] ='This script is not supported, always make complete backup before proceeding!<br />This operation can not be reverted!';
$string['pluginname'] = 'DB search and replace';
$string['replacewith'] = 'Replace with this string:';
$string['replacewith'] = 'Replace with this string';
$string['replacewithhelp'] = 'usually new server URL';
$string['searchwholedb'] = 'Search whole database for:';
$string['searchwholedb'] = 'Search whole database for';
$string['searchwholedbhelp'] = 'usually previous server URL';
$string['shortenoversized'] = 'Shorten result if necessary';
2 changes: 1 addition & 1 deletion admin/tool/replace/version.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

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

$plugin->version = 2013110500; // The current plugin version (Date: YYYYMMDDXX)
$plugin->version = 2013110501; // The current plugin version (Date: YYYYMMDDXX)
$plugin->requires = 2013110500; // Requires this Moodle version
$plugin->component = 'tool_replace'; // Full name of the plugin (used for diagnostics)

Expand Down
9 changes: 4 additions & 5 deletions lib/adminlib.php
Original file line number Diff line number Diff line change
Expand Up @@ -6741,11 +6741,8 @@ function db_replace($search, $replace) {

if ($columns = $DB->get_columns($table)) {
$DB->set_debug(true);
foreach ($columns as $column => $data) {
if (in_array($data->meta_type, array('C', 'X'))) { // Text stuff only
//TODO: this should be definitively moved to DML driver to do the actual replace, this is not going to work for MSSQL and Oracle...
$DB->execute("UPDATE {".$table."} SET $column = REPLACE($column, ?, ?)", array($search, $replace));
}
foreach ($columns as $column) {
$DB->replace_all_text($table, $column, $search, $replace);
}
$DB->set_debug(false);
}
Expand Down Expand Up @@ -6776,6 +6773,8 @@ function db_replace($search, $replace) {
echo $OUTPUT->notification("...finished", 'notifysuccess');
}

purge_all_caches();

return true;
}

Expand Down
46 changes: 46 additions & 0 deletions lib/dml/moodle_database.php
Original file line number Diff line number Diff line change
Expand Up @@ -2126,6 +2126,52 @@ public function sql_regex($positivematch=true) {
return '';
}

/**
* Does this driver support tool_replace?
*
* @since 2.6.1
* @return bool
*/
public function replace_all_text_supported() {
return false;
}

/**
* Replace given text in all rows of column.
*
* @since 2.6.1
* @param string $table name of the table
* @param database_column_info $column
* @param string $search
* @param string $replace
*/
public function replace_all_text($table, database_column_info $column, $search, $replace) {
if (!$this->replace_all_text_supported()) {
return;
}

// NOTE: override this methods if following standard compliant SQL
// does not work for your driver.

$columnname = $column->name;
$sql = "UPDATE {".$table."}
SET $columnname = REPLACE($columnname, ?, ?)
WHERE $columnname IS NOT NULL";

if ($column->meta_type === 'X') {
$this->execute($sql, array($search, $replace));

} else if ($column->meta_type === 'C') {
if (core_text::strlen($search) < core_text::strlen($replace)) {
$colsize = $column->max_length;
$sql = "UPDATE {".$table."}
SET $columnname = SUBSTRING(REPLACE($columnname, ?, ?), 1, $colsize)
WHERE $columnname IS NOT NULL";
}
$this->execute($sql, array($search, $replace));
}
}

/**
* Checks and returns true if transactions are supported.
*
Expand Down
10 changes: 10 additions & 0 deletions lib/dml/mssql_native_moodle_database.php
Original file line number Diff line number Diff line change
Expand Up @@ -1252,6 +1252,16 @@ public function sql_substr($expr, $start, $length=false) {
}
}

/**
* Does this driver support tool_replace?
*
* @since 2.6.1
* @return bool
*/
public function replace_all_text_supported() {
return true;
}

public function session_lock_supported() {
return true;
}
Expand Down
10 changes: 10 additions & 0 deletions lib/dml/mysqli_native_moodle_database.php
Original file line number Diff line number Diff line change
Expand Up @@ -1392,6 +1392,16 @@ public function sql_cast_2signed($fieldname) {
return ' CAST(' . $fieldname . ' AS SIGNED) ';
}

/**
* Does this driver support tool_replace?
*
* @since 2.6.1
* @return bool
*/
public function replace_all_text_supported() {
return true;
}

public function session_lock_supported() {
return true;
}
Expand Down
10 changes: 10 additions & 0 deletions lib/dml/pgsql_native_moodle_database.php
Original file line number Diff line number Diff line change
Expand Up @@ -1216,6 +1216,16 @@ public function sql_regex($positivematch=true) {
return $positivematch ? '~*' : '!~*';
}

/**
* Does this driver support tool_replace?
*
* @since 2.6.1
* @return bool
*/
public function replace_all_text_supported() {
return true;
}

public function session_lock_supported() {
return true;
}
Expand Down
10 changes: 10 additions & 0 deletions lib/dml/sqlsrv_native_moodle_database.php
Original file line number Diff line number Diff line change
Expand Up @@ -1314,6 +1314,16 @@ public function sql_substr($expr, $start, $length = false) {
}
}

/**
* Does this driver support tool_replace?
*
* @since 2.6.1
* @return bool
*/
public function replace_all_text_supported() {
return true;
}

public function session_lock_supported() {
return true;
}
Expand Down
58 changes: 58 additions & 0 deletions lib/dml/tests/dml_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -4234,6 +4234,64 @@ public function test_get_records_sql_complicated() {
$this->assertCount($currentcount, $results);
}

public function test_replace_all_text() {
$DB = $this->tdb;
$dbman = $DB->get_manager();

if (!$DB->replace_all_text_supported()) {
$this->markTestSkipped($DB->get_name().' does not support replacing of texts');
}

$table = $this->get_test_table();
$tablename = $table->getName();

$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('name', XMLDB_TYPE_CHAR, '20', null, null);
$table->add_field('intro', XMLDB_TYPE_TEXT, 'big', null, null);
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
$dbman->create_table($table);

$id1 = (string)$DB->insert_record($tablename, array('name' => null, 'intro' => null));
$id2 = (string)$DB->insert_record($tablename, array('name' => '', 'intro' => ''));
$id3 = (string)$DB->insert_record($tablename, array('name' => 'xxyy', 'intro' => 'vvzz'));
$id4 = (string)$DB->insert_record($tablename, array('name' => 'aa bb aa bb', 'intro' => 'cc dd cc aa'));
$id5 = (string)$DB->insert_record($tablename, array('name' => 'kkllll', 'intro' => 'kkllll'));

$expected = $DB->get_records($tablename, array(), 'id ASC');

$columns = $DB->get_columns($tablename);

$DB->replace_all_text($tablename, $columns['name'], 'aa', 'o');
$result = $DB->get_records($tablename, array(), 'id ASC');
$expected[$id4]->name = 'o bb o bb';
$this->assertEquals($expected, $result);

$DB->replace_all_text($tablename, $columns['intro'], 'aa', 'o');
$result = $DB->get_records($tablename, array(), 'id ASC');
$expected[$id4]->intro = 'cc dd cc o';
$this->assertEquals($expected, $result);

$DB->replace_all_text($tablename, $columns['name'], '_', '*');
$DB->replace_all_text($tablename, $columns['name'], '?', '*');
$DB->replace_all_text($tablename, $columns['name'], '%', '*');
$DB->replace_all_text($tablename, $columns['intro'], '_', '*');
$DB->replace_all_text($tablename, $columns['intro'], '?', '*');
$DB->replace_all_text($tablename, $columns['intro'], '%', '*');
$result = $DB->get_records($tablename, array(), 'id ASC');
$this->assertEquals($expected, $result);

$long = '1234567890123456789';
$DB->replace_all_text($tablename, $columns['name'], 'kk', $long);
$result = $DB->get_records($tablename, array(), 'id ASC');
$expected[$id5]->name = core_text::substr($long.'llll', 0, 20);
$this->assertEquals($expected, $result);

$DB->replace_all_text($tablename, $columns['intro'], 'kk', $long);
$result = $DB->get_records($tablename, array(), 'id ASC');
$expected[$id5]->intro = $long.'llll';
$this->assertEquals($expected, $result);
}

public function test_onelevel_commit() {
$DB = $this->tdb;
$dbman = $DB->get_manager();
Expand Down

0 comments on commit 6c3ae51

Please sign in to comment.