From: Szymon Świerkosz Date: Mon, 16 Apr 2012 20:02:34 +0000 (+0200) Subject: (bug 18195) Allow changing preferences via API X-Git-Tag: 1.31.0-rc.0~23742^2 X-Git-Url: https://git.cyclocoop.org/%7B%24www_url%7Dadmin/compta/exercices/bilan.php?a=commitdiff_plain;h=a6cd69d83a631cc39277eb88bdc22ab688b45746;p=lhc%2Fweb%2Fwiklou.git (bug 18195) Allow changing preferences via API I have created an API module for changing the preferences. It allows resetting preferences (reset argument) and bulk changes of preferences (change argument) in a format: name1=value1|name2=value2 The change argument has a limitation imposed by the current API implementation as it cannot accept | in values. There is available a pair of arguments optionname and optionvalue, the latter accepts values with |. I have created optionstoken parameter in meta=userinfo to provide a token. There is already preferencestoken there, but I would like to have a consistent naming. Change-Id: I0d6c654a7354ba77e65e338423952a6a78c1150f --- diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index a1bbc3c0aa..5d81650738 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -315,6 +315,7 @@ $wgAutoloadLocalClasses = array( 'ApiMain' => 'includes/api/ApiMain.php', 'ApiMove' => 'includes/api/ApiMove.php', 'ApiOpenSearch' => 'includes/api/ApiOpenSearch.php', + 'ApiOptions' => 'includes/api/ApiOptions.php', 'ApiPageSet' => 'includes/api/ApiPageSet.php', 'ApiParamInfo' => 'includes/api/ApiParamInfo.php', 'ApiParse' => 'includes/api/ApiParse.php', diff --git a/includes/User.php b/includes/User.php index af923ffeeb..04e7bc87e0 100644 --- a/includes/User.php +++ b/includes/User.php @@ -2281,7 +2281,10 @@ class User { * Reset all options to the site defaults */ public function resetOptions() { + $this->load(); + $this->mOptions = self::getDefaultOptions(); + $this->mOptionsLoaded = true; } /** diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index 8c035dcd12..7f412bc108 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -80,6 +80,7 @@ class ApiMain extends ApiBase { 'patrol' => 'ApiPatrol', 'import' => 'ApiImport', 'userrights' => 'ApiUserrights', + 'options' => 'ApiOptions', ); /** diff --git a/includes/api/ApiOptions.php b/includes/api/ApiOptions.php new file mode 100644 index 0000000000..61da785b25 --- /dev/null +++ b/includes/api/ApiOptions.php @@ -0,0 +1,150 @@ +getUser(); + + if ( $user->isAnon() ) { + $this->dieUsage( 'Anonymous users cannot change preferences', 'notloggedin' ); + } + + $params = $this->extractRequestParams(); + $changes = 0; + + if ( isset( $params['optionvalue'] ) && !isset( $params['optionname'] ) ) { + $this->dieUsageMsg( array( 'missingparam', 'optionname' ) ); + } + + if ( $params['reset'] ) { + $user->resetOptions(); + $changes++; + } + if ( count( $params['change'] ) ) { + foreach ( $params['change'] as $entry ) { + $array = explode( '=', $entry, 2 ); + $user->setOption( $array[0], isset( $array[1] ) ? $array[1] : null ); + $changes++; + } + } + if ( isset( $params['optionname'] ) ) { + $newValue = isset( $params['optionvalue'] ) ? $params['optionvalue'] : null; + $user->setOption( $params['optionname'], $newValue ); + $changes++; + } + + if ( $changes ) { + // Commit changes + $user->saveSettings(); + } else { + $this->dieUsage( 'No changes were requested', 'nochanges' ); + } + + $this->getResult()->addValue( null, $this->getModuleName(), 'success' ); + } + + public function mustBePosted() { + return true; + } + + public function isWriteMode() { + return true; + } + + public function getAllowedParams() { + return array( + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), + 'reset' => false, + 'change' => array( + ApiBase::PARAM_ISMULTI => true, + ), + 'optionname' => array( + ApiBase::PARAM_TYPE => 'string', + ), + 'optionvalue' => array( + ApiBase::PARAM_TYPE => 'string', + ), + ); + } + + public function getParamDescription() { + return array( + 'token' => 'An options token previously obtained through the meta=userinfo', + 'reset' => 'Resets all preferences to the site defaults', + 'change' => 'Pipe-separated list of changes, formatted name=value (e.g. skin=vector), value cannot contain pipe characters', + 'optionname' => 'A name of a option which should have an optionvalue set', + 'optionvalue' => 'A value of the option specified by the optionname, can contain pipe characters', + ); + } + + public function getDescription() { + return 'Change preferences of the current user'; + } + + public function getPossibleErrors() { + return array_merge( parent::getPossibleErrors(), array( + array( 'notloggedin' ), + array( 'nochanges' ), + ) ); + } + + public function needsToken() { + return true; + } + + public function getTokenSalt() { + return ''; + } + + public function getExamples() { + return array( + 'api.php?action=options&reset=&token=123ABC', + 'api.php?action=options&change=skin=vector|hideminor=1&token=123ABC', + 'api.php?action=options&reset=&change=skin=monobook&optionname=nickname&optionvalue=[[User:Beau|Beau]]%20([[User_talk:Beau|talk]])&token=123ABC', + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id$'; + } +} diff --git a/includes/api/ApiQueryUserInfo.php b/includes/api/ApiQueryUserInfo.php index a0ee227fc7..6f33e57170 100644 --- a/includes/api/ApiQueryUserInfo.php +++ b/includes/api/ApiQueryUserInfo.php @@ -102,6 +102,12 @@ class ApiQueryUserInfo extends ApiQueryBase { $vals['options'] = $user->getOptions(); } + if ( isset( $this->prop['optionstoken'] ) && + is_null( $this->getMain()->getRequest()->getVal( 'callback' ) ) + ) { + $vals['optionstoken'] = $user->getEditToken( '', $this->getMain()->getRequest() ); + } + if ( isset( $this->prop['preferencestoken'] ) && is_null( $this->getMain()->getRequest()->getVal( 'callback' ) ) ) { @@ -197,6 +203,7 @@ class ApiQueryUserInfo extends ApiQueryBase { 'rights', 'changeablegroups', 'options', + 'optionstoken', 'preferencestoken', 'editcount', 'ratelimits', @@ -220,6 +227,7 @@ class ApiQueryUserInfo extends ApiQueryBase { ' rights - Lists all the rights the current user has', ' changeablegroups - Lists the groups the current user can add to and remove from', ' options - Lists all preferences the current user has set', + ' optionstoken - Get a token to change current user\'s preferences', ' preferencestoken - Get a token to change current user\'s preferences', ' editcount - Adds the current user\'s edit count', ' ratelimits - Lists all rate limits applying to the current user', diff --git a/tests/phpunit/includes/api/ApiOptionsTest.php b/tests/phpunit/includes/api/ApiOptionsTest.php new file mode 100644 index 0000000000..8acdc1cdd8 --- /dev/null +++ b/tests/phpunit/includes/api/ApiOptionsTest.php @@ -0,0 +1,223 @@ + 'success' ); + + function setUp() { + parent::setUp(); + + $this->mUserMock = $this->getMockBuilder( 'User' ) + ->disableOriginalConstructor() + ->getMock(); + + $this->mApiMainMock = $this->getMockBuilder( 'ApiBase' ) + ->disableOriginalConstructor() + ->getMock(); + + // Create a new context + $this->mContext = new DerivativeContext( new RequestContext() ); + $this->mContext->setUser( $this->mUserMock ); + + $this->mApiMainMock->expects( $this->any() ) + ->method( 'getContext' ) + ->will( $this->returnValue( $this->mContext ) ); + + $this->mApiMainMock->expects( $this->any() ) + ->method( 'getResult' ) + ->will( $this->returnValue( new ApiResult( $this->mApiMainMock ) ) ); + + + // Empty session + $this->mSession = array(); + + $this->mTested = new ApiOptions( $this->mApiMainMock, 'options' ); + } + + private function getSampleRequest( $custom = array() ) { + $request = array( + 'token' => '123ABC', + 'change' => null, + 'optionname' => null, + 'optionvalue' => null, + ); + return array_merge( $request, $custom ); + } + + private function executeQuery( $request ) { + $this->mContext->setRequest( new FauxRequest( $request, true, $this->mSession ) ); + $this->mTested->execute(); + return $this->mTested->getResult()->getData(); + } + + /** + * @expectedException UsageException + */ + public function testNoToken() { + $request = $this->getSampleRequest( array( 'token' => null ) ); + + $this->executeQuery( $request ); + } + + public function testAnon() { + $this->mUserMock->expects( $this->once() ) + ->method( 'isAnon' ) + ->will( $this->returnValue( true ) ); + + try { + $request = $this->getSampleRequest(); + + $this->executeQuery( $request ); + } catch ( UsageException $e ) { + $this->assertEquals( 'notloggedin', $e->getCodeString() ); + $this->assertEquals( 'Anonymous users cannot change preferences', $e->getMessage() ); + return; + } + $this->fail( "UsageException was not thrown" ); + } + + public function testNoOptionname() { + try { + $request = $this->getSampleRequest( array( 'optionvalue' => '1' ) ); + + $this->executeQuery( $request ); + } catch ( UsageException $e ) { + $this->assertEquals( 'nooptionname', $e->getCodeString() ); + $this->assertEquals( 'The optionname parameter must be set', $e->getMessage() ); + return; + } + $this->fail( "UsageException was not thrown" ); + } + + public function testNoChanges() { + $this->mUserMock->expects( $this->never() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->never() ) + ->method( 'setOption' ); + + $this->mUserMock->expects( $this->never() ) + ->method( 'saveSettings' ); + + try { + $request = $this->getSampleRequest(); + + $this->executeQuery( $request ); + } catch ( UsageException $e ) { + $this->assertEquals( 'nochanges', $e->getCodeString() ); + $this->assertEquals( 'No changes were requested', $e->getMessage() ); + return; + } + $this->fail( "UsageException was not thrown" ); + } + + public function testReset() { + $this->mUserMock->expects( $this->once() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->never() ) + ->method( 'setOption' ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'saveSettings' ); + + $request = $this->getSampleRequest( array( 'reset' => '' ) ); + + $response = $this->executeQuery( $request ); + + $this->assertEquals( self::$Success, $response ); + } + + public function testOptionWithValue() { + $this->mUserMock->expects( $this->never() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'name' ), $this->equalTo( 'value' ) ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'saveSettings' ); + + $request = $this->getSampleRequest( array( 'optionname' => 'name', 'optionvalue' => 'value' ) ); + + $response = $this->executeQuery( $request ); + + $this->assertEquals( self::$Success, $response ); + } + + public function testOptionResetValue() { + $this->mUserMock->expects( $this->never() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'name' ), $this->equalTo( null ) ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'saveSettings' ); + + $request = $this->getSampleRequest( array( 'optionname' => 'name' ) ); + $response = $this->executeQuery( $request ); + + $this->assertEquals( self::$Success, $response ); + } + + public function testChange() { + $this->mUserMock->expects( $this->never() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->at( 1 ) ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'willBeNull' ), $this->equalTo( null ) ); + + $this->mUserMock->expects( $this->at( 2 ) ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'willBeEmpty' ), $this->equalTo( '' ) ); + + $this->mUserMock->expects( $this->at( 3 ) ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'willBeHappy' ), $this->equalTo( 'Happy' ) ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'saveSettings' ); + + $request = $this->getSampleRequest( array( 'change' => 'willBeNull|willBeEmpty=|willBeHappy=Happy' ) ); + + $response = $this->executeQuery( $request ); + + $this->assertEquals( self::$Success, $response ); + } + + public function testResetChangeOption() { + $this->mUserMock->expects( $this->once() ) + ->method( 'resetOptions' ); + + $this->mUserMock->expects( $this->at( 2 ) ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'willBeHappy' ), $this->equalTo( 'Happy' ) ); + + $this->mUserMock->expects( $this->at( 3 ) ) + ->method( 'setOption' ) + ->with( $this->equalTo( 'name' ), $this->equalTo( 'value' ) ); + + $this->mUserMock->expects( $this->once() ) + ->method( 'saveSettings' ); + + $args = array( + 'reset' => '', + 'change' => 'willBeHappy=Happy', + 'optionname' => 'name', + 'optionvalue' => 'value' + ); + + $response = $this->executeQuery( $this->getSampleRequest( $args ) ); + + $this->assertEquals( self::$Success, $response ); + } +}