MDL-31625 fix multiple global search-replace issues

Includes following fixes:
* support for MS SQL Server
* optional trimming of of oversized VARCHAR fields
* conversion to forms library
* full localisation
* other cleanup
This commit is contained in:
Petr Škoda
2013-12-06 09:52:47 +08:00
parent 3e6e38b2e3
commit b4d2e7f267
11 changed files with 245 additions and 49 deletions
+69
View File
@@ -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;
}
}
+20 -39
View File
@@ -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
*/
@@ -31,56 +30,38 @@ require_once($CFG->libdir.'/adminlib.php');
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 '<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();
echo $OUTPUT->footer();
die;
}
echo $OUTPUT->box_start();
db_replace($search, $replace);
echo $OUTPUT->notification(get_string('notsupported', 'tool_replace'));
echo $OUTPUT->notification(get_string('excludedtables', 'tool_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');
$form = new tool_replace_form();
if (!$data = $form->get_data()) {
$form->display();
echo $OUTPUT->footer();
die();
}
// Scroll to the end when finished.
$PAGE->requires->js_init_code("window.scrollTo(0, 5000000);");
echo $OUTPUT->box_start();
db_replace($data->search, $data->replace);
echo $OUTPUT->box_end();
// Course caches are now rebuilt on the fly.
echo $OUTPUT->continue_button(new moodle_url('/admin/index.php'));
echo $OUTPUT->footer();
+7 -4
View File
@@ -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';
+1 -1
View File
@@ -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)
+4 -5
View File
@@ -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);
}
@@ -6776,6 +6773,8 @@ function db_replace($search, $replace) {
echo $OUTPUT->notification("...finished", 'notifysuccess');
}
purge_all_caches();
return true;
}
+46
View File
@@ -2126,6 +2126,52 @@ abstract class moodle_database {
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.
*
+10
View File
@@ -1252,6 +1252,16 @@ s only returning name of SQL substring function, it now requires all parameters.
}
}
/**
* 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;
}
+10
View File
@@ -1392,6 +1392,16 @@ class mysqli_native_moodle_database extends moodle_database {
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;
}
+10
View File
@@ -1216,6 +1216,16 @@ class pgsql_native_moodle_database extends moodle_database {
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;
}
+10
View File
@@ -1314,6 +1314,16 @@ class sqlsrv_native_moodle_database extends moodle_database {
}
}
/**
* 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;
}
+58
View File
@@ -4234,6 +4234,64 @@ class core_dml_testcase extends database_driver_testcase {
$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();