You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

migrate.php 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. <?php
  2. /**
  3. * ownCloud
  4. *
  5. * @author Tom Needham
  6. * @copyright 2012 Tom Needham tom@owncloud.com
  7. *
  8. * This library is free software; you can redistribute it and/or
  9. * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
  10. * License as published by the Free Software Foundation; either
  11. * version 3 of the License, or any later version.
  12. *
  13. * This library is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public
  19. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
  20. *
  21. */
  22. /**
  23. * provides an interface to migrate users and whole ownclouds
  24. */
  25. class OC_Migrate{
  26. // Array of OC_Migration_Provider objects
  27. static private $providers=array();
  28. // User id of the user to import/export
  29. static private $uid=false;
  30. // Holds the ZipArchive object
  31. static private $zip=false;
  32. // Stores the type of export
  33. static private $exporttype=false;
  34. // Holds the db object
  35. static private $migration_database=false;
  36. // Path to the sqlite db
  37. static private $dbpath=false;
  38. // Holds the path to the zip file
  39. static private $zippath=false;
  40. // Holds the OC_Migration_Content object
  41. static private $content=false;
  42. /**
  43. * register a new migration provider
  44. * @param OC_Migration_Provider $provider
  45. */
  46. public static function registerProvider($provider) {
  47. self::$providers[]=$provider;
  48. }
  49. /**
  50. * @brief finds and loads the providers
  51. */
  52. static private function findProviders() {
  53. // Find the providers
  54. $apps = OC_App::getAllApps();
  55. foreach($apps as $app) {
  56. $path = OC_App::getAppPath($app) . '/appinfo/migrate.php';
  57. if( file_exists( $path ) ) {
  58. include $path;
  59. }
  60. }
  61. }
  62. /**
  63. * @brief exports a user, or owncloud instance
  64. * @param string $uid user id of user to export if export type is user, defaults to current
  65. * @param string $type type of export, defualts to user
  66. * @param string $path path to zip output folder
  67. * @return string on error, path to zip on success
  68. */
  69. public static function export( $uid=null, $type='user', $path=null ) {
  70. $datadir = OC_Config::getValue( 'datadirectory' );
  71. // Validate export type
  72. $types = array( 'user', 'instance', 'system', 'userfiles' );
  73. if( !in_array( $type, $types ) ) {
  74. OC_Log::write( 'migration', 'Invalid export type', OC_Log::ERROR );
  75. return json_encode( array( 'success' => false ) );
  76. }
  77. self::$exporttype = $type;
  78. // Userid?
  79. if( self::$exporttype == 'user' ) {
  80. // Check user exists
  81. self::$uid = is_null($uid) ? OC_User::getUser() : $uid;
  82. if(!OC_User::userExists(self::$uid)) {
  83. return json_encode( array( 'success' => false) );
  84. }
  85. }
  86. // Calculate zipname
  87. if( self::$exporttype == 'user' ) {
  88. $zipname = 'oc_export_' . self::$uid . '_' . date("y-m-d_H-i-s") . '.zip';
  89. } else {
  90. $zipname = 'oc_export_' . self::$exporttype . '_' . date("y-m-d_H-i-s") . '.zip';
  91. }
  92. // Calculate path
  93. if( self::$exporttype == 'user' ) {
  94. self::$zippath = $datadir . '/' . self::$uid . '/' . $zipname;
  95. } else {
  96. if( !is_null( $path ) ) {
  97. // Validate custom path
  98. if( !file_exists( $path ) || !is_writeable( $path ) ) {
  99. OC_Log::write( 'migration', 'Path supplied is invalid.', OC_Log::ERROR );
  100. return json_encode( array( 'success' => false ) );
  101. }
  102. self::$zippath = $path . $zipname;
  103. } else {
  104. // Default path
  105. self::$zippath = get_temp_dir() . '/' . $zipname;
  106. }
  107. }
  108. // Create the zip object
  109. if( !self::createZip() ) {
  110. return json_encode( array( 'success' => false ) );
  111. }
  112. // Do the export
  113. self::findProviders();
  114. $exportdata = array();
  115. switch( self::$exporttype ) {
  116. case 'user':
  117. // Connect to the db
  118. self::$dbpath = $datadir . '/' . self::$uid . '/migration.db';
  119. if( !self::connectDB() ) {
  120. return json_encode( array( 'success' => false ) );
  121. }
  122. self::$content = new OC_Migration_Content( self::$zip, self::$migration_database );
  123. // Export the app info
  124. $exportdata = self::exportAppData();
  125. // Add the data dir to the zip
  126. self::$content->addDir(OC_User::getHome(self::$uid), true, '/' );
  127. break;
  128. case 'instance':
  129. self::$content = new OC_Migration_Content( self::$zip );
  130. // Creates a zip that is compatable with the import function
  131. $dbfile = tempnam( get_temp_dir(), "owncloud_export_data_" );
  132. OC_DB::getDbStructure( $dbfile, 'MDB2_SCHEMA_DUMP_ALL');
  133. // Now add in *dbname* and *dbprefix*
  134. $dbexport = file_get_contents( $dbfile );
  135. $dbnamestring = "<database>\n\n <name>" . OC_Config::getValue( "dbname", "owncloud" );
  136. $dbtableprefixstring = "<table>\n\n <name>" . OC_Config::getValue( "dbtableprefix", "oc_" );
  137. $dbexport = str_replace( $dbnamestring, "<database>\n\n <name>*dbname*", $dbexport );
  138. $dbexport = str_replace( $dbtableprefixstring, "<table>\n\n <name>*dbprefix*", $dbexport );
  139. // Add the export to the zip
  140. self::$content->addFromString( $dbexport, "dbexport.xml" );
  141. // Add user data
  142. foreach(OC_User::getUsers() as $user) {
  143. self::$content->addDir(OC_User::getHome($user), true, "/userdata/" );
  144. }
  145. break;
  146. case 'userfiles':
  147. self::$content = new OC_Migration_Content( self::$zip );
  148. // Creates a zip with all of the users files
  149. foreach(OC_User::getUsers() as $user) {
  150. self::$content->addDir(OC_User::getHome($user), true, "/" );
  151. }
  152. break;
  153. case 'system':
  154. self::$content = new OC_Migration_Content( self::$zip );
  155. // Creates a zip with the owncloud system files
  156. self::$content->addDir( OC::$SERVERROOT . '/', false, '/');
  157. foreach (array(
  158. ".git",
  159. "3rdparty",
  160. "apps",
  161. "core",
  162. "files",
  163. "l10n",
  164. "lib",
  165. "ocs",
  166. "search",
  167. "settings",
  168. "tests"
  169. ) as $dir) {
  170. self::$content->addDir( OC::$SERVERROOT . '/' . $dir, true, "/");
  171. }
  172. break;
  173. }
  174. if( !$info = self::getExportInfo( $exportdata ) ) {
  175. return json_encode( array( 'success' => false ) );
  176. }
  177. // Add the export info json to the export zip
  178. self::$content->addFromString( $info, 'export_info.json' );
  179. if( !self::$content->finish() ) {
  180. return json_encode( array( 'success' => false ) );
  181. }
  182. return json_encode( array( 'success' => true, 'data' => self::$zippath ) );
  183. }
  184. /**
  185. * @brief imports a user, or owncloud instance
  186. * @param string $path path to zip
  187. * @param string $type type of import (user or instance)
  188. * @param string|null|int $uid userid of new user
  189. * @return string
  190. */
  191. public static function import( $path, $type='user', $uid=null ) {
  192. $datadir = OC_Config::getValue( 'datadirectory' );
  193. // Extract the zip
  194. if( !$extractpath = self::extractZip( $path ) ) {
  195. return json_encode( array( 'success' => false ) );
  196. }
  197. // Get export_info.json
  198. $scan = scandir( $extractpath );
  199. // Check for export_info.json
  200. if( !in_array( 'export_info.json', $scan ) ) {
  201. OC_Log::write( 'migration', 'Invalid import file, export_info.json not found', OC_Log::ERROR );
  202. return json_encode( array( 'success' => false ) );
  203. }
  204. $json = json_decode( file_get_contents( $extractpath . 'export_info.json' ) );
  205. if( $json->exporttype != $type ) {
  206. OC_Log::write( 'migration', 'Invalid import file', OC_Log::ERROR );
  207. return json_encode( array( 'success' => false ) );
  208. }
  209. self::$exporttype = $type;
  210. $currentuser = OC_User::getUser();
  211. // Have we got a user if type is user
  212. if( self::$exporttype == 'user' ) {
  213. self::$uid = !is_null($uid) ? $uid : $currentuser;
  214. }
  215. // We need to be an admin if we are not importing our own data
  216. if(($type == 'user' && self::$uid != $currentuser) || $type != 'user' ) {
  217. if( !OC_User::isAdminUser($currentuser)) {
  218. // Naughty.
  219. OC_Log::write( 'migration', 'Import not permitted.', OC_Log::ERROR );
  220. return json_encode( array( 'success' => false ) );
  221. }
  222. }
  223. // Handle export types
  224. switch( self::$exporttype ) {
  225. case 'user':
  226. // Check user availability
  227. if( !OC_User::userExists( self::$uid ) ) {
  228. OC_Log::write( 'migration', 'User doesn\'t exist', OC_Log::ERROR );
  229. return json_encode( array( 'success' => false ) );
  230. }
  231. // Check if the username is valid
  232. if( preg_match( '/[^a-zA-Z0-9 _\.@\-]/', $json->exporteduser )) {
  233. OC_Log::write( 'migration', 'Username is not valid', OC_Log::ERROR );
  234. return json_encode( array( 'success' => false ) );
  235. }
  236. // Copy data
  237. $userfolder = $extractpath . $json->exporteduser;
  238. $newuserfolder = $datadir . '/' . self::$uid;
  239. foreach(scandir($userfolder) as $file){
  240. if($file !== '.' && $file !== '..' && is_dir($userfolder.'/'.$file)) {
  241. $file = str_replace(array('/', '\\'), '', $file);
  242. // Then copy the folder over
  243. OC_Helper::copyr($userfolder.'/'.$file, $newuserfolder.'/'.$file);
  244. }
  245. }
  246. // Import user app data
  247. if(file_exists($extractpath . $json->exporteduser . '/migration.db')) {
  248. if( !$appsimported = self::importAppData( $extractpath . $json->exporteduser . '/migration.db',
  249. $json,
  250. self::$uid ) ) {
  251. return json_encode( array( 'success' => false ) );
  252. }
  253. }
  254. // All done!
  255. if( !self::unlink_r( $extractpath ) ) {
  256. OC_Log::write( 'migration', 'Failed to delete the extracted zip', OC_Log::ERROR );
  257. }
  258. return json_encode( array( 'success' => true, 'data' => $appsimported ) );
  259. break;
  260. case 'instance':
  261. /*
  262. * EXPERIMENTAL
  263. // Check for new data dir and dbexport before doing anything
  264. // TODO
  265. // Delete current data folder.
  266. OC_Log::write( 'migration', "Deleting current data dir", OC_Log::INFO );
  267. if( !self::unlink_r( $datadir, false ) ) {
  268. OC_Log::write( 'migration', 'Failed to delete the current data dir', OC_Log::ERROR );
  269. return json_encode( array( 'success' => false ) );
  270. }
  271. // Copy over data
  272. if( !self::copy_r( $extractpath . 'userdata', $datadir ) ) {
  273. OC_Log::write( 'migration', 'Failed to copy over data directory', OC_Log::ERROR );
  274. return json_encode( array( 'success' => false ) );
  275. }
  276. // Import the db
  277. if( !OC_DB::replaceDB( $extractpath . 'dbexport.xml' ) ) {
  278. return json_encode( array( 'success' => false ) );
  279. }
  280. // Done
  281. return json_encode( array( 'success' => true ) );
  282. */
  283. break;
  284. }
  285. }
  286. /**
  287. * @brief recursively deletes a directory
  288. * @param string $dir path of dir to delete
  289. * @param bool $deleteRootToo delete the root directory
  290. * @return bool
  291. */
  292. private static function unlink_r( $dir, $deleteRootToo=true ) {
  293. if( !$dh = @opendir( $dir ) ) {
  294. return false;
  295. }
  296. while (false !== ($obj = readdir($dh))) {
  297. if($obj == '.' || $obj == '..') {
  298. continue;
  299. }
  300. if (!@unlink($dir . '/' . $obj)) {
  301. self::unlink_r($dir.'/'.$obj, true);
  302. }
  303. }
  304. closedir($dh);
  305. if ( $deleteRootToo ) {
  306. @rmdir($dir);
  307. }
  308. return true;
  309. }
  310. /**
  311. * @brief tries to extract the import zip
  312. * @param $path string path to the zip
  313. * @return string path to extract location (with a trailing slash) or false on failure
  314. */
  315. static private function extractZip( $path ) {
  316. self::$zip = new ZipArchive;
  317. // Validate path
  318. if( !file_exists( $path ) ) {
  319. OC_Log::write( 'migration', 'Zip not found', OC_Log::ERROR );
  320. return false;
  321. }
  322. if ( self::$zip->open( $path ) != true ) {
  323. OC_Log::write( 'migration', "Failed to open zip file", OC_Log::ERROR );
  324. return false;
  325. }
  326. $to = get_temp_dir() . '/oc_import_' . self::$exporttype . '_' . date("y-m-d_H-i-s") . '/';
  327. if( !self::$zip->extractTo( $to ) ) {
  328. return false;
  329. }
  330. self::$zip->close();
  331. return $to;
  332. }
  333. /**
  334. * @brief creates a migration.db in the users data dir with their app data in
  335. * @return bool whether operation was successfull
  336. */
  337. private static function exportAppData( ) {
  338. $success = true;
  339. $return = array();
  340. // Foreach provider
  341. foreach( self::$providers as $provider ) {
  342. // Check if the app is enabled
  343. if( OC_App::isEnabled( $provider->getID() ) ) {
  344. $success = true;
  345. // Does this app use the database?
  346. if( file_exists( OC_App::getAppPath($provider->getID()).'/appinfo/database.xml' ) ) {
  347. // Create some app tables
  348. $tables = self::createAppTables( $provider->getID() );
  349. if( is_array( $tables ) ) {
  350. // Save the table names
  351. foreach($tables as $table) {
  352. $return['apps'][$provider->getID()]['tables'][] = $table;
  353. }
  354. } else {
  355. // It failed to create the tables
  356. $success = false;
  357. }
  358. }
  359. // Run the export function?
  360. if( $success ) {
  361. // Set the provider properties
  362. $provider->setData( self::$uid, self::$content );
  363. $return['apps'][$provider->getID()]['success'] = $provider->export();
  364. } else {
  365. $return['apps'][$provider->getID()]['success'] = false;
  366. $return['apps'][$provider->getID()]['message'] = 'failed to create the app tables';
  367. }
  368. // Now add some app info the the return array
  369. $appinfo = OC_App::getAppInfo( $provider->getID() );
  370. $return['apps'][$provider->getID()]['version'] = OC_App::getAppVersion($provider->getID());
  371. }
  372. }
  373. return $return;
  374. }
  375. /**
  376. * @brief generates json containing export info, and merges any data supplied
  377. * @param array $array of data to include in the returned json
  378. * @return string
  379. */
  380. static private function getExportInfo( $array=array() ) {
  381. $info = array(
  382. 'ocversion' => OC_Util::getVersion(),
  383. 'exporttime' => time(),
  384. 'exportedby' => OC_User::getUser(),
  385. 'exporttype' => self::$exporttype,
  386. 'exporteduser' => self::$uid
  387. );
  388. if( !is_array( $array ) ) {
  389. OC_Log::write( 'migration', 'Supplied $array was not an array in getExportInfo()', OC_Log::ERROR );
  390. }
  391. // Merge in other data
  392. $info = array_merge( $info, (array)$array );
  393. // Create json
  394. $json = json_encode( $info );
  395. return $json;
  396. }
  397. /**
  398. * @brief connects to migration.db, or creates if not found
  399. * @param string $path to migration.db, defaults to user data dir
  400. * @return bool whether the operation was successful
  401. */
  402. static private function connectDB( $path=null ) {
  403. // Has the dbpath been set?
  404. self::$dbpath = !is_null( $path ) ? $path : self::$dbpath;
  405. if( !self::$dbpath ) {
  406. OC_Log::write( 'migration', 'connectDB() was called without dbpath being set', OC_Log::ERROR );
  407. return false;
  408. }
  409. // Already connected
  410. if(!self::$migration_database) {
  411. $datadir = OC_Config::getValue( "datadirectory", OC::$SERVERROOT."/data" );
  412. $connectionParams = array(
  413. 'path' => self::$dbpath,
  414. 'driver' => 'pdo_sqlite',
  415. );
  416. $connectionParams['adapter'] = '\OC\DB\AdapterSqlite';
  417. $connectionParams['wrapperClass'] = 'OC\DB\Connection';
  418. $connectionParams['tablePrefix'] = '';
  419. // Try to establish connection
  420. self::$migration_database = \Doctrine\DBAL\DriverManager::getConnection($connectionParams);
  421. }
  422. return true;
  423. }
  424. /**
  425. * @brief creates the tables in migration.db from an apps database.xml
  426. * @param string $appid id of the app
  427. * @return bool whether the operation was successful
  428. */
  429. static private function createAppTables( $appid ) {
  430. $schema_manager = new OC\DB\MDB2SchemaManager(self::$migration_database);
  431. // There is a database.xml file
  432. $content = file_get_contents(OC_App::getAppPath($appid) . '/appinfo/database.xml' );
  433. $file2 = 'static://db_scheme';
  434. // TODO get the relative path to migration.db from the data dir
  435. // For now just cheat
  436. $path = pathinfo( self::$dbpath );
  437. $content = str_replace( '*dbname*', self::$uid.'/migration', $content );
  438. $content = str_replace( '*dbprefix*', '', $content );
  439. $xml = new SimpleXMLElement($content);
  440. foreach($xml->table as $table) {
  441. $tables[] = (string)$table->name;
  442. }
  443. file_put_contents( $file2, $content );
  444. // Try to create tables
  445. try {
  446. $schema_manager->createDbFromStructure($file2);
  447. } catch(Exception $e) {
  448. unlink( $file2 );
  449. OC_Log::write( 'migration', 'Failed to create tables for: '.$appid, OC_Log::FATAL );
  450. OC_Log::write( 'migration', $e->getMessage(), OC_Log::FATAL );
  451. return false;
  452. }
  453. return $tables;
  454. }
  455. /**
  456. * @brief tries to create the zip
  457. * @return bool
  458. */
  459. static private function createZip() {
  460. self::$zip = new ZipArchive;
  461. // Check if properties are set
  462. if( !self::$zippath ) {
  463. OC_Log::write('migration', 'createZip() called but $zip and/or $zippath have not been set', OC_Log::ERROR);
  464. return false;
  465. }
  466. if ( self::$zip->open( self::$zippath, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE ) !== true ) {
  467. OC_Log::write('migration',
  468. 'Failed to create the zip with error: '.self::$zip->getStatusString(),
  469. OC_Log::ERROR);
  470. return false;
  471. } else {
  472. return true;
  473. }
  474. }
  475. /**
  476. * @brief returns an array of apps that support migration
  477. * @return array
  478. */
  479. static public function getApps() {
  480. $allapps = OC_App::getAllApps();
  481. foreach($allapps as $app) {
  482. $path = self::getAppPath($app) . '/lib/migrate.php';
  483. if( file_exists( $path ) ) {
  484. $supportsmigration[] = $app;
  485. }
  486. }
  487. return $supportsmigration;
  488. }
  489. /**
  490. * @brief imports a new user
  491. * @param string $db string path to migration.db
  492. * @param $info object of migration info
  493. * @param string|null|int $uid uid to use
  494. * @return array of apps with import statuses, or false on failure.
  495. */
  496. public static function importAppData( $db, $info, $uid=null ) {
  497. // Check if the db exists
  498. if( file_exists( $db ) ) {
  499. // Connect to the db
  500. if(!self::connectDB( $db )) {
  501. OC_Log::write('migration', 'Failed to connect to migration.db', OC_Log::ERROR);
  502. return false;
  503. }
  504. } else {
  505. OC_Log::write('migration', 'Migration.db not found at: '.$db, OC_Log::FATAL );
  506. return false;
  507. }
  508. // Find providers
  509. self::findProviders();
  510. // Generate importinfo array
  511. $importinfo = array(
  512. 'olduid' => $info->exporteduser,
  513. 'newuid' => self::$uid
  514. );
  515. foreach( self::$providers as $provider) {
  516. // Is the app in the export?
  517. $id = $provider->getID();
  518. if( isset( $info->apps->$id ) ) {
  519. // Is the app installed
  520. if( !OC_App::isEnabled( $id ) ) {
  521. OC_Log::write( 'migration',
  522. 'App: ' . $id . ' is not installed, can\'t import data.',
  523. OC_Log::INFO );
  524. $appsstatus[$id] = 'notsupported';
  525. } else {
  526. // Did it succeed on export?
  527. if( $info->apps->$id->success ) {
  528. // Give the provider the content object
  529. if( !self::connectDB( $db ) ) {
  530. return false;
  531. }
  532. $content = new OC_Migration_Content( self::$zip, self::$migration_database );
  533. $provider->setData( self::$uid, $content, $info );
  534. // Then do the import
  535. if( !$appsstatus[$id] = $provider->import( $info->apps->$id, $importinfo ) ) {
  536. // Failed to import app
  537. OC_Log::write( 'migration',
  538. 'Failed to import app data for user: ' . self::$uid . ' for app: ' . $id,
  539. OC_Log::ERROR );
  540. }
  541. } else {
  542. // Add to failed list
  543. $appsstatus[$id] = false;
  544. }
  545. }
  546. }
  547. }
  548. return $appsstatus;
  549. }
  550. /**
  551. * creates a new user in the database
  552. * @param string $uid user_id of the user to be created
  553. * @param string $hash hash of the user to be created
  554. * @return bool result of user creation
  555. */
  556. public static function createUser( $uid, $hash ) {
  557. // Check if userid exists
  558. if(OC_User::userExists( $uid )) {
  559. return false;
  560. }
  561. // Create the user
  562. $query = OC_DB::prepare( "INSERT INTO `*PREFIX*users` ( `uid`, `password` ) VALUES( ?, ? )" );
  563. $result = $query->execute( array( $uid, $hash));
  564. if( !$result ) {
  565. OC_Log::write('migration', 'Failed to create the new user "'.$uid."", OC_Log::ERROR);
  566. }
  567. return $result ? true : false;
  568. }
  569. }