username = $username; $this->realname = $realname; $this->email = $email; $this->groups = $groups; // don't allow user to hardcode or select passwords -- people sometimes run tests // on live wikis. Sometimes we create sysop users in these tests. A sysop user with // a known password would be a Bad Thing. $this->password = User::randomPassword(); $this->user = User::newFromName( $this->username ); $this->user->load(); // In an ideal world we'd have a new wiki (or mock data store) for every single test. // But for now, we just need to create or update the user with the desired properties. // we particularly need the new password, since we just generated it randomly. // In core MediaWiki, there is no functionality to delete users, so this is the best we can do. if ( !$this->user->getID() ) { // create the user $this->user = User::createNew( $this->username, array( "email" => $this->email, "real_name" => $this->realname ) ); if ( !$this->user ) { throw new Exception( "error creating user" ); } } // update the user to use the new random password and other details $this->user->setPassword( $this->password ); $this->user->setEmail( $this->email ); $this->user->setRealName( $this->realname ); // remove all groups, replace with any groups specified foreach ( $this->user->getGroups() as $group ) { $this->user->removeGroup( $group ); } if ( count( $this->groups ) ) { foreach ( $this->groups as $group ) { $this->user->addGroup( $group ); } } $this->user->saveSettings(); } } abstract class ApiTestCase extends MediaWikiTestCase { public static $users; function setUp() { global $wgContLang, $wgAuth, $wgMemc, $wgRequest, $wgUser; $wgMemc = new FakeMemCachedClient(); $wgContLang = Language::factory( 'en' ); $wgAuth = new StubObject( 'wgAuth', 'AuthPlugin' ); $wgRequest = new FauxRequest( array() ); self::$users = array( 'sysop' => new ApiTestUser( 'Apitestsysop', 'Api Test Sysop', 'api_test_sysop@sample.com', array( 'sysop' ) ), 'uploader' => new ApiTestUser( 'Apitestuser', 'Api Test User', 'api_test_user@sample.com', array() ) ); $wgUser = self::$users['sysop']->user; } protected function doApiRequest( $params, $session = null ) { $_SESSION = isset( $session ) ? $session : array(); $request = new FauxRequest( $params, true, $_SESSION ); $module = new ApiMain( $request, true ); $module->execute(); return array( $module->getResultData(), $request, $_SESSION ); } /** * Add an edit token to the API request * This is cheating a bit -- we grab a token in the correct format and then add it to the pseudo-session and to the * request, without actually requesting a "real" edit token * @param $params: key-value API params * @param $session: session array */ protected function doApiRequestWithToken( $params, $session ) { if ( $session['wsToken'] ) { // add edit token to fake session $session['wsEditToken'] = $session['wsToken']; // add token to request parameters $params['token'] = md5( $session['wsToken'] ) . User::EDIT_TOKEN_SUFFIX; return $this->doApiRequest( $params, $session ); } else { throw new Exception( "request data not in right format" ); } } } /** * @group Database * @group Destructive * * This is pretty sucky... needs to be prettified. */ class ApiUploadTest extends ApiTestCase { /** * Fixture -- run before every test */ public function setUp() { global $wgEnableUploads, $wgEnableAPI; parent::setUp(); $wgEnableUploads = true; $wgEnableAPI = true; wfSetupSession(); ini_set( 'log_errors', 1 ); ini_set( 'error_reporting', 1 ); ini_set( 'display_errors', 1 ); $this->clearFakeUploads(); } /** * Fixture -- run after every test * Clean up temporary files etc. */ function tearDown() { } /** * Testing login * XXX this is a funny way of getting session context */ function testLogin() { $user = self::$users['uploader']; $params = array( 'action' => 'login', 'lgname' => $user->username, 'lgpassword' => $user->password ); list( $result, , ) = $this->doApiRequest( $params ); $this->assertArrayHasKey( "login", $result ); $this->assertArrayHasKey( "result", $result['login'] ); $this->assertEquals( "NeedToken", $result['login']['result'] ); $token = $result['login']['token']; $params = array( 'action' => 'login', 'lgtoken' => $token, 'lgname' => $user->username, 'lgpassword' => $user->password ); list( $result, , $session ) = $this->doApiRequest( $params ); $this->assertArrayHasKey( "login", $result ); $this->assertArrayHasKey( "result", $result['login'] ); $this->assertEquals( "Success", $result['login']['result'] ); $this->assertArrayHasKey( 'lgtoken', $result['login'] ); return $session; } /** * @depends testLogin */ public function testUploadRequiresToken( $session ) { $exception = false; try { $this->doApiRequest( array( 'action' => 'upload' ) ); } catch ( UsageException $e ) { $exception = true; $this->assertEquals( "The token parameter must be set", $e->getMessage() ); } $this->assertTrue( $exception, "Got exception" ); } /** * @depends testLogin */ public function testUploadMissingParams( $session ) { global $wgUser; $wgUser = self::$users['uploader']->user; $exception = false; try { $this->doApiRequestWithToken( array( 'action' => 'upload', ), $session ); } catch ( UsageException $e ) { $exception = true; $this->assertEquals( "One of the parameters sessionkey, file, url, statuskey is required", $e->getMessage() ); } $this->assertTrue( $exception, "Got exception" ); } /** * @depends testLogin */ public function testUpload( $session ) { global $wgUser; $wgUser = self::$users['uploader']->user; $extension = 'png'; $mimeType = 'image/png'; try { $randomImageGenerator = new RandomImageGenerator(); } catch ( Exception $e ) { $this->markTestIncomplete( $e->getMessage() ); } $filePaths = $randomImageGenerator->writeImages( 1, $extension, wfTempDir() ); $filePath = $filePaths[0]; $fileSize = filesize( $filePath ); $fileName = basename( $filePath ); $this->deleteFileByFileName( $fileName ); $this->deleteFileByContent( $filePath ); if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) { $this->markTestIncomplete( "Couldn't upload file!\n" ); } $params = array( 'action' => 'upload', 'filename' => $fileName, 'file' => 'dummy content', 'comment' => 'dummy comment', 'text' => "This is the page text for $fileName", ); $exception = false; try { list( $result, , ) = $this->doApiRequestWithToken( $params, $session ); } catch ( UsageException $e ) { $exception = true; } $this->assertTrue( isset( $result['upload'] ) ); $this->assertEquals( 'Success', $result['upload']['result'] ); $this->assertEquals( $fileSize, ( int )$result['upload']['imageinfo']['size'] ); $this->assertEquals( $mimeType, $result['upload']['imageinfo']['mime'] ); $this->assertFalse( $exception ); // clean up $this->deleteFileByFilename( $fileName ); unlink( $filePath ); } /** * @depends testLogin */ public function testUploadZeroLength( $session ) { global $wgUser; $wgUser = self::$users['uploader']->user; $mimeType = 'image/png'; $filePath = tempnam( wfTempDir(), "" ); $fileName = "apiTestUploadZeroLength.png"; $this->deleteFileByFileName( $fileName ); if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) { $this->markTestIncomplete( "Couldn't upload file!\n" ); } $params = array( 'action' => 'upload', 'filename' => $fileName, 'file' => 'dummy content', 'comment' => 'dummy comment', 'text' => "This is the page text for $fileName", ); $exception = false; try { $this->doApiRequestWithToken( $params, $session ); } catch ( UsageException $e ) { $this->assertContains( 'The file you submitted was empty', $e->getMessage() ); $exception = true; } $this->assertTrue( $exception ); // clean up $this->deleteFileByFilename( $fileName ); unlink( $filePath ); } /** * @depends testLogin */ public function testUploadSameFileName( $session ) { global $wgUser; $wgUser = self::$users['uploader']->user; $extension = 'png'; $mimeType = 'image/png'; try { $randomImageGenerator = new RandomImageGenerator(); } catch ( Exception $e ) { $this->markTestIncomplete( $e->getMessage() ); } $filePaths = $randomImageGenerator->writeImages( 2, $extension, wfTempDir() ); // we'll reuse this filename $fileName = basename( $filePaths[0] ); // clear any other files with the same name $this->deleteFileByFileName( $fileName ); // we reuse these params $params = array( 'action' => 'upload', 'filename' => $fileName, 'file' => 'dummy content', 'comment' => 'dummy comment', 'text' => "This is the page text for $fileName", ); // first upload .... should succeed if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePaths[0] ) ) { $this->markTestIncomplete( "Couldn't upload file!\n" ); } $exception = false; try { list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session ); } catch ( UsageException $e ) { $exception = true; } $this->assertTrue( isset( $result['upload'] ) ); $this->assertEquals( 'Success', $result['upload']['result'] ); $this->assertFalse( $exception ); // second upload with the same name (but different content) if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePaths[1] ) ) { $this->markTestIncomplete( "Couldn't upload file!\n" ); } $exception = false; try { list( $result, , ) = $this->doApiRequestWithToken( $params, $session ); } catch ( UsageException $e ) { $exception = true; } $this->assertTrue( isset( $result['upload'] ) ); $this->assertEquals( 'Warning', $result['upload']['result'] ); $this->assertTrue( isset( $result['upload']['warnings'] ) ); $this->assertTrue( isset( $result['upload']['warnings']['exists'] ) ); $this->assertFalse( $exception ); // clean up $this->deleteFileByFilename( $fileName ); unlink( $filePaths[0] ); unlink( $filePaths[1] ); } /** * @depends testLogin */ public function testUploadSameContent( $session ) { global $wgUser; $wgUser = self::$users['uploader']->user; $extension = 'png'; $mimeType = 'image/png'; try { $randomImageGenerator = new RandomImageGenerator(); } catch ( Exception $e ) { $this->markTestIncomplete( $e->getMessage() ); } $filePaths = $randomImageGenerator->writeImages( 1, $extension, wfTempDir() ); $fileNames[0] = basename( $filePaths[0] ); $fileNames[1] = "SameContentAs" . $fileNames[0]; // clear any other files with the same name or content $this->deleteFileByContent( $filePaths[0] ); $this->deleteFileByFileName( $fileNames[0] ); $this->deleteFileByFileName( $fileNames[1] ); // first upload .... should succeed $params = array( 'action' => 'upload', 'filename' => $fileNames[0], 'file' => 'dummy content', 'comment' => 'dummy comment', 'text' => "This is the page text for " . $fileNames[0], ); if (! $this->fakeUploadFile( 'file', $fileNames[0], $mimeType, $filePaths[0] ) ) { $this->markTestIncomplete( "Couldn't upload file!\n" ); } $exception = false; try { list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session ); } catch ( UsageException $e ) { $exception = true; } $this->assertTrue( isset( $result['upload'] ) ); $this->assertEquals( 'Success', $result['upload']['result'] ); $this->assertFalse( $exception ); // second upload with the same content (but different name) if (! $this->fakeUploadFile( 'file', $fileNames[1], $mimeType, $filePaths[0] ) ) { $this->markTestIncomplete( "Couldn't upload file!\n" ); } $params = array( 'action' => 'upload', 'filename' => $fileNames[1], 'file' => 'dummy content', 'comment' => 'dummy comment', 'text' => "This is the page text for " . $fileNames[1], ); $exception = false; try { list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session ); } catch ( UsageException $e ) { $exception = true; } $this->assertTrue( isset( $result['upload'] ) ); $this->assertEquals( 'Warning', $result['upload']['result'] ); $this->assertTrue( isset( $result['upload']['warnings'] ) ); $this->assertTrue( isset( $result['upload']['warnings']['duplicate'] ) ); $this->assertFalse( $exception ); // clean up $this->deleteFileByFilename( $fileNames[0] ); $this->deleteFileByFilename( $fileNames[1] ); unlink( $filePaths[0] ); } /** * @depends testLogin */ public function testUploadStash( $session ) { global $wgUser; $wgUser = self::$users['uploader']->user; $extension = 'png'; $mimeType = 'image/png'; try { $randomImageGenerator = new RandomImageGenerator(); } catch ( Exception $e ) { $this->markTestIncomplete( $e->getMessage() ); } $filePaths = $randomImageGenerator->writeImages( 1, $extension, wfTempDir() ); $filePath = $filePaths[0]; $fileSize = filesize( $filePath ); $fileName = basename( $filePath ); $this->deleteFileByFileName( $fileName ); $this->deleteFileByContent( $filePath ); if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) { $this->markTestIncomplete( "Couldn't upload file!\n" ); } $params = array( 'action' => 'upload', 'stash' => 1, 'filename' => $fileName, 'file' => 'dummy content', 'comment' => 'dummy comment', 'text' => "This is the page text for $fileName", ); $exception = false; try { list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session ); } catch ( UsageException $e ) { $exception = true; } $this->assertFalse( $exception ); $this->assertTrue( isset( $result['upload'] ) ); $this->assertEquals( 'Success', $result['upload']['result'] ); $this->assertEquals( $fileSize, ( int )$result['upload']['imageinfo']['size'] ); $this->assertEquals( $mimeType, $result['upload']['imageinfo']['mime'] ); $this->assertTrue( isset( $result['upload']['sessionkey'] ) ); $sessionkey = $result['upload']['sessionkey']; // it should be visible from Special:UploadStash // XXX ...but how to test this, with a fake WebRequest with the session? // now we should try to release the file from stash $params = array( 'action' => 'upload', 'sessionkey' => $sessionkey, 'filename' => $fileName, 'comment' => 'dummy comment', 'text' => "This is the page text for $fileName, altered", ); $this->clearFakeUploads(); $exception = false; try { list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session ); } catch ( UsageException $e ) { $exception = true; } $this->assertTrue( isset( $result['upload'] ) ); $this->assertEquals( 'Success', $result['upload']['result'] ); $this->assertFalse( $exception ); // clean up $this->deleteFileByFilename( $fileName ); unlink( $filePath ); } /** * Helper function -- remove files and associated articles by Title * @param $title Title: title to be removed */ public function deleteFileByTitle( $title ) { if ( $title->exists() ) { $file = wfFindFile( $title, array( 'ignoreRedirect' => true ) ); $noOldArchive = ""; // yes this really needs to be set this way $comment = "removing for test"; $restrictDeletedVersions = false; $status = FileDeleteForm::doDelete( $title, $file, $noOldArchive, $comment, $restrictDeletedVersions ); if ( !$status->isGood() ) { return false; } $article = new Article( $title ); $article->doDeleteArticle( "removing for test" ); // see if it now doesn't exist; reload $title = Title::newFromText( $fileName, NS_FILE ); } return ! ( $title && $title instanceof Title && $title->exists() ); } /** * Helper function -- remove files and associated articles with a particular filename * @param $fileName String: filename to be removed */ public function deleteFileByFileName( $fileName ) { return $this->deleteFileByTitle( Title::newFromText( $fileName, NS_FILE ) ); } /** * Helper function -- given a file on the filesystem, find matching content in the db (and associated articles) and remove them. * @param $filePath String: path to file on the filesystem */ public function deleteFileByContent( $filePath ) { $hash = File::sha1Base36( $filePath ); $dupes = RepoGroup::singleton()->findBySha1( $hash ); $success = true; foreach ( $dupes as $dupe ) { $success &= $this->deleteFileByTitle( $dupe->getTitle() ); } return $success; } /** * Fake an upload by dumping the file into temp space, and adding info to $_FILES. * (This is what PHP would normally do). * @param $fieldName String: name this would have in the upload form * @param $fileName String: name to title this * @param $type String: mime type * @param $filePath String: path where to find file contents */ function fakeUploadFile( $fieldName, $fileName, $type, $filePath ) { $tmpName = tempnam( wfTempDir(), "" ); if ( !file_exists( $filePath ) ) { throw new Exception( "$filePath doesn't exist!" ); }; if ( !copy( $filePath, $tmpName ) ) { throw new Exception( "couldn't copy $filePath to $tmpName" ); } clearstatcache(); $size = filesize( $tmpName ); if ( $size === false ) { throw new Exception( "couldn't stat $tmpName" ); } $_FILES[ $fieldName ] = array( 'name' => $fileName, 'type' => $type, 'tmp_name' => $tmpName, 'size' => $size, 'error' => null ); return true; } /** * Remove traces of previous fake uploads */ function clearFakeUploads() { $_FILES = array(); } }