Merge "HTMLInfoField: Support 'rawrow' in OOUI mode"
[lhc/web/wiklou.git] / tests / phpunit / includes / page / WikiPageDbTestBase.php
1 <?php
2
3 use MediaWiki\Edit\PreparedEdit;
4 use MediaWiki\MediaWikiServices;
5 use MediaWiki\Storage\RevisionSlotsUpdate;
6 use MediaWiki\Storage\SlotRecord;
7 use PHPUnit\Framework\MockObject\MockObject;
8 use Wikimedia\TestingAccessWrapper;
9
10 /**
11 * @covers WikiPage
12 */
13 abstract class WikiPageDbTestBase extends MediaWikiLangTestCase {
14
15 private $pagesToDelete;
16
17 public function __construct( $name = null, array $data = [], $dataName = '' ) {
18 parent::__construct( $name, $data, $dataName );
19
20 $this->tablesUsed = array_merge(
21 $this->tablesUsed,
22 [ 'page',
23 'revision',
24 'redirect',
25 'archive',
26 'category',
27 'ip_changes',
28 'text',
29
30 'recentchanges',
31 'logging',
32
33 'page_props',
34 'pagelinks',
35 'categorylinks',
36 'langlinks',
37 'externallinks',
38 'imagelinks',
39 'templatelinks',
40 'iwlinks' ] );
41 }
42
43 protected function addCoreDBData() {
44 // Blank out. This would fail with a modified schema, and we don't need it.
45 }
46
47 /**
48 * @return int
49 */
50 abstract protected function getMcrMigrationStage();
51
52 /**
53 * @return string[]
54 */
55 abstract protected function getMcrTablesToReset();
56
57 protected function setUp() {
58 parent::setUp();
59
60 $this->tablesUsed += $this->getMcrTablesToReset();
61
62 $this->setMwGlobals( 'wgContentHandlerUseDB', $this->getContentHandlerUseDB() );
63 $this->setMwGlobals(
64 'wgMultiContentRevisionSchemaMigrationStage',
65 $this->getMcrMigrationStage()
66 );
67 $this->pagesToDelete = [];
68
69 $this->overrideMwServices();
70 }
71
72 protected function tearDown() {
73 foreach ( $this->pagesToDelete as $p ) {
74 /* @var $p WikiPage */
75
76 try {
77 if ( $p->exists() ) {
78 $p->doDeleteArticle( "testing done." );
79 }
80 } catch ( MWException $ex ) {
81 // fail silently
82 }
83 }
84 parent::tearDown();
85 }
86
87 abstract protected function getContentHandlerUseDB();
88
89 /**
90 * @param Title|string $title
91 * @param string|null $model
92 * @return WikiPage
93 */
94 private function newPage( $title, $model = null ) {
95 if ( is_string( $title ) ) {
96 $ns = $this->getDefaultWikitextNS();
97 $title = Title::newFromText( $title, $ns );
98 }
99
100 $p = new WikiPage( $title );
101
102 $this->pagesToDelete[] = $p;
103
104 return $p;
105 }
106
107 /**
108 * @param string|Title|WikiPage $page
109 * @param string|Content|Content[] $content
110 * @param int|null $model
111 *
112 * @return WikiPage
113 */
114 protected function createPage( $page, $content, $model = null, $user = null ) {
115 if ( is_string( $page ) || $page instanceof Title ) {
116 $page = $this->newPage( $page, $model );
117 }
118
119 if ( !$user ) {
120 $user = $this->getTestUser()->getUser();
121 }
122
123 if ( is_string( $content ) ) {
124 $content = ContentHandler::makeContent( $content, $page->getTitle(), $model );
125 }
126
127 if ( !is_array( $content ) ) {
128 $content = [ 'main' => $content ];
129 }
130
131 $updater = $page->newPageUpdater( $user );
132
133 foreach ( $content as $role => $cnt ) {
134 $updater->setContent( $role, $cnt );
135 }
136
137 $updater->saveRevision( CommentStoreComment::newUnsavedComment( "testing" ) );
138
139 return $page;
140 }
141
142 /**
143 * @covers WikiPage::prepareContentForEdit
144 */
145 public function testPrepareContentForEdit() {
146 $user = $this->getTestUser()->getUser();
147 $sysop = $this->getTestUser( [ 'sysop' ] )->getUser();
148
149 $page = $this->createPage( __METHOD__, __METHOD__, null, $user );
150 $title = $page->getTitle();
151
152 $content = ContentHandler::makeContent(
153 "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam "
154 . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.",
155 $title,
156 CONTENT_MODEL_WIKITEXT
157 );
158 $content2 = ContentHandler::makeContent(
159 "At vero eos et accusam et justo duo [[dolores]] et ea rebum. "
160 . "Stet clita kasd [[gubergren]], no sea takimata sanctus est. ~~~~",
161 $title,
162 CONTENT_MODEL_WIKITEXT
163 );
164
165 $edit = $page->prepareContentForEdit( $content, null, $user, null, false );
166
167 $this->assertInstanceOf(
168 ParserOptions::class,
169 $edit->popts,
170 "pops"
171 );
172 $this->assertContains( '</a>', $edit->output->getText(), "output" );
173 $this->assertContains(
174 'consetetur sadipscing elitr',
175 $edit->output->getText(),
176 "output"
177 );
178
179 $this->assertTrue( $content->equals( $edit->newContent ), "newContent field" );
180 $this->assertTrue( $content->equals( $edit->pstContent ), "pstContent field" );
181 $this->assertSame( $edit->output, $edit->output, "output field" );
182 $this->assertSame( $edit->popts, $edit->popts, "popts field" );
183 $this->assertSame( null, $edit->revid, "revid field" );
184
185 // Re-using the prepared info if possible
186 $sameEdit = $page->prepareContentForEdit( $content, null, $user, null, false );
187 $this->assertPreparedEditEquals( $edit, $sameEdit, 'equivalent PreparedEdit' );
188 $this->assertSame( $edit->pstContent, $sameEdit->pstContent, 're-use output' );
189 $this->assertSame( $edit->output, $sameEdit->output, 're-use output' );
190
191 // Not re-using the same PreparedEdit if not possible
192 $rev = $page->getRevision();
193 $edit2 = $page->prepareContentForEdit( $content2, null, $user, null, false );
194 $this->assertPreparedEditNotEquals( $edit, $edit2 );
195 $this->assertContains( 'At vero eos', $edit2->pstContent->serialize(), "content" );
196
197 // Check pre-safe transform
198 $this->assertContains( '[[gubergren]]', $edit2->pstContent->serialize() );
199 $this->assertNotContains( '~~~~', $edit2->pstContent->serialize() );
200
201 $edit3 = $page->prepareContentForEdit( $content2, null, $sysop, null, false );
202 $this->assertPreparedEditNotEquals( $edit2, $edit3 );
203
204 // TODO: test with passing revision, then same without revision.
205 }
206
207 /**
208 * @covers WikiPage::doEditUpdates
209 */
210 public function testDoEditUpdates() {
211 $user = $this->getTestUser()->getUser();
212
213 // NOTE: if site stats get out of whack and drop below 0,
214 // that causes a DB error during tear-down. So bump the
215 // numbers high enough to not drop below 0.
216 $siteStatsUpdate = SiteStatsUpdate::factory(
217 [ 'edits' => 1000, 'articles' => 1000, 'pages' => 1000 ]
218 );
219 $siteStatsUpdate->doUpdate();
220
221 $page = $this->createPage( __METHOD__, __METHOD__ );
222
223 $revision = new Revision(
224 [
225 'id' => 9989,
226 'page' => $page->getId(),
227 'title' => $page->getTitle(),
228 'comment' => __METHOD__,
229 'minor_edit' => true,
230 'text' => __METHOD__ . ' [[|foo]][[bar]]', // PST turns [[|foo]] into [[foo]]
231 'user' => $user->getId(),
232 'user_text' => $user->getName(),
233 'timestamp' => '20170707040404',
234 'content_model' => CONTENT_MODEL_WIKITEXT,
235 'content_format' => CONTENT_FORMAT_WIKITEXT,
236 ]
237 );
238
239 $page->doEditUpdates( $revision, $user );
240
241 // TODO: test various options; needs temporary hooks
242
243 $dbr = wfGetDB( DB_REPLICA );
244 $res = $dbr->select( 'pagelinks', '*', [ 'pl_from' => $page->getId() ] );
245 $n = $res->numRows();
246 $res->free();
247
248 $this->assertEquals( 1, $n, 'pagelinks should contain only one link if PST was not applied' );
249 }
250
251 /**
252 * @covers WikiPage::doEditContent
253 * @covers WikiPage::prepareContentForEdit
254 */
255 public function testDoEditContent() {
256 $this->setMwGlobals( 'wgPageCreationLog', true );
257
258 $page = $this->newPage( __METHOD__ );
259 $title = $page->getTitle();
260
261 $user1 = $this->getTestUser()->getUser();
262 // Use the confirmed group for user2 to make sure the user is different
263 $user2 = $this->getTestUser( [ 'confirmed' ] )->getUser();
264
265 $content = ContentHandler::makeContent(
266 "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam "
267 . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.",
268 $title,
269 CONTENT_MODEL_WIKITEXT
270 );
271
272 $preparedEditBefore = $page->prepareContentForEdit( $content, null, $user1 );
273
274 $status = $page->doEditContent( $content, "[[testing]] 1", EDIT_NEW, false, $user1 );
275
276 $this->assertTrue( $status->isOK(), 'OK' );
277 $this->assertTrue( $status->value['new'], 'new' );
278 $this->assertNotNull( $status->value['revision'], 'revision' );
279 $this->assertSame( $status->value['revision']->getId(), $page->getRevision()->getId() );
280 $this->assertSame( $status->value['revision']->getSha1(), $page->getRevision()->getSha1() );
281 $this->assertTrue( $status->value['revision']->getContent()->equals( $content ), 'equals' );
282
283 $rev = $page->getRevision();
284 $preparedEditAfter = $page->prepareContentForEdit( $content, $rev, $user1 );
285
286 $this->assertNotNull( $rev->getRecentChange() );
287 $this->assertSame( $rev->getId(), (int)$rev->getRecentChange()->getAttribute( 'rc_this_oldid' ) );
288
289 // make sure that cached ParserOutput gets re-used throughout
290 $this->assertSame( $preparedEditBefore->output, $preparedEditAfter->output );
291
292 $id = $page->getId();
293
294 // Test page creation logging
295 $this->assertSelect(
296 'logging',
297 [ 'log_type', 'log_action' ],
298 [ 'log_page' => $id ],
299 [ [ 'create', 'create' ] ]
300 );
301
302 $this->assertTrue( $title->getArticleID() > 0, "Title object should have new page id" );
303 $this->assertTrue( $id > 0, "WikiPage should have new page id" );
304 $this->assertTrue( $title->exists(), "Title object should indicate that the page now exists" );
305 $this->assertTrue( $page->exists(), "WikiPage object should indicate that the page now exists" );
306
307 # ------------------------
308 $dbr = wfGetDB( DB_REPLICA );
309 $res = $dbr->select( 'pagelinks', '*', [ 'pl_from' => $id ] );
310 $n = $res->numRows();
311 $res->free();
312
313 $this->assertEquals( 1, $n, 'pagelinks should contain one link from the page' );
314
315 # ------------------------
316 $page = new WikiPage( $title );
317
318 $retrieved = $page->getContent();
319 $this->assertTrue( $content->equals( $retrieved ), 'retrieved content doesn\'t equal original' );
320
321 # ------------------------
322 $page = new WikiPage( $title );
323
324 // try null edit, with a different user
325 $status = $page->doEditContent( $content, 'This changes nothing', EDIT_UPDATE, false, $user2 );
326 $this->assertTrue( $status->isOK(), 'OK' );
327 $this->assertFalse( $status->value['new'], 'new' );
328 $this->assertNull( $status->value['revision'], 'revision' );
329 $this->assertNotNull( $page->getRevision() );
330 $this->assertTrue( $page->getRevision()->getContent()->equals( $content ), 'equals' );
331
332 # ------------------------
333 $content = ContentHandler::makeContent(
334 "At vero eos et accusam et justo duo [[dolores]] et ea rebum. "
335 . "Stet clita kasd [[gubergren]], no sea takimata sanctus est. ~~~~",
336 $title,
337 CONTENT_MODEL_WIKITEXT
338 );
339
340 $status = $page->doEditContent( $content, "testing 2", EDIT_UPDATE );
341 $this->assertTrue( $status->isOK(), 'OK' );
342 $this->assertFalse( $status->value['new'], 'new' );
343 $this->assertNotNull( $status->value['revision'], 'revision' );
344 $this->assertSame( $status->value['revision']->getId(), $page->getRevision()->getId() );
345 $this->assertSame( $status->value['revision']->getSha1(), $page->getRevision()->getSha1() );
346 $this->assertFalse(
347 $status->value['revision']->getContent()->equals( $content ),
348 'not equals (PST must substitute signature)'
349 );
350
351 $rev = $page->getRevision();
352 $this->assertNotNull( $rev->getRecentChange() );
353 $this->assertSame( $rev->getId(), (int)$rev->getRecentChange()->getAttribute( 'rc_this_oldid' ) );
354
355 # ------------------------
356 $page = new WikiPage( $title );
357
358 $retrieved = $page->getContent();
359 $newText = $retrieved->serialize();
360 $this->assertContains( '[[gubergren]]', $newText, 'New text must replace old text.' );
361 $this->assertNotContains( '~~~~', $newText, 'PST must substitute signature.' );
362
363 # ------------------------
364 $dbr = wfGetDB( DB_REPLICA );
365 $res = $dbr->select( 'pagelinks', '*', [ 'pl_from' => $id ] );
366 $n = $res->numRows();
367 $res->free();
368
369 $this->assertEquals( 2, $n, 'pagelinks should contain two links from the page' );
370 }
371
372 /**
373 * @covers WikiPage::doEditContent
374 */
375 public function testDoEditContent_twice() {
376 $title = Title::newFromText( __METHOD__ );
377 $page = WikiPage::factory( $title );
378 $content = ContentHandler::makeContent( '$1 van $2', $title );
379
380 // Make sure we can do the exact same save twice.
381 // This tests checks that internal caches are reset as appropriate.
382 $status1 = $page->doEditContent( $content, __METHOD__ );
383 $status2 = $page->doEditContent( $content, __METHOD__ );
384
385 $this->assertTrue( $status1->isOK(), 'OK' );
386 $this->assertTrue( $status2->isOK(), 'OK' );
387
388 $this->assertTrue( isset( $status1->value['revision'] ), 'OK' );
389 $this->assertFalse( isset( $status2->value['revision'] ), 'OK' );
390 }
391
392 /**
393 * Undeletion is covered in PageArchiveTest::testUndeleteRevisions()
394 * TODO: Revision deletion
395 *
396 * @covers WikiPage::doDeleteArticle
397 * @covers WikiPage::doDeleteArticleReal
398 */
399 public function testDoDeleteArticle() {
400 $page = $this->createPage(
401 __METHOD__,
402 "[[original text]] foo",
403 CONTENT_MODEL_WIKITEXT
404 );
405 $id = $page->getId();
406
407 $page->doDeleteArticle( "testing deletion" );
408
409 $this->assertFalse(
410 $page->getTitle()->getArticleID() > 0,
411 "Title object should now have page id 0"
412 );
413 $this->assertFalse( $page->getId() > 0, "WikiPage should now have page id 0" );
414 $this->assertFalse(
415 $page->exists(),
416 "WikiPage::exists should return false after page was deleted"
417 );
418 $this->assertNull(
419 $page->getContent(),
420 "WikiPage::getContent should return null after page was deleted"
421 );
422
423 $t = Title::newFromText( $page->getTitle()->getPrefixedText() );
424 $this->assertFalse(
425 $t->exists(),
426 "Title::exists should return false after page was deleted"
427 );
428
429 // Run the job queue
430 JobQueueGroup::destroySingletons();
431 $jobs = new RunJobs;
432 $jobs->loadParamsAndArgs( null, [ 'quiet' => true ], null );
433 $jobs->execute();
434
435 # ------------------------
436 $dbr = wfGetDB( DB_REPLICA );
437 $res = $dbr->select( 'pagelinks', '*', [ 'pl_from' => $id ] );
438 $n = $res->numRows();
439 $res->free();
440
441 $this->assertEquals( 0, $n, 'pagelinks should contain no more links from the page' );
442 }
443
444 /**
445 * @covers WikiPage::doDeleteArticleReal
446 */
447 public function testDoDeleteArticleReal_user0() {
448 $page = $this->createPage(
449 __METHOD__,
450 "[[original text]] foo",
451 CONTENT_MODEL_WIKITEXT
452 );
453 $id = $page->getId();
454
455 $errorStack = '';
456 $status = $page->doDeleteArticleReal(
457 /* reason */ "testing user 0 deletion",
458 /* suppress */ false,
459 /* unused 1 */ null,
460 /* unused 2 */ null,
461 /* errorStack */ $errorStack,
462 null
463 );
464 $logId = $status->getValue();
465 $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
466 $this->assertSelect(
467 [ 'logging' ] + $actorQuery['tables'], /* table */
468 [
469 'log_type',
470 'log_action',
471 'log_comment',
472 'log_user' => $actorQuery['fields']['log_user'],
473 'log_user_text' => $actorQuery['fields']['log_user_text'],
474 'log_namespace',
475 'log_title',
476 ],
477 [ 'log_id' => $logId ],
478 [ [
479 'delete',
480 'delete',
481 'testing user 0 deletion',
482 '0',
483 '127.0.0.1',
484 (string)$page->getTitle()->getNamespace(),
485 $page->getTitle()->getDBkey(),
486 ] ],
487 [],
488 $actorQuery['joins']
489 );
490 }
491
492 /**
493 * @covers WikiPage::doDeleteArticleReal
494 */
495 public function testDoDeleteArticleReal_userSysop() {
496 $page = $this->createPage(
497 __METHOD__,
498 "[[original text]] foo",
499 CONTENT_MODEL_WIKITEXT
500 );
501 $id = $page->getId();
502
503 $user = $this->getTestSysop()->getUser();
504 $errorStack = '';
505 $status = $page->doDeleteArticleReal(
506 /* reason */ "testing sysop deletion",
507 /* suppress */ false,
508 /* unused 1 */ null,
509 /* unused 2 */ null,
510 /* errorStack */ $errorStack,
511 $user
512 );
513 $logId = $status->getValue();
514 $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
515 $this->assertSelect(
516 [ 'logging' ] + $actorQuery['tables'], /* table */
517 [
518 'log_type',
519 'log_action',
520 'log_comment',
521 'log_user' => $actorQuery['fields']['log_user'],
522 'log_user_text' => $actorQuery['fields']['log_user_text'],
523 'log_namespace',
524 'log_title',
525 ],
526 [ 'log_id' => $logId ],
527 [ [
528 'delete',
529 'delete',
530 'testing sysop deletion',
531 (string)$user->getId(),
532 $user->getName(),
533 (string)$page->getTitle()->getNamespace(),
534 $page->getTitle()->getDBkey(),
535 ] ],
536 [],
537 $actorQuery['joins']
538 );
539 }
540
541 /**
542 * TODO: Test more stuff about suppression.
543 *
544 * @covers WikiPage::doDeleteArticleReal
545 */
546 public function testDoDeleteArticleReal_suppress() {
547 $page = $this->createPage(
548 __METHOD__,
549 "[[original text]] foo",
550 CONTENT_MODEL_WIKITEXT
551 );
552 $id = $page->getId();
553
554 $user = $this->getTestSysop()->getUser();
555 $errorStack = '';
556 $status = $page->doDeleteArticleReal(
557 /* reason */ "testing deletion",
558 /* suppress */ true,
559 /* unused 1 */ null,
560 /* unused 2 */ null,
561 /* errorStack */ $errorStack,
562 $user
563 );
564 $logId = $status->getValue();
565 $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
566 $this->assertSelect(
567 [ 'logging' ] + $actorQuery['tables'], /* table */
568 [
569 'log_type',
570 'log_action',
571 'log_comment',
572 'log_user' => $actorQuery['fields']['log_user'],
573 'log_user_text' => $actorQuery['fields']['log_user_text'],
574 'log_namespace',
575 'log_title',
576 ],
577 [ 'log_id' => $logId ],
578 [ [
579 'suppress',
580 'delete',
581 'testing deletion',
582 (string)$user->getId(),
583 $user->getName(),
584 (string)$page->getTitle()->getNamespace(),
585 $page->getTitle()->getDBkey(),
586 ] ],
587 [],
588 $actorQuery['joins']
589 );
590
591 $this->assertNull(
592 $page->getContent( Revision::FOR_PUBLIC ),
593 "WikiPage::getContent should return null after the page was suppressed for general users"
594 );
595
596 $this->assertNull(
597 $page->getContent( Revision::FOR_THIS_USER, null ),
598 "WikiPage::getContent should return null after the page was suppressed for user zero"
599 );
600
601 $this->assertNull(
602 $page->getContent( Revision::FOR_THIS_USER, $user ),
603 "WikiPage::getContent should return null after the page was suppressed even for a sysop"
604 );
605 }
606
607 /**
608 * @covers WikiPage::doDeleteUpdates
609 */
610 public function testDoDeleteUpdates() {
611 $user = $this->getTestUser()->getUser();
612 $page = $this->createPage(
613 __METHOD__,
614 "[[original text]] foo",
615 CONTENT_MODEL_WIKITEXT
616 );
617 $id = $page->getId();
618 $page->loadPageData(); // make sure the current revision is cached.
619
620 // Similar to MovePage logic
621 wfGetDB( DB_MASTER )->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
622 $page->doDeleteUpdates( $page->getId(), $page->getContent(), $page->getRevision(), $user );
623
624 // Run the job queue
625 JobQueueGroup::destroySingletons();
626 $jobs = new RunJobs;
627 $jobs->loadParamsAndArgs( null, [ 'quiet' => true ], null );
628 $jobs->execute();
629
630 # ------------------------
631 $dbr = wfGetDB( DB_REPLICA );
632 $res = $dbr->select( 'pagelinks', '*', [ 'pl_from' => $id ] );
633 $n = $res->numRows();
634 $res->free();
635
636 $this->assertEquals( 0, $n, 'pagelinks should contain no more links from the page' );
637 }
638
639 /**
640 * @param string $name
641 *
642 * @return ContentHandler
643 */
644 protected function defineMockContentModelForUpdateTesting( $name ) {
645 /** @var ContentHandler|MockObject $handler */
646 $handler = $this->getMockBuilder( TextContentHandler::class )
647 ->setConstructorArgs( [ $name ] )
648 ->setMethods(
649 [ 'getSecondaryDataUpdates', 'getDeletionUpdates', 'unserializeContent' ]
650 )
651 ->getMock();
652
653 $dataUpdate = new MWCallableUpdate( 'time' );
654 $dataUpdate->_name = "$name data update";
655
656 $deletionUpdate = new MWCallableUpdate( 'time' );
657 $deletionUpdate->_name = "$name deletion update";
658
659 $handler->method( 'getSecondaryDataUpdates' )->willReturn( [ $dataUpdate ] );
660 $handler->method( 'getDeletionUpdates' )->willReturn( [ $deletionUpdate ] );
661 $handler->method( 'unserializeContent' )->willReturnCallback(
662 function ( $text ) use ( $handler ) {
663 return $this->createMockContent( $handler, $text );
664 }
665 );
666
667 $this->mergeMwGlobalArrayValue(
668 'wgContentHandlers', [
669 $name => function () use ( $handler ){
670 return $handler;
671 }
672 ]
673 );
674
675 return $handler;
676 }
677
678 /**
679 * @param ContentHandler $handler
680 * @param string $text
681 *
682 * @return Content
683 */
684 protected function createMockContent( ContentHandler $handler, $text ) {
685 /** @var Content|MockObject $content */
686 $content = $this->getMockBuilder( TextContent::class )
687 ->setConstructorArgs( [ $text ] )
688 ->setMethods( [ 'getModel', 'getContentHandler' ] )
689 ->getMock();
690
691 $content->method( 'getModel' )->willReturn( $handler->getModelID() );
692 $content->method( 'getContentHandler' )->willReturn( $handler );
693
694 return $content;
695 }
696
697 public function testGetDeletionUpdates() {
698 $m1 = $this->defineMockContentModelForUpdateTesting( 'M1' );
699
700 $mainContent1 = $this->createMockContent( $m1, 'main 1' );
701
702 $page = new WikiPage( Title::newFromText( __METHOD__ ) );
703 $page = $this->createPage(
704 $page,
705 [ 'main' => $mainContent1 ]
706 );
707
708 $dataUpdates = $page->getDeletionUpdates( $page->getRevisionRecord() );
709 $this->assertNotEmpty( $dataUpdates );
710
711 $updateNames = array_map( function ( $du ) {
712 return isset( $du->_name ) ? $du->_name : get_class( $du );
713 }, $dataUpdates );
714
715 $this->assertContains( LinksDeletionUpdate::class, $updateNames );
716 $this->assertContains( 'M1 deletion update', $updateNames );
717 }
718
719 /**
720 * @covers WikiPage::getRevision
721 */
722 public function testGetRevision() {
723 $page = $this->newPage( __METHOD__ );
724
725 $rev = $page->getRevision();
726 $this->assertNull( $rev );
727
728 # -----------------
729 $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
730
731 $rev = $page->getRevision();
732
733 $this->assertEquals( $page->getLatest(), $rev->getId() );
734 $this->assertEquals( "some text", $rev->getContent()->getNativeData() );
735 }
736
737 /**
738 * @covers WikiPage::getContent
739 */
740 public function testGetContent() {
741 $page = $this->newPage( __METHOD__ );
742
743 $content = $page->getContent();
744 $this->assertNull( $content );
745
746 # -----------------
747 $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
748
749 $content = $page->getContent();
750 $this->assertEquals( "some text", $content->getNativeData() );
751 }
752
753 /**
754 * @covers WikiPage::exists
755 */
756 public function testExists() {
757 $page = $this->newPage( __METHOD__ );
758 $this->assertFalse( $page->exists() );
759
760 # -----------------
761 $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
762 $this->assertTrue( $page->exists() );
763
764 $page = new WikiPage( $page->getTitle() );
765 $this->assertTrue( $page->exists() );
766
767 # -----------------
768 $page->doDeleteArticle( "done testing" );
769 $this->assertFalse( $page->exists() );
770
771 $page = new WikiPage( $page->getTitle() );
772 $this->assertFalse( $page->exists() );
773 }
774
775 public function provideHasViewableContent() {
776 return [
777 [ 'WikiPageTest_testHasViewableContent', false, true ],
778 [ 'Special:WikiPageTest_testHasViewableContent', false ],
779 [ 'MediaWiki:WikiPageTest_testHasViewableContent', false ],
780 [ 'Special:Userlogin', true ],
781 [ 'MediaWiki:help', true ],
782 ];
783 }
784
785 /**
786 * @dataProvider provideHasViewableContent
787 * @covers WikiPage::hasViewableContent
788 */
789 public function testHasViewableContent( $title, $viewable, $create = false ) {
790 $page = $this->newPage( $title );
791 $this->assertEquals( $viewable, $page->hasViewableContent() );
792
793 if ( $create ) {
794 $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
795 $this->assertTrue( $page->hasViewableContent() );
796
797 $page = new WikiPage( $page->getTitle() );
798 $this->assertTrue( $page->hasViewableContent() );
799 }
800 }
801
802 public function provideGetRedirectTarget() {
803 return [
804 [ 'WikiPageTest_testGetRedirectTarget_1', CONTENT_MODEL_WIKITEXT, "hello world", null ],
805 [
806 'WikiPageTest_testGetRedirectTarget_2',
807 CONTENT_MODEL_WIKITEXT,
808 "#REDIRECT [[hello world]]",
809 "Hello world"
810 ],
811 ];
812 }
813
814 /**
815 * @dataProvider provideGetRedirectTarget
816 * @covers WikiPage::getRedirectTarget
817 */
818 public function testGetRedirectTarget( $title, $model, $text, $target ) {
819 $this->setMwGlobals( [
820 'wgCapitalLinks' => true,
821 ] );
822
823 $page = $this->createPage( $title, $text, $model );
824
825 # sanity check, because this test seems to fail for no reason for some people.
826 $c = $page->getContent();
827 $this->assertEquals( WikitextContent::class, get_class( $c ) );
828
829 # now, test the actual redirect
830 $t = $page->getRedirectTarget();
831 $this->assertEquals( $target, is_null( $t ) ? null : $t->getPrefixedText() );
832 }
833
834 /**
835 * @dataProvider provideGetRedirectTarget
836 * @covers WikiPage::isRedirect
837 */
838 public function testIsRedirect( $title, $model, $text, $target ) {
839 $page = $this->createPage( $title, $text, $model );
840 $this->assertEquals( !is_null( $target ), $page->isRedirect() );
841 }
842
843 public function provideIsCountable() {
844 return [
845
846 // any
847 [ 'WikiPageTest_testIsCountable',
848 CONTENT_MODEL_WIKITEXT,
849 '',
850 'any',
851 true
852 ],
853 [ 'WikiPageTest_testIsCountable',
854 CONTENT_MODEL_WIKITEXT,
855 'Foo',
856 'any',
857 true
858 ],
859
860 // link
861 [ 'WikiPageTest_testIsCountable',
862 CONTENT_MODEL_WIKITEXT,
863 'Foo',
864 'link',
865 false
866 ],
867 [ 'WikiPageTest_testIsCountable',
868 CONTENT_MODEL_WIKITEXT,
869 'Foo [[bar]]',
870 'link',
871 true
872 ],
873
874 // redirects
875 [ 'WikiPageTest_testIsCountable',
876 CONTENT_MODEL_WIKITEXT,
877 '#REDIRECT [[bar]]',
878 'any',
879 false
880 ],
881 [ 'WikiPageTest_testIsCountable',
882 CONTENT_MODEL_WIKITEXT,
883 '#REDIRECT [[bar]]',
884 'link',
885 false
886 ],
887
888 // not a content namespace
889 [ 'Talk:WikiPageTest_testIsCountable',
890 CONTENT_MODEL_WIKITEXT,
891 'Foo',
892 'any',
893 false
894 ],
895 [ 'Talk:WikiPageTest_testIsCountable',
896 CONTENT_MODEL_WIKITEXT,
897 'Foo [[bar]]',
898 'link',
899 false
900 ],
901
902 // not a content namespace, different model
903 [ 'MediaWiki:WikiPageTest_testIsCountable.js',
904 null,
905 'Foo',
906 'any',
907 false
908 ],
909 [ 'MediaWiki:WikiPageTest_testIsCountable.js',
910 null,
911 'Foo [[bar]]',
912 'link',
913 false
914 ],
915 ];
916 }
917
918 /**
919 * @dataProvider provideIsCountable
920 * @covers WikiPage::isCountable
921 */
922 public function testIsCountable( $title, $model, $text, $mode, $expected ) {
923 global $wgContentHandlerUseDB;
924
925 $this->setMwGlobals( 'wgArticleCountMethod', $mode );
926
927 $title = Title::newFromText( $title );
928
929 if ( !$wgContentHandlerUseDB
930 && $model
931 && ContentHandler::getDefaultModelFor( $title ) != $model
932 ) {
933 $this->markTestSkipped( "Can not use non-default content model $model for "
934 . $title->getPrefixedDBkey() . " with \$wgContentHandlerUseDB disabled." );
935 }
936
937 $page = $this->createPage( $title, $text, $model );
938
939 $editInfo = $page->prepareContentForEdit( $page->getContent() );
940
941 $v = $page->isCountable();
942 $w = $page->isCountable( $editInfo );
943
944 $this->assertEquals(
945 $expected,
946 $v,
947 "isCountable( null ) returned unexpected value " . var_export( $v, true )
948 . " instead of " . var_export( $expected, true )
949 . " in mode `$mode` for text \"$text\""
950 );
951
952 $this->assertEquals(
953 $expected,
954 $w,
955 "isCountable( \$editInfo ) returned unexpected value " . var_export( $v, true )
956 . " instead of " . var_export( $expected, true )
957 . " in mode `$mode` for text \"$text\""
958 );
959 }
960
961 public function provideGetParserOutput() {
962 return [
963 [
964 CONTENT_MODEL_WIKITEXT,
965 "hello ''world''\n",
966 "<div class=\"mw-parser-output\"><p>hello <i>world</i></p></div>"
967 ],
968 // @todo more...?
969 ];
970 }
971
972 /**
973 * @dataProvider provideGetParserOutput
974 * @covers WikiPage::getParserOutput
975 */
976 public function testGetParserOutput( $model, $text, $expectedHtml ) {
977 $page = $this->createPage( __METHOD__, $text, $model );
978
979 $opt = $page->makeParserOptions( 'canonical' );
980 $po = $page->getParserOutput( $opt );
981 $text = $po->getText();
982
983 $text = trim( preg_replace( '/<!--.*?-->/sm', '', $text ) ); # strip injected comments
984 $text = preg_replace( '!\s*(</p>|</div>)!sm', '\1', $text ); # don't let tidy confuse us
985
986 $this->assertEquals( $expectedHtml, $text );
987
988 return $po;
989 }
990
991 /**
992 * @covers WikiPage::getParserOutput
993 */
994 public function testGetParserOutput_nonexisting() {
995 $page = new WikiPage( Title::newFromText( __METHOD__ ) );
996
997 $opt = new ParserOptions();
998 $po = $page->getParserOutput( $opt );
999
1000 $this->assertFalse( $po, "getParserOutput() shall return false for non-existing pages." );
1001 }
1002
1003 /**
1004 * @covers WikiPage::getParserOutput
1005 */
1006 public function testGetParserOutput_badrev() {
1007 $page = $this->createPage( __METHOD__, 'dummy', CONTENT_MODEL_WIKITEXT );
1008
1009 $opt = new ParserOptions();
1010 $po = $page->getParserOutput( $opt, $page->getLatest() + 1234 );
1011
1012 // @todo would be neat to also test deleted revision
1013
1014 $this->assertFalse( $po, "getParserOutput() shall return false for non-existing revisions." );
1015 }
1016
1017 public static $sections =
1018
1019 "Intro
1020
1021 == stuff ==
1022 hello world
1023
1024 == test ==
1025 just a test
1026
1027 == foo ==
1028 more stuff
1029 ";
1030
1031 public function dataReplaceSection() {
1032 // NOTE: assume the Help namespace to contain wikitext
1033 return [
1034 [ 'Help:WikiPageTest_testReplaceSection',
1035 CONTENT_MODEL_WIKITEXT,
1036 self::$sections,
1037 "0",
1038 "No more",
1039 null,
1040 trim( preg_replace( '/^Intro/sm', 'No more', self::$sections ) )
1041 ],
1042 [ 'Help:WikiPageTest_testReplaceSection',
1043 CONTENT_MODEL_WIKITEXT,
1044 self::$sections,
1045 "",
1046 "No more",
1047 null,
1048 "No more"
1049 ],
1050 [ 'Help:WikiPageTest_testReplaceSection',
1051 CONTENT_MODEL_WIKITEXT,
1052 self::$sections,
1053 "2",
1054 "== TEST ==\nmore fun",
1055 null,
1056 trim( preg_replace( '/^== test ==.*== foo ==/sm',
1057 "== TEST ==\nmore fun\n\n== foo ==",
1058 self::$sections ) )
1059 ],
1060 [ 'Help:WikiPageTest_testReplaceSection',
1061 CONTENT_MODEL_WIKITEXT,
1062 self::$sections,
1063 "8",
1064 "No more",
1065 null,
1066 trim( self::$sections )
1067 ],
1068 [ 'Help:WikiPageTest_testReplaceSection',
1069 CONTENT_MODEL_WIKITEXT,
1070 self::$sections,
1071 "new",
1072 "No more",
1073 "New",
1074 trim( self::$sections ) . "\n\n== New ==\n\nNo more"
1075 ],
1076 ];
1077 }
1078
1079 /**
1080 * @dataProvider dataReplaceSection
1081 * @covers WikiPage::replaceSectionContent
1082 */
1083 public function testReplaceSectionContent( $title, $model, $text, $section,
1084 $with, $sectionTitle, $expected
1085 ) {
1086 $page = $this->createPage( $title, $text, $model );
1087
1088 $content = ContentHandler::makeContent( $with, $page->getTitle(), $page->getContentModel() );
1089 $c = $page->replaceSectionContent( $section, $content, $sectionTitle );
1090
1091 $this->assertEquals( $expected, is_null( $c ) ? null : trim( $c->getNativeData() ) );
1092 }
1093
1094 /**
1095 * @dataProvider dataReplaceSection
1096 * @covers WikiPage::replaceSectionAtRev
1097 */
1098 public function testReplaceSectionAtRev( $title, $model, $text, $section,
1099 $with, $sectionTitle, $expected
1100 ) {
1101 $page = $this->createPage( $title, $text, $model );
1102 $baseRevId = $page->getLatest();
1103
1104 $content = ContentHandler::makeContent( $with, $page->getTitle(), $page->getContentModel() );
1105 $c = $page->replaceSectionAtRev( $section, $content, $sectionTitle, $baseRevId );
1106
1107 $this->assertEquals( $expected, is_null( $c ) ? null : trim( $c->getNativeData() ) );
1108 }
1109
1110 /**
1111 * @covers WikiPage::getOldestRevision
1112 */
1113 public function testGetOldestRevision() {
1114 $page = $this->newPage( __METHOD__ );
1115 $page->doEditContent(
1116 new WikitextContent( 'one' ),
1117 "first edit",
1118 EDIT_NEW
1119 );
1120 $rev1 = $page->getRevision();
1121
1122 $page = new WikiPage( $page->getTitle() );
1123 $page->doEditContent(
1124 new WikitextContent( 'two' ),
1125 "second edit",
1126 EDIT_UPDATE
1127 );
1128
1129 $page = new WikiPage( $page->getTitle() );
1130 $page->doEditContent(
1131 new WikitextContent( 'three' ),
1132 "third edit",
1133 EDIT_UPDATE
1134 );
1135
1136 // sanity check
1137 $this->assertNotEquals(
1138 $rev1->getId(),
1139 $page->getRevision()->getId(),
1140 '$page->getRevision()->getId()'
1141 );
1142
1143 // actual test
1144 $this->assertEquals(
1145 $rev1->getId(),
1146 $page->getOldestRevision()->getId(),
1147 '$page->getOldestRevision()->getId()'
1148 );
1149 }
1150
1151 /**
1152 * @covers WikiPage::doRollback
1153 * @covers WikiPage::commitRollback
1154 */
1155 public function testDoRollback() {
1156 // FIXME: fails under postgres
1157 $this->markTestSkippedIfDbType( 'postgres' );
1158
1159 $admin = $this->getTestSysop()->getUser();
1160 $user1 = $this->getTestUser()->getUser();
1161 // Use the confirmed group for user2 to make sure the user is different
1162 $user2 = $this->getTestUser( [ 'confirmed' ] )->getUser();
1163
1164 // make sure we can test autopatrolling
1165 $this->setMwGlobals( 'wgUseRCPatrol', true );
1166
1167 // TODO: MCR: test rollback of multiple slots!
1168 $page = $this->newPage( __METHOD__ );
1169
1170 // Make some edits
1171 $text = "one";
1172 $status1 = $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ),
1173 "section one", EDIT_NEW, false, $admin );
1174
1175 $text .= "\n\ntwo";
1176 $status2 = $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ),
1177 "adding section two", 0, false, $user1 );
1178
1179 $text .= "\n\nthree";
1180 $status3 = $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ),
1181 "adding section three", 0, false, $user2 );
1182
1183 /** @var Revision $rev1 */
1184 /** @var Revision $rev2 */
1185 /** @var Revision $rev3 */
1186 $rev1 = $status1->getValue()['revision'];
1187 $rev2 = $status2->getValue()['revision'];
1188 $rev3 = $status3->getValue()['revision'];
1189
1190 /**
1191 * We are having issues with doRollback spuriously failing. Apparently
1192 * the last revision somehow goes missing or not committed under some
1193 * circumstances. So, make sure the revisions have the correct usernames.
1194 */
1195 $this->assertEquals( 3, Revision::countByPageId( wfGetDB( DB_REPLICA ), $page->getId() ) );
1196 $this->assertEquals( $admin->getName(), $rev1->getUserText() );
1197 $this->assertEquals( $user1->getName(), $rev2->getUserText() );
1198 $this->assertEquals( $user2->getName(), $rev3->getUserText() );
1199
1200 // Now, try the actual rollback
1201 $token = $admin->getEditToken( 'rollback' );
1202 $rollbackErrors = $page->doRollback(
1203 $user2->getName(),
1204 "testing rollback",
1205 $token,
1206 false,
1207 $resultDetails,
1208 $admin
1209 );
1210
1211 if ( $rollbackErrors ) {
1212 $this->fail(
1213 "Rollback failed:\n" .
1214 print_r( $rollbackErrors, true ) . ";\n" .
1215 print_r( $resultDetails, true )
1216 );
1217 }
1218
1219 $page = new WikiPage( $page->getTitle() );
1220 $this->assertEquals( $rev2->getSha1(), $page->getRevision()->getSha1(),
1221 "rollback did not revert to the correct revision" );
1222 $this->assertEquals( "one\n\ntwo", $page->getContent()->getNativeData() );
1223
1224 $rc = MediaWikiServices::getInstance()->getRevisionStore()->getRecentChange(
1225 $page->getRevision()->getRevisionRecord()
1226 );
1227
1228 $this->assertNotNull( $rc, 'RecentChanges entry' );
1229 $this->assertEquals(
1230 RecentChange::PRC_AUTOPATROLLED,
1231 $rc->getAttribute( 'rc_patrolled' ),
1232 'rc_patrolled'
1233 );
1234
1235 // TODO: MCR: assert origin once we write slot data
1236 // $mainSlot = $page->getRevision()->getRevisionRecord()->getSlot( SlotRecord::MAIN );
1237 // $this->assertTrue( $mainSlot->isInherited(), 'isInherited' );
1238 // $this->assertSame( $rev2->getId(), $mainSlot->getOrigin(), 'getOrigin' );
1239 }
1240
1241 /**
1242 * @covers WikiPage::doRollback
1243 * @covers WikiPage::commitRollback
1244 */
1245 public function testDoRollbackFailureSameContent() {
1246 $admin = $this->getTestSysop()->getUser();
1247
1248 $text = "one";
1249 $page = $this->newPage( __METHOD__ );
1250 $page->doEditContent(
1251 ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
1252 "section one",
1253 EDIT_NEW,
1254 false,
1255 $admin
1256 );
1257 $rev1 = $page->getRevision();
1258
1259 $user1 = $this->getTestUser( [ 'sysop' ] )->getUser();
1260 $text .= "\n\ntwo";
1261 $page = new WikiPage( $page->getTitle() );
1262 $page->doEditContent(
1263 ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
1264 "adding section two",
1265 0,
1266 false,
1267 $user1
1268 );
1269
1270 # now, do a the rollback from the same user was doing the edit before
1271 $resultDetails = [];
1272 $token = $user1->getEditToken( 'rollback' );
1273 $errors = $page->doRollback(
1274 $user1->getName(),
1275 "testing revert same user",
1276 $token,
1277 false,
1278 $resultDetails,
1279 $admin
1280 );
1281
1282 $this->assertEquals( [], $errors, "Rollback failed same user" );
1283
1284 # now, try the rollback
1285 $resultDetails = [];
1286 $token = $admin->getEditToken( 'rollback' );
1287 $errors = $page->doRollback(
1288 $user1->getName(),
1289 "testing revert",
1290 $token,
1291 false,
1292 $resultDetails,
1293 $admin
1294 );
1295
1296 $this->assertEquals(
1297 [
1298 [
1299 'alreadyrolled',
1300 __METHOD__,
1301 $user1->getName(),
1302 $admin->getName(),
1303 ],
1304 ],
1305 $errors,
1306 "Rollback not failed"
1307 );
1308
1309 $page = new WikiPage( $page->getTitle() );
1310 $this->assertEquals( $rev1->getSha1(), $page->getRevision()->getSha1(),
1311 "rollback did not revert to the correct revision" );
1312 $this->assertEquals( "one", $page->getContent()->getNativeData() );
1313 }
1314
1315 /**
1316 * Tests tagging for edits that do rollback action
1317 * @covers WikiPage::doRollback
1318 */
1319 public function testDoRollbackTagging() {
1320 if ( !in_array( 'mw-rollback', ChangeTags::getSoftwareTags() ) ) {
1321 $this->markTestSkipped( 'Rollback tag deactivated, skipped the test.' );
1322 }
1323
1324 $admin = new User();
1325 $admin->setName( 'Administrator' );
1326 $admin->addToDatabase();
1327
1328 $text = 'First line';
1329 $page = $this->newPage( 'WikiPageTest_testDoRollbackTagging' );
1330 $page->doEditContent(
1331 ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
1332 'Added first line',
1333 EDIT_NEW,
1334 false,
1335 $admin
1336 );
1337
1338 $secondUser = new User();
1339 $secondUser->setName( '92.65.217.32' );
1340 $text .= '\n\nSecond line';
1341 $page = new WikiPage( $page->getTitle() );
1342 $page->doEditContent(
1343 ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
1344 'Adding second line',
1345 0,
1346 false,
1347 $secondUser
1348 );
1349
1350 // Now, try the rollback
1351 $admin->addGroup( 'sysop' ); // Make the test user a sysop
1352 $token = $admin->getEditToken( 'rollback' );
1353 $errors = $page->doRollback(
1354 $secondUser->getName(),
1355 'testing rollback',
1356 $token,
1357 false,
1358 $resultDetails,
1359 $admin
1360 );
1361
1362 // If doRollback completed without errors
1363 if ( $errors === [] ) {
1364 $tags = $resultDetails[ 'tags' ];
1365 $this->assertContains( 'mw-rollback', $tags );
1366 }
1367 }
1368
1369 public function provideGetAutoDeleteReason() {
1370 return [
1371 [
1372 [],
1373 false,
1374 false
1375 ],
1376
1377 [
1378 [
1379 [ "first edit", null ],
1380 ],
1381 "/first edit.*only contributor/",
1382 false
1383 ],
1384
1385 [
1386 [
1387 [ "first edit", null ],
1388 [ "second edit", null ],
1389 ],
1390 "/second edit.*only contributor/",
1391 true
1392 ],
1393
1394 [
1395 [
1396 [ "first edit", "127.0.2.22" ],
1397 [ "second edit", "127.0.3.33" ],
1398 ],
1399 "/second edit/",
1400 true
1401 ],
1402
1403 [
1404 [
1405 [
1406 "first edit: "
1407 . "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam "
1408 . " nonumy eirmod tempor invidunt ut labore et dolore magna "
1409 . "aliquyam erat, sed diam voluptua. At vero eos et accusam "
1410 . "et justo duo dolores et ea rebum. Stet clita kasd gubergren, "
1411 . "no sea takimata sanctus est Lorem ipsum dolor sit amet.'",
1412 null
1413 ],
1414 ],
1415 '/first edit:.*\.\.\."/',
1416 false
1417 ],
1418
1419 [
1420 [
1421 [ "first edit", "127.0.2.22" ],
1422 [ "", "127.0.3.33" ],
1423 ],
1424 "/before blanking.*first edit/",
1425 true
1426 ],
1427
1428 ];
1429 }
1430
1431 /**
1432 * @dataProvider provideGetAutoDeleteReason
1433 * @covers WikiPage::getAutoDeleteReason
1434 */
1435 public function testGetAutoDeleteReason( $edits, $expectedResult, $expectedHistory ) {
1436 global $wgUser;
1437
1438 // NOTE: assume Help namespace to contain wikitext
1439 $page = $this->newPage( "Help:WikiPageTest_testGetAutoDeleteReason" );
1440
1441 $c = 1;
1442
1443 foreach ( $edits as $edit ) {
1444 $user = new User();
1445
1446 if ( !empty( $edit[1] ) ) {
1447 $user->setName( $edit[1] );
1448 } else {
1449 $user = $wgUser;
1450 }
1451
1452 $content = ContentHandler::makeContent( $edit[0], $page->getTitle(), $page->getContentModel() );
1453
1454 $page->doEditContent( $content, "test edit $c", $c < 2 ? EDIT_NEW : 0, false, $user );
1455
1456 $c += 1;
1457 }
1458
1459 $reason = $page->getAutoDeleteReason( $hasHistory );
1460
1461 if ( is_bool( $expectedResult ) || is_null( $expectedResult ) ) {
1462 $this->assertEquals( $expectedResult, $reason );
1463 } else {
1464 $this->assertTrue( (bool)preg_match( $expectedResult, $reason ),
1465 "Autosummary didn't match expected pattern $expectedResult: $reason" );
1466 }
1467
1468 $this->assertEquals( $expectedHistory, $hasHistory,
1469 "expected \$hasHistory to be " . var_export( $expectedHistory, true ) );
1470
1471 $page->doDeleteArticle( "done" );
1472 }
1473
1474 public function providePreSaveTransform() {
1475 return [
1476 [ 'hello this is ~~~',
1477 "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]",
1478 ],
1479 [ 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
1480 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
1481 ],
1482 ];
1483 }
1484
1485 /**
1486 * @covers WikiPage::factory
1487 */
1488 public function testWikiPageFactory() {
1489 $title = Title::makeTitle( NS_FILE, 'Someimage.png' );
1490 $page = WikiPage::factory( $title );
1491 $this->assertEquals( WikiFilePage::class, get_class( $page ) );
1492
1493 $title = Title::makeTitle( NS_CATEGORY, 'SomeCategory' );
1494 $page = WikiPage::factory( $title );
1495 $this->assertEquals( WikiCategoryPage::class, get_class( $page ) );
1496
1497 $title = Title::makeTitle( NS_MAIN, 'SomePage' );
1498 $page = WikiPage::factory( $title );
1499 $this->assertEquals( WikiPage::class, get_class( $page ) );
1500 }
1501
1502 /**
1503 * @covers WikiPage::loadPageData
1504 * @covers WikiPage::wasLoadedFrom
1505 */
1506 public function testLoadPageData() {
1507 $title = Title::makeTitle( NS_MAIN, 'SomePage' );
1508 $page = WikiPage::factory( $title );
1509
1510 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
1511 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
1512 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
1513 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
1514
1515 $page->loadPageData( IDBAccessObject::READ_NORMAL );
1516 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
1517 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
1518 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
1519 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
1520
1521 $page->loadPageData( IDBAccessObject::READ_LATEST );
1522 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
1523 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
1524 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
1525 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
1526
1527 $page->loadPageData( IDBAccessObject::READ_LOCKING );
1528 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
1529 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
1530 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
1531 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
1532
1533 $page->loadPageData( IDBAccessObject::READ_EXCLUSIVE );
1534 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
1535 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
1536 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
1537 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
1538 }
1539
1540 /**
1541 * @dataProvider provideCommentMigrationOnDeletion
1542 *
1543 * @param int $writeStage
1544 * @param int $readStage
1545 */
1546 public function testCommentMigrationOnDeletion( $writeStage, $readStage ) {
1547 $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', $writeStage );
1548 $this->overrideMwServices();
1549
1550 $dbr = wfGetDB( DB_REPLICA );
1551
1552 $page = $this->createPage(
1553 __METHOD__,
1554 "foo",
1555 CONTENT_MODEL_WIKITEXT
1556 );
1557 $revid = $page->getLatest();
1558 if ( $writeStage > MIGRATION_OLD ) {
1559 $comment_id = $dbr->selectField(
1560 'revision_comment_temp',
1561 'revcomment_comment_id',
1562 [ 'revcomment_rev' => $revid ],
1563 __METHOD__
1564 );
1565 }
1566
1567 $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', $readStage );
1568 $this->overrideMwServices();
1569
1570 $page->doDeleteArticle( "testing deletion" );
1571
1572 if ( $readStage > MIGRATION_OLD ) {
1573 // Didn't leave behind any 'revision_comment_temp' rows
1574 $n = $dbr->selectField(
1575 'revision_comment_temp', 'COUNT(*)', [ 'revcomment_rev' => $revid ], __METHOD__
1576 );
1577 $this->assertEquals( 0, $n, 'no entry in revision_comment_temp after deletion' );
1578
1579 // Copied or upgraded the comment_id, as applicable
1580 $ar_comment_id = $dbr->selectField(
1581 'archive',
1582 'ar_comment_id',
1583 [ 'ar_rev_id' => $revid ],
1584 __METHOD__
1585 );
1586 if ( $writeStage > MIGRATION_OLD ) {
1587 $this->assertSame( $comment_id, $ar_comment_id );
1588 } else {
1589 $this->assertNotEquals( 0, $ar_comment_id );
1590 }
1591 }
1592
1593 // Copied rev_comment, if applicable
1594 if ( $readStage <= MIGRATION_WRITE_BOTH && $writeStage <= MIGRATION_WRITE_BOTH ) {
1595 $ar_comment = $dbr->selectField(
1596 'archive',
1597 'ar_comment',
1598 [ 'ar_rev_id' => $revid ],
1599 __METHOD__
1600 );
1601 $this->assertSame( 'testing', $ar_comment );
1602 }
1603 }
1604
1605 public function provideCommentMigrationOnDeletion() {
1606 return [
1607 [ MIGRATION_OLD, MIGRATION_OLD ],
1608 [ MIGRATION_OLD, MIGRATION_WRITE_BOTH ],
1609 [ MIGRATION_OLD, MIGRATION_WRITE_NEW ],
1610 [ MIGRATION_WRITE_BOTH, MIGRATION_OLD ],
1611 [ MIGRATION_WRITE_BOTH, MIGRATION_WRITE_BOTH ],
1612 [ MIGRATION_WRITE_BOTH, MIGRATION_WRITE_NEW ],
1613 [ MIGRATION_WRITE_BOTH, MIGRATION_NEW ],
1614 [ MIGRATION_WRITE_NEW, MIGRATION_WRITE_BOTH ],
1615 [ MIGRATION_WRITE_NEW, MIGRATION_WRITE_NEW ],
1616 [ MIGRATION_WRITE_NEW, MIGRATION_NEW ],
1617 [ MIGRATION_NEW, MIGRATION_WRITE_BOTH ],
1618 [ MIGRATION_NEW, MIGRATION_WRITE_NEW ],
1619 [ MIGRATION_NEW, MIGRATION_NEW ],
1620 ];
1621 }
1622
1623 /**
1624 * @covers WikiPage::updateCategoryCounts
1625 */
1626 public function testUpdateCategoryCounts() {
1627 $page = new WikiPage( Title::newFromText( __METHOD__ ) );
1628
1629 // Add an initial category
1630 $page->updateCategoryCounts( [ 'A' ], [], 0 );
1631
1632 $this->assertEquals( 1, Category::newFromName( 'A' )->getPageCount() );
1633 $this->assertEquals( 0, Category::newFromName( 'B' )->getPageCount() );
1634 $this->assertEquals( 0, Category::newFromName( 'C' )->getPageCount() );
1635
1636 // Add a new category
1637 $page->updateCategoryCounts( [ 'B' ], [], 0 );
1638
1639 $this->assertEquals( 1, Category::newFromName( 'A' )->getPageCount() );
1640 $this->assertEquals( 1, Category::newFromName( 'B' )->getPageCount() );
1641 $this->assertEquals( 0, Category::newFromName( 'C' )->getPageCount() );
1642
1643 // Add and remove a category
1644 $page->updateCategoryCounts( [ 'C' ], [ 'A' ], 0 );
1645
1646 $this->assertEquals( 0, Category::newFromName( 'A' )->getPageCount() );
1647 $this->assertEquals( 1, Category::newFromName( 'B' )->getPageCount() );
1648 $this->assertEquals( 1, Category::newFromName( 'C' )->getPageCount() );
1649 }
1650
1651 public function provideUpdateRedirectOn() {
1652 yield [ '#REDIRECT [[Foo]]', true, null, true, true, 0 ];
1653 yield [ '#REDIRECT [[Foo]]', true, 'Foo', true, false, 1 ];
1654 yield [ 'SomeText', false, null, false, true, 0 ];
1655 yield [ 'SomeText', false, 'Foo', false, false, 1 ];
1656 }
1657
1658 /**
1659 * @dataProvider provideUpdateRedirectOn
1660 * @covers WikiPage::updateRedirectOn
1661 *
1662 * @param string $initialText
1663 * @param bool $initialRedirectState
1664 * @param string|null $redirectTitle
1665 * @param bool|null $lastRevIsRedirect
1666 * @param bool $expectedSuccess
1667 * @param int $expectedRowCount
1668 */
1669 public function testUpdateRedirectOn(
1670 $initialText,
1671 $initialRedirectState,
1672 $redirectTitle,
1673 $lastRevIsRedirect,
1674 $expectedSuccess,
1675 $expectedRowCount
1676 ) {
1677 // FIXME: fails under sqlite and postgres
1678 $this->markTestSkippedIfDbType( 'sqlite' );
1679 $this->markTestSkippedIfDbType( 'postgres' );
1680 static $pageCounter = 0;
1681 $pageCounter++;
1682
1683 $page = $this->createPage( Title::newFromText( __METHOD__ . $pageCounter ), $initialText );
1684 $this->assertSame( $initialRedirectState, $page->isRedirect() );
1685
1686 $redirectTitle = is_string( $redirectTitle )
1687 ? Title::newFromText( $redirectTitle )
1688 : $redirectTitle;
1689
1690 $success = $page->updateRedirectOn( $this->db, $redirectTitle, $lastRevIsRedirect );
1691 $this->assertSame( $expectedSuccess, $success, 'Success assertion' );
1692 /**
1693 * updateRedirectOn explicitly updates the redirect table (and not the page table).
1694 * Most of core checks the page table for redirect status, so we have to be ugly and
1695 * assert a select from the table here.
1696 */
1697 $this->assertRedirectTableCountForPageId( $page->getId(), $expectedRowCount );
1698 }
1699
1700 private function assertRedirectTableCountForPageId( $pageId, $expected ) {
1701 $this->assertSelect(
1702 'redirect',
1703 'COUNT(*)',
1704 [ 'rd_from' => $pageId ],
1705 [ [ strval( $expected ) ] ]
1706 );
1707 }
1708
1709 /**
1710 * @covers WikiPage::insertRedirectEntry
1711 */
1712 public function testInsertRedirectEntry_insertsRedirectEntry() {
1713 $page = $this->createPage( Title::newFromText( __METHOD__ ), 'A' );
1714 $this->assertRedirectTableCountForPageId( $page->getId(), 0 );
1715
1716 $targetTitle = Title::newFromText( 'SomeTarget#Frag' );
1717 $targetTitle->mInterwiki = 'eninter';
1718 $page->insertRedirectEntry( $targetTitle, null );
1719
1720 $this->assertSelect(
1721 'redirect',
1722 [ 'rd_from', 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
1723 [ 'rd_from' => $page->getId() ],
1724 [ [
1725 strval( $page->getId() ),
1726 strval( $targetTitle->getNamespace() ),
1727 strval( $targetTitle->getDBkey() ),
1728 strval( $targetTitle->getFragment() ),
1729 strval( $targetTitle->getInterwiki() ),
1730 ] ]
1731 );
1732 }
1733
1734 /**
1735 * @covers WikiPage::insertRedirectEntry
1736 */
1737 public function testInsertRedirectEntry_insertsRedirectEntryWithPageLatest() {
1738 $page = $this->createPage( Title::newFromText( __METHOD__ ), 'A' );
1739 $this->assertRedirectTableCountForPageId( $page->getId(), 0 );
1740
1741 $targetTitle = Title::newFromText( 'SomeTarget#Frag' );
1742 $targetTitle->mInterwiki = 'eninter';
1743 $page->insertRedirectEntry( $targetTitle, $page->getLatest() );
1744
1745 $this->assertSelect(
1746 'redirect',
1747 [ 'rd_from', 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
1748 [ 'rd_from' => $page->getId() ],
1749 [ [
1750 strval( $page->getId() ),
1751 strval( $targetTitle->getNamespace() ),
1752 strval( $targetTitle->getDBkey() ),
1753 strval( $targetTitle->getFragment() ),
1754 strval( $targetTitle->getInterwiki() ),
1755 ] ]
1756 );
1757 }
1758
1759 /**
1760 * @covers WikiPage::insertRedirectEntry
1761 */
1762 public function testInsertRedirectEntry_doesNotInsertIfPageLatestIncorrect() {
1763 $page = $this->createPage( Title::newFromText( __METHOD__ ), 'A' );
1764 $this->assertRedirectTableCountForPageId( $page->getId(), 0 );
1765
1766 $targetTitle = Title::newFromText( 'SomeTarget#Frag' );
1767 $targetTitle->mInterwiki = 'eninter';
1768 $page->insertRedirectEntry( $targetTitle, 215251 );
1769
1770 $this->assertRedirectTableCountForPageId( $page->getId(), 0 );
1771 }
1772
1773 private function getRow( array $overrides = [] ) {
1774 $row = [
1775 'page_id' => '44',
1776 'page_len' => '76',
1777 'page_is_redirect' => '1',
1778 'page_latest' => '99',
1779 'page_namespace' => '3',
1780 'page_title' => 'JaJaTitle',
1781 'page_restrictions' => 'edit=autoconfirmed,sysop:move=sysop',
1782 'page_touched' => '20120101020202',
1783 'page_links_updated' => '20140101020202',
1784 ];
1785 foreach ( $overrides as $key => $value ) {
1786 $row[$key] = $value;
1787 }
1788 return (object)$row;
1789 }
1790
1791 public function provideNewFromRowSuccess() {
1792 yield 'basic row' => [
1793 $this->getRow(),
1794 function ( WikiPage $wikiPage, self $test ) {
1795 $test->assertSame( 44, $wikiPage->getId() );
1796 $test->assertSame( 76, $wikiPage->getTitle()->getLength() );
1797 $test->assertTrue( $wikiPage->isRedirect() );
1798 $test->assertSame( 99, $wikiPage->getLatest() );
1799 $test->assertSame( 3, $wikiPage->getTitle()->getNamespace() );
1800 $test->assertSame( 'JaJaTitle', $wikiPage->getTitle()->getDBkey() );
1801 $test->assertSame(
1802 [
1803 'edit' => [ 'autoconfirmed', 'sysop' ],
1804 'move' => [ 'sysop' ],
1805 ],
1806 $wikiPage->getTitle()->getAllRestrictions()
1807 );
1808 $test->assertSame( '20120101020202', $wikiPage->getTouched() );
1809 $test->assertSame( '20140101020202', $wikiPage->getLinksTimestamp() );
1810 }
1811 ];
1812 yield 'different timestamp formats' => [
1813 $this->getRow( [
1814 'page_touched' => '2012-01-01 02:02:02',
1815 'page_links_updated' => '2014-01-01 02:02:02',
1816 ] ),
1817 function ( WikiPage $wikiPage, self $test ) {
1818 $test->assertSame( '20120101020202', $wikiPage->getTouched() );
1819 $test->assertSame( '20140101020202', $wikiPage->getLinksTimestamp() );
1820 }
1821 ];
1822 yield 'no restrictions' => [
1823 $this->getRow( [
1824 'page_restrictions' => '',
1825 ] ),
1826 function ( WikiPage $wikiPage, self $test ) {
1827 $test->assertSame(
1828 [
1829 'edit' => [],
1830 'move' => [],
1831 ],
1832 $wikiPage->getTitle()->getAllRestrictions()
1833 );
1834 }
1835 ];
1836 yield 'not redirect' => [
1837 $this->getRow( [
1838 'page_is_redirect' => '0',
1839 ] ),
1840 function ( WikiPage $wikiPage, self $test ) {
1841 $test->assertFalse( $wikiPage->isRedirect() );
1842 }
1843 ];
1844 }
1845
1846 /**
1847 * @covers WikiPage::newFromRow
1848 * @covers WikiPage::loadFromRow
1849 * @dataProvider provideNewFromRowSuccess
1850 *
1851 * @param object $row
1852 * @param callable $assertions
1853 */
1854 public function testNewFromRow( $row, $assertions ) {
1855 $page = WikiPage::newFromRow( $row, 'fromdb' );
1856 $assertions( $page, $this );
1857 }
1858
1859 public function provideTestNewFromId_returnsNullOnBadPageId() {
1860 yield[ 0 ];
1861 yield[ -11 ];
1862 }
1863
1864 /**
1865 * @covers WikiPage::newFromID
1866 * @dataProvider provideTestNewFromId_returnsNullOnBadPageId
1867 */
1868 public function testNewFromId_returnsNullOnBadPageId( $pageId ) {
1869 $this->assertNull( WikiPage::newFromID( $pageId ) );
1870 }
1871
1872 /**
1873 * @covers WikiPage::newFromID
1874 */
1875 public function testNewFromId_appearsToFetchCorrectRow() {
1876 $createdPage = $this->createPage( __METHOD__, 'Xsfaij09' );
1877 $fetchedPage = WikiPage::newFromID( $createdPage->getId() );
1878 $this->assertSame( $createdPage->getId(), $fetchedPage->getId() );
1879 $this->assertEquals(
1880 $createdPage->getContent()->getNativeData(),
1881 $fetchedPage->getContent()->getNativeData()
1882 );
1883 }
1884
1885 /**
1886 * @covers WikiPage::newFromID
1887 */
1888 public function testNewFromId_returnsNullOnNonExistingId() {
1889 $this->assertNull( WikiPage::newFromID( 2147483647 ) );
1890 }
1891
1892 public function provideTestInsertProtectNullRevision() {
1893 // phpcs:disable Generic.Files.LineLength
1894 yield [
1895 'goat-message-key',
1896 [ 'edit' => 'sysop' ],
1897 [ 'edit' => '20200101040404' ],
1898 false,
1899 'Goat Reason',
1900 true,
1901 '(goat-message-key: WikiPageDbTestBase::testInsertProtectNullRevision, UTSysop)(colon-separator)Goat Reason(word-separator)(parentheses: (protect-summary-desc: (restriction-edit), (protect-level-sysop), (protect-expiring: 04:04, 1 (january) 2020, 1 (january) 2020, 04:04)))'
1902 ];
1903 yield [
1904 'goat-key',
1905 [ 'edit' => 'sysop', 'move' => 'something' ],
1906 [ 'edit' => '20200101040404', 'move' => '20210101050505' ],
1907 false,
1908 'Goat Goat',
1909 true,
1910 '(goat-key: WikiPageDbTestBase::testInsertProtectNullRevision, UTSysop)(colon-separator)Goat Goat(word-separator)(parentheses: (protect-summary-desc: (restriction-edit), (protect-level-sysop), (protect-expiring: 04:04, 1 (january) 2020, 1 (january) 2020, 04:04))(word-separator)(protect-summary-desc: (restriction-move), (protect-level-something), (protect-expiring: 05:05, 1 (january) 2021, 1 (january) 2021, 05:05)))'
1911 ];
1912 // phpcs:enable
1913 }
1914
1915 /**
1916 * @dataProvider provideTestInsertProtectNullRevision
1917 * @covers WikiPage::insertProtectNullRevision
1918 * @covers WikiPage::protectDescription
1919 *
1920 * @param string $revCommentMsg
1921 * @param array $limit
1922 * @param array $expiry
1923 * @param bool $cascade
1924 * @param string $reason
1925 * @param bool|null $user true if the test sysop should be used, or null
1926 * @param string $expectedComment
1927 */
1928 public function testInsertProtectNullRevision(
1929 $revCommentMsg,
1930 array $limit,
1931 array $expiry,
1932 $cascade,
1933 $reason,
1934 $user,
1935 $expectedComment
1936 ) {
1937 $this->setContentLang( 'qqx' );
1938
1939 $page = $this->createPage( __METHOD__, 'Goat' );
1940
1941 $user = $user === null ? $user : $this->getTestSysop()->getUser();
1942
1943 $result = $page->insertProtectNullRevision(
1944 $revCommentMsg,
1945 $limit,
1946 $expiry,
1947 $cascade,
1948 $reason,
1949 $user
1950 );
1951
1952 $this->assertTrue( $result instanceof Revision );
1953 $this->assertSame( $expectedComment, $result->getComment( Revision::RAW ) );
1954 }
1955
1956 /**
1957 * @covers WikiPage::updateRevisionOn
1958 */
1959 public function testUpdateRevisionOn_existingPage() {
1960 $user = $this->getTestSysop()->getUser();
1961 $page = $this->createPage( __METHOD__, 'StartText' );
1962
1963 $revision = new Revision(
1964 [
1965 'id' => 9989,
1966 'page' => $page->getId(),
1967 'title' => $page->getTitle(),
1968 'comment' => __METHOD__,
1969 'minor_edit' => true,
1970 'text' => __METHOD__ . '-text',
1971 'len' => strlen( __METHOD__ . '-text' ),
1972 'user' => $user->getId(),
1973 'user_text' => $user->getName(),
1974 'timestamp' => '20170707040404',
1975 'content_model' => CONTENT_MODEL_WIKITEXT,
1976 'content_format' => CONTENT_FORMAT_WIKITEXT,
1977 ]
1978 );
1979
1980 $result = $page->updateRevisionOn( $this->db, $revision );
1981 $this->assertTrue( $result );
1982 $this->assertSame( 9989, $page->getLatest() );
1983 $this->assertEquals( $revision, $page->getRevision() );
1984 }
1985
1986 /**
1987 * @covers WikiPage::updateRevisionOn
1988 */
1989 public function testUpdateRevisionOn_NonExistingPage() {
1990 $user = $this->getTestSysop()->getUser();
1991 $page = $this->createPage( __METHOD__, 'StartText' );
1992 $page->doDeleteArticle( 'reason' );
1993
1994 $revision = new Revision(
1995 [
1996 'id' => 9989,
1997 'page' => $page->getId(),
1998 'title' => $page->getTitle(),
1999 'comment' => __METHOD__,
2000 'minor_edit' => true,
2001 'text' => __METHOD__ . '-text',
2002 'len' => strlen( __METHOD__ . '-text' ),
2003 'user' => $user->getId(),
2004 'user_text' => $user->getName(),
2005 'timestamp' => '20170707040404',
2006 'content_model' => CONTENT_MODEL_WIKITEXT,
2007 'content_format' => CONTENT_FORMAT_WIKITEXT,
2008 ]
2009 );
2010
2011 $result = $page->updateRevisionOn( $this->db, $revision );
2012 $this->assertFalse( $result );
2013 }
2014
2015 /**
2016 * @covers WikiPage::updateIfNewerOn
2017 */
2018 public function testUpdateIfNewerOn_olderRevision() {
2019 $user = $this->getTestSysop()->getUser();
2020 $page = $this->createPage( __METHOD__, 'StartText' );
2021 $initialRevision = $page->getRevision();
2022
2023 $olderTimeStamp = wfTimestamp(
2024 TS_MW,
2025 wfTimestamp( TS_UNIX, $initialRevision->getTimestamp() ) - 1
2026 );
2027
2028 $olderRevison = new Revision(
2029 [
2030 'id' => 9989,
2031 'page' => $page->getId(),
2032 'title' => $page->getTitle(),
2033 'comment' => __METHOD__,
2034 'minor_edit' => true,
2035 'text' => __METHOD__ . '-text',
2036 'len' => strlen( __METHOD__ . '-text' ),
2037 'user' => $user->getId(),
2038 'user_text' => $user->getName(),
2039 'timestamp' => $olderTimeStamp,
2040 'content_model' => CONTENT_MODEL_WIKITEXT,
2041 'content_format' => CONTENT_FORMAT_WIKITEXT,
2042 ]
2043 );
2044
2045 $result = $page->updateIfNewerOn( $this->db, $olderRevison );
2046 $this->assertFalse( $result );
2047 }
2048
2049 /**
2050 * @covers WikiPage::updateIfNewerOn
2051 */
2052 public function testUpdateIfNewerOn_newerRevision() {
2053 $user = $this->getTestSysop()->getUser();
2054 $page = $this->createPage( __METHOD__, 'StartText' );
2055 $initialRevision = $page->getRevision();
2056
2057 $newerTimeStamp = wfTimestamp(
2058 TS_MW,
2059 wfTimestamp( TS_UNIX, $initialRevision->getTimestamp() ) + 1
2060 );
2061
2062 $newerRevision = new Revision(
2063 [
2064 'id' => 9989,
2065 'page' => $page->getId(),
2066 'title' => $page->getTitle(),
2067 'comment' => __METHOD__,
2068 'minor_edit' => true,
2069 'text' => __METHOD__ . '-text',
2070 'len' => strlen( __METHOD__ . '-text' ),
2071 'user' => $user->getId(),
2072 'user_text' => $user->getName(),
2073 'timestamp' => $newerTimeStamp,
2074 'content_model' => CONTENT_MODEL_WIKITEXT,
2075 'content_format' => CONTENT_FORMAT_WIKITEXT,
2076 ]
2077 );
2078 $result = $page->updateIfNewerOn( $this->db, $newerRevision );
2079 $this->assertTrue( $result );
2080 }
2081
2082 /**
2083 * @covers WikiPage::insertOn
2084 */
2085 public function testInsertOn() {
2086 $title = Title::newFromText( __METHOD__ );
2087 $page = new WikiPage( $title );
2088
2089 $startTimeStamp = wfTimestampNow();
2090 $result = $page->insertOn( $this->db );
2091 $endTimeStamp = wfTimestampNow();
2092
2093 $this->assertInternalType( 'int', $result );
2094 $this->assertTrue( $result > 0 );
2095
2096 $condition = [ 'page_id' => $result ];
2097
2098 // Check the default fields have been filled
2099 $this->assertSelect(
2100 'page',
2101 [
2102 'page_namespace',
2103 'page_title',
2104 'page_restrictions',
2105 'page_is_redirect',
2106 'page_is_new',
2107 'page_latest',
2108 'page_len',
2109 ],
2110 $condition,
2111 [ [
2112 '0',
2113 __METHOD__,
2114 '',
2115 '0',
2116 '1',
2117 '0',
2118 '0',
2119 ] ]
2120 );
2121
2122 // Check the page_random field has been filled
2123 $pageRandom = $this->db->selectField( 'page', 'page_random', $condition );
2124 $this->assertTrue( (float)$pageRandom < 1 && (float)$pageRandom > 0 );
2125
2126 // Assert the touched timestamp in the DB is roughly when we inserted the page
2127 $pageTouched = $this->db->selectField( 'page', 'page_touched', $condition );
2128 $this->assertTrue(
2129 wfTimestamp( TS_UNIX, $startTimeStamp )
2130 <= wfTimestamp( TS_UNIX, $pageTouched )
2131 );
2132 $this->assertTrue(
2133 wfTimestamp( TS_UNIX, $endTimeStamp )
2134 >= wfTimestamp( TS_UNIX, $pageTouched )
2135 );
2136
2137 // Try inserting the same page again and checking the result is false (no change)
2138 $result = $page->insertOn( $this->db );
2139 $this->assertFalse( $result );
2140 }
2141
2142 /**
2143 * @covers WikiPage::insertOn
2144 */
2145 public function testInsertOn_idSpecified() {
2146 $title = Title::newFromText( __METHOD__ );
2147 $page = new WikiPage( $title );
2148 $id = 1478952189;
2149
2150 $result = $page->insertOn( $this->db, $id );
2151
2152 $this->assertSame( $id, $result );
2153
2154 $condition = [ 'page_id' => $result ];
2155
2156 // Check there is actually a row in the db
2157 $this->assertSelect(
2158 'page',
2159 [ 'page_title' ],
2160 $condition,
2161 [ [ __METHOD__ ] ]
2162 );
2163 }
2164
2165 public function provideTestDoUpdateRestrictions_setBasicRestrictions() {
2166 // Note: Once the current dates passes the date in these tests they will fail.
2167 yield 'move something' => [
2168 true,
2169 [ 'move' => 'something' ],
2170 [],
2171 [ 'edit' => [], 'move' => [ 'something' ] ],
2172 [],
2173 ];
2174 yield 'move something, edit blank' => [
2175 true,
2176 [ 'move' => 'something', 'edit' => '' ],
2177 [],
2178 [ 'edit' => [], 'move' => [ 'something' ] ],
2179 [],
2180 ];
2181 yield 'edit sysop, with expiry' => [
2182 true,
2183 [ 'edit' => 'sysop' ],
2184 [ 'edit' => '21330101020202' ],
2185 [ 'edit' => [ 'sysop' ], 'move' => [] ],
2186 [ 'edit' => '21330101020202' ],
2187 ];
2188 yield 'move and edit, move with expiry' => [
2189 true,
2190 [ 'move' => 'something', 'edit' => 'another' ],
2191 [ 'move' => '22220202010101' ],
2192 [ 'edit' => [ 'another' ], 'move' => [ 'something' ] ],
2193 [ 'move' => '22220202010101' ],
2194 ];
2195 yield 'move and edit, edit with infinity expiry' => [
2196 true,
2197 [ 'move' => 'something', 'edit' => 'another' ],
2198 [ 'edit' => 'infinity' ],
2199 [ 'edit' => [ 'another' ], 'move' => [ 'something' ] ],
2200 [ 'edit' => 'infinity' ],
2201 ];
2202 yield 'non existing, create something' => [
2203 false,
2204 [ 'create' => 'something' ],
2205 [],
2206 [ 'create' => [ 'something' ] ],
2207 [],
2208 ];
2209 yield 'non existing, create something with expiry' => [
2210 false,
2211 [ 'create' => 'something' ],
2212 [ 'create' => '23451212112233' ],
2213 [ 'create' => [ 'something' ] ],
2214 [ 'create' => '23451212112233' ],
2215 ];
2216 }
2217
2218 /**
2219 * @dataProvider provideTestDoUpdateRestrictions_setBasicRestrictions
2220 * @covers WikiPage::doUpdateRestrictions
2221 */
2222 public function testDoUpdateRestrictions_setBasicRestrictions(
2223 $pageExists,
2224 array $limit,
2225 array $expiry,
2226 array $expectedRestrictions,
2227 array $expectedRestrictionExpiries
2228 ) {
2229 if ( $pageExists ) {
2230 $page = $this->createPage( __METHOD__, 'ABC' );
2231 } else {
2232 $page = new WikiPage( Title::newFromText( __METHOD__ . '-nonexist' ) );
2233 }
2234 $user = $this->getTestSysop()->getUser();
2235 $cascade = false;
2236
2237 $status = $page->doUpdateRestrictions( $limit, $expiry, $cascade, 'aReason', $user, [] );
2238
2239 $logId = $status->getValue();
2240 $allRestrictions = $page->getTitle()->getAllRestrictions();
2241
2242 $this->assertTrue( $status->isGood() );
2243 $this->assertInternalType( 'int', $logId );
2244 $this->assertSame( $expectedRestrictions, $allRestrictions );
2245 foreach ( $expectedRestrictionExpiries as $key => $value ) {
2246 $this->assertSame( $value, $page->getTitle()->getRestrictionExpiry( $key ) );
2247 }
2248
2249 // Make sure the log entry looks good
2250 // log_params is not checked here
2251 $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
2252 $this->assertSelect(
2253 [ 'logging' ] + $actorQuery['tables'],
2254 [
2255 'log_comment',
2256 'log_user' => $actorQuery['fields']['log_user'],
2257 'log_user_text' => $actorQuery['fields']['log_user_text'],
2258 'log_namespace',
2259 'log_title',
2260 ],
2261 [ 'log_id' => $logId ],
2262 [ [
2263 'aReason',
2264 (string)$user->getId(),
2265 $user->getName(),
2266 (string)$page->getTitle()->getNamespace(),
2267 $page->getTitle()->getDBkey(),
2268 ] ],
2269 [],
2270 $actorQuery['joins']
2271 );
2272 }
2273
2274 /**
2275 * @covers WikiPage::doUpdateRestrictions
2276 */
2277 public function testDoUpdateRestrictions_failsOnReadOnly() {
2278 $page = $this->createPage( __METHOD__, 'ABC' );
2279 $user = $this->getTestSysop()->getUser();
2280 $cascade = false;
2281
2282 // Set read only
2283 $readOnly = $this->getMockBuilder( ReadOnlyMode::class )
2284 ->disableOriginalConstructor()
2285 ->setMethods( [ 'isReadOnly', 'getReason' ] )
2286 ->getMock();
2287 $readOnly->expects( $this->once() )
2288 ->method( 'isReadOnly' )
2289 ->will( $this->returnValue( true ) );
2290 $readOnly->expects( $this->once() )
2291 ->method( 'getReason' )
2292 ->will( $this->returnValue( 'Some Read Only Reason' ) );
2293 $this->setService( 'ReadOnlyMode', $readOnly );
2294
2295 $status = $page->doUpdateRestrictions( [], [], $cascade, 'aReason', $user, [] );
2296 $this->assertFalse( $status->isOK() );
2297 $this->assertSame( 'readonlytext', $status->getMessage()->getKey() );
2298 }
2299
2300 /**
2301 * @covers WikiPage::doUpdateRestrictions
2302 */
2303 public function testDoUpdateRestrictions_returnsGoodIfNothingChanged() {
2304 $page = $this->createPage( __METHOD__, 'ABC' );
2305 $user = $this->getTestSysop()->getUser();
2306 $cascade = false;
2307 $limit = [ 'edit' => 'sysop' ];
2308
2309 $status = $page->doUpdateRestrictions(
2310 $limit,
2311 [],
2312 $cascade,
2313 'aReason',
2314 $user,
2315 []
2316 );
2317
2318 // The first entry should have a logId as it did something
2319 $this->assertTrue( $status->isGood() );
2320 $this->assertInternalType( 'int', $status->getValue() );
2321
2322 $status = $page->doUpdateRestrictions(
2323 $limit,
2324 [],
2325 $cascade,
2326 'aReason',
2327 $user,
2328 []
2329 );
2330
2331 // The second entry should not have a logId as nothing changed
2332 $this->assertTrue( $status->isGood() );
2333 $this->assertNull( $status->getValue() );
2334 }
2335
2336 /**
2337 * @covers WikiPage::doUpdateRestrictions
2338 */
2339 public function testDoUpdateRestrictions_logEntryTypeAndAction() {
2340 $page = $this->createPage( __METHOD__, 'ABC' );
2341 $user = $this->getTestSysop()->getUser();
2342 $cascade = false;
2343
2344 // Protect the page
2345 $status = $page->doUpdateRestrictions(
2346 [ 'edit' => 'sysop' ],
2347 [],
2348 $cascade,
2349 'aReason',
2350 $user,
2351 []
2352 );
2353 $this->assertTrue( $status->isGood() );
2354 $this->assertInternalType( 'int', $status->getValue() );
2355 $this->assertSelect(
2356 'logging',
2357 [ 'log_type', 'log_action' ],
2358 [ 'log_id' => $status->getValue() ],
2359 [ [ 'protect', 'protect' ] ]
2360 );
2361
2362 // Modify the protection
2363 $status = $page->doUpdateRestrictions(
2364 [ 'edit' => 'somethingElse' ],
2365 [],
2366 $cascade,
2367 'aReason',
2368 $user,
2369 []
2370 );
2371 $this->assertTrue( $status->isGood() );
2372 $this->assertInternalType( 'int', $status->getValue() );
2373 $this->assertSelect(
2374 'logging',
2375 [ 'log_type', 'log_action' ],
2376 [ 'log_id' => $status->getValue() ],
2377 [ [ 'protect', 'modify' ] ]
2378 );
2379
2380 // Remove the protection
2381 $status = $page->doUpdateRestrictions(
2382 [],
2383 [],
2384 $cascade,
2385 'aReason',
2386 $user,
2387 []
2388 );
2389 $this->assertTrue( $status->isGood() );
2390 $this->assertInternalType( 'int', $status->getValue() );
2391 $this->assertSelect(
2392 'logging',
2393 [ 'log_type', 'log_action' ],
2394 [ 'log_id' => $status->getValue() ],
2395 [ [ 'protect', 'unprotect' ] ]
2396 );
2397 }
2398
2399 /**
2400 * @covers WikiPage::newPageUpdater
2401 * @covers WikiPage::getDerivedDataUpdater
2402 */
2403 public function testNewPageUpdater() {
2404 $user = $this->getTestUser()->getUser();
2405 $page = $this->newPage( __METHOD__, __METHOD__ );
2406
2407 /** @var Content $content */
2408 $content = $this->getMockBuilder( WikitextContent::class )
2409 ->setConstructorArgs( [ 'Hello World' ] )
2410 ->setMethods( [ 'getParserOutput' ] )
2411 ->getMock();
2412 $content->expects( $this->once() )
2413 ->method( 'getParserOutput' )
2414 ->willReturn( new ParserOutput( 'HTML' ) );
2415
2416 $preparedEditBefore = $page->prepareContentForEdit( $content, null, $user );
2417
2418 // provide context, so the cache can be kept in place
2419 $slotsUpdate = new revisionSlotsUpdate();
2420 $slotsUpdate->modifyContent( SlotRecord::MAIN, $content );
2421
2422 $updater = $page->newPageUpdater( $user, $slotsUpdate );
2423 $updater->setContent( SlotRecord::MAIN, $content );
2424 $revision = $updater->saveRevision(
2425 CommentStoreComment::newUnsavedComment( 'test' ),
2426 EDIT_NEW
2427 );
2428
2429 $preparedEditAfter = $page->prepareContentForEdit( $content, $revision, $user );
2430
2431 $this->assertSame( $revision->getId(), $page->getLatest() );
2432
2433 // Parsed output must remain cached throughout.
2434 $this->assertSame( $preparedEditBefore->output, $preparedEditAfter->output );
2435 }
2436
2437 /**
2438 * @covers WikiPage::newPageUpdater
2439 * @covers WikiPage::getDerivedDataUpdater
2440 */
2441 public function testGetDerivedDataUpdater() {
2442 $admin = $this->getTestSysop()->getUser();
2443
2444 /** @var object $page */
2445 $page = $this->createPage( __METHOD__, __METHOD__ );
2446 $page = TestingAccessWrapper::newFromObject( $page );
2447
2448 $revision = $page->getRevision()->getRevisionRecord();
2449 $user = $revision->getUser();
2450
2451 $slotsUpdate = new RevisionSlotsUpdate();
2452 $slotsUpdate->modifyContent( SlotRecord::MAIN, new WikitextContent( 'Hello World' ) );
2453
2454 // get a virgin updater
2455 $updater1 = $page->getDerivedDataUpdater( $user );
2456 $this->assertFalse( $updater1->isUpdatePrepared() );
2457
2458 $updater1->prepareUpdate( $revision );
2459
2460 // Re-use updater with same revision or content, even if base changed
2461 $this->assertSame( $updater1, $page->getDerivedDataUpdater( $user, $revision ) );
2462
2463 $slotsUpdate = RevisionSlotsUpdate::newFromContent(
2464 [ SlotRecord::MAIN => $revision->getContent( SlotRecord::MAIN ) ]
2465 );
2466 $this->assertSame( $updater1, $page->getDerivedDataUpdater( $user, null, $slotsUpdate ) );
2467
2468 // Don't re-use for edit if base revision ID changed
2469 $this->assertNotSame(
2470 $updater1,
2471 $page->getDerivedDataUpdater( $user, null, $slotsUpdate, true )
2472 );
2473
2474 // Don't re-use with different user
2475 $updater2a = $page->getDerivedDataUpdater( $admin, null, $slotsUpdate );
2476 $updater2a->prepareContent( $admin, $slotsUpdate, false );
2477
2478 $updater2b = $page->getDerivedDataUpdater( $user, null, $slotsUpdate );
2479 $updater2b->prepareContent( $user, $slotsUpdate, false );
2480 $this->assertNotSame( $updater2a, $updater2b );
2481
2482 // Don't re-use with different content
2483 $updater3 = $page->getDerivedDataUpdater( $admin, null, $slotsUpdate );
2484 $updater3->prepareUpdate( $revision );
2485 $this->assertNotSame( $updater2b, $updater3 );
2486
2487 // Don't re-use if no context given
2488 $updater4 = $page->getDerivedDataUpdater( $admin );
2489 $updater4->prepareUpdate( $revision );
2490 $this->assertNotSame( $updater3, $updater4 );
2491
2492 // Don't re-use if AGAIN no context given
2493 $updater5 = $page->getDerivedDataUpdater( $admin );
2494 $this->assertNotSame( $updater4, $updater5 );
2495
2496 // Don't re-use cached "virgin" unprepared updater
2497 $updater6 = $page->getDerivedDataUpdater( $admin, $revision );
2498 $this->assertNotSame( $updater5, $updater6 );
2499 }
2500
2501 protected function assertPreparedEditEquals(
2502 PreparedEdit $edit, PreparedEdit $edit2, $message = ''
2503 ) {
2504 // suppress differences caused by a clock tick between generating the two PreparedEdits
2505 if ( abs( $edit->timestamp - $edit2->timestamp ) < 3 ) {
2506 $edit2 = clone $edit2;
2507 $edit2->timestamp = $edit->timestamp;
2508 }
2509 $this->assertEquals( $edit, $edit2, $message );
2510 }
2511
2512 protected function assertPreparedEditNotEquals(
2513 PreparedEdit $edit, PreparedEdit $edit2, $message = ''
2514 ) {
2515 if ( abs( $edit->timestamp - $edit2->timestamp ) < 3 ) {
2516 $edit2 = clone $edit2;
2517 $edit2->timestamp = $edit->timestamp;
2518 }
2519 $this->assertNotEquals( $edit, $edit2, $message );
2520 }
2521
2522 }