Merge "Drop cache interwiki"
[mediawiki.git] / tests / phpunit / includes / specials / ContribsPagerTest.php
blob688a384f0b4eedf1530a9ac2b0c4418e5514efc3
1 <?php
3 use MediaWiki\Cache\LinkBatchFactory;
4 use MediaWiki\CommentFormatter\CommentFormatter;
5 use MediaWiki\Config\HashConfig;
6 use MediaWiki\Context\RequestContext;
7 use MediaWiki\HookContainer\HookContainer;
8 use MediaWiki\Linker\LinkRenderer;
9 use MediaWiki\MainConfigNames;
10 use MediaWiki\Pager\ContribsPager;
11 use MediaWiki\Pager\IndexPager;
12 use MediaWiki\Permissions\SimpleAuthority;
13 use MediaWiki\Revision\RevisionRecord;
14 use MediaWiki\Revision\RevisionStore;
15 use MediaWiki\Tests\User\TempUser\TempUserTestTrait;
16 use MediaWiki\Title\NamespaceInfo;
17 use MediaWiki\Title\Title;
18 use MediaWiki\User\UserIdentity;
19 use MediaWiki\User\UserIdentityValue;
20 use Wikimedia\Rdbms\FakeResultWrapper;
21 use Wikimedia\Rdbms\IConnectionProvider;
22 use Wikimedia\TestingAccessWrapper;
24 /**
25 * @group Database
26 * @covers \MediaWiki\Pager\ContribsPager
27 * @covers \MediaWiki\Pager\ContributionsPager
29 class ContribsPagerTest extends MediaWikiIntegrationTestCase {
30 use TempUserTestTrait;
32 /** @var ContribsPager */
33 private $pager;
35 /** @var LinkRenderer */
36 private $linkRenderer;
38 /** @var RevisionStore */
39 private $revisionStore;
41 /** @var LinkBatchFactory */
42 private $linkBatchFactory;
44 /** @var HookContainer */
45 private $hookContainer;
47 /** @var IConnectionProvider */
48 private $dbProvider;
50 /** @var NamespaceInfo */
51 private $namespaceInfo;
53 /** @var CommentFormatter */
54 private $commentFormatter;
56 protected function setUp(): void {
57 parent::setUp();
59 $services = $this->getServiceContainer();
60 $this->linkRenderer = $services->getLinkRenderer();
61 $this->revisionStore = $services->getRevisionStore();
62 $this->linkBatchFactory = $services->getLinkBatchFactory();
63 $this->hookContainer = $services->getHookContainer();
64 $this->dbProvider = $services->getConnectionProvider();
65 $this->namespaceInfo = $services->getNamespaceInfo();
66 $this->commentFormatter = $services->getCommentFormatter();
67 $this->pager = $this->getContribsPager( [
68 'start' => '2017-01-01',
69 'end' => '2017-02-02',
70 ] );
73 private function getContribsPager( array $options, ?UserIdentity $targetUser = null ) {
74 return new ContribsPager(
75 new RequestContext(),
76 $options,
77 $this->linkRenderer,
78 $this->linkBatchFactory,
79 $this->hookContainer,
80 $this->dbProvider,
81 $this->revisionStore,
82 $this->namespaceInfo,
83 $targetUser,
84 $this->commentFormatter
88 /**
89 * Tests enabling/disabling ContribsPager::reallyDoQuery hook via the revisionsOnly option to restrict
90 * extensions are able to insert their own revisions
92 public function testRevisionsOnlyOption() {
93 $this->setTemporaryHook( 'ContribsPager::reallyDoQuery', static function ( &$data ) {
94 $fakeRow = (object)[ 'rev_timestamp' => '20200717192356' ];
95 $fakeRowWrapper = new FakeResultWrapper( [ $fakeRow ] );
96 $data[] = $fakeRowWrapper;
97 } );
99 $allContribsPager = $this->getContribsPager( [] );
100 $allContribsResults = $allContribsPager->reallyDoQuery( '', 2, IndexPager::QUERY_DESCENDING );
101 $this->assertSame( 1, $allContribsResults->numRows() );
103 $revOnlyPager = $this->getContribsPager( [ 'revisionsOnly' => true ] );
104 $revOnlyResults = $revOnlyPager->reallyDoQuery( '', 2, IndexPager::QUERY_DESCENDING );
105 $this->assertSame( 0, $revOnlyResults->numRows() );
109 * @dataProvider dateFilterOptionProcessingProvider
110 * @param array $inputOpts Input options
111 * @param array $expectedOpts Expected options
113 public function testDateFilterOptionProcessing( array $inputOpts, array $expectedOpts ) {
114 $this->assertArraySubmapSame(
115 $expectedOpts,
116 ContribsPager::processDateFilter( $inputOpts ),
117 "Matching date filter options"
121 public static function dateFilterOptionProcessingProvider() {
122 return [
125 'start' => '2016-05-01',
126 'end' => '2016-06-01',
127 'year' => null,
128 'month' => null
131 'start' => '2016-05-01',
132 'end' => '2016-06-01'
137 'start' => '2016-05-01',
138 'end' => '2016-06-01',
139 'year' => '',
140 'month' => ''
143 'start' => '2016-05-01',
144 'end' => '2016-06-01'
149 'start' => '2016-05-01',
150 'end' => '2016-06-01',
151 'year' => '2012',
152 'month' => '5'
155 'start' => '',
156 'end' => '2012-05-31'
161 'start' => '',
162 'end' => '',
163 'year' => '2012',
164 'month' => '5'
167 'start' => '',
168 'end' => '2012-05-31'
173 'start' => '',
174 'end' => '',
175 'year' => '2012',
176 'month' => ''
179 'start' => '',
180 'end' => '2012-12-31'
187 * @dataProvider provideQueryableRanges
189 public function testQueryableRanges( $ipRange ) {
190 $config = new HashConfig( [
191 MainConfigNames::RangeContributionsCIDRLimit => [
192 'IPv4' => 16,
193 'IPv6' => 32,
195 ] );
197 $this->assertTrue(
198 ContribsPager::isQueryableRange( $ipRange, $config ),
199 "$ipRange is a queryable IP range"
203 public static function provideQueryableRanges() {
204 return [
205 [ '116.17.184.5/32' ],
206 [ '0.17.184.5/16' ],
207 [ '2000::/32' ],
208 [ '2001:db8::/128' ],
213 * @dataProvider provideUnqueryableRanges
215 public function testUnqueryableRanges( $ipRange ) {
216 $config = new HashConfig( [
217 MainConfigNames::RangeContributionsCIDRLimit => [
218 'IPv4' => 16,
219 'IPv6' => 32,
221 ] );
223 $this->assertFalse(
224 ContribsPager::isQueryableRange( $ipRange, $config ),
225 "$ipRange is not a queryable IP range"
229 public static function provideUnqueryableRanges() {
230 return [
231 [ '116.17.184.5/33' ],
232 [ '0.17.184.5/15' ],
233 [ '2000::/31' ],
234 [ '2001:db8::/9999' ],
238 public function testUniqueSortOrderWithoutIpChanges() {
239 $pager = $this->getContribsPager( [
240 'start' => '',
241 'end' => '',
242 ] );
244 /** @var ContribsPager $pager */
245 $pager = TestingAccessWrapper::newFromObject( $pager );
246 $queryInfo = $pager->buildQueryInfo( '', 1, false );
248 $this->assertNotContains( 'ip_changes', $queryInfo[0] );
249 $this->assertArrayNotHasKey( 'ip_changes', $queryInfo[5] );
250 $this->assertContains( 'rev_timestamp', $queryInfo[1] );
251 $this->assertContains( 'rev_id', $queryInfo[1] );
252 $this->assertSame( [ 'rev_timestamp DESC', 'rev_id DESC' ], $queryInfo[4]['ORDER BY'] );
255 public function testUniqueSortOrderOnIpChanges() {
256 $pager = $this->getContribsPager( [
257 'target' => '116.17.184.5/32',
258 'start' => '',
259 'end' => '',
260 ] );
262 /** @var ContribsPager $pager */
263 $pager = TestingAccessWrapper::newFromObject( $pager );
264 $queryInfo = $pager->buildQueryInfo( '', 1, false );
266 $this->assertContains( 'ip_changes', $queryInfo[0] );
267 $this->assertArrayHasKey( 'revision', $queryInfo[5] );
268 $this->assertSame( [ 'ipc_rev_timestamp DESC', 'ipc_rev_id DESC' ], $queryInfo[4]['ORDER BY'] );
271 public function testCreateRevision() {
272 $title = Title::makeTitle( NS_MAIN, __METHOD__ );
274 $pager = $this->getContribsPager( [
275 'target' => '116.17.184.5/32',
276 'start' => '',
277 'end' => '',
278 ] );
280 $invalidObject = new class() {
281 public $rev_id;
283 $this->assertNull( $pager->tryCreatingRevisionRecord( $invalidObject, $title ) );
285 $invalidRow = (object)[
286 'foo' => 'bar'
289 $this->assertNull( $pager->tryCreatingRevisionRecord( $invalidRow, $title ) );
291 $validRow = (object)[
292 'rev_id' => '2',
293 'rev_page' => '2',
294 'page_namespace' => $title->getNamespace(),
295 'page_title' => $title->getDBkey(),
296 'rev_text_id' => '47',
297 'rev_timestamp' => '20180528192356',
298 'rev_minor_edit' => '0',
299 'rev_deleted' => '0',
300 'rev_len' => '700',
301 'rev_parent_id' => '0',
302 'rev_sha1' => 'deadbeef',
303 'rev_comment_text' => 'whatever',
304 'rev_comment_data' => null,
305 'rev_comment_cid' => null,
306 'rev_user' => '1',
307 'rev_user_text' => 'Editor',
308 'rev_actor' => '11',
309 'rev_content_format' => null,
310 'rev_content_model' => null,
313 $this->assertNotNull( $pager->tryCreatingRevisionRecord( $validRow, $title ) );
317 * Flow uses ContribsPager::reallyDoQuery hook to provide something other then
318 * stdClass as a row, and then manually formats its own row in ContributionsLineEnding.
319 * Emulate this behaviour and check that it works.
321 public function testContribProvidedByHook() {
322 $this->setTemporaryHook( 'ContribsPager::reallyDoQuery', static function ( &$data ) {
323 $data = [ [ new class() {
324 public $rev_timestamp = 12345;
325 public $testing = 'TESTING';
326 } ] ];
327 } );
328 $this->setTemporaryHook( 'ContributionsLineEnding', function ( $pager, &$ret, $row ) {
329 $this->assertSame( 'TESTING', $row->testing );
330 $ret .= 'FROM_HOOK!';
331 } );
332 $pager = $this->getContribsPager( [] );
333 $this->assertStringContainsString( 'FROM_HOOK!', $pager->getBody() );
336 public static function provideEmptyResultIntegration() {
337 $cases = [
338 [ 'target' => '127.0.0.1' ],
339 [ 'target' => '127.0.0.1/24' ],
340 [ 'testUser' => true ],
341 [ 'target' => '127.0.0.1', 'namespace' => 0 ],
342 [ 'target' => '127.0.0.1', 'namespace' => 0, 'nsInvert' => true ],
343 [ 'target' => '127.0.0.1', 'namespace' => 0, 'associated' => true ],
344 [ 'target' => '127.0.0.1', 'tagfilter' => 'tag' ],
345 [ 'target' => '127.0.0.1', 'topOnly' => true ],
346 [ 'target' => '127.0.0.1', 'newOnly' => true ],
347 [ 'target' => '127.0.0.1', 'hideMinor' => true ],
348 [ 'target' => '127.0.0.1', 'revisionsOnly' => true ],
349 [ 'target' => '127.0.0.1', 'deletedOnly' => true ],
350 [ 'target' => '127.0.0.1', 'start' => '20010115000000' ],
351 [ 'target' => '127.0.0.1', 'end' => '20210101000000' ],
352 [ 'target' => '127.0.0.1', 'start' => '20010115000000', 'end' => '20210101000000' ],
354 foreach ( $cases as $case ) {
355 yield [ $case ];
360 * This DB integration test confirms that the query is valid for various
361 * filter options, by running the query on an empty DB.
363 * @dataProvider provideEmptyResultIntegration
365 public function testEmptyResultIntegration( $options ) {
366 if ( !empty( $options['testUser'] ) ) {
367 $targetUser = new UserIdentityValue( 1, 'User' );
368 } else {
369 $targetUser = $this->getServiceContainer()->getUserFactory()
370 ->newFromName( $options['target'] );
372 $pager = $this->getContribsPager( $options, $targetUser );
373 $this->assertIsString( $pager->getBody() );
374 $this->assertSame( 0, $pager->getNumRows() );
378 * DB integration test for an IP range target with a few edits.
380 public function testPopulatedIntegration() {
381 $this->disableAutoCreateTempUser();
382 $user = new SimpleAuthority( new UserIdentityValue( 0, '127.0.0.1' ), [] );
383 $title = Title::makeTitle( NS_MAIN, 'ContribsPagerTest' );
384 $this->editPage( $title, '', '', NS_MAIN, $user );
385 $this->editPage( $title, 'Test content.', '', NS_MAIN, $user );
386 $pager = $this->getContribsPager( [ 'target' => '127.0.0.1/16' ] );
387 $this->assertIsString( $pager->getBody() );
388 $this->assertSame( 2, $pager->getNumRows() );
392 * DB integration test for a reader with permissions to delete and rollback.
394 public function testPopulatedIntegrationWithPermissions() {
395 $this->setGroupPermissions( [ '*' => [
396 'deletedhistory' => true,
397 'deleterevision' => true,
398 'rollback' => true,
399 ] ] );
400 $sysop = $this->getTestsysop()->getUser();
401 $user = $this->getTestUser()->getUser();
402 $title = Title::makeTitle( NS_MAIN, 'ContribsPagerTest' );
404 // Edit from a different user so we show rollback links
405 $this->editPage( $title, '', '', NS_MAIN, $sysop );
406 $this->editPage( $title, 'Test content.', '', NS_MAIN, $user );
408 $this->getDb()->newUpdateQueryBuilder()
409 ->update( 'revision' )
410 ->set( [
411 'rev_deleted' => RevisionRecord::DELETED_USER,
412 // Make a couple of alterations to ensure these paths are covered
413 'rev_minor_edit' => 1,
414 'rev_parent_id' => null,
416 ->where( [
417 'rev_actor' => $user->getActorId()
419 ->execute();
421 $pager = $this->getContribsPager( [], $user );
422 $this->assertIsString( $pager->getBody() );
423 $this->assertSame( 1, $pager->getNumRows() );