summaryrefslogtreecommitdiffstats
path: root/lib/private/migrate.php
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/migrate.php')
-rw-r--r--lib/private/migrate.php693
1 files changed, 693 insertions, 0 deletions
diff --git a/lib/private/migrate.php b/lib/private/migrate.php
new file mode 100644
index 00000000000..0b319177400
--- /dev/null
+++ b/lib/private/migrate.php
@@ -0,0 +1,693 @@
+<?php
+/**
+ * ownCloud
+ *
+ * @author Tom Needham
+ * @copyright 2012 Tom Needham tom@owncloud.com
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This library 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 AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+/**
+ * provides an interface to migrate users and whole ownclouds
+ */
+class OC_Migrate{
+
+
+ // Array of OC_Migration_Provider objects
+ static private $providers=array();
+ // User id of the user to import/export
+ static private $uid=false;
+ // Holds the ZipArchive object
+ static private $zip=false;
+ // Stores the type of export
+ static private $exporttype=false;
+ // Array of temp files to be deleted after zip creation
+ static private $tmpfiles=array();
+ // Holds the db object
+ static private $MDB2=false;
+ // Schema db object
+ static private $schema=false;
+ // Path to the sqlite db
+ static private $dbpath=false;
+ // Holds the path to the zip file
+ static private $zippath=false;
+ // Holds the OC_Migration_Content object
+ static private $content=false;
+
+ /**
+ * register a new migration provider
+ * @param OC_Migrate_Provider $provider
+ */
+ public static function registerProvider($provider) {
+ self::$providers[]=$provider;
+ }
+
+ /**
+ * @brief finds and loads the providers
+ */
+ static private function findProviders() {
+ // Find the providers
+ $apps = OC_App::getAllApps();
+
+ foreach($apps as $app) {
+ $path = OC_App::getAppPath($app) . '/appinfo/migrate.php';
+ if( file_exists( $path ) ) {
+ include $path;
+ }
+ }
+ }
+
+ /**
+ * @brief exports a user, or owncloud instance
+ * @param optional $uid string user id of user to export if export type is user, defaults to current
+ * @param ootional $type string type of export, defualts to user
+ * @param otional $path string path to zip output folder
+ * @return false on error, path to zip on success
+ */
+ public static function export( $uid=null, $type='user', $path=null ) {
+ $datadir = OC_Config::getValue( 'datadirectory' );
+ // Validate export type
+ $types = array( 'user', 'instance', 'system', 'userfiles' );
+ if( !in_array( $type, $types ) ) {
+ OC_Log::write( 'migration', 'Invalid export type', OC_Log::ERROR );
+ return json_encode( array( 'success' => false ) );
+ }
+ self::$exporttype = $type;
+ // Userid?
+ if( self::$exporttype == 'user' ) {
+ // Check user exists
+ self::$uid = is_null($uid) ? OC_User::getUser() : $uid;
+ if(!OC_User::userExists(self::$uid)) {
+ return json_encode( array( 'success' => false) );
+ }
+ }
+ // Calculate zipname
+ if( self::$exporttype == 'user' ) {
+ $zipname = 'oc_export_' . self::$uid . '_' . date("y-m-d_H-i-s") . '.zip';
+ } else {
+ $zipname = 'oc_export_' . self::$exporttype . '_' . date("y-m-d_H-i-s") . '.zip';
+ }
+ // Calculate path
+ if( self::$exporttype == 'user' ) {
+ self::$zippath = $datadir . '/' . self::$uid . '/' . $zipname;
+ } else {
+ if( !is_null( $path ) ) {
+ // Validate custom path
+ if( !file_exists( $path ) || !is_writeable( $path ) ) {
+ OC_Log::write( 'migration', 'Path supplied is invalid.', OC_Log::ERROR );
+ return json_encode( array( 'success' => false ) );
+ }
+ self::$zippath = $path . $zipname;
+ } else {
+ // Default path
+ self::$zippath = get_temp_dir() . '/' . $zipname;
+ }
+ }
+ // Create the zip object
+ if( !self::createZip() ) {
+ return json_encode( array( 'success' => false ) );
+ }
+ // Do the export
+ self::findProviders();
+ $exportdata = array();
+ switch( self::$exporttype ) {
+ case 'user':
+ // Connect to the db
+ self::$dbpath = $datadir . '/' . self::$uid . '/migration.db';
+ if( !self::connectDB() ) {
+ return json_encode( array( 'success' => false ) );
+ }
+ self::$content = new OC_Migration_Content( self::$zip, self::$MDB2 );
+ // Export the app info
+ $exportdata = self::exportAppData();
+ // Add the data dir to the zip
+ self::$content->addDir(OC_User::getHome(self::$uid), true, '/' );
+ break;
+ case 'instance':
+ self::$content = new OC_Migration_Content( self::$zip );
+ // Creates a zip that is compatable with the import function
+ $dbfile = tempnam( get_temp_dir(), "owncloud_export_data_" );
+ OC_DB::getDbStructure( $dbfile, 'MDB2_SCHEMA_DUMP_ALL');
+
+ // Now add in *dbname* and *dbprefix*
+ $dbexport = file_get_contents( $dbfile );
+ $dbnamestring = "<database>\n\n <name>" . OC_Config::getValue( "dbname", "owncloud" );
+ $dbtableprefixstring = "<table>\n\n <name>" . OC_Config::getValue( "dbtableprefix", "oc_" );
+ $dbexport = str_replace( $dbnamestring, "<database>\n\n <name>*dbname*", $dbexport );
+ $dbexport = str_replace( $dbtableprefixstring, "<table>\n\n <name>*dbprefix*", $dbexport );
+ // Add the export to the zip
+ self::$content->addFromString( $dbexport, "dbexport.xml" );
+ // Add user data
+ foreach(OC_User::getUsers() as $user) {
+ self::$content->addDir(OC_User::getHome($user), true, "/userdata/" );
+ }
+ break;
+ case 'userfiles':
+ self::$content = new OC_Migration_Content( self::$zip );
+ // Creates a zip with all of the users files
+ foreach(OC_User::getUsers() as $user) {
+ self::$content->addDir(OC_User::getHome($user), true, "/" );
+ }
+ break;
+ case 'system':
+ self::$content = new OC_Migration_Content( self::$zip );
+ // Creates a zip with the owncloud system files
+ self::$content->addDir( OC::$SERVERROOT . '/', false, '/');
+ foreach (array(
+ ".git",
+ "3rdparty",
+ "apps",
+ "core",
+ "files",
+ "l10n",
+ "lib",
+ "ocs",
+ "search",
+ "settings",
+ "tests"
+ ) as $dir) {
+ self::$content->addDir( OC::$SERVERROOT . '/' . $dir, true, "/");
+ }
+ break;
+ }
+ if( !$info = self::getExportInfo( $exportdata ) ) {
+ return json_encode( array( 'success' => false ) );
+ }
+ // Add the export info json to the export zip
+ self::$content->addFromString( $info, 'export_info.json' );
+ if( !self::$content->finish() ) {
+ return json_encode( array( 'success' => false ) );
+ }
+ return json_encode( array( 'success' => true, 'data' => self::$zippath ) );
+ }
+
+ /**
+ * @brief imports a user, or owncloud instance
+ * @param $path string path to zip
+ * @param optional $type type of import (user or instance)
+ * @param optional $uid userid of new user
+ */
+ public static function import( $path, $type='user', $uid=null ) {
+
+ $datadir = OC_Config::getValue( 'datadirectory' );
+ // Extract the zip
+ if( !$extractpath = self::extractZip( $path ) ) {
+ return json_encode( array( 'success' => false ) );
+ }
+ // Get export_info.json
+ $scan = scandir( $extractpath );
+ // Check for export_info.json
+ if( !in_array( 'export_info.json', $scan ) ) {
+ OC_Log::write( 'migration', 'Invalid import file, export_info.json not found', OC_Log::ERROR );
+ return json_encode( array( 'success' => false ) );
+ }
+ $json = json_decode( file_get_contents( $extractpath . 'export_info.json' ) );
+ if( $json->exporttype != $type ) {
+ OC_Log::write( 'migration', 'Invalid import file', OC_Log::ERROR );
+ return json_encode( array( 'success' => false ) );
+ }
+ self::$exporttype = $type;
+
+ $currentuser = OC_User::getUser();
+
+ // Have we got a user if type is user
+ if( self::$exporttype == 'user' ) {
+ self::$uid = !is_null($uid) ? $uid : $currentuser;
+ }
+
+ // We need to be an admin if we are not importing our own data
+ if(($type == 'user' && self::$uid != $currentuser) || $type != 'user' ) {
+ if( !OC_User::isAdminUser($currentuser)) {
+ // Naughty.
+ OC_Log::write( 'migration', 'Import not permitted.', OC_Log::ERROR );
+ return json_encode( array( 'success' => false ) );
+ }
+ }
+
+ // Handle export types
+ switch( self::$exporttype ) {
+ case 'user':
+ // Check user availability
+ if( !OC_User::userExists( self::$uid ) ) {
+ OC_Log::write( 'migration', 'User doesn\'t exist', OC_Log::ERROR );
+ return json_encode( array( 'success' => false ) );
+ }
+
+ // Check if the username is valid
+ if( preg_match( '/[^a-zA-Z0-9 _\.@\-]/', $json->exporteduser )) {
+ OC_Log::write( 'migration', 'Username is not valid', OC_Log::ERROR );
+ return json_encode( array( 'success' => false ) );
+ }
+
+ // Copy data
+ $userfolder = $extractpath . $json->exporteduser;
+ $newuserfolder = $datadir . '/' . self::$uid;
+ foreach(scandir($userfolder) as $file){
+ if($file !== '.' && $file !== '..' && is_dir($file)) {
+ $file = str_replace(array('/', '\\'), '', $file);
+
+ // Then copy the folder over
+ OC_Helper::copyr($userfolder.'/'.$file, $newuserfolder.'/'.$file);
+ }
+ }
+ // Import user app data
+ if(file_exists($extractpath . $json->exporteduser . '/migration.db')) {
+ if( !$appsimported = self::importAppData( $extractpath . $json->exporteduser . '/migration.db',
+ $json,
+ self::$uid ) ) {
+ return json_encode( array( 'success' => false ) );
+ }
+ }
+ // All done!
+ if( !self::unlink_r( $extractpath ) ) {
+ OC_Log::write( 'migration', 'Failed to delete the extracted zip', OC_Log::ERROR );
+ }
+ return json_encode( array( 'success' => true, 'data' => $appsimported ) );
+ break;
+ case 'instance':
+ /*
+ * EXPERIMENTAL
+ // Check for new data dir and dbexport before doing anything
+ // TODO
+
+ // Delete current data folder.
+ OC_Log::write( 'migration', "Deleting current data dir", OC_Log::INFO );
+ if( !self::unlink_r( $datadir, false ) ) {
+ OC_Log::write( 'migration', 'Failed to delete the current data dir', OC_Log::ERROR );
+ return json_encode( array( 'success' => false ) );
+ }
+
+ // Copy over data
+ if( !self::copy_r( $extractpath . 'userdata', $datadir ) ) {
+ OC_Log::write( 'migration', 'Failed to copy over data directory', OC_Log::ERROR );
+ return json_encode( array( 'success' => false ) );
+ }
+
+ // Import the db
+ if( !OC_DB::replaceDB( $extractpath . 'dbexport.xml' ) ) {
+ return json_encode( array( 'success' => false ) );
+ }
+ // Done
+ return json_encode( array( 'success' => true ) );
+ */
+ break;
+ }
+
+ }
+
+ /**
+ * @brief recursively deletes a directory
+ * @param $dir string path of dir to delete
+ * $param optional $deleteRootToo bool delete the root directory
+ * @return bool
+ */
+ private static function unlink_r( $dir, $deleteRootToo=true ) {
+ if( !$dh = @opendir( $dir ) ) {
+ return false;
+ }
+ while (false !== ($obj = readdir($dh))) {
+ if($obj == '.' || $obj == '..') {
+ continue;
+ }
+ if (!@unlink($dir . '/' . $obj)) {
+ self::unlink_r($dir.'/'.$obj, true);
+ }
+ }
+ closedir($dh);
+ if ( $deleteRootToo ) {
+ @rmdir($dir);
+ }
+ return true;
+ }
+
+ /**
+ * @brief tries to extract the import zip
+ * @param $path string path to the zip
+ * @return string path to extract location (with a trailing slash) or false on failure
+ */
+ static private function extractZip( $path ) {
+ self::$zip = new ZipArchive;
+ // Validate path
+ if( !file_exists( $path ) ) {
+ OC_Log::write( 'migration', 'Zip not found', OC_Log::ERROR );
+ return false;
+ }
+ if ( self::$zip->open( $path ) != true ) {
+ OC_Log::write( 'migration', "Failed to open zip file", OC_Log::ERROR );
+ return false;
+ }
+ $to = get_temp_dir() . '/oc_import_' . self::$exporttype . '_' . date("y-m-d_H-i-s") . '/';
+ if( !self::$zip->extractTo( $to ) ) {
+ return false;
+ }
+ self::$zip->close();
+ return $to;
+ }
+
+ /**
+ * @brief connects to a MDB2 database scheme
+ * @returns bool
+ */
+ static private function connectScheme() {
+ // We need a mdb2 database connection
+ self::$MDB2->loadModule( 'Manager' );
+ self::$MDB2->loadModule( 'Reverse' );
+
+ // Connect if this did not happen before
+ if( !self::$schema ) {
+ require_once 'MDB2/Schema.php';
+ self::$schema=MDB2_Schema::factory( self::$MDB2 );
+ }
+
+ return true;
+ }
+
+ /**
+ * @brief creates a migration.db in the users data dir with their app data in
+ * @return bool whether operation was successfull
+ */
+ private static function exportAppData( ) {
+
+ $success = true;
+ $return = array();
+
+ // Foreach provider
+ foreach( self::$providers as $provider ) {
+ // Check if the app is enabled
+ if( OC_App::isEnabled( $provider->getID() ) ) {
+ $success = true;
+ // Does this app use the database?
+ if( file_exists( OC_App::getAppPath($provider->getID()).'/appinfo/database.xml' ) ) {
+ // Create some app tables
+ $tables = self::createAppTables( $provider->getID() );
+ if( is_array( $tables ) ) {
+ // Save the table names
+ foreach($tables as $table) {
+ $return['apps'][$provider->getID()]['tables'][] = $table;
+ }
+ } else {
+ // It failed to create the tables
+ $success = false;
+ }
+ }
+
+ // Run the export function?
+ if( $success ) {
+ // Set the provider properties
+ $provider->setData( self::$uid, self::$content );
+ $return['apps'][$provider->getID()]['success'] = $provider->export();
+ } else {
+ $return['apps'][$provider->getID()]['success'] = false;
+ $return['apps'][$provider->getID()]['message'] = 'failed to create the app tables';
+ }
+
+ // Now add some app info the the return array
+ $appinfo = OC_App::getAppInfo( $provider->getID() );
+ $return['apps'][$provider->getID()]['version'] = OC_App::getAppVersion($provider->getID());
+ }
+ }
+
+ return $return;
+
+ }
+
+
+ /**
+ * @brief generates json containing export info, and merges any data supplied
+ * @param optional $array array of data to include in the returned json
+ * @return bool
+ */
+ static private function getExportInfo( $array=array() ) {
+ $info = array(
+ 'ocversion' => OC_Util::getVersion(),
+ 'exporttime' => time(),
+ 'exportedby' => OC_User::getUser(),
+ 'exporttype' => self::$exporttype,
+ 'exporteduser' => self::$uid
+ );
+
+ if( !is_array( $array ) ) {
+ OC_Log::write( 'migration', 'Supplied $array was not an array in getExportInfo()', OC_Log::ERROR );
+ }
+ // Merge in other data
+ $info = array_merge( $info, (array)$array );
+ // Create json
+ $json = json_encode( $info );
+ return $json;
+ }
+
+ /**
+ * @brief connects to migration.db, or creates if not found
+ * @param $db optional path to migration.db, defaults to user data dir
+ * @return bool whether the operation was successful
+ */
+ static private function connectDB( $path=null ) {
+ // Has the dbpath been set?
+ self::$dbpath = !is_null( $path ) ? $path : self::$dbpath;
+ if( !self::$dbpath ) {
+ OC_Log::write( 'migration', 'connectDB() was called without dbpath being set', OC_Log::ERROR );
+ return false;
+ }
+ // Already connected
+ if(!self::$MDB2) {
+ require_once 'MDB2.php';
+
+ $datadir = OC_Config::getValue( "datadirectory", OC::$SERVERROOT."/data" );
+
+ // DB type
+ if( class_exists( 'SQLite3' ) ) {
+ $dbtype = 'sqlite3';
+ } else if( is_callable( 'sqlite_open' ) ) {
+ $dbtype = 'sqlite';
+ } else {
+ OC_Log::write( 'migration', 'SQLite not found', OC_Log::ERROR );
+ return false;
+ }
+
+ // Prepare options array
+ $options = array(
+ 'portability' => MDB2_PORTABILITY_ALL & (!MDB2_PORTABILITY_FIX_CASE),
+ 'log_line_break' => '<br>',
+ 'idxname_format' => '%s',
+ 'debug' => true,
+ 'quote_identifier' => true
+ );
+ $dsn = array(
+ 'phptype' => $dbtype,
+ 'database' => self::$dbpath,
+ 'mode' => '0644'
+ );
+
+ // Try to establish connection
+ self::$MDB2 = MDB2::factory( $dsn, $options );
+ // Die if we could not connect
+ if( PEAR::isError( self::$MDB2 ) ) {
+ die( self::$MDB2->getMessage() );
+ OC_Log::write( 'migration', 'Failed to create/connect to migration.db', OC_Log::FATAL );
+ OC_Log::write( 'migration', self::$MDB2->getUserInfo(), OC_Log::FATAL );
+ OC_Log::write( 'migration', self::$MDB2->getMessage(), OC_Log::FATAL );
+ return false;
+ }
+ // We always, really always want associative arrays
+ self::$MDB2->setFetchMode(MDB2_FETCHMODE_ASSOC);
+ }
+ return true;
+
+ }
+
+ /**
+ * @brief creates the tables in migration.db from an apps database.xml
+ * @param $appid string id of the app
+ * @return bool whether the operation was successful
+ */
+ static private function createAppTables( $appid ) {
+
+ if( !self::connectScheme() ) {
+ return false;
+ }
+
+ // There is a database.xml file
+ $content = file_get_contents(OC_App::getAppPath($appid) . '/appinfo/database.xml' );
+
+ $file2 = 'static://db_scheme';
+ // TODO get the relative path to migration.db from the data dir
+ // For now just cheat
+ $path = pathinfo( self::$dbpath );
+ $content = str_replace( '*dbname*', self::$uid.'/migration', $content );
+ $content = str_replace( '*dbprefix*', '', $content );
+
+ $xml = new SimpleXMLElement($content);
+ foreach($xml->table as $table) {
+ $tables[] = (string)$table->name;
+ }
+
+ file_put_contents( $file2, $content );
+
+ // Try to create tables
+ $definition = self::$schema->parseDatabaseDefinitionFile( $file2 );
+
+ unlink( $file2 );
+
+ // Die in case something went wrong
+ if( $definition instanceof MDB2_Schema_Error ) {
+ OC_Log::write( 'migration', 'Failed to parse database.xml for: '.$appid, OC_Log::FATAL );
+ OC_Log::write( 'migration', $definition->getMessage().': '.$definition->getUserInfo(), OC_Log::FATAL );
+ return false;
+ }
+
+ $definition['overwrite'] = true;
+
+ $ret = self::$schema->createDatabase( $definition );
+
+ // Die in case something went wrong
+ if( $ret instanceof MDB2_Error ) {
+ OC_Log::write( 'migration', 'Failed to create tables for: '.$appid, OC_Log::FATAL );
+ OC_Log::write( 'migration', $ret->getMessage().': '.$ret->getUserInfo(), OC_Log::FATAL );
+ return false;
+ }
+ return $tables;
+
+ }
+
+ /**
+ * @brief tries to create the zip
+ * @param $path string path to zip destination
+ * @return bool
+ */
+ static private function createZip() {
+ self::$zip = new ZipArchive;
+ // Check if properties are set
+ if( !self::$zippath ) {
+ OC_Log::write('migration', 'createZip() called but $zip and/or $zippath have not been set', OC_Log::ERROR);
+ return false;
+ }
+ if ( self::$zip->open( self::$zippath, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE ) !== true ) {
+ OC_Log::write('migration',
+ 'Failed to create the zip with error: '.self::$zip->getStatusString(),
+ OC_Log::ERROR);
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * @brief returns an array of apps that support migration
+ * @return array
+ */
+ static public function getApps() {
+ $allapps = OC_App::getAllApps();
+ foreach($allapps as $app) {
+ $path = self::getAppPath($app) . '/lib/migrate.php';
+ if( file_exists( $path ) ) {
+ $supportsmigration[] = $app;
+ }
+ }
+ return $supportsmigration;
+ }
+
+ /**
+ * @brief imports a new user
+ * @param $db string path to migration.db
+ * @param $info object of migration info
+ * @param $uid optional uid to use
+ * @return array of apps with import statuses, or false on failure.
+ */
+ public static function importAppData( $db, $info, $uid=null ) {
+ // Check if the db exists
+ if( file_exists( $db ) ) {
+ // Connect to the db
+ if(!self::connectDB( $db )) {
+ OC_Log::write('migration', 'Failed to connect to migration.db', OC_Log::ERROR);
+ return false;
+ }
+ } else {
+ OC_Log::write('migration', 'Migration.db not found at: '.$db, OC_Log::FATAL );
+ return false;
+ }
+
+ // Find providers
+ self::findProviders();
+
+ // Generate importinfo array
+ $importinfo = array(
+ 'olduid' => $info->exporteduser,
+ 'newuid' => self::$uid
+ );
+
+ foreach( self::$providers as $provider) {
+ // Is the app in the export?
+ $id = $provider->getID();
+ if( isset( $info->apps->$id ) ) {
+ // Is the app installed
+ if( !OC_App::isEnabled( $id ) ) {
+ OC_Log::write( 'migration',
+ 'App: ' . $id . ' is not installed, can\'t import data.',
+ OC_Log::INFO );
+ $appsstatus[$id] = 'notsupported';
+ } else {
+ // Did it succeed on export?
+ if( $info->apps->$id->success ) {
+ // Give the provider the content object
+ if( !self::connectDB( $db ) ) {
+ return false;
+ }
+ $content = new OC_Migration_Content( self::$zip, self::$MDB2 );
+ $provider->setData( self::$uid, $content, $info );
+ // Then do the import
+ if( !$appsstatus[$id] = $provider->import( $info->apps->$id, $importinfo ) ) {
+ // Failed to import app
+ OC_Log::write( 'migration',
+ 'Failed to import app data for user: ' . self::$uid . ' for app: ' . $id,
+ OC_Log::ERROR );
+ }
+ } else {
+ // Add to failed list
+ $appsstatus[$id] = false;
+ }
+ }
+ }
+ }
+
+ return $appsstatus;
+
+ }
+
+ /*
+ * @brief creates a new user in the database
+ * @param $uid string user_id of the user to be created
+ * @param $hash string hash of the user to be created
+ * @return bool result of user creation
+ */
+ public static function createUser( $uid, $hash ) {
+
+ // Check if userid exists
+ if(OC_User::userExists( $uid )) {
+ return false;
+ }
+
+ // Create the user
+ $query = OC_DB::prepare( "INSERT INTO `*PREFIX*users` ( `uid`, `password` ) VALUES( ?, ? )" );
+ $result = $query->execute( array( $uid, $hash));
+ if( !$result ) {
+ OC_Log::write('migration', 'Failed to create the new user "'.$uid."", OC_Log::ERROR);
+ }
+ return $result ? true : false;
+
+ }
+
+}