Yurik has uploaded a new change for review.
https://gerrit.wikimedia.org/r/197384
Change subject: Unit tests and a number of minor bug fixes
......................................................................
Unit tests and a number of minor bug fixes
To run, from vagrant's /vagrant/mediawiki dir:
$ php tests/phpunit/phpunit.php
extensions/Gather/tests/phpunit/api/ApiQueryLists.php
Bug fixes:
* watchlist list row is no longer created unless action=editlist
tries to change something to non-default value
(description, privacy, etc)
* Security fix - incorrect check with ids= parameter allowed
access to private list under a certain condition
* float -> int for counts (apparent in unit tests, not output)
Change-Id: I392fd6e44c7fc4117fdab8c9c9dfe02fcdbfa2e1
---
M includes/api/ApiEditList.php
M includes/api/ApiQueryLists.php
A tests/phpunit/api/ApiQueryLists.php
3 files changed, 363 insertions(+), 18 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Gather
refs/changes/84/197384/1
diff --git a/includes/api/ApiEditList.php b/includes/api/ApiEditList.php
index 89c4b7e..42c1fdc 100644
--- a/includes/api/ApiEditList.php
+++ b/includes/api/ApiEditList.php
@@ -287,27 +287,43 @@
* @param $isWatchlist
*/
private function createRow( DatabaseBase $dbw, User $user, array
$params, &$isWatchlist ) {
- $id = $dbw->nextSequenceValue( 'gather_list_gl_id_seq' );
$label = $isWatchlist ? '' : $params['label'];
- $dbw->insert( 'gather_list', array(
- 'gl_id' => $id,
- 'gl_user' => $user->getId(),
- 'gl_label' => $label,
- 'gl_info' => $this->updateInfo( new stdClass(), $params
),
- ), __METHOD__, 'IGNORE' );
- $id = $dbw->insertId();
+ $info = $this->updateInfo( new stdClass(), $params );
+ $createRow = !$isWatchlist || $info;
+
+ if ( $createRow ) {
+ $id = $dbw->nextSequenceValue( 'gather_list_gl_id_seq'
);
+ $dbw->insert( 'gather_list', array(
+ 'gl_id' => $id,
+ 'gl_user' => $user->getId(),
+ 'gl_label' => $label,
+ 'gl_info' => $info,
+ ), __METHOD__, 'IGNORE' );
+ $id = $dbw->insertId();
+ } else {
+ $id = 0;
+ }
+
if ( $id === 0 ) {
- // Already exists, update instead
+ // List already exists, update instead, or might not
need it
$row = $dbw->selectRow( 'gather_list',
array( 'gl_id', 'gl_user', 'gl_label',
'gl_info' ),
array( 'gl_user' => $user->getId(), 'gl_label'
=> $label ),
__METHOD__
);
if ( $row === false ) {
- // If creation failed, the second query should
have succeeded
- $this->dieDebug( "List was not found", 'badid'
);
+ if ( $createRow ) {
+ // If creation failed, the second query
should have succeeded
+ $this->dieDebug( "List was not found",
'badid' );
+ }
+ $this->getResult()->addValue( null,
$this->getModuleName(), array(
+ 'status' => 'nochanges',
+ 'id' => 0,
+ ) );
+ } else {
+ $isWatchlist = $row->gl_label === '';
+ $this->updateListDb( $dbw, $params, $row );
}
- $this->updateListDb( $dbw, $params, $row );
} else {
$this->getResult()->addValue( null,
$this->getModuleName(), array(
'status' => 'created',
@@ -325,7 +341,7 @@
*/
private function updateListDb( DatabaseBase $dbw, array $params, $row )
{
$update = array();
- if ( $params['label'] && $row->gl_label !== $params['label'] ) {
+ if ( $params['label'] !== null && $row->gl_label !==
$params['label'] ) {
$update['gl_label'] = $params['label'];
}
$info = self::parseListInfo( $row->gl_info, $row->gl_id, true );
diff --git a/includes/api/ApiQueryLists.php b/includes/api/ApiQueryLists.php
index 33b8b6d..89e3d23 100644
--- a/includes/api/ApiQueryLists.php
+++ b/includes/api/ApiQueryLists.php
@@ -44,7 +44,17 @@
public function execute() {
$params = $this->extractRequestParams();
+
$ids = $params['ids'];
+ if ( $ids ) {
+ $findWatchlist = array_search( 0, $ids );
+ if( $findWatchlist !== false) {
+ unset( $ids[$findWatchlist] );
+ $findWatchlist = true;
+ }
+ } else {
+ $findWatchlist = false;
+ }
/** @var User $owner */
list( $owner, $showPrivate ) = $this->calcPermissions( $params,
$ids );
@@ -57,8 +67,15 @@
if ( $owner ) {
$this->addWhereFld( 'gl_user', $owner->getId() );
}
- if ( $ids ) {
- $this->addWhereFld( 'gl_id', $ids );
+ if ( $ids || $findWatchlist ) {
+ $cond = array();
+ if ( $ids ) {
+ $cond['gl_id'] = $ids;
+ }
+ if ( $findWatchlist ) {
+ $cond['gl_label'] = '';
+ }
+ $this->addWhere( $db->makeList( $cond, LIST_OR ) );
}
$continue = $params['continue'];
@@ -106,7 +123,7 @@
// Determine if this gather_list row is viewable by the
current user
if ( $showPrivate === false && !ApiEditList::isPublic(
$info ) ) {
return true;
- } elseif ( $showPrivate === null && $row->gl_user ===
$currUserId ) {
+ } elseif ( $showPrivate === null && $row->gl_user !==
$currUserId ) {
return true;
}
@@ -326,9 +343,9 @@
// TODO: estimateRowCount() might be much faster, TBD
if ok
$db = $this->getQuery()->getNamedDB( 'watchlist',
DB_SLAVE, 'watchlist' );
// Must divide in two because of duplicate talk pages
(same as the special page)
- $counts[$wlListId] = floor(
+ $counts[$wlListId] = intval( floor(
$db->selectRowCount( 'watchlist', '*', array(
'wl_user' => $wlUserId ),
- __METHOD__ ) / 2 );
+ __METHOD__ ) / 2 ) );
}
if ( count( $ids ) > 0 ) {
$db = $this->getDB();
diff --git a/tests/phpunit/api/ApiQueryLists.php
b/tests/phpunit/api/ApiQueryLists.php
new file mode 100644
index 0000000..c0bfeef
--- /dev/null
+++ b/tests/phpunit/api/ApiQueryLists.php
@@ -0,0 +1,312 @@
+<?php
+/**
+ *
+ * Created on Feb 6, 2013
+ *
+ * Copyright © 2013 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
+ *
+ * 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
+ */
+
+/**
+ * These tests validate basic functionality of the api query module
+ *
+ * @group API
+ * @group Database
+ * @group medium
+ * @covers ApiQuery
+ */
+class ApiQueryLists extends ApiQueryTestBase {
+ protected $exceptionFromAddDBData;
+ /** @var TestUser */
+ private static $noWlUser, $wlUser, $wlOnly;
+
+ static function initUsers() {
+ if ( !self::$wlUser ) {
+ self::$wlUser = new TestUser( 'GatherWL', 'GatherWL',
'[email protected]' );
+ self::$noWlUser = new TestUser( 'GatherNoWL',
'GatherNoWL', '[email protected]' );
+ self::$wlOnly = new TestUser( 'GatherNoLst',
'GatherNoLst', '[email protected]' );
+
+ User::createNew( self::$wlUser->getUser()->getName() );
+ User::createNew( self::$noWlUser->getUser()->getName()
);
+ User::createNew( self::$wlOnly->getUser()->getName() );
+ }
+ self::$users['GatherWL'] = self::$wlUser;
+ self::$users['GatherNoWL'] = self::$noWlUser;
+ self::$users['GatherNoLst'] = self::$wlOnly;
+ }
+
+ /**
+ * Create a set of pages. These must not change, otherwise the tests
might give wrong results.
+ * @see MediaWikiTestCase::addDBData()
+ */
+ public function addDBData() {
+ try {
+ if ( !Title::newFromText( 'Gather-All' )->exists() ) {
+ $this->editPage( 'Gather-ListA', 'a' );
+ $this->editPage( 'Gather-ListB', 'b' );
+ $this->editPage( 'Gather-ListAB', 'ab' );
+ $this->editPage( 'Gather-ListW', 'w' );
+ $this->editPage( 'Gather-ListWA', 'wa' );
+ $this->editPage( 'Gather-ListWAB', 'wab' );
+ }
+ } catch ( Exception $e ) {
+ $this->exceptionFromAddDBData = $e;
+ }
+ }
+
+ protected function setUp() {
+ parent::setUp();
+ self::initUsers();
+ }
+
+ public function testAnonymous() {
+ $a = User::newFromId( 0 );
+
+ $this->assertUsage( 'an-0', '{ "list": "lists" }', $a );
+ $this->assertUsage( 'an-1', '{ "list": "lists", "lstids": 0 }',
$a );
+ }
+
+ public function testProps() {
+ $wl = self::$wlUser->getUser();
+
+ $res = $this->callApi( 'p-a0', '{ "list": "lists" }', $wl );
+ $this->assertListsEquals( 'p-a0', $res, '[{"id":0,
"watchlist":true, "label":"Watchlist"}]' );
+
+ $token = $this->getToken( $wl );
+ $this->legacyAddToWatchlist( 'p-a1',
'Gather-ListW|Gather-ListWA|Gather-ListWAB', $wl, $token );
+ $res = $this->callApi( 'p-a1', '{ "list": "lists" }', $wl );
+ $this->assertListsEquals( 'p-a1', $res, '[{"id":0,
"watchlist":true, "label":"Watchlist"}]' );
+
+ $this->addToList( 'p-a2a', 'A', 'Gather-ListWA|Gather-ListWAB',
$wl, $token );
+ $this->addToList( 'p-a2b', 'B', 'Gather-ListWAB', $wl, $token );
+ $res = $this->callApi( 'p-a2', '{ "list": "lists" }', $wl );
+ $this->assertListsEquals( 'p-a2', $res,
+ '[{"id":0, "watchlist":true,"label":"Watchlist"},
{"label":"A"}, {"label":"B"}]' );
+ }
+
+ public function testWatchlistOnly() {
+ $u = self::$wlOnly->getUser();
+ $token = $this->getToken( $u );
+ $wlOnly = '[{"id":0, "watchlist":true, "label":"Watchlist"}]';
+
+ $res = $this->callApi( 'nc-a0', '{ "list": "lists" }', $u );
+ $this->assertListsEquals( 'nc-a0', $res, $wlOnly );
+
+ $res = $this->callApi( 'nc-a1', '{ "list": "lists", "lstids": 0
}', $u );
+ $this->assertListsEquals( 'nc-a1', $res, $wlOnly );
+
+ $res = $this->callApi( 'nc-a2', '{ "list": "lists", "lstlimit":
1 }', $u );
+ $this->assertListsEquals( 'nc-a2', $res, $wlOnly );
+
+ $res = $this->callApi( 'nc-a3',
+ '{ "list": "lists", "lstprop":
"label|description|public|count" }', $u );
+ $this->assertListsEquals( 'nc-a3',$res,
+
'[{"id":0,"watchlist":true,"count":0,"label":"Watchlist","description":"","public":false}]'
+ );
+
+
+
+ $this->legacyAddToWatchlist( 'nc-b0', 'Gather-ListW', $u,
$token );
+ $res = $this->callApi( 'nc-b0', '{ "list": "lists", "lstprop":
"count" }', $u );
+ $this->assertListsEquals( 'nc-b0', $res, '[{"id": 0,
"watchlist":true, "count": 1}]' );
+
+
+ $this->addToList( 'nc-c0', 0, 'Gather-ListW', $u, $token );
+ $res = $this->callApi( 'nc-c0', '{ "list": "lists" }', $u );
+ $this->assertListsEquals( 'nc-c0', $res, $wlOnly );
+
+ $res = $this->callApi( 'nc-c1', '{ "list": "lists", "lstids": 0
}', $u );
+ $this->assertListsEquals( 'nc-c1', $res, $wlOnly );
+
+ $res = $this->callApi( 'nc-c3', '{ "list": "lists", "lstprop":
"count" }', $u );
+ $this->assertListsEquals( 'nc-c3', $res, '[{"id":0,
"watchlist":true, "count": 1}]' );
+
+
+ // What can others see from this user
+ $n = $u->getName();
+ $u2 = self::$wlUser->getUser();
+
+ $res = $this->callApi( 'nc-e0', '{ "list": "lists", "lstowner":
"' . $n . '" }', $u2 );
+ $this->assertListsEquals( 'nc-e0', $res, '[]' );
+
+ $res = $this->callApi( 'nc-e1', '{ "list": "lists", "lstowner":
"' . $n .
+ '", "lstids": 0 }', $u2 );
+ $this->assertListsEquals( 'nc-e1', $res, '[]' );
+
+
+ // Create watchlist list DB record
+ $res = $this->updateList( 'nc-f0', '{ "id":0,
"description":"aa" }', $u, $token );
+ $this->assertEquals( 'created', $res['status'], 'nc-f0' );
+ $this->assertNotEquals( 0, $res['id'], 'nc-f1' );
+ $id = $res['id'];
+ $wlOnly = array( array( 'id' => $id, 'watchlist' => true,
'label' => 'Watchlist' ) );
+
+ $res = $this->callApi( 'nc-f2', '{ "list": "lists" }', $u );
+ $this->assertListsEquals( 'nc-f2', $res, $wlOnly, false );
+
+ $res = $this->callApi( 'nc-f3', '{ "list": "lists", "lstids": 0
}', $u );
+ $this->assertListsEquals( 'nc-f3', $res, $wlOnly, false );
+
+ $res = $this->callApi( 'nc-f4',
+ '{ "list": "lists", "lstprop":
"label|description|public|count" }', $u );
+ $this->assertListsEquals( 'nc-f4',$res,
+ '[{"id":' . $id .
+
',"watchlist":true,"count":1,"label":"Watchlist","description":"aa","public":false}]',
+ false );
+
+
+ // Others still can't see the watchlist
+ $res = $this->callApi( 'nc-g0', '{ "list": "lists", "lstowner":
"' . $n . '" }', $u2 );
+ $this->assertListsEquals( 'nc-g0', $res, '[]' );
+
+ $res = $this->callApi( 'nc-g1', '{ "list": "lists", "lstowner":
"' . $n .
+ '", "lstids": 0 }', $u2 );
+ $this->assertListsEquals( 'nc-g1', $res, '[]' );
+
+
+ $res = $this->callApi( 'nc-g2', '{ "list": "lists", "lstids": '
. $id . ' }', $u2 );
+ $this->assertListsEquals( 'nc-g2', $res, '[]' );
+
+ $res = $this->callApi( 'nc-g3', '{ "list": "lists", "lstowner":
"' . $n . '", "lstids": ' .
+ $id . ' }', $u2 );
+ $this->assertListsEquals( 'nc-g3', $res, '[]' );
+
+ $this->assertUsage( 'nc-i0', '{ "action": "editlist", "id":0,
"label":"bb" }', $u );
+ $this->assertUsage( 'nc-i1', '{ "action": "editlist", "id":' .
$id . ', "label":"bb" }', $u );
+ }
+
+ private function assertListsEquals( $message, $actual, $expected,
$removeIds = true ) {
+ $actual = $this->checkResult( $message, $message, '"query",
"lists"', $actual );
+ $expected = $this->toArray( $message, $expected );
+ if ( $removeIds ) {
+ $actual = self::removeIds( $actual );
+ }
+ $this->assertArrayEquals( $expected, $actual, true, true,
$message );
+ }
+
+ private function updateList( $message, $params, $user, $token ) {
+ $params = $this->toArray( $message, $params );
+ if ( !isset( $params['action'] ) ) {
+ $params['action'] = 'editlist';
+ }
+ if ( !isset( $params['token'] ) ) {
+ $params['token'] = $token;
+ }
+ return $this->callApi( $message, $params, $user, array(
$params['action'] ) );
+ }
+
+ private function addToList( $message, $label, $titles, $user, $token ) {
+ $params = array(
+ 'action' => 'editlist',
+ 'titles' => $titles,
+ 'token' => $token,
+ );
+ $params[is_string( $label ) ? 'label' : 'id'] = $label;
+ $this->callApi( $message, $params, $user, '"editlist", "pages",
0, "added"' );
+ }
+
+ private function legacyAddToWatchlist( $message, $titles, $user, $token
) {
+ $params = array(
+ 'action' => 'watch',
+ 'titles' => $titles,
+ 'token' => $token,
+ );
+ $this->callApi( $message, $params, $user, '"watch", 0,
"watched"' );
+ }
+
+ private function getToken( User $user ) {
+ return $this->callApi( 'token-' . $user->getName(), array(
+ 'meta' => 'tokens',
+ 'type' => 'watch',
+ ), $user, '"query", "tokens", "watchtoken"' );
+ }
+
+ private function assertUsage( $message, $params, User $user = null ) {
+ try {
+ $params = $this->toApiParams( $message, $params );
+ $result = $this->callApi( $message, $params, $user );
+ $params = $this->toStr( $params );
+ $this->fail( "No UsageException for $params,
received:\n" .
+ $this->toStr( $result[0], true ) );
+ } catch ( UsageException $e ) {
+ $this->assertTrue( true );
+ }
+ }
+
+ private function callApi( $message, $params, User $user = null, $path =
'' ) {
+ $params = $this->toApiParams( $message, $params );
+ $result = $this->doApiRequest( $params, null, false, $user );
+ return $path ? $this->checkResult( $message, $params, $path,
$result ) : $result;
+ }
+
+ private function toApiParams( $message, $params ) {
+ $params = $this->toArray( $message, $params );
+ if ( !isset( $params['action'] ) ) {
+ $params['action'] = 'query';
+ }
+ if ( $params['action'] === 'query' && !isset(
$params['continue'] ) ) {
+ $params['continue'] = '';
+ }
+ return $params;
+ }
+
+ private function toArray( $message, $params ) {
+ if ( is_string( $params ) && $params ) {
+ $p = $params[0] !== '[' && $params[0] !== '{' ?
"[$params]" : $params;
+ $st = FormatJson::parse( $p, FormatJson::FORCE_ASSOC );
+ $this->assertTrue( $st->isOK(), 'invalid JSON value ' .
$params, $message );
+ $params = $st->getValue();
+ return $params;
+ }
+ return $params;
+ }
+
+ private function toStr( $params, $pretty = false ) {
+ if ( is_string( $params ) ) {
+ return $params;
+ }
+ return FormatJson::encode( $params, $pretty, FormatJson::ALL_OK
);
+ }
+
+ private static function removeIds( $arr ) {
+ foreach ( $arr as &$v ) {
+ if ( array_key_exists( 'id', $v ) && $v['id'] !== 0 ) {
+ unset( $v['id'] );
+ }
+ }
+ return $arr;
+ }
+
+ private function checkResult( $message, $params, $path, $result ) {
+ $path = $this->toArray( $message, $path );
+ array_unshift( $path, 0 );
+ $res = $result;
+ foreach ( $path as $p ) {
+ if ( !array_key_exists( $p, $res ) ) {
+ $params = $this->toStr( $params );
+ $path = $this->toStr( $path );
+ $this->fail( "$message: Request $params has no
key $p of $path in result\n" .
+ $this->toStr( $result,
true ) );
+ }
+ $res = $res[$p];
+ }
+ $result = $res;
+ return $result;
+ }
+}
--
To view, visit https://gerrit.wikimedia.org/r/197384
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I392fd6e44c7fc4117fdab8c9c9dfe02fcdbfa2e1
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Gather
Gerrit-Branch: master
Gerrit-Owner: Yurik <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits