diff options
Diffstat (limited to 'lib/migrate.php')
-rw-r--r-- | lib/migrate.php | 714 |
1 files changed, 714 insertions, 0 deletions
diff --git a/lib/migrate.php b/lib/migrate.php new file mode 100644 index 00000000000..dff3abe9e93 --- /dev/null +++ b/lib/migrate.php @@ -0,0 +1,714 @@ +<?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; + } + + /** + * @breif finds and loads the providers + */ + static private function findProviders(){ + // Find the providers + $apps = OC_App::getAllApps(); + + foreach($apps as $app){ + $path = OC::$SERVERROOT . '/apps/' . $app . '/appinfo/migrate.php'; + if( file_exists( $path ) ){ + include( $path ); + } + } + } + + /** + * @breif 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( array( 'success' => false ) ) ); + } + self::$exporttype = $type; + // Userid? + if( self::$exporttype == 'user' ){ + // Check user exists + if( !is_null($uid) ){ + if( !OC_User_Database::userExists( $uid ) ){ + OC_Log::write('migration', 'User: '.$uid.' is not in the database and so cannot be exported.', OC_Log::ERROR); + return json_encode( array( 'success' => false ) ); + } + self::$uid = $uid; + } else { + self::$uid = OC_User::getUser(); + } + } + // 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( $datadir . '/' . 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( "/tmp", "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( $datadir . '/' . $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( $datadir . '/' . $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 ) ); + } + + /** + * @breif 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 ){ + OC_Util::checkAdminUser(); + $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 note 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; + + // Have we got a user if type is user + if( self::$exporttype == 'user' ){ + if( !$uid ){ + self::$uid = $json->exporteduser; + } else { + self::$uid = $uid; + } + } + + // Handle export types + switch( self::$exporttype ){ + case 'user': + // Check user availability + if( OC_User::userExists( self::$uid ) ){ + OC_Log::write( 'migration', 'User already exists', OC_Log::ERROR ); + return json_encode( array( 'success' => false ) ); + } + $run = true; + OC_Hook::emit( "OC_User", "pre_createUser", array( "run" => &$run, "uid" => self::$uid, "password" => $json->hash )); + if( !$run ){ + // Something stopped the user creation + OC_Log::write( 'migration', 'User creation failed', OC_Log::ERROR ); + return json_encode( array( 'success' => false ) ); + } + // Create the user + if( !self::createUser( self::$uid, $json->hash ) ){ + return json_encode( array( 'success' => false ) ); + } + // Emit the post_createUser hook (password is already hashed, will cause problems + OC_Hook::emit( "OC_User", "post_createUser", array( "uid" => self::$uid, "password" => $json->hash )); + // Make the new users data dir + $path = $datadir . '/' . self::$uid; + if( !mkdir( $path, 0755, true ) ){ + OC_Log::write( 'migration', 'Failed to create users data dir: '.$path, OC_Log::ERROR ); + return json_encode( array( 'success' => false ) ); + } + // Copy data + if( !self::copy_r( $extractpath . $json->exporteduser, $datadir . '/' . self::$uid ) ){ + return json_encode( array( 'success' => false ) ); + } + // Import user app data + 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( 'success' => true ); + */ + break; + } + + } + + /** + * @breif 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; + } + + /** + * @breif copies recursively + * @param $path string path to source folder + * @param $dest string path to destination + * @return bool + */ + private static function copy_r( $path, $dest ){ + if( is_dir($path) ){ + @mkdir( $dest ); + $objects = scandir( $path ); + if( sizeof( $objects ) > 0 ){ + foreach( $objects as $file ){ + if( $file == "." || $file == ".." ) + continue; + // go on + if( is_dir( $path . '/' . $file ) ){ + self::copy_r( $path .'/' . $file, $dest . '/' . $file ); + } else { + copy( $path . '/' . $file, $dest . '/' . $file ); + } + } + } + return true; + } + elseif( is_file( $path ) ){ + return copy( $path, $dest ); + } else { + return false; + } + } + + /** + * @breif 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; + } + + /** + * @breif 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 ){ + $success = true; + // Does this app use the database? + if( file_exists( OC::$SERVERROOT.'/apps/'.$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'] = $appinfo['version']; + + } + + return $return; + + } + + + /** + * @breif 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 + ); + // Add hash if user export + if( self::$exporttype == 'user' ){ + $query = OC_DB::prepare( "SELECT password FROM *PREFIX*users WHERE uid LIKE ?" ); + $result = $query->execute( array( self::$uid ) ); + $row = $result->fetchRow(); + $hash = $row ? $row['password'] : false; + if( !$hash ){ + OC_Log::write( 'migration', 'Failed to get the users password hash', OC_log::ERROR); + return false; + } + $info['hash'] = $hash; + $info['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; + } + + /** + * @breif 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; + + } + + /** + * @breif 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::$SERVERROOT . '/apps/' . $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; + + } + + /** + * @breif 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; + } + } + + /** + * @breif returns an array of apps that support migration + * @return array + */ + static public function getApps(){ + $allapps = OC_App::getAllApps(); + foreach($allapps as $app){ + $path = OC::$SERVERROOT . '/apps/' . $app . '/lib/migrate.php'; + if( file_exists( $path ) ){ + $supportsmigration[] = $app; + } + } + return $supportsmigration; + } + + /** + * @breif 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; + + } + + /* + * @breif 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.""); + } + return $result ? true : false; + + } + +} |