(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
This commit is contained in:
Szymon Świerkosz 2012-04-16 22:02:34 +02:00 committed by Andrew Garrett
parent 0e6dc67f12
commit a6cd69d83a
6 changed files with 386 additions and 0 deletions

View file

@ -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',

View file

@ -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;
}
/**

View file

@ -80,6 +80,7 @@ class ApiMain extends ApiBase {
'patrol' => 'ApiPatrol',
'import' => 'ApiImport',
'userrights' => 'ApiUserrights',
'options' => 'ApiOptions',
);
/**

150
includes/api/ApiOptions.php Normal file
View file

@ -0,0 +1,150 @@
<?php
/**
*
*
* Created on Apr 15, 2012
*
* Copyright © 2012 Szymon Świerkosz beau@adres.pl
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
/**
* API module that facilitates the changing of user's preferences.
* Requires API write mode to be enabled.
*
* @ingroup API
*/
class ApiOptions extends ApiBase {
public function __construct( $main, $action ) {
parent::__construct( $main, $action );
}
/**
* Changes preferences of the current user.
*/
public function execute() {
$user = $this->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$';
}
}

View file

@ -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',

View file

@ -0,0 +1,223 @@
<?php
/**
* @group API
*/
class ApiOptionsTest extends MediaWikiLangTestCase {
private $mTested, $mApiMainMock, $mUserMock, $mContext, $mSession;
private static $Success = array( 'options' => '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 );
}
}