1<?php
2
3namespace MediaWiki\Tests\Storage;
4
5use CommentStoreComment;
6use Content;
7use MediaWiki\MediaWikiServices;
8use MediaWiki\Revision\RenderedRevision;
9use MediaWiki\Revision\RevisionRecord;
10use MediaWiki\Revision\SlotRecord;
11use MediaWikiIntegrationTestCase;
12use ParserOptions;
13use RecentChange;
14use Revision;
15use Status;
16use TextContent;
17use Title;
18use User;
19use Wikimedia\AtEase\AtEase;
20use WikiPage;
21
22/**
23 * @covers \MediaWiki\Storage\PageUpdater
24 * @group Database
25 */
26class PageUpdaterTest extends MediaWikiIntegrationTestCase {
27
28	protected function setUp() : void {
29		parent::setUp();
30
31		$slotRoleRegistry = MediaWikiServices::getInstance()->getSlotRoleRegistry();
32
33		if ( !$slotRoleRegistry->isDefinedRole( 'aux' ) ) {
34			$slotRoleRegistry->defineRoleWithModel(
35				'aux',
36				CONTENT_MODEL_WIKITEXT
37			);
38		}
39
40		$this->tablesUsed[] = 'logging';
41		$this->tablesUsed[] = 'recentchanges';
42	}
43
44	private function getDummyTitle( $method ) {
45		return Title::newFromText( $method, $this->getDefaultWikitextNS() );
46	}
47
48	/**
49	 * @param int $revId
50	 *
51	 * @return null|RecentChange
52	 */
53	private function getRecentChangeFor( $revId ) {
54		$qi = RecentChange::getQueryInfo();
55		$row = $this->db->selectRow(
56			$qi['tables'],
57			$qi['fields'],
58			[ 'rc_this_oldid' => $revId ],
59			__METHOD__,
60			[],
61			$qi['joins']
62		);
63
64		return $row ? RecentChange::newFromRow( $row ) : null;
65	}
66
67	// TODO: test setAjaxEditStash();
68
69	/**
70	 * @covers \MediaWiki\Storage\PageUpdater::saveRevision()
71	 * @covers \WikiPage::newPageUpdater()
72	 */
73	public function testCreatePage() {
74		$this->hideDeprecated( 'WikiPage::getRevision' );
75		$this->hideDeprecated( "MediaWiki\Storage\PageUpdater::doCreate status get 'revision'" );
76		$this->hideDeprecated( "MediaWiki\Storage\PageUpdater::doModify status get 'revision'" );
77		$this->hideDeprecated( 'Revision::__construct' );
78
79		$user = $this->getTestUser()->getUser();
80
81		$title = $this->getDummyTitle( __METHOD__ );
82		$page = WikiPage::factory( $title );
83		$updater = $page->newPageUpdater( $user );
84
85		$oldStats = $this->db->selectRow( 'site_stats', '*', '1=1' );
86
87		$this->assertFalse( $updater->wasCommitted(), 'wasCommitted' );
88
89		$updater->addTag( 'foo' );
90		$updater->addTags( [ 'bar', 'qux' ] );
91
92		$tags = $updater->getExplicitTags();
93		sort( $tags );
94		$this->assertSame( [ 'bar', 'foo', 'qux' ], $tags, 'getExplicitTags' );
95
96		// TODO: MCR: test additional slots
97		$content = new TextContent( 'Lorem Ipsum' );
98		$updater->setContent( SlotRecord::MAIN, $content );
99
100		$parent = $updater->grabParentRevision();
101
102		$this->assertNull( $parent, 'getParentRevision' );
103		$this->assertFalse( $updater->wasCommitted(), 'wasCommitted' );
104
105		// TODO: test that hasEditConflict() grabs the parent revision
106		$this->assertFalse( $updater->hasEditConflict( 0 ), 'hasEditConflict' );
107		$this->assertTrue( $updater->hasEditConflict( 1 ), 'hasEditConflict' );
108
109		// TODO: test failure with EDIT_UPDATE
110		// TODO: test EDIT_MINOR, EDIT_BOT, etc
111		$summary = CommentStoreComment::newUnsavedComment( 'Just a test' );
112		$rev = $updater->saveRevision( $summary );
113
114		$this->assertNotNull( $rev );
115		$this->assertSame( 0, $rev->getParentId() );
116		$this->assertSame( $summary->text, $rev->getComment( RevisionRecord::RAW )->text );
117		$this->assertSame( $user->getName(), $rev->getUser( RevisionRecord::RAW )->getName() );
118
119		$this->assertTrue( $updater->wasCommitted(), 'wasCommitted()' );
120		$this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' );
121		$this->assertTrue( $updater->getStatus()->isOK(), 'getStatus()->isOK()' );
122		$this->assertTrue( $updater->isNew(), 'isNew()' );
123		$this->assertFalse( $updater->isUnchanged(), 'isUnchanged()' );
124		$this->assertNotNull( $updater->getNewRevision(), 'getNewRevision()' );
125		$this->assertInstanceOf( Revision::class, $updater->getStatus()->value['revision'] );
126
127		// check the EditResult object
128		$this->assertFalse( $updater->getEditResult()->getOriginalRevisionId(),
129			'EditResult::getOriginalRevisionId()' );
130		$this->assertSame( 0, $updater->getEditResult()->getUndidRevId(),
131			'EditResult::getUndidRevId()' );
132		$this->assertTrue( $updater->getEditResult()->isNew(), 'EditResult::isNew()' );
133		$this->assertFalse( $updater->getEditResult()->isRevert(), 'EditResult::isRevert()' );
134
135		$rev = $updater->getNewRevision();
136		$revContent = $rev->getContent( SlotRecord::MAIN );
137		$this->assertSame( 'Lorem Ipsum', $revContent->serialize(), 'revision content' );
138
139		// were the WikiPage and Title objects updated?
140		$this->assertTrue( $page->exists(), 'WikiPage::exists()' );
141		$this->assertTrue( $title->exists(), 'Title::exists()' );
142		$this->assertSame( $rev->getId(), $page->getLatest(), 'WikiPage::getRevision()' );
143		$this->assertNotNull( $page->getRevision(), 'WikiPage::getRevision()' );
144
145		// re-load
146		$page2 = WikiPage::factory( $title );
147		$this->assertTrue( $page2->exists(), 'WikiPage::exists()' );
148		$this->assertSame( $rev->getId(), $page2->getLatest(), 'WikiPage::getRevision()' );
149		$this->assertNotNull( $page2->getRevision(), 'WikiPage::getRevision()' );
150
151		// Check RC entry
152		$rc = $this->getRecentChangeFor( $rev->getId() );
153		$this->assertNotNull( $rc, 'RecentChange' );
154
155		// check site stats - this asserts that derived data updates where run.
156		$stats = $this->db->selectRow( 'site_stats', '*', '1=1' );
157		$this->assertSame( $oldStats->ss_total_pages + 1, (int)$stats->ss_total_pages );
158		$this->assertSame( $oldStats->ss_total_edits + 1, (int)$stats->ss_total_edits );
159
160		// re-edit with same content - should be a "null-edit"
161		$updater = $page->newPageUpdater( $user );
162		$updater->setContent( SlotRecord::MAIN, $content );
163
164		$summary = CommentStoreComment::newUnsavedComment( 'to to re-edit' );
165		$rev = $updater->saveRevision( $summary );
166		$status = $updater->getStatus();
167
168		$this->assertNull( $rev, 'getNewRevision()' );
169		$this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
170		$this->assertTrue( $updater->isUnchanged(), 'isUnchanged' );
171		$this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' );
172		$this->assertTrue( $status->isOK(), 'getStatus()->isOK()' );
173		$this->assertTrue( $status->hasMessage( 'edit-no-change' ), 'edit-no-change' );
174	}
175
176	/**
177	 * @covers \MediaWiki\Storage\PageUpdater::saveRevision()
178	 * @covers \WikiPage::newPageUpdater()
179	 */
180	public function testUpdatePage() {
181		$this->hideDeprecated( 'WikiPage::getRevision' );
182		$this->hideDeprecated( "MediaWiki\Storage\PageUpdater::doCreate status get 'revision'" );
183		$this->hideDeprecated( "MediaWiki\Storage\PageUpdater::doModify status get 'revision'" );
184		$this->hideDeprecated( 'Revision::__construct' );
185
186		$user = $this->getTestUser()->getUser();
187
188		$title = $this->getDummyTitle( __METHOD__ );
189		$this->insertPage( $title );
190
191		$page = WikiPage::factory( $title );
192		$parentId = $page->getLatest();
193
194		$updater = $page->newPageUpdater( $user );
195
196		$oldStats = $this->db->selectRow( 'site_stats', '*', '1=1' );
197
198		$updater->setOriginalRevisionId( 7 );
199
200		$this->assertFalse( $updater->hasEditConflict( $parentId ), 'hasEditConflict' );
201		$this->assertTrue( $updater->hasEditConflict( $parentId - 1 ), 'hasEditConflict' );
202		$this->assertTrue( $updater->hasEditConflict( 0 ), 'hasEditConflict' );
203
204		// TODO: MCR: test additional slots
205		$updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
206
207		// TODO: test all flags for saveRevision()!
208		$summary = CommentStoreComment::newUnsavedComment( 'Just a test' );
209		$rev = $updater->saveRevision( $summary );
210
211		$this->assertNotNull( $rev );
212		$this->assertSame( $parentId, $rev->getParentId() );
213		$this->assertSame( $summary->text, $rev->getComment( RevisionRecord::RAW )->text );
214		$this->assertSame( $user->getName(), $rev->getUser( RevisionRecord::RAW )->getName() );
215
216		$this->assertTrue( $updater->wasCommitted(), 'wasCommitted()' );
217		$this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' );
218		$this->assertTrue( $updater->getStatus()->isOK(), 'getStatus()->isOK()' );
219		$this->assertFalse( $updater->isNew(), 'isNew()' );
220		$this->assertNotNull( $updater->getNewRevision(), 'getNewRevision()' );
221		$this->assertInstanceOf( Revision::class, $updater->getStatus()->value['revision'] );
222		$this->assertFalse( $updater->isUnchanged(), 'isUnchanged()' );
223
224		// check the EditResult object
225		$this->assertSame( 7, $updater->getEditResult()->getOriginalRevisionId(),
226			'EditResult::getOriginalRevisionId()' );
227		$this->assertSame( 0, $updater->getEditResult()->getUndidRevId(),
228			'EditResult::getUndidRevId()' );
229		$this->assertFalse( $updater->getEditResult()->isNew(), 'EditResult::isNew()' );
230		$this->assertFalse( $updater->getEditResult()->isRevert(), 'EditResult::isRevert()' );
231
232		// TODO: Test null revision (with different user): new revision!
233
234		$rev = $updater->getNewRevision();
235		$revContent = $rev->getContent( SlotRecord::MAIN );
236		$this->assertSame( 'Lorem Ipsum', $revContent->serialize(), 'revision content' );
237
238		// were the WikiPage and Title objects updated?
239		$this->assertTrue( $page->exists(), 'WikiPage::exists()' );
240		$this->assertTrue( $title->exists(), 'Title::exists()' );
241		$this->assertSame( $rev->getId(), $page->getLatest(), 'WikiPage::getRevision()' );
242		$this->assertNotNull( $page->getRevision(), 'WikiPage::getRevision()' );
243
244		// re-load
245		$page2 = WikiPage::factory( $title );
246		$this->assertTrue( $page2->exists(), 'WikiPage::exists()' );
247		$this->assertSame( $rev->getId(), $page2->getLatest(), 'WikiPage::getRevision()' );
248		$this->assertNotNull( $page2->getRevision(), 'WikiPage::getRevision()' );
249
250		// Check RC entry
251		$rc = $this->getRecentChangeFor( $rev->getId() );
252		$this->assertNotNull( $rc, 'RecentChange' );
253
254		// re-edit
255		$updater = $page->newPageUpdater( $user );
256		$updater->setContent( SlotRecord::MAIN, new TextContent( 'dolor sit amet' ) );
257
258		$summary = CommentStoreComment::newUnsavedComment( 're-edit' );
259		$updater->saveRevision( $summary );
260		$this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' );
261		$this->assertTrue( $updater->getStatus()->isOK(), 'getStatus()->isOK()' );
262		$this->assertNotNull( $updater->getNewRevision(), 'getNewRevision()' );
263
264		$topRevisionId = $updater->getNewRevision()->getId();
265
266		// perform a null edit
267		$updater = $page->newPageUpdater( $user );
268		$updater->setContent( SlotRecord::MAIN, new TextContent( 'dolor sit amet' ) );
269		$summary = CommentStoreComment::newUnsavedComment( 'null edit' );
270		$updater->saveRevision( $summary );
271
272		$this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' );
273		$this->assertTrue( $updater->getStatus()->isOK(), 'getStatus()->isOK()' );
274		$this->assertTrue( $updater->isUnchanged(), 'isUnchanged()' );
275		$this->assertTrue(
276			$updater->getEditResult()->isNullEdit(),
277			'getEditResult()->isNullEdit()'
278		);
279		$this->assertSame(
280			$topRevisionId,
281			$updater->getEditResult()->getOriginalRevisionId(),
282			'getEditResult()->getOriginalRevisionId()'
283		);
284
285		// check site stats - this asserts that derived data updates where run.
286		$stats = $this->db->selectRow( 'site_stats', '*', '1=1' );
287		$this->assertNotNull( $stats, 'site_stats' );
288		$this->assertSame( $oldStats->ss_total_pages + 0, (int)$stats->ss_total_pages );
289		$this->assertSame( $oldStats->ss_total_edits + 2, (int)$stats->ss_total_edits );
290	}
291
292	/**
293	 * Creates a revision in the database.
294	 *
295	 * @param WikiPage $page
296	 * @param string|Message|CommentStoreComment $summary
297	 * @param null|string|Content $content
298	 *
299	 * @return RevisionRecord|null
300	 */
301	private function createRevision( WikiPage $page, $summary, $content = null ) {
302		$user = $this->getTestUser()->getUser();
303		$comment = CommentStoreComment::newUnsavedComment( $summary );
304
305		if ( !$content instanceof Content ) {
306			$content = new TextContent( $content ?? $summary );
307		}
308
309		$updater = $page->newPageUpdater( $user );
310		$updater->setContent( SlotRecord::MAIN, $content );
311		$rev = $updater->saveRevision( $comment );
312		return $rev;
313	}
314
315	/**
316	 * Verify that MultiContentSave hook is called by saveRevision() with correct parameters.
317	 * @covers \MediaWiki\Storage\PageUpdater::saveRevision()
318	 */
319	public function testMultiContentSaveHook() {
320		$user = $this->getTestUser()->getUser();
321		$title = $this->getDummyTitle( __METHOD__ );
322
323		// TODO: MCR: test additional slots
324		$slots = [
325			SlotRecord::MAIN => new TextContent( 'Lorem Ipsum' )
326		];
327
328		// start editing non-existing page
329		$page = WikiPage::factory( $title );
330		$updater = $page->newPageUpdater( $user );
331		foreach ( $slots as $slot => $content ) {
332			$updater->setContent( $slot, $content );
333		}
334
335		$summary = CommentStoreComment::newUnsavedComment( 'Just a test' );
336
337		$expected = [
338			'user' => $user,
339			'title' => $title,
340			'slots' => $slots,
341			'summary' => $summary
342		];
343		$hookFired = false;
344		$this->setTemporaryHook( 'MultiContentSave',
345			function ( RenderedRevision $renderedRevision, User $user,
346				$summary, $flags, Status $hookStatus
347			) use ( &$hookFired, $expected ) {
348				$hookFired = true;
349
350				$this->assertSame( $expected['summary'], $summary );
351				$this->assertSame( EDIT_NEW, $flags );
352
353				$title = $renderedRevision->getRevision()->getPageAsLinkTarget();
354				$this->assertSame( $expected['title']->getFullText(), $title->getFullText() );
355
356				$slots = $renderedRevision->getRevision()->getSlots();
357				foreach ( $expected['slots'] as $slot => $content ) {
358					$this->assertSame( $content, $slots->getSlot( $slot )->getContent() );
359				}
360
361				// Don't abort this edit.
362				return true;
363			}
364		);
365
366		$rev = $updater->saveRevision( $summary );
367		$this->assertTrue( $hookFired, "MultiContentSave hook wasn't called." );
368		$this->assertNotNull( $rev,
369			"MultiContentSave returned true, but revision wasn't created." );
370	}
371
372	/**
373	 * Verify that MultiContentSave hook can abort saveRevision() by returning false.
374	 * @covers \MediaWiki\Storage\PageUpdater::saveRevision()
375	 */
376	public function testMultiContentSaveHookAbort() {
377		$user = $this->getTestUser()->getUser();
378		$title = $this->getDummyTitle( __METHOD__ );
379
380		// start editing non-existing page
381		$page = WikiPage::factory( $title );
382		$updater = $page->newPageUpdater( $user );
383		$updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
384
385		$summary = CommentStoreComment::newUnsavedComment( 'Just a test' );
386
387		$expectedError = 'aborted-by-test-hook';
388		$this->setTemporaryHook( 'MultiContentSave',
389			function ( RenderedRevision $renderedRevision, User $user,
390				$summary, $flags, Status $hookStatus
391			) use ( $expectedError ) {
392				$hookStatus->fatal( $expectedError );
393
394				// Returning false should disallow saveRevision() to continue saving this revision.
395				return false;
396			}
397		);
398
399		$rev = $updater->saveRevision( $summary );
400		$this->assertNull( $rev,
401			"MultiContentSave returned false, but revision was still created." );
402
403		$status = $updater->getStatus();
404		$this->assertFalse( $status->isOK(),
405			"MultiContentSave returned false, but Status is not fatal." );
406		$this->assertSame( $expectedError, $status->getMessage()->getKey() );
407	}
408
409	/**
410	 * @covers \MediaWiki\Storage\PageUpdater::grabParentRevision()
411	 * @covers \MediaWiki\Storage\PageUpdater::saveRevision()
412	 */
413	public function testCompareAndSwapFailure() {
414		$user = $this->getTestUser()->getUser();
415
416		$title = $this->getDummyTitle( __METHOD__ );
417
418		// start editing non-existing page
419		$page = WikiPage::factory( $title );
420		$updater = $page->newPageUpdater( $user );
421		$updater->grabParentRevision();
422
423		// create page concurrently
424		$concurrentPage = WikiPage::factory( $title );
425		$this->createRevision( $concurrentPage, __METHOD__ . '-one' );
426
427		// try creating the page - should trigger CAS failure.
428		$summary = CommentStoreComment::newUnsavedComment( 'create?!' );
429		$updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem ipsum' ) );
430		$updater->saveRevision( $summary );
431		$status = $updater->getStatus();
432
433		$this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' );
434		$this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
435		$this->assertFalse( $status->isOK(), 'getStatus()->isOK()' );
436		$this->assertTrue( $status->hasMessage( 'edit-already-exists' ), 'edit-conflict' );
437
438		// start editing existing page
439		$page = WikiPage::factory( $title );
440		$updater = $page->newPageUpdater( $user );
441		$updater->grabParentRevision();
442
443		// update page concurrently
444		$concurrentPage = WikiPage::factory( $title );
445		$this->createRevision( $concurrentPage, __METHOD__ . '-two' );
446
447		// try creating the page - should trigger CAS failure.
448		$summary = CommentStoreComment::newUnsavedComment( 'edit?!' );
449		$updater->setContent( SlotRecord::MAIN, new TextContent( 'dolor sit amet' ) );
450		$updater->saveRevision( $summary );
451		$status = $updater->getStatus();
452
453		$this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' );
454		$this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
455		$this->assertFalse( $status->isOK(), 'getStatus()->isOK()' );
456		$this->assertTrue( $status->hasMessage( 'edit-conflict' ), 'edit-conflict' );
457	}
458
459	/**
460	 * @covers \MediaWiki\Storage\PageUpdater::saveRevision()
461	 */
462	public function testFailureOnEditFlags() {
463		$user = $this->getTestUser()->getUser();
464
465		$title = $this->getDummyTitle( __METHOD__ );
466
467		// start editing non-existing page
468		$page = WikiPage::factory( $title );
469		$updater = $page->newPageUpdater( $user );
470
471		// update with EDIT_UPDATE flag should fail
472		$summary = CommentStoreComment::newUnsavedComment( 'udpate?!' );
473		$updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem ipsum' ) );
474		$updater->saveRevision( $summary, EDIT_UPDATE );
475		$status = $updater->getStatus();
476
477		$this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' );
478		$this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
479		$this->assertFalse( $status->isOK(), 'getStatus()->isOK()' );
480		$this->assertTrue( $status->hasMessage( 'edit-gone-missing' ), 'edit-gone-missing' );
481
482		// create the page
483		$this->createRevision( $page, __METHOD__ );
484
485		// update with EDIT_NEW flag should fail
486		$summary = CommentStoreComment::newUnsavedComment( 'create?!' );
487		$updater = $page->newPageUpdater( $user );
488		$updater->setContent( SlotRecord::MAIN, new TextContent( 'dolor sit amet' ) );
489		$updater->saveRevision( $summary, EDIT_NEW );
490		$status = $updater->getStatus();
491
492		$this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' );
493		$this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
494		$this->assertFalse( $status->isOK(), 'getStatus()->isOK()' );
495		$this->assertTrue( $status->hasMessage( 'edit-already-exists' ), 'edit-already-exists' );
496	}
497
498	/**
499	 * @covers \MediaWiki\Storage\PageUpdater::saveRevision()
500	 */
501	public function testFailureOnBadContentModel() {
502		$user = $this->getTestUser()->getUser();
503		$title = $this->getDummyTitle( __METHOD__ );
504
505		// start editing non-existing page
506		$page = WikiPage::factory( $title );
507		$updater = $page->newPageUpdater( $user );
508
509		// plain text content should fail in aux slot (the main slot doesn't care)
510		$updater->setContent( 'main', new TextContent( 'Main Content' ) );
511		$updater->setContent( 'aux', new TextContent( 'Aux Content' ) );
512
513		$summary = CommentStoreComment::newUnsavedComment( 'udpate?!' );
514		$updater->saveRevision( $summary, EDIT_UPDATE );
515		$status = $updater->getStatus();
516
517		$this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' );
518		$this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
519		$this->assertFalse( $status->isOK(), 'getStatus()->isOK()' );
520		$this->assertTrue(
521			$status->hasMessage( 'content-not-allowed-here' ),
522			'content-not-allowed-here'
523		);
524	}
525
526	public function provideSetRcPatrolStatus( $patrolled ) {
527		yield [ RecentChange::PRC_UNPATROLLED ];
528		yield [ RecentChange::PRC_AUTOPATROLLED ];
529	}
530
531	/**
532	 * @dataProvider provideSetRcPatrolStatus
533	 * @covers \MediaWiki\Storage\PageUpdater::setRcPatrolStatus()
534	 */
535	public function testSetRcPatrolStatus( $patrolled ) {
536		$revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
537
538		$user = $this->getTestUser()->getUser();
539
540		$title = $this->getDummyTitle( __METHOD__ );
541
542		$page = WikiPage::factory( $title );
543		$updater = $page->newPageUpdater( $user );
544
545		$summary = CommentStoreComment::newUnsavedComment( 'Lorem ipsum ' . $patrolled );
546		$updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem ipsum ' . $patrolled ) );
547		$updater->setRcPatrolStatus( $patrolled );
548		$rev = $updater->saveRevision( $summary );
549
550		$rc = $revisionStore->getRecentChange( $rev );
551		$this->assertEquals( $patrolled, $rc->getAttribute( 'rc_patrolled' ) );
552	}
553
554	/**
555	 * @covers \MediaWiki\Storage\PageUpdater::makeNewRevision()
556	 */
557	public function testStalePageID() {
558		$user = $this->getTestUser()->getUser();
559		$title = $this->getDummyTitle( __METHOD__ );
560		$summary = CommentStoreComment::newUnsavedComment( 'testing...' );
561
562		// Create page
563		$page = WikiPage::factory( $title );
564		$updater = $page->newPageUpdater( $user );
565		$updater->setContent( 'main', new TextContent( 'Content 1' ) );
566		$updater->saveRevision( $summary, EDIT_NEW );
567		$this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' );
568
569		// Create a clone of $title and $page.
570		$title = Title::makeTitle( $title->getNamespace(), $title->getDBkey() );
571		$page = WikiPage::factory( $title );
572
573		// start editing existing page using bad page ID
574		$updater = $page->newPageUpdater( $user );
575		$updater->grabParentRevision();
576
577		$updater->setContent( 'main', new TextContent( 'Content 2' ) );
578
579		// Force the article ID to something invalid,
580		// to emulate confusion due to a page move.
581		$title->resetArticleID( 886655 );
582
583		AtEase::suppressWarnings();
584		$updater->saveRevision( $summary, EDIT_UPDATE );
585		AtEase::restoreWarnings();
586
587		$this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' );
588	}
589
590	/**
591	 * @covers \MediaWiki\Storage\PageUpdater::inheritSlot()
592	 * @covers \MediaWiki\Storage\PageUpdater::setContent()
593	 */
594	public function testInheritSlot() {
595		$user = $this->getTestUser()->getUser();
596		$title = $this->getDummyTitle( __METHOD__ );
597		$page = WikiPage::factory( $title );
598
599		$updater = $page->newPageUpdater( $user );
600		$summary = CommentStoreComment::newUnsavedComment( 'one' );
601		$updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem ipsum' ) );
602		$rev1 = $updater->saveRevision( $summary, EDIT_NEW );
603
604		$updater = $page->newPageUpdater( $user );
605		$summary = CommentStoreComment::newUnsavedComment( 'two' );
606		$updater->setContent( SlotRecord::MAIN, new TextContent( 'Foo Bar' ) );
607		$rev2 = $updater->saveRevision( $summary, EDIT_UPDATE );
608
609		$updater = $page->newPageUpdater( $user );
610		$summary = CommentStoreComment::newUnsavedComment( 'three' );
611		$updater->inheritSlot( $rev1->getSlot( SlotRecord::MAIN ) );
612		$rev3 = $updater->saveRevision( $summary, EDIT_UPDATE );
613
614		$this->assertNotSame( $rev1->getId(), $rev3->getId() );
615		$this->assertNotSame( $rev2->getId(), $rev3->getId() );
616
617		$main1 = $rev1->getSlot( SlotRecord::MAIN );
618		$main3 = $rev3->getSlot( SlotRecord::MAIN );
619
620		$this->assertNotSame( $main1->getRevision(), $main3->getRevision() );
621		$this->assertSame( $main1->getAddress(), $main3->getAddress() );
622		$this->assertTrue( $main1->getContent()->equals( $main3->getContent() ) );
623	}
624
625	// TODO: MCR: test adding multiple slots, inheriting parent slots, and removing slots.
626
627	public function testSetUseAutomaticEditSummaries() {
628		$this->setContentLang( 'qqx' );
629		$user = $this->getTestUser()->getUser();
630
631		$title = $this->getDummyTitle( __METHOD__ );
632		$page = WikiPage::factory( $title );
633
634		$updater = $page->newPageUpdater( $user );
635		$updater->setUseAutomaticEditSummaries( true );
636		$updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
637
638		// empty comment triggers auto-summary
639		$summary = CommentStoreComment::newUnsavedComment( '' );
640		$updater->saveRevision( $summary, EDIT_AUTOSUMMARY );
641
642		$rev = $updater->getNewRevision();
643		$comment = $rev->getComment( RevisionRecord::RAW );
644		$this->assertSame( '(autosumm-new: Lorem Ipsum)', $comment->text, 'comment text' );
645
646		// check that this also works when blanking the page
647		$updater = $page->newPageUpdater( $user );
648		$updater->setUseAutomaticEditSummaries( true );
649		$updater->setContent( SlotRecord::MAIN, new TextContent( '' ) );
650
651		$summary = CommentStoreComment::newUnsavedComment( '' );
652		$updater->saveRevision( $summary, EDIT_AUTOSUMMARY );
653
654		$rev = $updater->getNewRevision();
655		$comment = $rev->getComment( RevisionRecord::RAW );
656		$this->assertSame( '(autosumm-blank)', $comment->text, 'comment text' );
657
658		// check that we can also disable edit-summaries
659		$title2 = $this->getDummyTitle( __METHOD__ . '/2' );
660		$page2 = WikiPage::factory( $title2 );
661
662		$updater = $page2->newPageUpdater( $user );
663		$updater->setUseAutomaticEditSummaries( false );
664		$updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
665
666		$summary = CommentStoreComment::newUnsavedComment( '' );
667		$updater->saveRevision( $summary, EDIT_AUTOSUMMARY );
668
669		$rev = $updater->getNewRevision();
670		$comment = $rev->getComment( RevisionRecord::RAW );
671		$this->assertSame( '', $comment->text, 'comment text should still be lank' );
672
673		// check that we don't do auto.summaries without the EDIT_AUTOSUMMARY flag
674		$updater = $page2->newPageUpdater( $user );
675		$updater->setUseAutomaticEditSummaries( true );
676		$updater->setContent( SlotRecord::MAIN, new TextContent( '' ) );
677
678		$summary = CommentStoreComment::newUnsavedComment( '' );
679		$updater->saveRevision( $summary, 0 );
680
681		$rev = $updater->getNewRevision();
682		$comment = $rev->getComment( RevisionRecord::RAW );
683		$this->assertSame( '', $comment->text, 'comment text' );
684	}
685
686	public function provideSetUsePageCreationLog() {
687		yield [ true, [ [ 'create', 'create' ] ] ];
688		yield [ false, [] ];
689	}
690
691	/**
692	 * @dataProvider provideSetUsePageCreationLog
693	 * @param bool $use
694	 */
695	public function testSetUsePageCreationLog( $use, $expected ) {
696		$user = $this->getTestUser()->getUser();
697		$title = $this->getDummyTitle( __METHOD__ . ( $use ? '_logged' : '_unlogged' ) );
698		$page = WikiPage::factory( $title );
699
700		$updater = $page->newPageUpdater( $user );
701		$updater->setUsePageCreationLog( $use );
702		$summary = CommentStoreComment::newUnsavedComment( 'cmt' );
703		$updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) );
704		$updater->saveRevision( $summary, EDIT_NEW );
705
706		$rev = $updater->getNewRevision();
707		$this->assertSelect(
708			'logging',
709			[ 'log_type', 'log_action' ],
710			[ 'log_page' => $rev->getPageId() ],
711			$expected
712		);
713	}
714
715	public function provideMagicWords() {
716		yield 'PAGEID' => [
717			'Test {{PAGEID}} Test',
718			function ( RevisionRecord $rev ) {
719				return $rev->getPageId();
720			}
721		];
722
723		yield 'REVISIONID' => [
724			'Test {{REVISIONID}} Test',
725			function ( RevisionRecord $rev ) {
726				return $rev->getId();
727			}
728		];
729
730		yield 'REVISIONUSER' => [
731			'Test {{REVISIONUSER}} Test',
732			function ( RevisionRecord $rev ) {
733				return $rev->getUser()->getName();
734			}
735		];
736
737		yield 'REVISIONTIMESTAMP' => [
738			'Test {{REVISIONTIMESTAMP}} Test',
739			function ( RevisionRecord $rev ) {
740				return $rev->getTimestamp();
741			}
742		];
743
744		yield 'subst:REVISIONUSER' => [
745			'Test {{subst:REVISIONUSER}} Test',
746			function ( RevisionRecord $rev ) {
747				return $rev->getUser()->getName();
748			},
749			'subst'
750		];
751
752		yield 'subst:PAGENAME' => [
753			'Test {{subst:PAGENAME}} Test',
754			function ( RevisionRecord $rev ) {
755				return 'PageUpdaterTest::testMagicWords';
756			},
757			'subst'
758		];
759	}
760
761	/**
762	 * @covers \MediaWiki\Storage\PageUpdater::saveRevision()
763	 *
764	 * Integration test for PageUpdater, DerivedPageDataUpdater, RevisionRenderer
765	 * and RenderedRevision, that ensures that magic words depending on revision meta-data
766	 * are handled correctly. Note that each magic word needs to be tested separately,
767	 * to assert correct behavior for each "vary" flag in the ParserOutput.
768	 *
769	 * @dataProvider provideMagicWords
770	 */
771	public function testMagicWords( $wikitext, $callback, $subst = false ) {
772		$user = User::newFromName( 'A user for ' . __METHOD__ );
773		$user->addToDatabase();
774
775		$title = $this->getDummyTitle( __METHOD__ . '-' . $this->getName() );
776		$this->insertPage( $title );
777
778		$page = WikiPage::factory( $title );
779		$updater = $page->newPageUpdater( $user );
780
781		$updater->setContent( SlotRecord::MAIN, new \WikitextContent( $wikitext ) );
782
783		$summary = CommentStoreComment::newUnsavedComment( 'Just a test' );
784		$rev = $updater->saveRevision( $summary, EDIT_UPDATE );
785
786		if ( !$rev ) {
787			$this->fail( $updater->getStatus()->getWikiText() );
788		}
789
790		$expected = strval( $callback( $rev ) );
791
792		$output = $page->getParserOutput( ParserOptions::newCanonical( 'canonical' ) );
793		$html = $output->getText();
794		$text = $rev->getContent( SlotRecord::MAIN )->serialize();
795
796		if ( $subst ) {
797			$this->assertStringContainsString( $expected, $text, 'In Wikitext' );
798		}
799
800		$this->assertStringContainsString( $expected, $html, 'In HTML' );
801	}
802
803}
804