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