PageRenderTime 586ms CodeModel.GetById 141ms app.highlight 284ms RepoModel.GetById 80ms app.codeStats 1ms

/lib/migrate.php

https://github.com/sezuan/core
PHP | 693 lines | 530 code | 39 blank | 124 comment | 55 complexity | 7ae70673eab99766d0a99034c1fa1fee MD5 | raw file
  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
 24/**
 25 * provides an interface to migrate users and whole ownclouds
 26 */
 27class OC_Migrate{
 28
 29
 30	// Array of OC_Migration_Provider objects
 31	static private $providers=array();
 32	// User id of the user to import/export
 33	static private $uid=false;
 34	// Holds the ZipArchive object
 35	static private $zip=false;
 36	// Stores the type of export
 37	static private $exporttype=false;
 38	// Array of temp files to be deleted after zip creation
 39	static private $tmpfiles=array();
 40	// Holds the db object
 41	static private $MDB2=false;
 42	// Schema db object
 43	static private $schema=false;
 44	// Path to the sqlite db
 45	static private $dbpath=false;
 46	// Holds the path to the zip file
 47	static private $zippath=false;
 48	// Holds the OC_Migration_Content object
 49	static private $content=false;
 50
 51	/**
 52	 * register a new migration provider
 53	 * @param OC_Migrate_Provider $provider
 54	 */
 55	public static function registerProvider($provider) {
 56		self::$providers[]=$provider;
 57	}
 58
 59	/**
 60	* @brief finds and loads the providers
 61	*/
 62	static private function findProviders() {
 63		// Find the providers
 64		$apps = OC_App::getAllApps();
 65
 66		foreach($apps as $app) {
 67			$path = OC_App::getAppPath($app) . '/appinfo/migrate.php';
 68			if( file_exists( $path ) ) {
 69				include $path;
 70			}
 71		}
 72	}
 73
 74	/**
 75	 * @brief exports a user, or owncloud instance
 76	 * @param optional $uid string user id of user to export if export type is user, defaults to current
 77	 * @param ootional $type string type of export, defualts to user
 78	 * @param otional $path string path to zip output folder
 79	 * @return false on error, path to zip on success
 80	 */
 81	public static function export( $uid=null, $type='user', $path=null ) {
 82		$datadir = OC_Config::getValue( 'datadirectory' );
 83		// Validate export type
 84		$types = array( 'user', 'instance', 'system', 'userfiles' );
 85		if( !in_array( $type, $types ) ) {
 86			OC_Log::write( 'migration', 'Invalid export type', OC_Log::ERROR );
 87			return json_encode( array( 'success' => false )  );
 88		}
 89		self::$exporttype = $type;
 90		// Userid?
 91		if( self::$exporttype == 'user' ) {
 92			// Check user exists
 93			self::$uid = is_null($uid) ? OC_User::getUser() : $uid;
 94			if(!OC_User::userExists(self::$uid)) {
 95				return json_encode( array( 'success' => false) );
 96			}
 97		}
 98		// Calculate zipname
 99		if( self::$exporttype == 'user' ) {
100			$zipname = 'oc_export_' . self::$uid . '_' . date("y-m-d_H-i-s") . '.zip';
101		} else {
102			$zipname = 'oc_export_' . self::$exporttype . '_' . date("y-m-d_H-i-s") . '.zip';
103		}
104		// Calculate path
105		if( self::$exporttype == 'user' ) {
106			self::$zippath = $datadir . '/' . self::$uid . '/' . $zipname;
107		} else {
108			if( !is_null( $path ) ) {
109				// Validate custom path
110				if( !file_exists( $path ) || !is_writeable( $path ) ) {
111					OC_Log::write( 'migration', 'Path supplied is invalid.', OC_Log::ERROR );
112					return json_encode( array( 'success' => false ) );
113				}
114				self::$zippath = $path . $zipname;
115			} else {
116				// Default path
117				self::$zippath = get_temp_dir() . '/' . $zipname;
118			}
119		}
120		// Create the zip object
121		if( !self::createZip() ) {
122			return json_encode( array( 'success' => false ) );
123		}
124		// Do the export
125		self::findProviders();
126		$exportdata = array();
127		switch( self::$exporttype ) {
128			case 'user':
129				// Connect to the db
130				self::$dbpath = $datadir . '/' . self::$uid . '/migration.db';
131				if( !self::connectDB() ) {
132					return json_encode( array( 'success' => false ) );
133				}
134				self::$content = new OC_Migration_Content( self::$zip, self::$MDB2 );
135				// Export the app info
136				$exportdata = self::exportAppData();
137				// Add the data dir to the zip
138				self::$content->addDir(OC_User::getHome(self::$uid), true, '/' );
139				break;
140			case 'instance':
141				self::$content = new OC_Migration_Content( self::$zip );
142				// Creates a zip that is compatable with the import function
143				$dbfile = tempnam( get_temp_dir(), "owncloud_export_data_" );
144				OC_DB::getDbStructure( $dbfile, 'MDB2_SCHEMA_DUMP_ALL');
145
146				// Now add in *dbname* and *dbprefix*
147				$dbexport = file_get_contents( $dbfile );
148				$dbnamestring = "<database>\n\n <name>" . OC_Config::getValue( "dbname", "owncloud" );
149				$dbtableprefixstring = "<table>\n\n  <name>" . OC_Config::getValue( "dbtableprefix", "oc_" );
150				$dbexport = str_replace( $dbnamestring, "<database>\n\n <name>*dbname*", $dbexport );
151				$dbexport = str_replace( $dbtableprefixstring, "<table>\n\n  <name>*dbprefix*", $dbexport );
152				// Add the export to the zip
153				self::$content->addFromString( $dbexport, "dbexport.xml" );
154				// Add user data
155				foreach(OC_User::getUsers() as $user) {
156					self::$content->addDir(OC_User::getHome($user), true, "/userdata/" );
157				}
158				break;
159			case 'userfiles':
160				self::$content = new OC_Migration_Content( self::$zip );
161				// Creates a zip with all of the users files
162				foreach(OC_User::getUsers() as $user) {
163					self::$content->addDir(OC_User::getHome($user), true, "/" );
164				}
165				break;
166			case 'system':
167				self::$content = new OC_Migration_Content( self::$zip );
168				// Creates a zip with the owncloud system files
169				self::$content->addDir( OC::$SERVERROOT . '/', false, '/');
170				foreach (array(
171					".git",
172					"3rdparty",
173					"apps",
174					"core",
175					"files",
176					"l10n",
177					"lib",
178					"ocs",
179					"search",
180					"settings",
181					"tests"
182				) as $dir) {
183					self::$content->addDir( OC::$SERVERROOT . '/' . $dir, true, "/");
184				}
185				break;
186		}
187		if( !$info = self::getExportInfo( $exportdata ) ) {
188			return json_encode( array( 'success' => false ) );
189		}
190		// Add the export info json to the export zip
191		self::$content->addFromString( $info, 'export_info.json' );
192		if( !self::$content->finish() ) {
193			return json_encode( array( 'success' => false ) );
194		}
195		return json_encode( array( 'success' => true, 'data' => self::$zippath ) );
196	}
197
198	/**
199	* @brief imports a user, or owncloud instance
200	* @param $path string path to zip
201	* @param optional $type type of import (user or instance)
202	* @param optional $uid userid of new user
203	*/
204	public static function import( $path, $type='user', $uid=null ) {
205
206		$datadir = OC_Config::getValue( 'datadirectory' );
207		// Extract the zip
208		if( !$extractpath = self::extractZip( $path ) ) {
209			return json_encode( array( 'success' => false ) );
210		}
211		// Get export_info.json
212		$scan = scandir( $extractpath );
213		// Check for export_info.json
214		if( !in_array( 'export_info.json', $scan ) ) {
215			OC_Log::write( 'migration', 'Invalid import file, export_info.json not found', OC_Log::ERROR );
216			return json_encode( array( 'success' => false ) );
217		}
218		$json = json_decode( file_get_contents( $extractpath . 'export_info.json' ) );
219		if( $json->exporttype != $type ) {
220			OC_Log::write( 'migration', 'Invalid import file', OC_Log::ERROR );
221			return json_encode( array( 'success' => false ) );
222		}
223		self::$exporttype = $type;
224
225		$currentuser = OC_User::getUser();
226
227		// Have we got a user if type is user
228		if( self::$exporttype == 'user' ) {
229			self::$uid = !is_null($uid) ? $uid : $currentuser;
230		}
231
232		// We need to be an admin if we are not importing our own data
233		if(($type == 'user' && self::$uid != $currentuser) || $type != 'user' ) {
234			if( !OC_User::isAdminUser($currentuser)) {
235				// Naughty.
236				OC_Log::write( 'migration', 'Import not permitted.', OC_Log::ERROR );
237				return json_encode( array( 'success' => false ) );
238			}
239		}
240
241		// Handle export types
242		switch( self::$exporttype ) {
243			case 'user':
244				// Check user availability
245				if( !OC_User::userExists( self::$uid ) ) {
246					OC_Log::write( 'migration', 'User doesn\'t exist', OC_Log::ERROR );
247					return json_encode( array( 'success' => false ) );
248				}
249
250				// Check if the username is valid
251				if( preg_match( '/[^a-zA-Z0-9 _\.@\-]/', $json->exporteduser )) {
252					OC_Log::write( 'migration', 'Username is not valid', OC_Log::ERROR );
253					return json_encode( array( 'success' => false ) );
254				}
255
256				// Copy data
257				$userfolder = $extractpath . $json->exporteduser;
258				$newuserfolder = $datadir . '/' . self::$uid;
259				foreach(scandir($userfolder) as $file){
260					if($file !== '.' && $file !== '..' && is_dir($file)) {
261						$file = str_replace(array('/', '\\'), '',  $file);
262
263						// Then copy the folder over
264						OC_Helper::copyr($userfolder.'/'.$file, $newuserfolder.'/'.$file);
265					}
266				}
267				// Import user app data
268				if(file_exists($extractpath . $json->exporteduser . '/migration.db')) {
269					if( !$appsimported = self::importAppData( $extractpath . $json->exporteduser . '/migration.db',
270						$json,
271						self::$uid ) ) {
272						return json_encode( array( 'success' => false ) );
273					}
274				}
275				// All done!
276				if( !self::unlink_r( $extractpath ) ) {
277					OC_Log::write( 'migration', 'Failed to delete the extracted zip', OC_Log::ERROR );
278				}
279				return json_encode( array( 'success' => true, 'data' => $appsimported ) );
280				break;
281			case 'instance':
282					/*
283					 * EXPERIMENTAL
284					// Check for new data dir and dbexport before doing anything
285					// TODO
286
287					// Delete current data folder.
288					OC_Log::write( 'migration', "Deleting current data dir", OC_Log::INFO );
289					if( !self::unlink_r( $datadir, false ) ) {
290						OC_Log::write( 'migration', 'Failed to delete the current data dir', OC_Log::ERROR );
291						return json_encode( array( 'success' => false ) );
292					}
293
294					// Copy over data
295					if( !self::copy_r( $extractpath . 'userdata', $datadir ) ) {
296						OC_Log::write( 'migration', 'Failed to copy over data directory', OC_Log::ERROR );
297						return json_encode( array( 'success' => false ) );
298					}
299
300					// Import the db
301					if( !OC_DB::replaceDB( $extractpath . 'dbexport.xml' ) ) {
302						return json_encode( array( 'success' => false ) );
303					}
304					// Done
305					return json_encode( array( 'success' => true ) );
306					*/
307				break;
308		}
309
310	}
311
312	/**
313	* @brief recursively deletes a directory
314	* @param $dir string path of dir to delete
315	* $param optional $deleteRootToo bool delete the root directory
316	* @return bool
317	*/
318	private static function unlink_r( $dir, $deleteRootToo=true ) {
319		if( !$dh = @opendir( $dir ) ) {
320			return false;
321		}
322		while (false !== ($obj = readdir($dh))) {
323			if($obj == '.' || $obj == '..') {
324				continue;
325			}
326			if (!@unlink($dir . '/' . $obj)) {
327				self::unlink_r($dir.'/'.$obj, true);
328			}
329		}
330		closedir($dh);
331		if ( $deleteRootToo ) {
332			@rmdir($dir);
333		}
334		return true;
335	}
336
337	/**
338	* @brief tries to extract the import zip
339	* @param $path string path to the zip
340	* @return string path to extract location (with a trailing slash) or false on failure
341	*/
342	static private function extractZip( $path ) {
343		self::$zip = new ZipArchive;
344		// Validate path
345		if( !file_exists( $path ) ) {
346			OC_Log::write( 'migration', 'Zip not found', OC_Log::ERROR );
347			return false;
348		}
349		if ( self::$zip->open( $path ) != true ) {
350			OC_Log::write( 'migration', "Failed to open zip file", OC_Log::ERROR );
351			return false;
352		}
353		$to = get_temp_dir() . '/oc_import_' . self::$exporttype . '_' . date("y-m-d_H-i-s") . '/';
354		if( !self::$zip->extractTo( $to ) ) {
355			return false;
356		}
357		self::$zip->close();
358		return $to;
359	}
360
361	/**
362	 * @brief connects to a MDB2 database scheme
363	 * @returns bool
364	 */
365	static private function connectScheme() {
366		// We need a mdb2 database connection
367		self::$MDB2->loadModule( 'Manager' );
368		self::$MDB2->loadModule( 'Reverse' );
369
370		// Connect if this did not happen before
371		if( !self::$schema ) {
372			require_once 'MDB2/Schema.php';
373			self::$schema=MDB2_Schema::factory( self::$MDB2 );
374		}
375
376		return true;
377	}
378
379	/**
380	 * @brief creates a migration.db in the users data dir with their app data in
381	 * @return bool whether operation was successfull
382	 */
383	private static function exportAppData( ) {
384
385		$success = true;
386		$return = array();
387
388		// Foreach provider
389		foreach( self::$providers as $provider ) {
390			// Check if the app is enabled
391			if( OC_App::isEnabled( $provider->getID() ) ) {
392				$success = true;
393				// Does this app use the database?
394				if( file_exists( OC_App::getAppPath($provider->getID()).'/appinfo/database.xml' ) ) {
395					// Create some app tables
396					$tables = self::createAppTables( $provider->getID() );
397					if( is_array( $tables ) ) {
398						// Save the table names
399						foreach($tables as $table) {
400							$return['apps'][$provider->getID()]['tables'][] = $table;
401						}
402					} else {
403						// It failed to create the tables
404						$success = false;
405					}
406				}
407
408				// Run the export function?
409				if( $success ) {
410					// Set the provider properties
411					$provider->setData( self::$uid, self::$content );
412					$return['apps'][$provider->getID()]['success'] = $provider->export();
413				} else {
414					$return['apps'][$provider->getID()]['success'] = false;
415					$return['apps'][$provider->getID()]['message'] = 'failed to create the app tables';
416				}
417
418				// Now add some app info the the return array
419				$appinfo = OC_App::getAppInfo( $provider->getID() );
420				$return['apps'][$provider->getID()]['version'] = OC_App::getAppVersion($provider->getID());
421			}
422		}
423
424		return $return;
425
426	}
427
428
429	/**
430	 * @brief generates json containing export info, and merges any data supplied
431	 * @param optional $array array of data to include in the returned json
432	 * @return bool
433	 */
434	static private function getExportInfo( $array=array() ) {
435		$info = array(
436						'ocversion' => OC_Util::getVersion(),
437						'exporttime' => time(),
438						'exportedby' => OC_User::getUser(),
439						'exporttype' => self::$exporttype,
440						'exporteduser' => self::$uid
441					);
442
443		if( !is_array( $array ) ) {
444			OC_Log::write( 'migration', 'Supplied $array was not an array in getExportInfo()', OC_Log::ERROR );
445		}
446		// Merge in other data
447		$info = array_merge( $info, (array)$array );
448		// Create json
449		$json = json_encode( $info );
450		return $json;
451	}
452
453	/**
454	 * @brief connects to migration.db, or creates if not found
455	 * @param $db optional path to migration.db, defaults to user data dir
456	 * @return bool whether the operation was successful
457	 */
458	static private function connectDB( $path=null ) {
459		// Has the dbpath been set?
460		self::$dbpath = !is_null( $path ) ? $path : self::$dbpath;
461		if( !self::$dbpath ) {
462			OC_Log::write( 'migration', 'connectDB() was called without dbpath being set', OC_Log::ERROR );
463			return false;
464		}
465		// Already connected
466		if(!self::$MDB2) {
467			require_once 'MDB2.php';
468
469			$datadir = OC_Config::getValue( "datadirectory", OC::$SERVERROOT."/data" );
470
471			// DB type
472			if( class_exists( 'SQLite3' ) ) {
473				$dbtype = 'sqlite3';
474			} else if( is_callable( 'sqlite_open' ) ) {
475				$dbtype = 'sqlite';
476			} else {
477				OC_Log::write( 'migration', 'SQLite not found', OC_Log::ERROR );
478				return false;
479			}
480
481			// Prepare options array
482			$options = array(
483				'portability' => MDB2_PORTABILITY_ALL & (!MDB2_PORTABILITY_FIX_CASE),
484				'log_line_break' => '<br>',
485				'idxname_format' => '%s',
486				'debug' => true,
487				'quote_identifier' => true
488				);
489			$dsn = array(
490				'phptype'  => $dbtype,
491				'database' => self::$dbpath,
492				'mode' => '0644'
493			);
494
495			// Try to establish connection
496			self::$MDB2 = MDB2::factory( $dsn, $options );
497			// Die if we could not connect
498			if( PEAR::isError( self::$MDB2 ) ) {
499				die( self::$MDB2->getMessage() );
500				OC_Log::write( 'migration', 'Failed to create/connect to migration.db', OC_Log::FATAL );
501				OC_Log::write( 'migration', self::$MDB2->getUserInfo(), OC_Log::FATAL );
502				OC_Log::write( 'migration', self::$MDB2->getMessage(), OC_Log::FATAL );
503				return false;
504			}
505			// We always, really always want associative arrays
506			self::$MDB2->setFetchMode(MDB2_FETCHMODE_ASSOC);
507		}
508		return true;
509
510	}
511
512	/**
513	 * @brief creates the tables in migration.db from an apps database.xml
514	 * @param $appid string id of the app
515	 * @return bool whether the operation was successful
516	 */
517	static private function createAppTables( $appid ) {
518
519		if( !self::connectScheme() ) {
520			return false;
521		}
522
523		// There is a database.xml file
524		$content = file_get_contents(OC_App::getAppPath($appid) . '/appinfo/database.xml' );
525
526		$file2 = 'static://db_scheme';
527		// TODO get the relative path to migration.db from the data dir
528		// For now just cheat
529		$path = pathinfo( self::$dbpath );
530		$content = str_replace( '*dbname*', self::$uid.'/migration', $content );
531		$content = str_replace( '*dbprefix*', '', $content );
532
533		$xml = new SimpleXMLElement($content);
534		foreach($xml->table as $table) {
535			$tables[] = (string)$table->name;
536		}
537
538		file_put_contents( $file2, $content );
539
540		// Try to create tables
541		$definition = self::$schema->parseDatabaseDefinitionFile( $file2 );
542
543		unlink( $file2 );
544
545		// Die in case something went wrong
546		if( $definition instanceof MDB2_Schema_Error ) {
547			OC_Log::write( 'migration', 'Failed to parse database.xml for: '.$appid, OC_Log::FATAL );
548			OC_Log::write( 'migration', $definition->getMessage().': '.$definition->getUserInfo(), OC_Log::FATAL );
549			return false;
550		}
551
552		$definition['overwrite'] = true;
553
554		$ret = self::$schema->createDatabase( $definition );
555
556		// Die in case something went wrong
557		if( $ret instanceof MDB2_Error ) {
558			OC_Log::write( 'migration', 'Failed to create tables for: '.$appid, OC_Log::FATAL );
559			OC_Log::write( 'migration', $ret->getMessage().': '.$ret->getUserInfo(), OC_Log::FATAL );
560			return false;
561		}
562		return $tables;
563
564	}
565
566	/**
567	* @brief tries to create the zip
568	* @param $path string path to zip destination
569	* @return bool
570	*/
571	static private function createZip() {
572		self::$zip = new ZipArchive;
573		// Check if properties are set
574		if( !self::$zippath ) {
575			OC_Log::write('migration', 'createZip() called but $zip and/or $zippath have not been set', OC_Log::ERROR);
576			return false;
577		}
578		if ( self::$zip->open( self::$zippath, ZIPARCHIVE::CREATE | ZIPARCHIVE::OVERWRITE ) !== true ) {
579			OC_Log::write('migration',
580				'Failed to create the zip with error: '.self::$zip->getStatusString(),
581				OC_Log::ERROR);
582			return false;
583		} else {
584			return true;
585		}
586	}
587
588	/**
589	* @brief returns an array of apps that support migration
590	* @return array
591	*/
592	static public function getApps() {
593		$allapps = OC_App::getAllApps();
594		foreach($allapps as $app) {
595			$path = self::getAppPath($app) . '/lib/migrate.php';
596			if( file_exists( $path ) ) {
597				$supportsmigration[] = $app;
598			}
599		}
600		return $supportsmigration;
601	}
602
603	/**
604	* @brief imports a new user
605	* @param $db string path to migration.db
606	* @param $info object of migration info
607	* @param $uid optional uid to use
608	* @return array of apps with import statuses, or false on failure.
609	*/
610	public static function importAppData( $db, $info, $uid=null ) {
611		// Check if the db exists
612		if( file_exists( $db ) ) {
613			// Connect to the db
614			if(!self::connectDB( $db )) {
615				OC_Log::write('migration', 'Failed to connect to migration.db', OC_Log::ERROR);
616				return false;
617			}
618		} else {
619			OC_Log::write('migration', 'Migration.db not found at: '.$db, OC_Log::FATAL );
620			return false;
621		}
622
623		// Find providers
624		self::findProviders();
625
626		// Generate importinfo array
627		$importinfo = array(
628							'olduid' => $info->exporteduser,
629							'newuid' => self::$uid
630							);
631
632		foreach( self::$providers as $provider) {
633			// Is the app in the export?
634			$id = $provider->getID();
635			if( isset( $info->apps->$id ) ) {
636				// Is the app installed
637				if( !OC_App::isEnabled( $id ) ) {
638					OC_Log::write( 'migration',
639					'App: ' . $id . ' is not installed, can\'t import data.',
640					OC_Log::INFO );
641					$appsstatus[$id] = 'notsupported';
642				} else {
643					// Did it succeed on export?
644					if( $info->apps->$id->success ) {
645						// Give the provider the content object
646						if( !self::connectDB( $db ) ) {
647							return false;
648						}
649						$content = new OC_Migration_Content( self::$zip, self::$MDB2 );
650						$provider->setData( self::$uid, $content, $info );
651						// Then do the import
652						if( !$appsstatus[$id] = $provider->import( $info->apps->$id, $importinfo ) ) {
653							// Failed to import app
654							OC_Log::write( 'migration',
655								'Failed to import app data for user: ' . self::$uid . ' for app: ' . $id,
656								OC_Log::ERROR );
657						}
658					} else {
659						// Add to failed list
660						$appsstatus[$id] = false;
661					}
662				}
663			}
664		}
665
666		return $appsstatus;
667
668	}
669
670	/*
671	* @brief creates a new user in the database
672	* @param $uid string user_id of the user to be created
673	* @param $hash string hash of the user to be created
674	* @return bool result of user creation
675	*/
676	public static function createUser( $uid, $hash ) {
677
678		// Check if userid exists
679		if(OC_User::userExists( $uid )) {
680			return false;
681		}
682
683		// Create the user
684		$query = OC_DB::prepare( "INSERT INTO `*PREFIX*users` ( `uid`, `password` ) VALUES( ?, ? )" );
685		$result = $query->execute( array( $uid, $hash));
686		if( !$result ) {
687			OC_Log::write('migration', 'Failed to create the new user "'.$uid."", OC_Log::ERROR);
688		}
689		return $result ? true : false;
690
691	}
692
693}