diff options
Diffstat (limited to 'lib/private/migrate.php')
-rw-r--r-- | lib/private/migrate.php | 693 |
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; + + } + +} |