diff --git a/include/class.export.php b/include/class.export.php index 372cf62e2d6c55f09e25c59be29ba9691f075df5..dccb3880024953801fa922ff7f150ee328b008a0 100644 --- a/include/class.export.php +++ b/include/class.export.php @@ -1,7 +1,7 @@ <?php /************************************************************************* class.export.php - + Exports stuff (details to follow) Jared Hancock <jared@osticket.com> @@ -15,7 +15,7 @@ **********************************************************************/ class Export { - + /* static */ function dumpQuery($sql, $headers, $how='csv', $filter=false) { $exporters = array( 'csv' => CsvResultsExporter, @@ -137,3 +137,80 @@ class JsonResultsExporter extends ResultSetExporter { echo $exp->encode($rows); } } + +require_once INCLUDE_DIR . 'class.json.php'; +require_once INCLUDE_DIR . 'class.migrater.php'; +require_once INCLUDE_DIR . 'class.signal.php'; + +class DatabaseExporter { + + var $stream; + var $tables = array(CONFIG_TABLE, SYSLOG_TABLE, FILE_TABLE, + 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, + TICKET_LOCK_TABLE, TICKET_EVENT_TABLE, TICKET_EMAIL_INFO_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); + + function DatabaseExporter($stream) { + $this->stream = $stream; + } + + function write_block($what) { + fwrite($this->stream, JsonDataEncoder::encode($what)); + fwrite($this->stream, "\x1e"); + } + + function dump($error_stream) { + // 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); + + foreach ($this->tables as $t) { + if ($error_stream) $error_stream->write("$t\n"); + // Inspect schema + $table = $indexes = array(); + $res = db_query("show columns from $t"); + while ($field = db_fetch_array($res)) + $table[] = $field; + + $res = db_query("show indexes from $t"); + while ($col = db_fetch_array($res)) + $indexes[] = $col; + + $res = db_query("select * from $t"); + $types = array(); + + if (!$table) { + if ($error_stream) $error_stream->write( + $t.': Cannot export table with no fields'."\n"); + die(); + } + $this->write_block( + array('table', substr($t, strlen(TABLE_PREFIX)), $table, + $indexes)); + + // Dump row data + while ($row = db_fetch_row($res)) + $this->write_block($row); + + $this->write_block(array('end-table')); + } + } +} diff --git a/include/class.json.php b/include/class.json.php index f983c73e42fe0394e5a133baebab5e6b26fa639f..b5a589cfac122e008acc207f3bd2942931f31fe7 100644 --- a/include/class.json.php +++ b/include/class.json.php @@ -26,6 +26,10 @@ class JsonDataParser { while (!feof($stream)) { $contents .= fread($stream, 8192); } + return self::decode($contents); + } + + function decode($contents) { if (function_exists("json_decode")) { return json_decode($contents, true); } else { @@ -56,7 +60,11 @@ class JsonDataParser { class JsonDataEncoder { function encode($var) { - $decoder = new Services_JSON(); - return $decoder->encode($var); + if (function_exists('json_encode')) + return json_encode($var); + else { + $decoder = new Services_JSON(); + return $decoder->encode($var); + } } } diff --git a/include/mysql.php b/include/mysql.php index 7375616d9571f009433f0e88086f824621f52820..2a479072cf57eff4dcf6da1e2e7c026949c1a357 100644 --- a/include/mysql.php +++ b/include/mysql.php @@ -198,4 +198,8 @@ function db_errno() { return mysql_errno(); } + + function db_field_type($res, $col=0) { + return mysql_field_type($res, $col); + } ?> diff --git a/include/mysqli.php b/include/mysqli.php index 52ce52763c6619ac9829c1bf6d3462124671d64f..ced95434a971b729e5934b3f5754f8f25166aad8 100644 --- a/include/mysqli.php +++ b/include/mysqli.php @@ -214,6 +214,11 @@ function db_input($var, $quote=true) { return db_real_escape($var, $quote); } +function db_field_type($res, $col=0) { + global $__db; + return $res->fetch_field_direct($col); +} + function db_connect_error() { global $__db; return $__db->connect_error; diff --git a/setup/cli/manage.php b/setup/cli/manage.php index e11bfafeb3378a550993338117dc94cfcaf10d29..bfd59f9f0fe2e1b85fcf3cbbbac90744ea4b2290 100755 --- a/setup/cli/manage.php +++ b/setup/cli/manage.php @@ -3,6 +3,9 @@ require_once "modules/class.module.php"; +if (!function_exists('noop')) { function noop() {} } +session_set_save_handler('noop','noop','noop','noop','noop','noop'); + class Manager extends Module { var $prologue = "Manage one or more osTicket installations"; @@ -31,12 +34,12 @@ class Manager extends Module { echo str_pad($name, 20) . $mod->prologue . "\n"; } - function run() { - if ($this->getOption('help') && !$this->getArgument('action')) + function run($args, $options) { + if ($options['help'] && !$args['action']) $this->showHelp(); else { - $action = $this->getArgument('action'); + $action = $args['action']; global $argv; foreach ($argv as $idx=>$val) @@ -45,8 +48,11 @@ class Manager extends Module { foreach (glob(dirname(__file__).'/modules/*.php') as $script) include_once $script; - $module = Module::getInstance($action); - $module->_run(); + if (($module = Module::getInstance($action))) + return $module->_run($args['action']); + + $this->stderr->write("Unknown action given\n"); + $this->showHelp(); } } } @@ -56,6 +62,6 @@ if (php_sapi_name() != "cli") $manager = new Manager(); $manager->parseOptions(); -$manager->run(); +$manager->_run(basename(__file__)); ?> diff --git a/setup/cli/modules/class.module.php b/setup/cli/modules/class.module.php index 142a00283a23c1896a0ad64d2b83de499fc55b66..437f87c609eb6b63f6799978ce324f437a456ca5 100644 --- a/setup/cli/modules/class.module.php +++ b/setup/cli/modules/class.module.php @@ -39,6 +39,8 @@ class Option { $value = null; elseif ($value) $nargs = 1; + if ($this->type == 'int') + $value = (int)$value; switch ($this->action) { case 'store_true': $value = true; @@ -49,10 +51,17 @@ class Option { case 'store_const': $value = $this->const; break; + case 'append': + if (!isset($destination[$this->dest])) + $destination[$this->dest] = array($value); + else { + $T = &$destination[$this->dest]; + $T[] = $value; + $value = $T; + } + break; case 'store': default: - if ($this->type == 'int') - $value = (int)$value; break; } $destination[$this->dest] = $value; @@ -71,7 +80,7 @@ class Option { else $switches = sprintf(" %s, %s", $short[0], $long[0]); $help = preg_replace('/\s+/', ' ', $this->help); - if (strlen($switches) > 24) + if (strlen($switches) > 23) $help = "\n" . str_repeat(" ", 24) . $help; else $switches = str_pad($switches, 24); @@ -103,6 +112,7 @@ class Module { var $epilog = ""; var $usage = '$script [options] $args [arguments]'; var $autohelp = true; + var $module_name; var $stdout; var $stderr; @@ -128,11 +138,13 @@ class Module { if ($this->prologue) echo $this->prologue . "\n\n"; - echo "Usage:\n"; global $argv; + $manager = @$argv[0]; + + echo "Usage:\n"; echo " " . str_replace( array('$script', '$args'), - array($argv[0], implode(' ', array_keys($this->arguments))), + array($manager ." ". $this->module_name, implode(' ', array_keys($this->arguments))), $this->usage) . "\n"; ksort($this->options); @@ -205,15 +217,18 @@ class Module { die(); } - function _run() { + function _run($module_name) { + $this->module_name = $module_name; $this->parseOptions(); return $this->run($this->_args, $this->_options); } - /* abstract */ function run($args, $options) { + /* abstract */ + function run($args, $options) { } - /* static */ function register($action, $class) { + /* static */ + function register($action, $class) { global $registered_modules; $registered_modules[$action] = new $class(); } diff --git a/setup/cli/modules/export.php b/setup/cli/modules/export.php new file mode 100644 index 0000000000000000000000000000000000000000..9320fa7452687e9d45c5e3037e556dd2d2c4e8b2 --- /dev/null +++ b/setup/cli/modules/export.php @@ -0,0 +1,50 @@ +<?php +/********************************************************************* + cli/export.php + + osTicket data exporter, used for migration and backup + + 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.export.php'; + +define('OSTICKET_BACKUP_SIGNATURE', 'osTicket-Backup'); +define('OSTICKET_BACKUP_VERSION', 'A'); + +class Exporter extends Module { + var $prologue = + "Dumps the osTicket database in formats suitable for the importer"; + + var $options = array( + '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"), + ); + + function run($args, $options) { + global $ost; + + $stream = $options['stream']; + if ($options['compress']) $stream = "compress.zlib://$stream"; + $stream = fopen($stream, 'w'); + + $x = new DatabaseExporter($stream); + $x->dump($this->stderr); + } +} + +Module::register('export', 'Exporter'); +?> 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'); +?>