diff --git a/setup/cli/modules/export.php b/setup/cli/modules/export.php index 3b7589536fc8da44fd43b000e1be9c9412158e30..33d1320523b36db0b2299261093bc9cc391b25e2 100644 --- a/setup/cli/modules/export.php +++ b/setup/cli/modules/export.php @@ -18,6 +18,8 @@ require_once dirname(__file__) . "/class.module.php"; require_once dirname(__file__) . '/../../../main.inc.php'; require_once INCLUDE_DIR . 'class.json.php'; +require_once INCLUDE_DIR . 'class.migrater.php'; +require_once INCLUDE_DIR . 'class.signal.php'; define('OSTICKET_BACKUP_SIGNATURE', 'osTicket-Backup'); define('OSTICKET_BACKUP_VERSION', 'A'); @@ -30,16 +32,19 @@ class Exporter extends Module { FILE_CHUNK_TABLE, STAFF_TABLE, DEPT_TABLE, TOPIC_TABLE, GROUP_TABLE, GROUP_DEPT_TABLE, TEAM_TABLE, TEAM_MEMBER_TABLE, FAQ_TABLE, FAQ_ATTACHMENT_TABLE, FAQ_TOPIC_TABLE, FAQ_CATEGORY_TABLE, - CANNED_TABLE, CANNED_ATTACHMENT_TABLE, TICKET_TABLE, TICKET_THREAD_TABLE, - TICKET_ATTACHMENT_TABLE, TICKET_PRIORITY_TABLE, PRIORITY_TABLE, + CANNED_TABLE, CANNED_ATTACHMENT_TABLE, TICKET_TABLE, + TICKET_THREAD_TABLE, TICKET_ATTACHMENT_TABLE, TICKET_PRIORITY_TABLE, TICKET_LOCK_TABLE, TICKET_EVENT_TABLE, TICKET_EMAIL_INFO_TABLE, - EMAIL_TABLE, EMAIL_TEMPLATE_TABLE, FILTER_TABLE, FILTER_RULE_TABLE, - SLA_TABLE, API_KEY_TABLE, TIMEZONE_TABLE, PAGE_TABLE); + EMAIL_TABLE, EMAIL_TEMPLATE_TABLE, EMAIL_TEMPLATE_GRP_TABLE, + FILTER_TABLE, FILTER_RULE_TABLE, SLA_TABLE, API_KEY_TABLE, + TIMEZONE_TABLE, SESSION_TABLE, PAGE_TABLE); var $options = array( - 'stream' => array('-o', '--output', 'default'=>'compress.zlib://php://stdout', - "File or stream to receive the exported output. As a default, - zlib compressed output is sent to standard out.") + 'stream' => array('-o', '--output', 'default'=>'php://stdout', + 'help'=> "File or stream to receive the exported output. As a + default, zlib compressed output is sent to standard out."), + 'compress' => array('-z', '--compress', 'action'=>'store_true', + 'help'=> "Send zlib compress data to the output stream"), ); var $stream; @@ -51,13 +56,23 @@ class Exporter extends Module { function run($args, $options) { global $ost; - $this->stream = fopen($options['stream'], 'w'); + + $stream = $options['stream']; + if ($options['compress']) $stream = "compress.zlib://$stream"; + $this->stream = fopen($stream, 'w'); + + // Allow plugins to change the tables exported + Signal::send('export.tables', $this, $this->tables); + $header = array( array(OSTICKET_BACKUP_SIGNATURE, OSTICKET_BACKUP_VERSION), array( 'version'=>THIS_VERSION, 'table_prefix'=>TABLE_PREFIX, 'salt'=>SECRET_SALT, + 'dbtype'=>DBTYPE, + 'streams'=>DatabaseMigrater::getUpgradeStreams( + UPGRADE_DIR . 'streams/'), ), ); $this->write_block($header); @@ -77,6 +92,11 @@ class Exporter extends Module { $res = db_query("select * from $t"); $types = array(); + if (!$table) { + $this->stderr->write( + $t.': Cannot export table with no fields'."\n"); + die(); + } $this->write_block( array('table', substr($t, strlen(TABLE_PREFIX)), $table, $indexes)); diff --git a/setup/cli/modules/import.php b/setup/cli/modules/import.php new file mode 100644 index 0000000000000000000000000000000000000000..461b60ab0005f358885bc4fa2fc5e79a530c2c6c --- /dev/null +++ b/setup/cli/modules/import.php @@ -0,0 +1,241 @@ +<? +/********************************************************************* + cli/import.php + + osTicket data importer, used for migration and backup recovery + + Jared Hancock <jared@osticket.com> + Copyright (c) 2006-2013 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + See LICENSE.TXT for details. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ +require_once dirname(__file__) . "/class.module.php"; + +require_once dirname(__file__) . '/../../../main.inc.php'; + +require_once INCLUDE_DIR . 'class.json.php'; + +class Importer extends Module { + var $prologue = + "Imports data from a previous backup (using the exporter)"; + + var $options = array( + 'stream' => array('-i', '--input', 'default'=>'php://stdin', + 'metavar'=>'FILE', 'help'=> + "File or stream from which to read the export. As a default, + data is received from standard in."), + 'compress' => array('-z', '--compress', 'action'=>'store_true', + 'help'=>'Read zlib compressed data (use -z with the export + command)'), + 'tables' => array('-t', '--table', 'action'=>'append', + 'metavar'=>'TABLE', 'help'=> + "Table to be restored from the backup. Default is to restore all + tables. This option can be specified more than once"), + 'drop' => array('-D', '--drop', 'action'=>'store_true', 'help'=> + 'Issue DROP TABLE statements before the create statemente'), + ); + + var $epilog = + "The SQL of the import is written to standard outout"; + + var $stream; + var $header; + var $source_ost_info; + + function verify_header() { + list($header, $info) = $this->read_block(); + if (!$header || $header[0] != OSTICKET_BACKUP_SIGNATURE) { + $this->stderr->write('Header mismatch -- not an osTicket backup'); + return false; + } + else + $this->header = $header; + + if (!$info || $info['dbtype'] != 'mysql') { + $this->stderr->write('Only mysql imports are supported currently'); + return false; + } + $this->source_ost_info = $info; + return true; + } + + function read_block() { + $block = ''; + while (!feof($this->stream) && (($c = fgetc($this->stream)) != "\x1e")) + $block .= $c; + + if ($json = JsonDataParser::decode($block)) + return $json; + + if (strlen($block)) { + $this->stderr->write("Unable to read block from input"); + die(); + } + } + + function send_statement($stmt) { + if ($this->getOption('prime-time')) + db_query($stmt); + else { + $this->stdout->write($stmt); + $this->stdout->write(";\n"); + } + } + + function import_table() { + if (!($header = $this->read_block())) + return false; + + else if ($header[0] != 'table') { + $this->stderr->write('Unable to read table header'); + return false; + } + + // TODO: Consider included tables and excluded tables + + $this->stderr->write("Importing table: {$header[1]}\n"); + $this->create_table($header); + $this->create_indexes($header); + + while (($row=$this->read_block())) { + if (isset($row[0]) && ($row[0] == 'end-table')) { + $this->load_row(null, null, true); + return true; + } + $this->load_row($header, $row); + } + return false; + } + + function create_table($info) { + if ($this->getOption('drop')) + $this->send_statement('DROP TABLE IF EXISTS `'.TABLE_PREFIX.'`'); + $sql = 'CREATE TABLE `'.TABLE_PREFIX.$info[1].'` ('; + $pk = array(); + $fields = array(); + foreach ($info[2] as $col) { + $field = "`{$col['Field']}` {$col['Type']}"; + if ($col['Null'] == 'NO') + $field .= ' NOT NULL '; + if ($col['Default'] == 'CURRENT_TIMESTAMP') + $field .= ' DEFAULT CURRENT_TIMESTAMP'; + elseif ($col['Default'] !== null) + $field .= ' DEFAULT '.db_input($col['Default']); + $field .= ' '.$col['Extra']; + $fields[] = $field; + } + // Generate PRIMARY KEY + foreach ($info[3] as $idx) { + if ($idx['Key_name'] == 'PRIMARY') { + $col = '`'.$idx['Column_name'].'`'; + if ($idx['Collation'] != 'A') + $col .= ' DESC'; + $pk[(int)$idx['Seq_in_index']] = $col; + } + } + $sql .= implode(", ", $fields); + if ($pk) + $sql .= ', PRIMARY KEY ('.implode(',',$pk).')'; + $sql .= ') DEFAULT CHARSET=utf8'; + $queries[] = $sql; + $this->send_statement($sql); + } + + function create_indexes($header) { + $indexes = array(); + foreach ($header[3] as $idx) { + if ($idx['Key_name'] == 'PRIMARY') + continue; + if (!isset($indexes[$idx['Key_name']])) + $indexes[$idx['Key_name']] = array( + 'cols'=>array(), + // XXX: Drop table-prefix + 'table'=>substr($idx['Table'], + strlen($this->source_ost_info['table_prefix'])), + 'type'=>$idx['Index_type'], + 'unique'=>!$idx['Non_unique']); + $index = &$indexes[$idx['Key_name']]; + $col = '`'.$idx['Column_name'].'`'; + if ($idx['Collation'] != 'A') + $col .= ' DESC'; + $index[(int)$idx['Seq_in_index']] = $col; + $index['cols'][] = $col; + } + foreach ($indexes as $name=>$info) { + $cols = array(); + $this->send_statement('CREATE ' + .(($info['unique']) ? 'UNIQUE ' : '') + .'INDEX `'.$name + .'` USING '.$info['type'] + .' ON `'.TABLE_PREFIX.$info['table'].'` (' + .implode(',', $info['cols']) + .')'); + } + } + + function truncate_table($info) { + $this->send_statement('TRUNCATE TABLE '.TABLE_PREFIX.$info[1]); + $indexes = array(); + foreach ($info[3] as $idx) { + if ($idx['Key_name'] == 'PRIMARY') + continue; + $indexes[$idx['Key_name']] = + '`'.TABLE_PREFIX.$info[1].'`.`'.$idx['Key_name'].'`'; + } + foreach ($indexes as $T=>$fqn) + $this->send_statement('DROP INDEX IF EXISTS '.$fqn); + } + + function load_row($info, $row, $flush=false) { + static $header = null; + static $rows = array(); + static $length = 0; + + if ($info && $header === null) { + $header = "INSERT INTO `".TABLE_PREFIX.$info[1].'` ('; + $cols = array(); + foreach ($info[2] as $col) + $cols[] = "`{$col['Field']}`"; + $header .= implode(', ', $cols); + $header .= ") VALUES "; + } + if ($row) { + $values = array(); + foreach ($info[2] as $i=>$col) + $values[] = (is_numeric($row[$i])) + ? $row[$i] + : ($row[$i] ? '0x'.bin2hex($row[$i]) : "''"); + $values = "(" . implode(', ', $values) . ")"; + $length += strlen($values); + $rows[] = &$values; + } + if (($flush || $length > 16000) && $header) { + $this->send_statement($header . implode(',', $rows)); + $header = null; + $rows = array(); + $length = 0; + } + } + + function run($args, $options) { + $stream = $options['stream']; + if ($options['compress']) $stream = "compress.zlib://$stream"; + if (!($this->stream = fopen($stream, 'rb'))) { + $this->stderr->write('Unable to open input stream'); + die(); + } + + if (!$this->verify_header()) + die('Unable to verify backup header'); + + while ($this->import_table()); + @fclose($this->stream); + } +} + +Module::register('import', 'Importer'); +?>