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');
+?>