diff --git a/admin/dbtransfer/database_export_form.php b/admin/dbtransfer/database_export_form.php new file mode 100644 index 00000000000..9ea1e37750c --- /dev/null +++ b/admin/dbtransfer/database_export_form.php @@ -0,0 +1,15 @@ +libdir.'/formslib.php'; + +class database_export_form extends moodleform { + + function definition() { + $mform = $this->_form; + + $mform->addElement('header', 'database', get_string('dbexport', 'dbtransfer')); + $mform->addElement('textarea', 'description', get_string('description'), array('rows'=>5, 'cols'=>60)); + + $this->add_action_buttons(false, get_string('exportdata', 'dbtransfer')); + } +} diff --git a/admin/dbtransfer/database_transfer_form.php b/admin/dbtransfer/database_transfer_form.php new file mode 100644 index 00000000000..4dcd1d3164d --- /dev/null +++ b/admin/dbtransfer/database_transfer_form.php @@ -0,0 +1,47 @@ +libdir.'/formslib.php'; + +class database_transfer_form extends moodleform { + + function definition() { + $mform = $this->_form; + + $mform->addElement('header', 'database', get_string('dbtransfer', 'dbtransfer')); + + $supported = array ( + 'mysqli/adodb', + 'mysql/adodb', + 'postgres7/adodb', + 'mssql_n/adodb', + 'mssql/adodb', + 'odbc_mssql/adodb', + 'oci8po/adodb', + 'sqlite3/pdo', + ); + $drivers = array(); + foreach($supported as $driver) { + list($dbtype, $dblibrary) = explode('/', $driver); + $targetdb = moodle_database::get_driver_instance($dbtype, $dblibrary); + if ($targetdb->driver_installed() !== true) { + continue; + } + $drivers[$driver] = $driver; + } + + $mform->addElement('select', 'driver', get_string('dbtype', 'install'), $drivers); + $mform->addElement('text', 'dbhost', get_string('dbhost', 'install')); + $mform->addElement('text', 'dbname', get_string('database', 'install')); + $mform->addElement('text', 'dbuser', get_string('user')); + $mform->addElement('text', 'dbpass', get_string('password')); + $mform->addElement('text', 'prefix', get_string('dbprefix', 'install')); + + $mform->addRule('dbhost', get_string('required'), 'required', null); + $mform->addRule('dbname', get_string('required'), 'required', null); + $mform->addRule('dbuser', get_string('required'), 'required', null); + $mform->addRule('dbpass', get_string('required'), 'required', null); + $mform->addRule('prefix', get_string('required'), 'required', null); + + $this->add_action_buttons(false, get_string('transferdata', 'dbtransfer')); + } +} diff --git a/admin/dbtransfer/dbexport.php b/admin/dbtransfer/dbexport.php new file mode 100644 index 00000000000..02d28f0e0d0 --- /dev/null +++ b/admin/dbtransfer/dbexport.php @@ -0,0 +1,21 @@ +get_data()) { + dbtransfer_export_xml_database($data->description, $DB); + die; +} + +admin_externalpage_print_header(); +// TODO: add some more info here +$form->display(); +admin_externalpage_print_footer(); diff --git a/admin/dbtransfer/index.php b/admin/dbtransfer/index.php new file mode 100644 index 00000000000..cb25530dbeb --- /dev/null +++ b/admin/dbtransfer/index.php @@ -0,0 +1,34 @@ +get_data()) { + list($dbtype, $dblibrary) = explode('/', $data->driver); + $targetdb = moodle_database::get_driver_instance($dbtype, $dblibrary); + if (!$targetdb->connect($data->dbhost, $data->dbuser, $data->dbpass, $data->dbname, false, $data->prefix, null)) { + throw new dbtransfer_exception('notargetconectexception', null, "$CFG->wwwroot/$CFG->admin/dbtransfer/"); + } + if ($targetdb->get_tables()) { + // TODO add exception or string... + error('Sorry, tables already exist in selected database. Can not continue.'); + } + admin_externalpage_print_header(); + dbtransfer_transfer_database($DB, $targetdb); + notify(get_string('success'), 'notifysuccess'); + print_continue("$CFG->wwwroot/$CFG->admin/"); + admin_externalpage_print_footer(); + die; +} + +admin_externalpage_print_header(); +// TODO: add some more info here +$form->display(); +admin_externalpage_print_footer(); diff --git a/admin/dbtransfer/lib.php b/admin/dbtransfer/lib.php new file mode 100644 index 00000000000..4241c303950 --- /dev/null +++ b/admin/dbtransfer/lib.php @@ -0,0 +1,52 @@ +2GB fails in 32bit operating systems - needs warning + - we may run out of disk space exporting to srever file - we must verify the file is not truncated; read from the end of file? + - when sending file >4GB - FAT32 limit, Apache limit, browser limit - needs warning + - there must be some form of progress bar during export, transfer - new tracking class could be passed around + - command line operation - could work around some 2G/4G limits in PHP; useful for cron full backups + - by default allow exporting into empty database only (no tables with the same prefix yet) + - all dangerous operation (like deleting of all data) should be confirmed by key found in special file in dataroot + (user would need file access to dataroot which might prevent various "accidents") + - implement "Export/import running" notification in lib/setup.php (similar to new upgrade flag in config table) + - gzip compression when storing xml file - the xml is very verbose and full of repeated tags (zip is not suitable here at all) + this could help us keep the files bellow 2G (expected ratio is > 10:1) + +*/ + +require_once($CFG->libdir.'/adminlib.php'); +require_once($CFG->libdir.'/dtllib.php'); + + +function dbtransfer_export_xml_database($description, $mdb) { + @set_time_limit(0); + + session_write_close(); // release session + + header('Content-Type: application/xhtml+xml'); + header('Content-Disposition: attachment; filename=database.xml'); + header('Expires: 0'); + header('Cache-Control: must-revalidate,post-check=0,pre-check=0'); + header('Pragma: public'); + + while(@ob_flush()); + + $var = new file_xml_database_exporter('php://output', $mdb); + $var->export_database($description); + + // no more output + die; +} + + +function dbtransfer_transfer_database($sourcedb, $targetdb) { + @set_time_limit(0); + + session_write_close(); // release session + + $var = new database_mover($sourcedb, $targetdb); + $var->export_database(null); +} diff --git a/admin/settings/misc.php b/admin/settings/misc.php index b4cb7160976..21ef81e0d4d 100644 --- a/admin/settings/misc.php +++ b/admin/settings/misc.php @@ -5,10 +5,12 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page // Experimental settings page - $temp = new admin_settingpage('experimental', get_string('experimental', 'admin')); + $ADMIN->add('misc', new admin_category('experimental', get_string('experimental','admin'))); + + $temp = new admin_settingpage('experimentalsettings', get_string('experimentalsettings', 'admin')); $temp->add(new admin_setting_configcheckbox('enableglobalsearch', get_string('enableglobalsearch', 'admin'), get_string('configenableglobalsearch', 'admin'), 0)); $temp->add(new admin_setting_configcheckbox('smartpix', get_string('smartpix', 'admin'), get_string('configsmartpix', 'admin'), 0)); - $item = new admin_setting_configcheckbox('enablehtmlpurifier', get_string('enablehtmlpurifier', 'admin'), get_string('configenablehtmlpurifier', 'admin'), 0); + $item = new admin_setting_configcheckbox('enablehtmlpurifier', get_string('enablehtmlpurifier', 'admin'), get_string('configenablehtmlpurifier', 'admin'), 0); $item->set_updatedcallback('reset_text_filters_cache'); $temp->add($item); @@ -17,7 +19,11 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page $temp->add(new admin_setting_configcheckbox('enablecompletion', get_string('enablecompletion','completion'), get_string('configenablecompletion','completion'), COMPLETION_DISABLED)); $temp->add(new admin_setting_pickroles('progresstrackedroles', get_string('progresstrackedroles','completion'), get_string('configprogresstrackedroles', 'completion'), array('moodle/legacy:student'))); - $ADMIN->add('misc', $temp); + $ADMIN->add('experimental', $temp); + + // DB transfer related pages + $ADMIN->add('experimental', new admin_externalpage('dbtransfer', get_string('dbtransfer', 'dbtransfer'), $CFG->wwwroot.'/'.$CFG->admin.'/dbtransfer/index.php', 'moodle/site:config', false)); + $ADMIN->add('experimental', new admin_externalpage('dbexport', get_string('dbexport', 'dbtransfer'), $CFG->wwwroot.'/'.$CFG->admin.'/dbtransfer/dbexport.php', 'moodle/site:config', false)); // XMLDB editor $ADMIN->add('misc', new admin_externalpage('xmldbeditor', get_string('xmldbeditor'), "$CFG->wwwroot/$CFG->admin/xmldb/")); diff --git a/lang/en_utf8/admin.php b/lang/en_utf8/admin.php index a17d998c95b..88f29d7f064 100644 --- a/lang/en_utf8/admin.php +++ b/lang/en_utf8/admin.php @@ -361,6 +361,7 @@ $string['errors'] = 'Errors'; $string['errorsetting'] = 'Could not save setting:'; $string['errorwithsettings'] = 'Some settings were not changed due to an error.'; $string['experimental'] = 'Experimental'; +$string['experimentalsettings'] = 'Experimental settings'; $string['extendedusernamechars'] = 'Allow extended characters in usernames'; $string['filecreated'] = 'New file created'; $string['filestoredin'] = 'Save file into folder :'; diff --git a/lang/en_utf8/dbtransfer.php b/lang/en_utf8/dbtransfer.php new file mode 100644 index 00000000000..3adbaba86b6 --- /dev/null +++ b/lang/en_utf8/dbtransfer.php @@ -0,0 +1,13 @@ + $a'; +$string['importschemaexception'] = 'Current database structure does not match all install.xml files.
$a'; +$string['importversionmismatchexception'] = 'Current version $->currentver does match exported version $a->schemaver.'; +$string['malformedxmlexception'] = 'Malformed XML found, can not continue.'; +$string['notargetconectexception'] = 'Can not connect target database, sorry.'; +$string['transferdata'] = 'Transfer data'; +$string['unknowntableexception'] = 'Unknown table $a found in export file.'; diff --git a/lib/dtl/database_exporter.php b/lib/dtl/database_exporter.php index 2c6b573c28c..34789ab5a0b 100644 --- a/lib/dtl/database_exporter.php +++ b/lib/dtl/database_exporter.php @@ -104,25 +104,31 @@ abstract class database_exporter { * @see $check_schema is true), queries the database and calls * appropiate callbacks. * - * @exception export_exception if any checking (e.g. database schema) fails + * @exception dbtransfer_exception if any checking (e.g. database schema) fails * * @param string $description a user description of the data. */ public function export_database($description=null) { global $CFG; - if ($this->check_schema and $this->manager->check_database_schema($this->schema)) { - //TODO put message in error lang - throw new export_exception('XMLDB schema does not match database schema.'); + if ($this->check_schema and $errors = $this->manager->check_database_schema($this->schema)) { + $details = ''; + foreach ($errors as $table=>$items) { + $details .= '
'.get_string('table').' '.$table.':'; + $details .= '
'; + } + throw new dbtransfer_exception('exportschemaexception', $details); } $tables = $this->schema->getTables(); $this->begin_database_export($CFG->version, $CFG->release, date('c'), $description); foreach ($tables as $table) { $rs = $this->mdb->get_recordset_sql('SELECT * FROM {'.$table->getName().'}'); - //TODO remove this when dml will have exceptions if (!$rs) { - //TODO put message in error lang - throw new export_exception('An error occured while reading the database.'); + throw new ddl_table_missing_exception($table->getName()); } $this->begin_table_export($table); foreach ($rs as $row) { diff --git a/lib/dtl/database_importer.php b/lib/dtl/database_importer.php index 24cf79d7d90..e88f66c12c9 100644 --- a/lib/dtl/database_importer.php +++ b/lib/dtl/database_importer.php @@ -57,7 +57,7 @@ class database_importer { * operation, before any database changes are made. It will check the database * schema if @see check_schema is true * - * @exception import_exception if any checking (e.g. database schema, Moodle + * @exception dbtransfer_exception if any checking (e.g. database schema, Moodle * version) fails * * @param float $version the version of the system which generated the data @@ -73,13 +73,21 @@ class database_importer { } if (round($version, 2) !== round($CFG->version, 2)) { // version might be in decimal format too - //TODO put message in error lang - throw new import_exception('Current Moodle version does not match exported Moodle version.'); + $a = (object)array('schemaver'=>$version, 'currentver'=>$CFG->version); + throw new dbtransfer_exception('importversionmismatchexception', $a); } - if ($this->check_schema && $this->manager->check_database_schema($this->schema)) { - //TODO put message in error lang - throw new import_exception('XMLDB schema does not match database schema.'); + if ($this->check_schema and $errors = $this->manager->check_database_schema($this->schema)) { + $details = ''; + foreach ($errors as $table=>$items) { + $details .= '
'.get_string('table').' '.$table.':'; + $details .= '
'; + } + throw new dbtransfer_exception('importschemaexception', $details); } $this->mdb->begin_sql(); } @@ -88,7 +96,7 @@ class database_importer { * Callback function. Should be called only once per table import operation, * before any table changes are made. It will delete all table data. * - * @exception import_exception an unknown table import is attempted + * @exception dbtransfer_exception an unknown table import is attempted * @exception ddl_table_missing_exception if the table is missing * * @param string $tablename - the name of the table that will be imported @@ -97,16 +105,13 @@ class database_importer { */ public function begin_table_import($tablename, $schemaHash) { if (!$table = $this->schema->getTable($tablename)) { - //TODO put message in error lang - throw new import_exception('Unknown table in import data'); + throw new dbtransfer_exception('unknowntableexception', $tablename); } if ($schemaHash != $table->getHash()) { - throw new import_exception('XMLDB schema does not match database schema.'); + throw new dbtransfer_exception('differenttableexception', $tablename); } // this should not happen, unless someone drops tables after import started if (!$this->manager->table_exists($table)) { - // in the future, missing tables will be recreated with - //$this->manager->create_table($table); throw new ddl_table_missing_exception($tablename); } $this->mdb->delete_records($tablename); diff --git a/lib/dtl/database_mover.php b/lib/dtl/database_mover.php index cdadf243efe..415b753d8d1 100644 --- a/lib/dtl/database_mover.php +++ b/lib/dtl/database_mover.php @@ -7,15 +7,15 @@ class database_mover extends database_exporter { /** * Object constructor. * - * @param moodle_database $mdb_target Connection to the target database (a - * @see moodle_database object). * @param moodle_database $mdb Connection to the source database (a * @see moodle_database object). + * @param moodle_database $mdb_target Connection to the target database (a + * @see moodle_database object). * @param boolean $check_schema - whether or not to check that XML database * schema matches the RDBMS database schema before exporting (used by * @see export_database). */ - public function __construct(moodle_database $mdb_target, moodle_database $mdb_source, $check_schema=true) { + public function __construct(moodle_database $mdb_source, moodle_database $mdb_target, $check_schema=true) { parent::__construct($mdb_source, $check_schema); $this->importer = new database_importer($mdb_target, $check_schema); } diff --git a/lib/dtl/file_xml_database_exporter.php b/lib/dtl/file_xml_database_exporter.php index 6fec2e4610d..e0f5c3e268a 100644 --- a/lib/dtl/file_xml_database_exporter.php +++ b/lib/dtl/file_xml_database_exporter.php @@ -12,7 +12,7 @@ class file_xml_database_exporter extends xml_database_exporter { /** * Object constructor. * - * @param string $filepath - path to the XML data file. Use null for PHP + * @param string $filepath - path to the XML data file. Use 'php://output' for PHP * output stream. * @param moodle_database $mdb Connection to the source database * @see xml_database_exporter::__construct() @@ -20,9 +20,6 @@ class file_xml_database_exporter extends xml_database_exporter { * @see xml_database_exporter::__construct() */ public function __construct($filepath, moodle_database $mdb, $check_schema=true) { - if (is_null($filepath)) { - $filepath = 'php://output'; - } parent::__construct($mdb, $check_schema); $this->filepath = $filepath; } @@ -38,7 +35,7 @@ class file_xml_database_exporter extends xml_database_exporter { * Specific implementation for file exporting the database: it opens output stream, calls * superclass @see database_exporter::export_database() and closes output stream. * - * @exception export_exception if any checking (e.g. database schema) fails + * @exception dbtransfer_exception if any checking (e.g. database schema) fails * * @param string $description a user description of the data. */ diff --git a/lib/dtl/file_xml_database_importer.php b/lib/dtl/file_xml_database_importer.php index 8d6dfc63091..5a0ef0f22c9 100644 --- a/lib/dtl/file_xml_database_importer.php +++ b/lib/dtl/file_xml_database_importer.php @@ -10,7 +10,7 @@ class file_xml_database_importer extends xml_database_importer { /** * Object constructor. * - * @param string $filepath - path to the XML data file. Use null for PHP + * @param string $filepath - path to the XML data file. Use 'php://input' for PHP * input stream. * @param moodle_database $mdb Connection to the target database * @see xml_database_importer::__construct() @@ -18,9 +18,6 @@ class file_xml_database_importer extends xml_database_importer { * @see xml_database_importer::__construct() */ public function __construct($filepath, moodle_database $mdb, $check_schema=true) { - if (is_null($filepath)) { - $filepath = 'php://input'; - } $this->filepath = $filepath; parent::__construct($mdb, $check_schema); } @@ -35,8 +32,7 @@ class file_xml_database_importer extends xml_database_importer { $parser = $this->get_parser(); while ($data = fread($file, 65536)) { if (!xml_parse($parser, $data, feof($file))) { - //TODO localize - throw new import_exception("XML data not well-formed."); + throw new dbtransfer_exception('malformedxmlexception'); } } xml_parser_free($parser); diff --git a/lib/dtl/string_xml_database_exporter.php b/lib/dtl/string_xml_database_exporter.php index 84649929191..3bc9e8e641e 100644 --- a/lib/dtl/string_xml_database_exporter.php +++ b/lib/dtl/string_xml_database_exporter.php @@ -26,7 +26,7 @@ class string_xml_database_exporter extends xml_database_exporter { * Specific implementation for memory exporting the database: it clear the buffer * and calls superclass @see database_exporter::export_database(). * - * @exception export_exception if any checking (e.g. database schema) fails + * @exception dbtransfer_exception if any checking (e.g. database schema) fails * @param string $description a user description of the data. * @return void */ diff --git a/lib/dtl/string_xml_database_importer.php b/lib/dtl/string_xml_database_importer.php index 983d566df5d..42e8a1391b5 100644 --- a/lib/dtl/string_xml_database_importer.php +++ b/lib/dtl/string_xml_database_importer.php @@ -29,8 +29,7 @@ class string_xml_database_importer extends xml_database_importer { public function import_database() { $parser = $this->get_parser(); if (!xml_parse($parser, $this->data, true)) { - //TODO localize - throw new import_exception("XML data not well-formed."); + throw new dbtransfer_exception('malformedxmlexception'); } xml_parser_free($parser); } diff --git a/lib/dtl/xml_database_importer.php b/lib/dtl/xml_database_importer.php index 4badbc2edb7..4f4e9c6f567 100644 --- a/lib/dtl/xml_database_importer.php +++ b/lib/dtl/xml_database_importer.php @@ -39,29 +39,29 @@ abstract class xml_database_importer extends database_importer { switch ($tag) { case 'moodle_database' : if (empty($attributes['version']) || empty($attributes['timestamp'])) { - throw new import_exception('Missing tag attribute in data file.'); + throw new dbtransfer_exception('malformedxmlexception'); } $this->begin_database_import($attributes['version'], $attributes['timestamp']); break; case 'table' : if (isset($this->current_table)) { - throw new import_exception('Unexpected tag in data file.'); + throw new dbtransfer_exception('malformedxmlexception'); } if (empty($attributes['name']) || empty($attributes['schemaHash'])) { - throw new import_exception('Missing tag attribute in data file.'); + throw new dbtransfer_exception('malformedxmlexception'); } $this->current_table = $attributes['name']; $this->begin_table_import($this->current_table, $attributes['schemaHash']); break; case 'record' : if (isset($this->current_row) || !isset($this->current_table)) { - throw new import_exception('Unexpected tag in data file.'); + throw new dbtransfer_exception('malformedxmlexception'); } $this->current_row = new object(); break; case 'field' : if (isset($this->current_field) || !isset($this->current_row)) { - throw new import_exception('Unexpected tag in data file.'); + throw new dbtransfer_exception('malformedxmlexception'); } $this->current_field = $attributes['name']; $this->current_data = ''; @@ -72,8 +72,7 @@ abstract class xml_database_importer extends database_importer { } break; default : - //TODO localize - throw new import_exception('XML content not valid for import operation.'); + throw new dbtransfer_exception('malformedxmlexception'); } } @@ -113,8 +112,7 @@ abstract class xml_database_importer extends database_importer { break; default : - //TODO put message in error lang - throw new import_exception('XML content not valid for import operation.'); + throw new dbtransfer_exception('malformedxmlexception'); } } diff --git a/lib/dtllib.php b/lib/dtllib.php index d5e8874fc10..34ce69436a7 100644 --- a/lib/dtllib.php +++ b/lib/dtllib.php @@ -41,23 +41,16 @@ require_once($CFG->libdir.'/dtl/file_xml_database_importer.php'); require_once($CFG->libdir.'/dtl/string_xml_database_importer.php'); /** - * Exception class for export operations. + * Exception class for db transfer * @see moodle_exception - * TODO subclass for specific purposes */ -class export_exception extends moodle_exception { - function __construct($errorcode, $a=null, $debuginfo=null) { - parent::__construct($errorcode, '', '', $a, $debuginfo); +class dbtransfer_exception extends moodle_exception { + function __construct($errorcode, $a=null, $link='', $debuginfo=null) { + global $CFG; + if (empty($link)) { + $link = "$CFG->wwwroot/$CFG->admin/"; + } + parent::__construct($errorcode, 'dbtransfer', $link, $a, $debuginfo); } } -/** - * Exception class for import operations. - * @see moodle_exception - * TODO subclass for specific purposes - */ -class import_exception extends moodle_exception { - function __construct($errorcode, $a=null, $debuginfo=null) { - parent::__construct($errorcode, '', '', $a, $debuginfo); - } -}