MediaWiki REL1_33
WikiPageDbTestBase.php
Go to the documentation of this file.
1<?php
2
9
14
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
50 abstract protected function getMcrMigrationStage();
51
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',
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
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
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 if ( !$updater->wasSuccessful() ) {
139 $this->fail( $updater->getStatus()->getWikiText() );
140 }
141
142 return $page;
143 }
144
148 public function testPrepareContentForEdit() {
149 $user = $this->getTestUser()->getUser();
150 $sysop = $this->getTestUser( [ 'sysop' ] )->getUser();
151
152 $page = $this->createPage( __METHOD__, __METHOD__, null, $user );
153 $title = $page->getTitle();
154
155 $content = ContentHandler::makeContent(
156 "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam "
157 . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.",
158 $title,
160 );
161 $content2 = ContentHandler::makeContent(
162 "At vero eos et accusam et justo duo [[dolores]] et ea rebum. "
163 . "Stet clita kasd [[gubergren]], no sea takimata sanctus est. ~~~~",
164 $title,
166 );
167
168 $edit = $page->prepareContentForEdit( $content, null, $user, null, false );
169
170 $this->assertInstanceOf(
171 ParserOptions::class,
172 $edit->popts,
173 "pops"
174 );
175 $this->assertContains( '</a>', $edit->output->getText(), "output" );
176 $this->assertContains(
177 'consetetur sadipscing elitr',
178 $edit->output->getText(),
179 "output"
180 );
181
182 $this->assertTrue( $content->equals( $edit->newContent ), "newContent field" );
183 $this->assertTrue( $content->equals( $edit->pstContent ), "pstContent field" );
184 $this->assertSame( $edit->output, $edit->output, "output field" );
185 $this->assertSame( $edit->popts, $edit->popts, "popts field" );
186 $this->assertSame( null, $edit->revid, "revid field" );
187
188 // Re-using the prepared info if possible
189 $sameEdit = $page->prepareContentForEdit( $content, null, $user, null, false );
190 $this->assertPreparedEditEquals( $edit, $sameEdit, 'equivalent PreparedEdit' );
191 $this->assertSame( $edit->pstContent, $sameEdit->pstContent, 're-use output' );
192 $this->assertSame( $edit->output, $sameEdit->output, 're-use output' );
193
194 // Not re-using the same PreparedEdit if not possible
195 $rev = $page->getRevision();
196 $edit2 = $page->prepareContentForEdit( $content2, null, $user, null, false );
197 $this->assertPreparedEditNotEquals( $edit, $edit2 );
198 $this->assertContains( 'At vero eos', $edit2->pstContent->serialize(), "content" );
199
200 // Check pre-safe transform
201 $this->assertContains( '[[gubergren]]', $edit2->pstContent->serialize() );
202 $this->assertNotContains( '~~~~', $edit2->pstContent->serialize() );
203
204 $edit3 = $page->prepareContentForEdit( $content2, null, $sysop, null, false );
205 $this->assertPreparedEditNotEquals( $edit2, $edit3 );
206
207 // TODO: test with passing revision, then same without revision.
208 }
209
213 public function testDoEditUpdates() {
214 $user = $this->getTestUser()->getUser();
215
216 // NOTE: if site stats get out of whack and drop below 0,
217 // that causes a DB error during tear-down. So bump the
218 // numbers high enough to not drop below 0.
219 $siteStatsUpdate = SiteStatsUpdate::factory(
220 [ 'edits' => 1000, 'articles' => 1000, 'pages' => 1000 ]
221 );
222 $siteStatsUpdate->doUpdate();
223
224 $page = $this->createPage( __METHOD__, __METHOD__ );
225
226 $revision = new Revision(
227 [
228 'id' => 9989,
229 'page' => $page->getId(),
230 'title' => $page->getTitle(),
231 'comment' => __METHOD__,
232 'minor_edit' => true,
233 'text' => __METHOD__ . ' [[|foo]][[bar]]', // PST turns [[|foo]] into [[foo]]
234 'user' => $user->getId(),
235 'user_text' => $user->getName(),
236 'timestamp' => '20170707040404',
237 'content_model' => CONTENT_MODEL_WIKITEXT,
238 'content_format' => CONTENT_FORMAT_WIKITEXT,
239 ]
240 );
241
242 $page->doEditUpdates( $revision, $user );
243
244 // TODO: test various options; needs temporary hooks
245
247 $res = $dbr->select( 'pagelinks', '*', [ 'pl_from' => $page->getId() ] );
248 $n = $res->numRows();
249 $res->free();
250
251 $this->assertEquals( 1, $n, 'pagelinks should contain only one link if PST was not applied' );
252 }
253
258 public function testDoEditContent() {
259 $this->setMwGlobals( 'wgPageCreationLog', true );
260
261 $page = $this->newPage( __METHOD__ );
262 $title = $page->getTitle();
263
264 $user1 = $this->getTestUser()->getUser();
265 // Use the confirmed group for user2 to make sure the user is different
266 $user2 = $this->getTestUser( [ 'confirmed' ] )->getUser();
267
268 $content = ContentHandler::makeContent(
269 "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam "
270 . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.",
271 $title,
273 );
274
275 $preparedEditBefore = $page->prepareContentForEdit( $content, null, $user1 );
276
277 $status = $page->doEditContent( $content, "[[testing]] 1", EDIT_NEW, false, $user1 );
278
279 $this->assertTrue( $status->isOK(), 'OK' );
280 $this->assertTrue( $status->value['new'], 'new' );
281 $this->assertNotNull( $status->value['revision'], 'revision' );
282 $this->assertSame( $status->value['revision']->getId(), $page->getRevision()->getId() );
283 $this->assertSame( $status->value['revision']->getSha1(), $page->getRevision()->getSha1() );
284 $this->assertTrue( $status->value['revision']->getContent()->equals( $content ), 'equals' );
285
286 $rev = $page->getRevision();
287 $preparedEditAfter = $page->prepareContentForEdit( $content, $rev, $user1 );
288
289 $this->assertNotNull( $rev->getRecentChange() );
290 $this->assertSame( $rev->getId(), (int)$rev->getRecentChange()->getAttribute( 'rc_this_oldid' ) );
291
292 // make sure that cached ParserOutput gets re-used throughout
293 $this->assertSame( $preparedEditBefore->output, $preparedEditAfter->output );
294
295 $id = $page->getId();
296
297 // Test page creation logging
298 $this->assertSelect(
299 'logging',
300 [ 'log_type', 'log_action' ],
301 [ 'log_page' => $id ],
302 [ [ 'create', 'create' ] ]
303 );
304
305 $this->assertTrue( $title->getArticleID() > 0, "Title object should have new page id" );
306 $this->assertTrue( $id > 0, "WikiPage should have new page id" );
307 $this->assertTrue( $title->exists(), "Title object should indicate that the page now exists" );
308 $this->assertTrue( $page->exists(), "WikiPage object should indicate that the page now exists" );
309
310 # ------------------------
312 $res = $dbr->select( 'pagelinks', '*', [ 'pl_from' => $id ] );
313 $n = $res->numRows();
314 $res->free();
315
316 $this->assertEquals( 1, $n, 'pagelinks should contain one link from the page' );
317
318 # ------------------------
319 $page = new WikiPage( $title );
320
321 $retrieved = $page->getContent();
322 $this->assertTrue( $content->equals( $retrieved ), 'retrieved content doesn\'t equal original' );
323
324 # ------------------------
325 $page = new WikiPage( $title );
326
327 // try null edit, with a different user
328 $status = $page->doEditContent( $content, 'This changes nothing', EDIT_UPDATE, false, $user2 );
329 $this->assertTrue( $status->isOK(), 'OK' );
330 $this->assertFalse( $status->value['new'], 'new' );
331 $this->assertNull( $status->value['revision'], 'revision' );
332 $this->assertNotNull( $page->getRevision() );
333 $this->assertTrue( $page->getRevision()->getContent()->equals( $content ), 'equals' );
334
335 # ------------------------
336 $content = ContentHandler::makeContent(
337 "At vero eos et accusam et justo duo [[dolores]] et ea rebum. "
338 . "Stet clita kasd [[gubergren]], no sea takimata sanctus est. ~~~~",
339 $title,
341 );
342
343 $status = $page->doEditContent( $content, "testing 2", EDIT_UPDATE );
344 $this->assertTrue( $status->isOK(), 'OK' );
345 $this->assertFalse( $status->value['new'], 'new' );
346 $this->assertNotNull( $status->value['revision'], 'revision' );
347 $this->assertSame( $status->value['revision']->getId(), $page->getRevision()->getId() );
348 $this->assertSame( $status->value['revision']->getSha1(), $page->getRevision()->getSha1() );
349 $this->assertFalse(
350 $status->value['revision']->getContent()->equals( $content ),
351 'not equals (PST must substitute signature)'
352 );
353
354 $rev = $page->getRevision();
355 $this->assertNotNull( $rev->getRecentChange() );
356 $this->assertSame( $rev->getId(), (int)$rev->getRecentChange()->getAttribute( 'rc_this_oldid' ) );
357
358 # ------------------------
359 $page = new WikiPage( $title );
360
361 $retrieved = $page->getContent();
362 $newText = $retrieved->serialize();
363 $this->assertContains( '[[gubergren]]', $newText, 'New text must replace old text.' );
364 $this->assertNotContains( '~~~~', $newText, 'PST must substitute signature.' );
365
366 # ------------------------
368 $res = $dbr->select( 'pagelinks', '*', [ 'pl_from' => $id ] );
369 $n = $res->numRows();
370 $res->free();
371
372 $this->assertEquals( 2, $n, 'pagelinks should contain two links from the page' );
373 }
374
378 public function testDoEditContent_twice() {
379 $title = Title::newFromText( __METHOD__ );
380 $page = WikiPage::factory( $title );
381 $content = ContentHandler::makeContent( '$1 van $2', $title );
382
383 // Make sure we can do the exact same save twice.
384 // This tests checks that internal caches are reset as appropriate.
385 $status1 = $page->doEditContent( $content, __METHOD__ );
386 $status2 = $page->doEditContent( $content, __METHOD__ );
387
388 $this->assertTrue( $status1->isOK(), 'OK' );
389 $this->assertTrue( $status2->isOK(), 'OK' );
390
391 $this->assertTrue( isset( $status1->value['revision'] ), 'OK' );
392 $this->assertFalse( isset( $status2->value['revision'] ), 'OK' );
393 }
394
402 public function testDoDeleteArticle() {
403 $page = $this->createPage(
404 __METHOD__,
405 "[[original text]] foo",
407 );
408 $id = $page->getId();
409
410 $page->doDeleteArticle( "testing deletion" );
411
412 $this->assertFalse(
413 $page->getTitle()->getArticleID() > 0,
414 "Title object should now have page id 0"
415 );
416 $this->assertFalse( $page->getId() > 0, "WikiPage should now have page id 0" );
417 $this->assertFalse(
418 $page->exists(),
419 "WikiPage::exists should return false after page was deleted"
420 );
421 $this->assertNull(
422 $page->getContent(),
423 "WikiPage::getContent should return null after page was deleted"
424 );
425
426 $t = Title::newFromText( $page->getTitle()->getPrefixedText() );
427 $this->assertFalse(
428 $t->exists(),
429 "Title::exists should return false after page was deleted"
430 );
431
432 // Run the job queue
433 JobQueueGroup::destroySingletons();
434 $jobs = new RunJobs;
435 $jobs->loadParamsAndArgs( null, [ 'quiet' => true ], null );
436 $jobs->execute();
437
438 # ------------------------
440 $res = $dbr->select( 'pagelinks', '*', [ 'pl_from' => $id ] );
441 $n = $res->numRows();
442 $res->free();
443
444 $this->assertEquals( 0, $n, 'pagelinks should contain no more links from the page' );
445 }
446
451 $page = $this->createPage(
452 __METHOD__,
453 "[[original text]] foo",
455 );
456 $id = $page->getId();
457
458 $errorStack = '';
459 $status = $page->doDeleteArticleReal(
460 /* reason */ "testing user 0 deletion",
461 /* suppress */ false,
462 /* unused 1 */ null,
463 /* unused 2 */ null,
464 /* errorStack */ $errorStack,
465 null
466 );
467 $logId = $status->getValue();
468 $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
469 $commentQuery = MediaWikiServices::getInstance()->getCommentStore()->getJoin( 'log_comment' );
470 $this->assertSelect(
471 [ 'logging' ] + $actorQuery['tables'] + $commentQuery['tables'], /* table */
472 [
473 'log_type',
474 'log_action',
475 'log_comment' => $commentQuery['fields']['log_comment_text'],
476 'log_user' => $actorQuery['fields']['log_user'],
477 'log_user_text' => $actorQuery['fields']['log_user_text'],
478 'log_namespace',
479 'log_title',
480 ],
481 [ 'log_id' => $logId ],
482 [ [
483 'delete',
484 'delete',
485 'testing user 0 deletion',
486 null,
487 '127.0.0.1',
488 (string)$page->getTitle()->getNamespace(),
489 $page->getTitle()->getDBkey(),
490 ] ],
491 [],
492 $actorQuery['joins'] + $commentQuery['joins']
493 );
494 }
495
500 $page = $this->createPage(
501 __METHOD__,
502 "[[original text]] foo",
504 );
505 $id = $page->getId();
506
507 $user = $this->getTestSysop()->getUser();
508 $errorStack = '';
509 $status = $page->doDeleteArticleReal(
510 /* reason */ "testing sysop deletion",
511 /* suppress */ false,
512 /* unused 1 */ null,
513 /* unused 2 */ null,
514 /* errorStack */ $errorStack,
515 $user
516 );
517 $logId = $status->getValue();
518 $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
519 $commentQuery = MediaWikiServices::getInstance()->getCommentStore()->getJoin( 'log_comment' );
520 $this->assertSelect(
521 [ 'logging' ] + $actorQuery['tables'] + $commentQuery['tables'], /* table */
522 [
523 'log_type',
524 'log_action',
525 'log_comment' => $commentQuery['fields']['log_comment_text'],
526 'log_user' => $actorQuery['fields']['log_user'],
527 'log_user_text' => $actorQuery['fields']['log_user_text'],
528 'log_namespace',
529 'log_title',
530 ],
531 [ 'log_id' => $logId ],
532 [ [
533 'delete',
534 'delete',
535 'testing sysop deletion',
536 (string)$user->getId(),
537 $user->getName(),
538 (string)$page->getTitle()->getNamespace(),
539 $page->getTitle()->getDBkey(),
540 ] ],
541 [],
542 $actorQuery['joins'] + $commentQuery['joins']
543 );
544 }
545
552 $page = $this->createPage(
553 __METHOD__,
554 "[[original text]] foo",
556 );
557 $id = $page->getId();
558
559 $user = $this->getTestSysop()->getUser();
560 $errorStack = '';
561 $status = $page->doDeleteArticleReal(
562 /* reason */ "testing deletion",
563 /* suppress */ true,
564 /* unused 1 */ null,
565 /* unused 2 */ null,
566 /* errorStack */ $errorStack,
567 $user
568 );
569 $logId = $status->getValue();
570 $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
571 $commentQuery = MediaWikiServices::getInstance()->getCommentStore()->getJoin( 'log_comment' );
572 $this->assertSelect(
573 [ 'logging' ] + $actorQuery['tables'] + $commentQuery['tables'], /* table */
574 [
575 'log_type',
576 'log_action',
577 'log_comment' => $commentQuery['fields']['log_comment_text'],
578 'log_user' => $actorQuery['fields']['log_user'],
579 'log_user_text' => $actorQuery['fields']['log_user_text'],
580 'log_namespace',
581 'log_title',
582 ],
583 [ 'log_id' => $logId ],
584 [ [
585 'suppress',
586 'delete',
587 'testing deletion',
588 (string)$user->getId(),
589 $user->getName(),
590 (string)$page->getTitle()->getNamespace(),
591 $page->getTitle()->getDBkey(),
592 ] ],
593 [],
594 $actorQuery['joins'] + $commentQuery['joins']
595 );
596
597 $this->assertNull(
598 $page->getContent( Revision::FOR_PUBLIC ),
599 "WikiPage::getContent should return null after the page was suppressed for general users"
600 );
601
602 $this->assertNull(
603 $page->getContent( Revision::FOR_THIS_USER, null ),
604 "WikiPage::getContent should return null after the page was suppressed for user zero"
605 );
606
607 $this->assertNull(
608 $page->getContent( Revision::FOR_THIS_USER, $user ),
609 "WikiPage::getContent should return null after the page was suppressed even for a sysop"
610 );
611 }
612
616 public function testDoDeleteUpdates() {
617 $user = $this->getTestUser()->getUser();
618 $page = $this->createPage(
619 __METHOD__,
620 "[[original text]] foo",
622 );
623 $id = $page->getId();
624 $page->loadPageData(); // make sure the current revision is cached.
625
626 // Similar to MovePage logic
627 wfGetDB( DB_MASTER )->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
628 $page->doDeleteUpdates( $page->getId(), $page->getContent(), $page->getRevision(), $user );
629
630 // Run the job queue
631 JobQueueGroup::destroySingletons();
632 $jobs = new RunJobs;
633 $jobs->loadParamsAndArgs( null, [ 'quiet' => true ], null );
634 $jobs->execute();
635
636 # ------------------------
638 $res = $dbr->select( 'pagelinks', '*', [ 'pl_from' => $id ] );
639 $n = $res->numRows();
640 $res->free();
641
642 $this->assertEquals( 0, $n, 'pagelinks should contain no more links from the page' );
643 }
644
650 protected function defineMockContentModelForUpdateTesting( $name ) {
652 $handler = $this->getMockBuilder( TextContentHandler::class )
653 ->setConstructorArgs( [ $name ] )
654 ->setMethods(
655 [ 'getSecondaryDataUpdates', 'getDeletionUpdates', 'unserializeContent' ]
656 )
657 ->getMock();
658
659 $dataUpdate = new MWCallableUpdate( 'time' );
660 $dataUpdate->_name = "$name data update";
661
662 $deletionUpdate = new MWCallableUpdate( 'time' );
663 $deletionUpdate->_name = "$name deletion update";
664
665 $handler->method( 'getSecondaryDataUpdates' )->willReturn( [ $dataUpdate ] );
666 $handler->method( 'getDeletionUpdates' )->willReturn( [ $deletionUpdate ] );
667 $handler->method( 'unserializeContent' )->willReturnCallback(
668 function ( $text ) use ( $handler ) {
669 return $this->createMockContent( $handler, $text );
670 }
671 );
672
674 'wgContentHandlers', [
675 $name => function () use ( $handler ){
676 return $handler;
677 }
678 ]
679 );
680
681 return $handler;
682 }
683
690 protected function createMockContent( ContentHandler $handler, $text ) {
692 $content = $this->getMockBuilder( TextContent::class )
693 ->setConstructorArgs( [ $text ] )
694 ->setMethods( [ 'getModel', 'getContentHandler' ] )
695 ->getMock();
696
697 $content->method( 'getModel' )->willReturn( $handler->getModelID() );
698 $content->method( 'getContentHandler' )->willReturn( $handler );
699
700 return $content;
701 }
702
703 public function testGetDeletionUpdates() {
704 $m1 = $this->defineMockContentModelForUpdateTesting( 'M1' );
705
706 $mainContent1 = $this->createMockContent( $m1, 'main 1' );
707
708 $page = new WikiPage( Title::newFromText( __METHOD__ ) );
709 $page = $this->createPage(
710 $page,
711 [ 'main' => $mainContent1 ]
712 );
713
714 $dataUpdates = $page->getDeletionUpdates( $page->getRevisionRecord() );
715 $this->assertNotEmpty( $dataUpdates );
716
717 $updateNames = array_map( function ( $du ) {
718 return isset( $du->_name ) ? $du->_name : get_class( $du );
719 }, $dataUpdates );
720
721 $this->assertContains( LinksDeletionUpdate::class, $updateNames );
722 $this->assertContains( 'M1 deletion update', $updateNames );
723 }
724
728 public function testGetRevision() {
729 $page = $this->newPage( __METHOD__ );
730
731 $rev = $page->getRevision();
732 $this->assertNull( $rev );
733
734 # -----------------
735 $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
736
737 $rev = $page->getRevision();
738
739 $this->assertEquals( $page->getLatest(), $rev->getId() );
740 $this->assertEquals( "some text", $rev->getContent()->getText() );
741 }
742
746 public function testGetContent() {
747 $page = $this->newPage( __METHOD__ );
748
749 $content = $page->getContent();
750 $this->assertNull( $content );
751
752 # -----------------
753 $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
754
755 $content = $page->getContent();
756 $this->assertEquals( "some text", $content->getText() );
757 }
758
762 public function testExists() {
763 $page = $this->newPage( __METHOD__ );
764 $this->assertFalse( $page->exists() );
765
766 # -----------------
767 $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
768 $this->assertTrue( $page->exists() );
769
770 $page = new WikiPage( $page->getTitle() );
771 $this->assertTrue( $page->exists() );
772
773 # -----------------
774 $page->doDeleteArticle( "done testing" );
775 $this->assertFalse( $page->exists() );
776
777 $page = new WikiPage( $page->getTitle() );
778 $this->assertFalse( $page->exists() );
779 }
780
781 public function provideHasViewableContent() {
782 return [
783 [ 'WikiPageTest_testHasViewableContent', false, true ],
784 [ 'Special:WikiPageTest_testHasViewableContent', false ],
785 [ 'MediaWiki:WikiPageTest_testHasViewableContent', false ],
786 [ 'Special:Userlogin', true ],
787 [ 'MediaWiki:help', true ],
788 ];
789 }
790
795 public function testHasViewableContent( $title, $viewable, $create = false ) {
796 $page = $this->newPage( $title );
797 $this->assertEquals( $viewable, $page->hasViewableContent() );
798
799 if ( $create ) {
800 $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
801 $this->assertTrue( $page->hasViewableContent() );
802
803 $page = new WikiPage( $page->getTitle() );
804 $this->assertTrue( $page->hasViewableContent() );
805 }
806 }
807
808 public function provideGetRedirectTarget() {
809 return [
810 [ 'WikiPageTest_testGetRedirectTarget_1', CONTENT_MODEL_WIKITEXT, "hello world", null ],
811 [
812 'WikiPageTest_testGetRedirectTarget_2',
814 "#REDIRECT [[hello world]]",
815 "Hello world"
816 ],
817 // The below added to protect against Media namespace
818 // redirects which throw a fatal: (T203942)
819 [
820 'WikiPageTest_testGetRedirectTarget_3',
822 "#REDIRECT [[Media:hello_world]]",
823 "File:Hello world"
824 ],
825 // Test fragments longer than 255 bytes (T207876)
826 [
827 'WikiPageTest_testGetRedirectTarget_4',
829 // phpcs:ignore Generic.Files.LineLength
830 '#REDIRECT [[Foobar#🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿]]',
831 // phpcs:ignore Generic.Files.LineLength
832 'Foobar#🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬...'
833 ]
834 ];
835 }
836
841 public function testGetRedirectTarget( $title, $model, $text, $target ) {
842 $this->setMwGlobals( [
843 'wgCapitalLinks' => true,
844 ] );
845
846 $page = $this->createPage( $title, $text, $model );
847
848 # sanity check, because this test seems to fail for no reason for some people.
849 $c = $page->getContent();
850 $this->assertEquals( WikitextContent::class, get_class( $c ) );
851
852 # now, test the actual redirect
853 $t = $page->getRedirectTarget();
854 $this->assertEquals( $target, $t ? $t->getFullText() : null );
855 }
856
861 public function testIsRedirect( $title, $model, $text, $target ) {
862 $page = $this->createPage( $title, $text, $model );
863 $this->assertEquals( !is_null( $target ), $page->isRedirect() );
864 }
865
866 public function provideIsCountable() {
867 return [
868
869 // any
870 [ 'WikiPageTest_testIsCountable',
872 '',
873 'any',
874 true
875 ],
876 [ 'WikiPageTest_testIsCountable',
878 'Foo',
879 'any',
880 true
881 ],
882
883 // link
884 [ 'WikiPageTest_testIsCountable',
886 'Foo',
887 'link',
888 false
889 ],
890 [ 'WikiPageTest_testIsCountable',
892 'Foo [[bar]]',
893 'link',
894 true
895 ],
896
897 // redirects
898 [ 'WikiPageTest_testIsCountable',
900 '#REDIRECT [[bar]]',
901 'any',
902 false
903 ],
904 [ 'WikiPageTest_testIsCountable',
906 '#REDIRECT [[bar]]',
907 'link',
908 false
909 ],
910
911 // not a content namespace
912 [ 'Talk:WikiPageTest_testIsCountable',
914 'Foo',
915 'any',
916 false
917 ],
918 [ 'Talk:WikiPageTest_testIsCountable',
920 'Foo [[bar]]',
921 'link',
922 false
923 ],
924
925 // not a content namespace, different model
926 [ 'MediaWiki:WikiPageTest_testIsCountable.js',
927 null,
928 'Foo',
929 'any',
930 false
931 ],
932 [ 'MediaWiki:WikiPageTest_testIsCountable.js',
933 null,
934 'Foo [[bar]]',
935 'link',
936 false
937 ],
938 ];
939 }
940
945 public function testIsCountable( $title, $model, $text, $mode, $expected ) {
947
948 $this->setMwGlobals( 'wgArticleCountMethod', $mode );
949
950 $title = Title::newFromText( $title );
951
953 && $model
954 && ContentHandler::getDefaultModelFor( $title ) != $model
955 ) {
956 $this->markTestSkipped( "Can not use non-default content model $model for "
957 . $title->getPrefixedDBkey() . " with \$wgContentHandlerUseDB disabled." );
958 }
959
960 $page = $this->createPage( $title, $text, $model );
961
962 $editInfo = $page->prepareContentForEdit( $page->getContent() );
963
964 $v = $page->isCountable();
965 $w = $page->isCountable( $editInfo );
966
967 $this->assertEquals(
968 $expected,
969 $v,
970 "isCountable( null ) returned unexpected value " . var_export( $v, true )
971 . " instead of " . var_export( $expected, true )
972 . " in mode `$mode` for text \"$text\""
973 );
974
975 $this->assertEquals(
976 $expected,
977 $w,
978 "isCountable( \$editInfo ) returned unexpected value " . var_export( $v, true )
979 . " instead of " . var_export( $expected, true )
980 . " in mode `$mode` for text \"$text\""
981 );
982 }
983
984 public function provideGetParserOutput() {
985 return [
986 [
988 "hello ''world''\n",
989 "<div class=\"mw-parser-output\"><p>hello <i>world</i></p></div>"
990 ],
991 // @todo more...?
992 ];
993 }
994
999 public function testGetParserOutput( $model, $text, $expectedHtml ) {
1000 $page = $this->createPage( __METHOD__, $text, $model );
1001
1002 $opt = $page->makeParserOptions( 'canonical' );
1003 $po = $page->getParserOutput( $opt );
1004 $text = $po->getText();
1005
1006 $text = trim( preg_replace( '/<!--.*?-->/sm', '', $text ) ); # strip injected comments
1007 $text = preg_replace( '!\s*(</p>|</div>)!sm', '\1', $text ); # don't let tidy confuse us
1008
1009 $this->assertEquals( $expectedHtml, $text );
1010
1011 return $po;
1012 }
1013
1017 public function testGetParserOutput_nonexisting() {
1018 $page = new WikiPage( Title::newFromText( __METHOD__ ) );
1019
1020 $opt = new ParserOptions();
1021 $po = $page->getParserOutput( $opt );
1022
1023 $this->assertFalse( $po, "getParserOutput() shall return false for non-existing pages." );
1024 }
1025
1029 public function testGetParserOutput_badrev() {
1030 $page = $this->createPage( __METHOD__, 'dummy', CONTENT_MODEL_WIKITEXT );
1031
1032 $opt = new ParserOptions();
1033 $po = $page->getParserOutput( $opt, $page->getLatest() + 1234 );
1034
1035 // @todo would be neat to also test deleted revision
1036
1037 $this->assertFalse( $po, "getParserOutput() shall return false for non-existing revisions." );
1038 }
1039
1040 public static $sections =
1041
1042 "Intro
1043
1044== stuff ==
1045hello world
1046
1047== test ==
1048just a test
1049
1050== foo ==
1051more stuff
1052";
1053
1054 public function dataReplaceSection() {
1055 // NOTE: assume the Help namespace to contain wikitext
1056 return [
1057 [ 'Help:WikiPageTest_testReplaceSection',
1058 CONTENT_MODEL_WIKITEXT,
1059 self::$sections,
1060 "0",
1061 "No more",
1062 null,
1063 trim( preg_replace( '/^Intro/sm', 'No more', self::$sections ) )
1064 ],
1065 [ 'Help:WikiPageTest_testReplaceSection',
1066 CONTENT_MODEL_WIKITEXT,
1067 self::$sections,
1068 "",
1069 "No more",
1070 null,
1071 "No more"
1072 ],
1073 [ 'Help:WikiPageTest_testReplaceSection',
1074 CONTENT_MODEL_WIKITEXT,
1075 self::$sections,
1076 "2",
1077 "== TEST ==\nmore fun",
1078 null,
1079 trim( preg_replace( '/^== test ==.*== foo ==/sm',
1080 "== TEST ==\nmore fun\n\n== foo ==",
1081 self::$sections ) )
1082 ],
1083 [ 'Help:WikiPageTest_testReplaceSection',
1084 CONTENT_MODEL_WIKITEXT,
1085 self::$sections,
1086 "8",
1087 "No more",
1088 null,
1089 trim( self::$sections )
1090 ],
1091 [ 'Help:WikiPageTest_testReplaceSection',
1092 CONTENT_MODEL_WIKITEXT,
1093 self::$sections,
1094 "new",
1095 "No more",
1096 "New",
1097 trim( self::$sections ) . "\n\n== New ==\n\nNo more"
1098 ],
1099 ];
1100 }
1101
1106 public function testReplaceSectionContent( $title, $model, $text, $section,
1107 $with, $sectionTitle, $expected
1108 ) {
1109 $page = $this->createPage( $title, $text, $model );
1110
1111 $content = ContentHandler::makeContent( $with, $page->getTitle(), $page->getContentModel() );
1113 $c = $page->replaceSectionContent( $section, $content, $sectionTitle );
1114
1115 $this->assertEquals( $expected, $c ? trim( $c->getText() ) : null );
1116 }
1117
1122 public function testReplaceSectionAtRev( $title, $model, $text, $section,
1123 $with, $sectionTitle, $expected
1124 ) {
1125 $page = $this->createPage( $title, $text, $model );
1126 $baseRevId = $page->getLatest();
1127
1128 $content = ContentHandler::makeContent( $with, $page->getTitle(), $page->getContentModel() );
1130 $c = $page->replaceSectionAtRev( $section, $content, $sectionTitle, $baseRevId );
1131
1132 $this->assertEquals( $expected, $c ? trim( $c->getText() ) : null );
1133 }
1134
1138 public function testGetOldestRevision() {
1139 $page = $this->newPage( __METHOD__ );
1140 $page->doEditContent(
1141 new WikitextContent( 'one' ),
1142 "first edit",
1143 EDIT_NEW
1144 );
1145 $rev1 = $page->getRevision();
1146
1147 $page = new WikiPage( $page->getTitle() );
1148 $page->doEditContent(
1149 new WikitextContent( 'two' ),
1150 "second edit",
1151 EDIT_UPDATE
1152 );
1153
1154 $page = new WikiPage( $page->getTitle() );
1155 $page->doEditContent(
1156 new WikitextContent( 'three' ),
1157 "third edit",
1158 EDIT_UPDATE
1159 );
1160
1161 // sanity check
1162 $this->assertNotEquals(
1163 $rev1->getId(),
1164 $page->getRevision()->getId(),
1165 '$page->getRevision()->getId()'
1166 );
1167
1168 // actual test
1169 $this->assertEquals(
1170 $rev1->getId(),
1171 $page->getOldestRevision()->getId(),
1172 '$page->getOldestRevision()->getId()'
1173 );
1174 }
1175
1180 public function testDoRollback() {
1181 // FIXME: fails under postgres
1182 $this->markTestSkippedIfDbType( 'postgres' );
1183
1184 $admin = $this->getTestSysop()->getUser();
1185 $user1 = $this->getTestUser()->getUser();
1186 // Use the confirmed group for user2 to make sure the user is different
1187 $user2 = $this->getTestUser( [ 'confirmed' ] )->getUser();
1188
1189 // make sure we can test autopatrolling
1190 $this->setMwGlobals( 'wgUseRCPatrol', true );
1191
1192 // TODO: MCR: test rollback of multiple slots!
1193 $page = $this->newPage( __METHOD__ );
1194
1195 // Make some edits
1196 $text = "one";
1197 $status1 = $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ),
1198 "section one", EDIT_NEW, false, $admin );
1199
1200 $text .= "\n\ntwo";
1201 $status2 = $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ),
1202 "adding section two", 0, false, $user1 );
1203
1204 $text .= "\n\nthree";
1205 $status3 = $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ),
1206 "adding section three", 0, false, $user2 );
1207
1211 $rev1 = $status1->getValue()['revision'];
1212 $rev2 = $status2->getValue()['revision'];
1213 $rev3 = $status3->getValue()['revision'];
1214
1220 $this->assertEquals( 3, Revision::countByPageId( wfGetDB( DB_REPLICA ), $page->getId() ) );
1221 $this->assertEquals( $admin->getName(), $rev1->getUserText() );
1222 $this->assertEquals( $user1->getName(), $rev2->getUserText() );
1223 $this->assertEquals( $user2->getName(), $rev3->getUserText() );
1224
1225 // Now, try the actual rollback
1226 $token = $admin->getEditToken( 'rollback' );
1227 $rollbackErrors = $page->doRollback(
1228 $user2->getName(),
1229 "testing rollback",
1230 $token,
1231 false,
1232 $resultDetails,
1233 $admin
1234 );
1235
1236 if ( $rollbackErrors ) {
1237 $this->fail(
1238 "Rollback failed:\n" .
1239 print_r( $rollbackErrors, true ) . ";\n" .
1240 print_r( $resultDetails, true )
1241 );
1242 }
1243
1244 $page = new WikiPage( $page->getTitle() );
1245 $this->assertEquals( $rev2->getSha1(), $page->getRevision()->getSha1(),
1246 "rollback did not revert to the correct revision" );
1247 $this->assertEquals( "one\n\ntwo", $page->getContent()->getText() );
1248
1249 $rc = MediaWikiServices::getInstance()->getRevisionStore()->getRecentChange(
1250 $page->getRevision()->getRevisionRecord()
1251 );
1252
1253 $this->assertNotNull( $rc, 'RecentChanges entry' );
1254 $this->assertEquals(
1255 RecentChange::PRC_AUTOPATROLLED,
1256 $rc->getAttribute( 'rc_patrolled' ),
1257 'rc_patrolled'
1258 );
1259
1260 // TODO: MCR: assert origin once we write slot data
1261 // $mainSlot = $page->getRevision()->getRevisionRecord()->getSlot( SlotRecord::MAIN );
1262 // $this->assertTrue( $mainSlot->isInherited(), 'isInherited' );
1263 // $this->assertSame( $rev2->getId(), $mainSlot->getOrigin(), 'getOrigin' );
1264 }
1265
1270 public function testDoRollbackFailureSameContent() {
1271 $admin = $this->getTestSysop()->getUser();
1272
1273 $text = "one";
1274 $page = $this->newPage( __METHOD__ );
1275 $page->doEditContent(
1276 ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
1277 "section one",
1278 EDIT_NEW,
1279 false,
1280 $admin
1281 );
1282 $rev1 = $page->getRevision();
1283
1284 $user1 = $this->getTestUser( [ 'sysop' ] )->getUser();
1285 $text .= "\n\ntwo";
1286 $page = new WikiPage( $page->getTitle() );
1287 $page->doEditContent(
1288 ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
1289 "adding section two",
1290 0,
1291 false,
1292 $user1
1293 );
1294
1295 # now, do a the rollback from the same user was doing the edit before
1296 $resultDetails = [];
1297 $token = $user1->getEditToken( 'rollback' );
1298 $errors = $page->doRollback(
1299 $user1->getName(),
1300 "testing revert same user",
1301 $token,
1302 false,
1303 $resultDetails,
1304 $admin
1305 );
1306
1307 $this->assertEquals( [], $errors, "Rollback failed same user" );
1308
1309 # now, try the rollback
1310 $resultDetails = [];
1311 $token = $admin->getEditToken( 'rollback' );
1312 $errors = $page->doRollback(
1313 $user1->getName(),
1314 "testing revert",
1315 $token,
1316 false,
1317 $resultDetails,
1318 $admin
1319 );
1320
1321 $this->assertEquals(
1322 [
1323 [
1324 'alreadyrolled',
1325 __METHOD__,
1326 $user1->getName(),
1327 $admin->getName(),
1328 ],
1329 ],
1330 $errors,
1331 "Rollback not failed"
1332 );
1333
1334 $page = new WikiPage( $page->getTitle() );
1335 $this->assertEquals( $rev1->getSha1(), $page->getRevision()->getSha1(),
1336 "rollback did not revert to the correct revision" );
1337 $this->assertEquals( "one", $page->getContent()->getText() );
1338 }
1339
1344 public function testDoRollbackTagging() {
1345 if ( !in_array( 'mw-rollback', ChangeTags::getSoftwareTags() ) ) {
1346 $this->markTestSkipped( 'Rollback tag deactivated, skipped the test.' );
1347 }
1348
1349 $admin = new User();
1350 $admin->setName( 'Administrator' );
1351 $admin->addToDatabase();
1352
1353 $text = 'First line';
1354 $page = $this->newPage( 'WikiPageTest_testDoRollbackTagging' );
1355 $page->doEditContent(
1356 ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
1357 'Added first line',
1358 EDIT_NEW,
1359 false,
1360 $admin
1361 );
1362
1363 $secondUser = new User();
1364 $secondUser->setName( '92.65.217.32' );
1365 $text .= '\n\nSecond line';
1366 $page = new WikiPage( $page->getTitle() );
1367 $page->doEditContent(
1368 ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
1369 'Adding second line',
1370 0,
1371 false,
1372 $secondUser
1373 );
1374
1375 // Now, try the rollback
1376 $admin->addGroup( 'sysop' ); // Make the test user a sysop
1377 $token = $admin->getEditToken( 'rollback' );
1378 $errors = $page->doRollback(
1379 $secondUser->getName(),
1380 'testing rollback',
1381 $token,
1382 false,
1383 $resultDetails,
1384 $admin
1385 );
1386
1387 // If doRollback completed without errors
1388 if ( $errors === [] ) {
1389 $tags = $resultDetails[ 'tags' ];
1390 $this->assertContains( 'mw-rollback', $tags );
1391 }
1392 }
1393
1394 public function provideGetAutoDeleteReason() {
1395 return [
1396 [
1397 [],
1398 false,
1399 false
1400 ],
1401
1402 [
1403 [
1404 [ "first edit", null ],
1405 ],
1406 "/first edit.*only contributor/",
1407 false
1408 ],
1409
1410 [
1411 [
1412 [ "first edit", null ],
1413 [ "second edit", null ],
1414 ],
1415 "/second edit.*only contributor/",
1416 true
1417 ],
1418
1419 [
1420 [
1421 [ "first edit", "127.0.2.22" ],
1422 [ "second edit", "127.0.3.33" ],
1423 ],
1424 "/second edit/",
1425 true
1426 ],
1427
1428 [
1429 [
1430 [
1431 "first edit: "
1432 . "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam "
1433 . " nonumy eirmod tempor invidunt ut labore et dolore magna "
1434 . "aliquyam erat, sed diam voluptua. At vero eos et accusam "
1435 . "et justo duo dolores et ea rebum. Stet clita kasd gubergren, "
1436 . "no sea takimata sanctus est Lorem ipsum dolor sit amet.'",
1437 null
1438 ],
1439 ],
1440 '/first edit:.*\.\.\."/',
1441 false
1442 ],
1443
1444 [
1445 [
1446 [ "first edit", "127.0.2.22" ],
1447 [ "", "127.0.3.33" ],
1448 ],
1449 "/before blanking.*first edit/",
1450 true
1451 ],
1452
1453 ];
1454 }
1455
1460 public function testGetAutoDeleteReason( $edits, $expectedResult, $expectedHistory ) {
1461 global $wgUser;
1462
1463 // NOTE: assume Help namespace to contain wikitext
1464 $page = $this->newPage( "Help:WikiPageTest_testGetAutoDeleteReason" );
1465
1466 $c = 1;
1467
1468 foreach ( $edits as $edit ) {
1469 $user = new User();
1470
1471 if ( !empty( $edit[1] ) ) {
1472 $user->setName( $edit[1] );
1473 } else {
1474 $user = $wgUser;
1475 }
1476
1477 $content = ContentHandler::makeContent( $edit[0], $page->getTitle(), $page->getContentModel() );
1478
1479 $page->doEditContent( $content, "test edit $c", $c < 2 ? EDIT_NEW : 0, false, $user );
1480
1481 $c += 1;
1482 }
1483
1484 $reason = $page->getAutoDeleteReason( $hasHistory );
1485
1486 if ( is_bool( $expectedResult ) || is_null( $expectedResult ) ) {
1487 $this->assertEquals( $expectedResult, $reason );
1488 } else {
1489 $this->assertTrue( (bool)preg_match( $expectedResult, $reason ),
1490 "Autosummary didn't match expected pattern $expectedResult: $reason" );
1491 }
1492
1493 $this->assertEquals( $expectedHistory, $hasHistory,
1494 "expected \$hasHistory to be " . var_export( $expectedHistory, true ) );
1495
1496 $page->doDeleteArticle( "done" );
1497 }
1498
1499 public function providePreSaveTransform() {
1500 return [
1501 [ 'hello this is ~~~',
1502 "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]",
1503 ],
1504 [ 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
1505 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
1506 ],
1507 ];
1508 }
1509
1513 public function testWikiPageFactory() {
1514 $title = Title::makeTitle( NS_FILE, 'Someimage.png' );
1515 $page = WikiPage::factory( $title );
1516 $this->assertEquals( WikiFilePage::class, get_class( $page ) );
1517
1518 $title = Title::makeTitle( NS_CATEGORY, 'SomeCategory' );
1519 $page = WikiPage::factory( $title );
1520 $this->assertEquals( WikiCategoryPage::class, get_class( $page ) );
1521
1522 $title = Title::makeTitle( NS_MAIN, 'SomePage' );
1523 $page = WikiPage::factory( $title );
1524 $this->assertEquals( WikiPage::class, get_class( $page ) );
1525 }
1526
1531 public function testLoadPageData() {
1532 $title = Title::makeTitle( NS_MAIN, 'SomePage' );
1533 $page = WikiPage::factory( $title );
1534
1535 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
1536 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
1537 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
1538 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
1539
1540 $page->loadPageData( IDBAccessObject::READ_NORMAL );
1541 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
1542 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
1543 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
1544 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
1545
1546 $page->loadPageData( IDBAccessObject::READ_LATEST );
1547 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
1548 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
1549 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
1550 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
1551
1552 $page->loadPageData( IDBAccessObject::READ_LOCKING );
1553 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
1554 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
1555 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
1556 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
1557
1558 $page->loadPageData( IDBAccessObject::READ_EXCLUSIVE );
1559 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
1560 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
1561 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
1562 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
1563 }
1564
1568 public function testUpdateCategoryCounts() {
1569 $page = new WikiPage( Title::newFromText( __METHOD__ ) );
1570
1571 // Add an initial category
1572 $page->updateCategoryCounts( [ 'A' ], [], 0 );
1573
1574 $this->assertEquals( 1, Category::newFromName( 'A' )->getPageCount() );
1575 $this->assertEquals( 0, Category::newFromName( 'B' )->getPageCount() );
1576 $this->assertEquals( 0, Category::newFromName( 'C' )->getPageCount() );
1577
1578 // Add a new category
1579 $page->updateCategoryCounts( [ 'B' ], [], 0 );
1580
1581 $this->assertEquals( 1, Category::newFromName( 'A' )->getPageCount() );
1582 $this->assertEquals( 1, Category::newFromName( 'B' )->getPageCount() );
1583 $this->assertEquals( 0, Category::newFromName( 'C' )->getPageCount() );
1584
1585 // Add and remove a category
1586 $page->updateCategoryCounts( [ 'C' ], [ 'A' ], 0 );
1587
1588 $this->assertEquals( 0, Category::newFromName( 'A' )->getPageCount() );
1589 $this->assertEquals( 1, Category::newFromName( 'B' )->getPageCount() );
1590 $this->assertEquals( 1, Category::newFromName( 'C' )->getPageCount() );
1591 }
1592
1593 public function provideUpdateRedirectOn() {
1594 yield [ '#REDIRECT [[Foo]]', true, null, true, true, 0 ];
1595 yield [ '#REDIRECT [[Foo]]', true, 'Foo', true, false, 1 ];
1596 yield [ 'SomeText', false, null, false, true, 0 ];
1597 yield [ 'SomeText', false, 'Foo', false, false, 1 ];
1598 }
1599
1611 public function testUpdateRedirectOn(
1612 $initialText,
1613 $initialRedirectState,
1614 $redirectTitle,
1615 $lastRevIsRedirect,
1616 $expectedSuccess,
1617 $expectedRowCount
1618 ) {
1619 // FIXME: fails under sqlite and postgres
1620 $this->markTestSkippedIfDbType( 'sqlite' );
1621 $this->markTestSkippedIfDbType( 'postgres' );
1622 static $pageCounter = 0;
1623 $pageCounter++;
1624
1625 $page = $this->createPage( Title::newFromText( __METHOD__ . $pageCounter ), $initialText );
1626 $this->assertSame( $initialRedirectState, $page->isRedirect() );
1627
1628 $redirectTitle = is_string( $redirectTitle )
1629 ? Title::newFromText( $redirectTitle )
1630 : $redirectTitle;
1631
1632 $success = $page->updateRedirectOn( $this->db, $redirectTitle, $lastRevIsRedirect );
1633 $this->assertSame( $expectedSuccess, $success, 'Success assertion' );
1639 $this->assertRedirectTableCountForPageId( $page->getId(), $expectedRowCount );
1640 }
1641
1642 private function assertRedirectTableCountForPageId( $pageId, $expected ) {
1643 $this->assertSelect(
1644 'redirect',
1645 'COUNT(*)',
1646 [ 'rd_from' => $pageId ],
1647 [ [ strval( $expected ) ] ]
1648 );
1649 }
1650
1654 public function testInsertRedirectEntry_insertsRedirectEntry() {
1655 $page = $this->createPage( Title::newFromText( __METHOD__ ), 'A' );
1656 $this->assertRedirectTableCountForPageId( $page->getId(), 0 );
1657
1658 $targetTitle = Title::newFromText( 'SomeTarget#Frag' );
1659 $targetTitle->mInterwiki = 'eninter';
1660 $page->insertRedirectEntry( $targetTitle, null );
1661
1662 $this->assertSelect(
1663 'redirect',
1664 [ 'rd_from', 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
1665 [ 'rd_from' => $page->getId() ],
1666 [ [
1667 strval( $page->getId() ),
1668 strval( $targetTitle->getNamespace() ),
1669 strval( $targetTitle->getDBkey() ),
1670 strval( $targetTitle->getFragment() ),
1671 strval( $targetTitle->getInterwiki() ),
1672 ] ]
1673 );
1674 }
1675
1679 public function testInsertRedirectEntry_insertsRedirectEntryWithPageLatest() {
1680 $page = $this->createPage( Title::newFromText( __METHOD__ ), 'A' );
1681 $this->assertRedirectTableCountForPageId( $page->getId(), 0 );
1682
1683 $targetTitle = Title::newFromText( 'SomeTarget#Frag' );
1684 $targetTitle->mInterwiki = 'eninter';
1685 $page->insertRedirectEntry( $targetTitle, $page->getLatest() );
1686
1687 $this->assertSelect(
1688 'redirect',
1689 [ 'rd_from', 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
1690 [ 'rd_from' => $page->getId() ],
1691 [ [
1692 strval( $page->getId() ),
1693 strval( $targetTitle->getNamespace() ),
1694 strval( $targetTitle->getDBkey() ),
1695 strval( $targetTitle->getFragment() ),
1696 strval( $targetTitle->getInterwiki() ),
1697 ] ]
1698 );
1699 }
1700
1704 public function testInsertRedirectEntry_doesNotInsertIfPageLatestIncorrect() {
1705 $page = $this->createPage( Title::newFromText( __METHOD__ ), 'A' );
1706 $this->assertRedirectTableCountForPageId( $page->getId(), 0 );
1707
1708 $targetTitle = Title::newFromText( 'SomeTarget#Frag' );
1709 $targetTitle->mInterwiki = 'eninter';
1710 $page->insertRedirectEntry( $targetTitle, 215251 );
1711
1712 $this->assertRedirectTableCountForPageId( $page->getId(), 0 );
1713 }
1714
1715 private function getRow( array $overrides = [] ) {
1716 $row = [
1717 'page_id' => '44',
1718 'page_len' => '76',
1719 'page_is_redirect' => '1',
1720 'page_latest' => '99',
1721 'page_namespace' => '3',
1722 'page_title' => 'JaJaTitle',
1723 'page_restrictions' => 'edit=autoconfirmed,sysop:move=sysop',
1724 'page_touched' => '20120101020202',
1725 'page_links_updated' => '20140101020202',
1726 ];
1727 foreach ( $overrides as $key => $value ) {
1728 $row[$key] = $value;
1729 }
1730 return (object)$row;
1731 }
1732
1733 public function provideNewFromRowSuccess() {
1734 yield 'basic row' => [
1735 $this->getRow(),
1736 function ( WikiPage $wikiPage, self $test ) {
1737 $test->assertSame( 44, $wikiPage->getId() );
1738 $test->assertSame( 76, $wikiPage->getTitle()->getLength() );
1739 $test->assertTrue( $wikiPage->isRedirect() );
1740 $test->assertSame( 99, $wikiPage->getLatest() );
1741 $test->assertSame( 3, $wikiPage->getTitle()->getNamespace() );
1742 $test->assertSame( 'JaJaTitle', $wikiPage->getTitle()->getDBkey() );
1743 $test->assertSame(
1744 [
1745 'edit' => [ 'autoconfirmed', 'sysop' ],
1746 'move' => [ 'sysop' ],
1747 ],
1748 $wikiPage->getTitle()->getAllRestrictions()
1749 );
1750 $test->assertSame( '20120101020202', $wikiPage->getTouched() );
1751 $test->assertSame( '20140101020202', $wikiPage->getLinksTimestamp() );
1752 }
1753 ];
1754 yield 'different timestamp formats' => [
1755 $this->getRow( [
1756 'page_touched' => '2012-01-01 02:02:02',
1757 'page_links_updated' => '2014-01-01 02:02:02',
1758 ] ),
1759 function ( WikiPage $wikiPage, self $test ) {
1760 $test->assertSame( '20120101020202', $wikiPage->getTouched() );
1761 $test->assertSame( '20140101020202', $wikiPage->getLinksTimestamp() );
1762 }
1763 ];
1764 yield 'no restrictions' => [
1765 $this->getRow( [
1766 'page_restrictions' => '',
1767 ] ),
1768 function ( WikiPage $wikiPage, self $test ) {
1769 $test->assertSame(
1770 [
1771 'edit' => [],
1772 'move' => [],
1773 ],
1774 $wikiPage->getTitle()->getAllRestrictions()
1775 );
1776 }
1777 ];
1778 yield 'not redirect' => [
1779 $this->getRow( [
1780 'page_is_redirect' => '0',
1781 ] ),
1782 function ( WikiPage $wikiPage, self $test ) {
1783 $test->assertFalse( $wikiPage->isRedirect() );
1784 }
1785 ];
1786 }
1787
1796 public function testNewFromRow( $row, $assertions ) {
1797 $page = WikiPage::newFromRow( $row, 'fromdb' );
1798 $assertions( $page, $this );
1799 }
1800
1801 public function provideTestNewFromId_returnsNullOnBadPageId() {
1802 yield[ 0 ];
1803 yield[ -11 ];
1804 }
1805
1810 public function testNewFromId_returnsNullOnBadPageId( $pageId ) {
1811 $this->assertNull( WikiPage::newFromID( $pageId ) );
1812 }
1813
1817 public function testNewFromId_appearsToFetchCorrectRow() {
1818 $createdPage = $this->createPage( __METHOD__, 'Xsfaij09' );
1819 $fetchedPage = WikiPage::newFromID( $createdPage->getId() );
1820 $this->assertSame( $createdPage->getId(), $fetchedPage->getId() );
1821 $this->assertEquals(
1822 $createdPage->getContent()->getText(),
1823 $fetchedPage->getContent()->getText()
1824 );
1825 }
1826
1830 public function testNewFromId_returnsNullOnNonExistingId() {
1831 $this->assertNull( WikiPage::newFromID( 2147483647 ) );
1832 }
1833
1834 public function provideTestInsertProtectNullRevision() {
1835 // phpcs:disable Generic.Files.LineLength
1836 yield [
1837 'goat-message-key',
1838 [ 'edit' => 'sysop' ],
1839 [ 'edit' => '20200101040404' ],
1840 false,
1841 'Goat Reason',
1842 true,
1843 '(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)))'
1844 ];
1845 yield [
1846 'goat-key',
1847 [ 'edit' => 'sysop', 'move' => 'something' ],
1848 [ 'edit' => '20200101040404', 'move' => '20210101050505' ],
1849 false,
1850 'Goat Goat',
1851 true,
1852 '(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)))'
1853 ];
1854 // phpcs:enable
1855 }
1856
1870 public function testInsertProtectNullRevision(
1871 $revCommentMsg,
1872 array $limit,
1873 array $expiry,
1874 $cascade,
1875 $reason,
1876 $user,
1877 $expectedComment
1878 ) {
1879 $this->setContentLang( 'qqx' );
1880
1881 $page = $this->createPage( __METHOD__, 'Goat' );
1882
1883 $user = $user === null ? $user : $this->getTestSysop()->getUser();
1884
1885 $result = $page->insertProtectNullRevision(
1886 $revCommentMsg,
1887 $limit,
1888 $expiry,
1889 $cascade,
1890 $reason,
1891 $user
1892 );
1893
1894 $this->assertTrue( $result instanceof Revision );
1895 $this->assertSame( $expectedComment, $result->getComment( Revision::RAW ) );
1896 }
1897
1901 public function testUpdateRevisionOn_existingPage() {
1902 $user = $this->getTestSysop()->getUser();
1903 $page = $this->createPage( __METHOD__, 'StartText' );
1904
1905 $revision = new Revision(
1906 [
1907 'id' => 9989,
1908 'page' => $page->getId(),
1909 'title' => $page->getTitle(),
1910 'comment' => __METHOD__,
1911 'minor_edit' => true,
1912 'text' => __METHOD__ . '-text',
1913 'len' => strlen( __METHOD__ . '-text' ),
1914 'user' => $user->getId(),
1915 'user_text' => $user->getName(),
1916 'timestamp' => '20170707040404',
1917 'content_model' => CONTENT_MODEL_WIKITEXT,
1918 'content_format' => CONTENT_FORMAT_WIKITEXT,
1919 ]
1920 );
1921
1922 $result = $page->updateRevisionOn( $this->db, $revision );
1923 $this->assertTrue( $result );
1924 $this->assertSame( 9989, $page->getLatest() );
1925 $this->assertEquals( $revision, $page->getRevision() );
1926 }
1927
1931 public function testUpdateRevisionOn_NonExistingPage() {
1932 $user = $this->getTestSysop()->getUser();
1933 $page = $this->createPage( __METHOD__, 'StartText' );
1934 $page->doDeleteArticle( 'reason' );
1935
1936 $revision = new Revision(
1937 [
1938 'id' => 9989,
1939 'page' => $page->getId(),
1940 'title' => $page->getTitle(),
1941 'comment' => __METHOD__,
1942 'minor_edit' => true,
1943 'text' => __METHOD__ . '-text',
1944 'len' => strlen( __METHOD__ . '-text' ),
1945 'user' => $user->getId(),
1946 'user_text' => $user->getName(),
1947 'timestamp' => '20170707040404',
1948 'content_model' => CONTENT_MODEL_WIKITEXT,
1949 'content_format' => CONTENT_FORMAT_WIKITEXT,
1950 ]
1951 );
1952
1953 $result = $page->updateRevisionOn( $this->db, $revision );
1954 $this->assertFalse( $result );
1955 }
1956
1960 public function testUpdateIfNewerOn_olderRevision() {
1961 $user = $this->getTestSysop()->getUser();
1962 $page = $this->createPage( __METHOD__, 'StartText' );
1963 $initialRevision = $page->getRevision();
1964
1965 $olderTimeStamp = wfTimestamp(
1966 TS_MW,
1967 wfTimestamp( TS_UNIX, $initialRevision->getTimestamp() ) - 1
1968 );
1969
1970 $olderRevision = new Revision(
1971 [
1972 'id' => 9989,
1973 'page' => $page->getId(),
1974 'title' => $page->getTitle(),
1975 'comment' => __METHOD__,
1976 'minor_edit' => true,
1977 'text' => __METHOD__ . '-text',
1978 'len' => strlen( __METHOD__ . '-text' ),
1979 'user' => $user->getId(),
1980 'user_text' => $user->getName(),
1981 'timestamp' => $olderTimeStamp,
1982 'content_model' => CONTENT_MODEL_WIKITEXT,
1983 'content_format' => CONTENT_FORMAT_WIKITEXT,
1984 ]
1985 );
1986
1987 $result = $page->updateIfNewerOn( $this->db, $olderRevision );
1988 $this->assertFalse( $result );
1989 }
1990
1994 public function testUpdateIfNewerOn_newerRevision() {
1995 $user = $this->getTestSysop()->getUser();
1996 $page = $this->createPage( __METHOD__, 'StartText' );
1997 $initialRevision = $page->getRevision();
1998
1999 $newerTimeStamp = wfTimestamp(
2000 TS_MW,
2001 wfTimestamp( TS_UNIX, $initialRevision->getTimestamp() ) + 1
2002 );
2003
2004 $newerRevision = new Revision(
2005 [
2006 'id' => 9989,
2007 'page' => $page->getId(),
2008 'title' => $page->getTitle(),
2009 'comment' => __METHOD__,
2010 'minor_edit' => true,
2011 'text' => __METHOD__ . '-text',
2012 'len' => strlen( __METHOD__ . '-text' ),
2013 'user' => $user->getId(),
2014 'user_text' => $user->getName(),
2015 'timestamp' => $newerTimeStamp,
2016 'content_model' => CONTENT_MODEL_WIKITEXT,
2017 'content_format' => CONTENT_FORMAT_WIKITEXT,
2018 ]
2019 );
2020 $result = $page->updateIfNewerOn( $this->db, $newerRevision );
2021 $this->assertTrue( $result );
2022 }
2023
2027 public function testInsertOn() {
2028 $title = Title::newFromText( __METHOD__ );
2029 $page = new WikiPage( $title );
2030
2031 $startTimeStamp = wfTimestampNow();
2032 $result = $page->insertOn( $this->db );
2033 $endTimeStamp = wfTimestampNow();
2034
2035 $this->assertInternalType( 'int', $result );
2036 $this->assertTrue( $result > 0 );
2037
2038 $condition = [ 'page_id' => $result ];
2039
2040 // Check the default fields have been filled
2041 $this->assertSelect(
2042 'page',
2043 [
2044 'page_namespace',
2045 'page_title',
2046 'page_restrictions',
2047 'page_is_redirect',
2048 'page_is_new',
2049 'page_latest',
2050 'page_len',
2051 ],
2052 $condition,
2053 [ [
2054 '0',
2055 __METHOD__,
2056 '',
2057 '0',
2058 '1',
2059 '0',
2060 '0',
2061 ] ]
2062 );
2063
2064 // Check the page_random field has been filled
2065 $pageRandom = $this->db->selectField( 'page', 'page_random', $condition );
2066 $this->assertTrue( (float)$pageRandom < 1 && (float)$pageRandom > 0 );
2067
2068 // Assert the touched timestamp in the DB is roughly when we inserted the page
2069 $pageTouched = $this->db->selectField( 'page', 'page_touched', $condition );
2070 $this->assertTrue(
2071 wfTimestamp( TS_UNIX, $startTimeStamp )
2072 <= wfTimestamp( TS_UNIX, $pageTouched )
2073 );
2074 $this->assertTrue(
2075 wfTimestamp( TS_UNIX, $endTimeStamp )
2076 >= wfTimestamp( TS_UNIX, $pageTouched )
2077 );
2078
2079 // Try inserting the same page again and checking the result is false (no change)
2080 $result = $page->insertOn( $this->db );
2081 $this->assertFalse( $result );
2082 }
2083
2087 public function testInsertOn_idSpecified() {
2088 $title = Title::newFromText( __METHOD__ );
2089 $page = new WikiPage( $title );
2090 $id = 1478952189;
2091
2092 $result = $page->insertOn( $this->db, $id );
2093
2094 $this->assertSame( $id, $result );
2095
2096 $condition = [ 'page_id' => $result ];
2097
2098 // Check there is actually a row in the db
2099 $this->assertSelect(
2100 'page',
2101 [ 'page_title' ],
2102 $condition,
2103 [ [ __METHOD__ ] ]
2104 );
2105 }
2106
2107 public function provideTestDoUpdateRestrictions_setBasicRestrictions() {
2108 // Note: Once the current dates passes the date in these tests they will fail.
2109 yield 'move something' => [
2110 true,
2111 [ 'move' => 'something' ],
2112 [],
2113 [ 'edit' => [], 'move' => [ 'something' ] ],
2114 [],
2115 ];
2116 yield 'move something, edit blank' => [
2117 true,
2118 [ 'move' => 'something', 'edit' => '' ],
2119 [],
2120 [ 'edit' => [], 'move' => [ 'something' ] ],
2121 [],
2122 ];
2123 yield 'edit sysop, with expiry' => [
2124 true,
2125 [ 'edit' => 'sysop' ],
2126 [ 'edit' => '21330101020202' ],
2127 [ 'edit' => [ 'sysop' ], 'move' => [] ],
2128 [ 'edit' => '21330101020202' ],
2129 ];
2130 yield 'move and edit, move with expiry' => [
2131 true,
2132 [ 'move' => 'something', 'edit' => 'another' ],
2133 [ 'move' => '22220202010101' ],
2134 [ 'edit' => [ 'another' ], 'move' => [ 'something' ] ],
2135 [ 'move' => '22220202010101' ],
2136 ];
2137 yield 'move and edit, edit with infinity expiry' => [
2138 true,
2139 [ 'move' => 'something', 'edit' => 'another' ],
2140 [ 'edit' => 'infinity' ],
2141 [ 'edit' => [ 'another' ], 'move' => [ 'something' ] ],
2142 [ 'edit' => 'infinity' ],
2143 ];
2144 yield 'non existing, create something' => [
2145 false,
2146 [ 'create' => 'something' ],
2147 [],
2148 [ 'create' => [ 'something' ] ],
2149 [],
2150 ];
2151 yield 'non existing, create something with expiry' => [
2152 false,
2153 [ 'create' => 'something' ],
2154 [ 'create' => '23451212112233' ],
2155 [ 'create' => [ 'something' ] ],
2156 [ 'create' => '23451212112233' ],
2157 ];
2158 }
2159
2164 public function testDoUpdateRestrictions_setBasicRestrictions(
2165 $pageExists,
2166 array $limit,
2167 array $expiry,
2168 array $expectedRestrictions,
2169 array $expectedRestrictionExpiries
2170 ) {
2171 if ( $pageExists ) {
2172 $page = $this->createPage( __METHOD__, 'ABC' );
2173 } else {
2174 $page = new WikiPage( Title::newFromText( __METHOD__ . '-nonexist' ) );
2175 }
2176 $user = $this->getTestSysop()->getUser();
2177 $cascade = false;
2178
2179 $status = $page->doUpdateRestrictions( $limit, $expiry, $cascade, 'aReason', $user, [] );
2180
2181 $logId = $status->getValue();
2182 $allRestrictions = $page->getTitle()->getAllRestrictions();
2183
2184 $this->assertTrue( $status->isGood() );
2185 $this->assertInternalType( 'int', $logId );
2186 $this->assertSame( $expectedRestrictions, $allRestrictions );
2187 foreach ( $expectedRestrictionExpiries as $key => $value ) {
2188 $this->assertSame( $value, $page->getTitle()->getRestrictionExpiry( $key ) );
2189 }
2190
2191 // Make sure the log entry looks good
2192 // log_params is not checked here
2193 $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
2194 $commentQuery = MediaWikiServices::getInstance()->getCommentStore()->getJoin( 'log_comment' );
2195 $this->assertSelect(
2196 [ 'logging' ] + $actorQuery['tables'] + $commentQuery['tables'],
2197 [
2198 'log_comment' => $commentQuery['fields']['log_comment_text'],
2199 'log_user' => $actorQuery['fields']['log_user'],
2200 'log_user_text' => $actorQuery['fields']['log_user_text'],
2201 'log_namespace',
2202 'log_title',
2203 ],
2204 [ 'log_id' => $logId ],
2205 [ [
2206 'aReason',
2207 (string)$user->getId(),
2208 $user->getName(),
2209 (string)$page->getTitle()->getNamespace(),
2210 $page->getTitle()->getDBkey(),
2211 ] ],
2212 [],
2213 $actorQuery['joins'] + $commentQuery['joins']
2214 );
2215 }
2216
2220 public function testDoUpdateRestrictions_failsOnReadOnly() {
2221 $page = $this->createPage( __METHOD__, 'ABC' );
2222 $user = $this->getTestSysop()->getUser();
2223 $cascade = false;
2224
2225 // Set read only
2226 $readOnly = $this->getMockBuilder( ReadOnlyMode::class )
2227 ->disableOriginalConstructor()
2228 ->setMethods( [ 'isReadOnly', 'getReason' ] )
2229 ->getMock();
2230 $readOnly->expects( $this->once() )
2231 ->method( 'isReadOnly' )
2232 ->will( $this->returnValue( true ) );
2233 $readOnly->expects( $this->once() )
2234 ->method( 'getReason' )
2235 ->will( $this->returnValue( 'Some Read Only Reason' ) );
2236 $this->setService( 'ReadOnlyMode', $readOnly );
2237
2238 $status = $page->doUpdateRestrictions( [], [], $cascade, 'aReason', $user, [] );
2239 $this->assertFalse( $status->isOK() );
2240 $this->assertSame( 'readonlytext', $status->getMessage()->getKey() );
2241 }
2242
2246 public function testDoUpdateRestrictions_returnsGoodIfNothingChanged() {
2247 $page = $this->createPage( __METHOD__, 'ABC' );
2248 $user = $this->getTestSysop()->getUser();
2249 $cascade = false;
2250 $limit = [ 'edit' => 'sysop' ];
2251
2252 $status = $page->doUpdateRestrictions(
2253 $limit,
2254 [],
2255 $cascade,
2256 'aReason',
2257 $user,
2258 []
2259 );
2260
2261 // The first entry should have a logId as it did something
2262 $this->assertTrue( $status->isGood() );
2263 $this->assertInternalType( 'int', $status->getValue() );
2264
2265 $status = $page->doUpdateRestrictions(
2266 $limit,
2267 [],
2268 $cascade,
2269 'aReason',
2270 $user,
2271 []
2272 );
2273
2274 // The second entry should not have a logId as nothing changed
2275 $this->assertTrue( $status->isGood() );
2276 $this->assertNull( $status->getValue() );
2277 }
2278
2282 public function testDoUpdateRestrictions_logEntryTypeAndAction() {
2283 $page = $this->createPage( __METHOD__, 'ABC' );
2284 $user = $this->getTestSysop()->getUser();
2285 $cascade = false;
2286
2287 // Protect the page
2288 $status = $page->doUpdateRestrictions(
2289 [ 'edit' => 'sysop' ],
2290 [],
2291 $cascade,
2292 'aReason',
2293 $user,
2294 []
2295 );
2296 $this->assertTrue( $status->isGood() );
2297 $this->assertInternalType( 'int', $status->getValue() );
2298 $this->assertSelect(
2299 'logging',
2300 [ 'log_type', 'log_action' ],
2301 [ 'log_id' => $status->getValue() ],
2302 [ [ 'protect', 'protect' ] ]
2303 );
2304
2305 // Modify the protection
2306 $status = $page->doUpdateRestrictions(
2307 [ 'edit' => 'somethingElse' ],
2308 [],
2309 $cascade,
2310 'aReason',
2311 $user,
2312 []
2313 );
2314 $this->assertTrue( $status->isGood() );
2315 $this->assertInternalType( 'int', $status->getValue() );
2316 $this->assertSelect(
2317 'logging',
2318 [ 'log_type', 'log_action' ],
2319 [ 'log_id' => $status->getValue() ],
2320 [ [ 'protect', 'modify' ] ]
2321 );
2322
2323 // Remove the protection
2324 $status = $page->doUpdateRestrictions(
2325 [],
2326 [],
2327 $cascade,
2328 'aReason',
2329 $user,
2330 []
2331 );
2332 $this->assertTrue( $status->isGood() );
2333 $this->assertInternalType( 'int', $status->getValue() );
2334 $this->assertSelect(
2335 'logging',
2336 [ 'log_type', 'log_action' ],
2337 [ 'log_id' => $status->getValue() ],
2338 [ [ 'protect', 'unprotect' ] ]
2339 );
2340 }
2341
2346 public function testNewPageUpdater() {
2347 $user = $this->getTestUser()->getUser();
2348 $page = $this->newPage( __METHOD__, __METHOD__ );
2349
2351 $content = $this->getMockBuilder( WikitextContent::class )
2352 ->setConstructorArgs( [ 'Hello World' ] )
2353 ->setMethods( [ 'getParserOutput' ] )
2354 ->getMock();
2355 $content->expects( $this->once() )
2356 ->method( 'getParserOutput' )
2357 ->willReturn( new ParserOutput( 'HTML' ) );
2358
2359 $preparedEditBefore = $page->prepareContentForEdit( $content, null, $user );
2360
2361 // provide context, so the cache can be kept in place
2362 $slotsUpdate = new revisionSlotsUpdate();
2363 $slotsUpdate->modifyContent( SlotRecord::MAIN, $content );
2364
2365 $updater = $page->newPageUpdater( $user, $slotsUpdate );
2366 $updater->setContent( SlotRecord::MAIN, $content );
2367 $revision = $updater->saveRevision(
2368 CommentStoreComment::newUnsavedComment( 'test' ),
2369 EDIT_NEW
2370 );
2371
2372 $preparedEditAfter = $page->prepareContentForEdit( $content, $revision, $user );
2373
2374 $this->assertSame( $revision->getId(), $page->getLatest() );
2375
2376 // Parsed output must remain cached throughout.
2377 $this->assertSame( $preparedEditBefore->output, $preparedEditAfter->output );
2378 }
2379
2384 public function testGetDerivedDataUpdater() {
2385 $admin = $this->getTestSysop()->getUser();
2386
2388 $page = $this->createPage( __METHOD__, __METHOD__ );
2389 $page = TestingAccessWrapper::newFromObject( $page );
2390
2391 $revision = $page->getRevision()->getRevisionRecord();
2392 $user = $revision->getUser();
2393
2394 $slotsUpdate = new RevisionSlotsUpdate();
2395 $slotsUpdate->modifyContent( SlotRecord::MAIN, new WikitextContent( 'Hello World' ) );
2396
2397 // get a virgin updater
2398 $updater1 = $page->getDerivedDataUpdater( $user );
2399 $this->assertFalse( $updater1->isUpdatePrepared() );
2400
2401 $updater1->prepareUpdate( $revision );
2402
2403 // Re-use updater with same revision or content, even if base changed
2404 $this->assertSame( $updater1, $page->getDerivedDataUpdater( $user, $revision ) );
2405
2406 $slotsUpdate = RevisionSlotsUpdate::newFromContent(
2407 [ SlotRecord::MAIN => $revision->getContent( SlotRecord::MAIN ) ]
2408 );
2409 $this->assertSame( $updater1, $page->getDerivedDataUpdater( $user, null, $slotsUpdate ) );
2410
2411 // Don't re-use for edit if base revision ID changed
2412 $this->assertNotSame(
2413 $updater1,
2414 $page->getDerivedDataUpdater( $user, null, $slotsUpdate, true )
2415 );
2416
2417 // Don't re-use with different user
2418 $updater2a = $page->getDerivedDataUpdater( $admin, null, $slotsUpdate );
2419 $updater2a->prepareContent( $admin, $slotsUpdate, false );
2420
2421 $updater2b = $page->getDerivedDataUpdater( $user, null, $slotsUpdate );
2422 $updater2b->prepareContent( $user, $slotsUpdate, false );
2423 $this->assertNotSame( $updater2a, $updater2b );
2424
2425 // Don't re-use with different content
2426 $updater3 = $page->getDerivedDataUpdater( $admin, null, $slotsUpdate );
2427 $updater3->prepareUpdate( $revision );
2428 $this->assertNotSame( $updater2b, $updater3 );
2429
2430 // Don't re-use if no context given
2431 $updater4 = $page->getDerivedDataUpdater( $admin );
2432 $updater4->prepareUpdate( $revision );
2433 $this->assertNotSame( $updater3, $updater4 );
2434
2435 // Don't re-use if AGAIN no context given
2436 $updater5 = $page->getDerivedDataUpdater( $admin );
2437 $this->assertNotSame( $updater4, $updater5 );
2438
2439 // Don't re-use cached "virgin" unprepared updater
2440 $updater6 = $page->getDerivedDataUpdater( $admin, $revision );
2441 $this->assertNotSame( $updater5, $updater6 );
2442 }
2443
2444 protected function assertPreparedEditEquals(
2445 PreparedEdit $edit, PreparedEdit $edit2, $message = ''
2446 ) {
2447 // suppress differences caused by a clock tick between generating the two PreparedEdits
2448 if ( abs( $edit->timestamp - $edit2->timestamp ) < 3 ) {
2449 $edit2 = clone $edit2;
2450 $edit2->timestamp = $edit->timestamp;
2451 }
2452 $this->assertEquals( $edit, $edit2, $message );
2453 }
2454
2455 protected function assertPreparedEditNotEquals(
2456 PreparedEdit $edit, PreparedEdit $edit2, $message = ''
2457 ) {
2458 if ( abs( $edit->timestamp - $edit2->timestamp ) < 3 ) {
2459 $edit2 = clone $edit2;
2460 $edit2->timestamp = $edit->timestamp;
2461 }
2462 $this->assertNotEquals( $edit, $edit2, $message );
2463 }
2464
2465}
could not be made into a sysop(Did you enter the name correctly?) &lt
and that you know you can do these things To protect your we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights These restrictions translate to certain responsibilities for you if you distribute copies of the or if you modify it For if you distribute copies of such a whether gratis or for a you must give the recipients all the rights that you have You must make sure that receive or can get the source code And you must show them these terms so they know their rights We protect your rights with two and(2) offer you this license which gives you legal permission to copy
$wgContentHandlerUseDB
Set to false to disable use of the database fields introduced by the ContentHandler facility.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
A content handler knows how do deal with a specific type of content on a wiki page.
Deferrable Update for closure/callback.
MediaWiki exception.
loadParamsAndArgs( $self=null, $opts=null, $args=null)
Process command line arguments $mOptions becomes an array with keys set to the option names $mArgs be...
Base class that store and restore the Language objects.
getDefaultWikitextNS()
Returns the ID of a namespace that defaults to Wikitext.
static getTestSysop()
Convenience method for getting an immutable admin test user.
overrideMwServices(Config $configOverrides=null, array $services=[])
Stashes the global instance of MediaWikiServices, and installs a new one, allowing test cases to over...
mergeMwGlobalArrayValue( $name, $values)
Merges the given values into a MW global array variable.
setMwGlobals( $pairs, $value=null)
Sets a global, maintaining a stashed version of the previous global to be restored in tearDown.
static getTestUser( $groups=[])
Convenience method for getting an immutable test user.
assertSelect( $table, $fields, $condition, array $expectedRows, array $options=[], array $join_conds=[])
Asserts that the given database query yields the rows given by $expectedRows.
Represents information returned by WikiPage::prepareContentForEdit()
MediaWikiServices is the service locator for the application scope of MediaWiki.
Value object representing a content slot associated with a page revision.
Value object representing a modification of revision slots.
const FOR_PUBLIC
Definition Revision.php:54
const FOR_THIS_USER
Definition Revision.php:55
Maintenance script that runs pending jobs.
Definition runJobs.php:36
Represents a title within MediaWiki.
Definition Title.php:40
testDoEditContent_twice()
WikiPage::doEditContent.
testGetRedirectTarget( $title, $model, $text, $target)
provideGetRedirectTarget WikiPage::getRedirectTarget
testGetContent()
WikiPage::getContent.
newPage( $title, $model=null)
testPrepareContentForEdit()
WikiPage::prepareContentForEdit.
testExists()
WikiPage::exists.
assertPreparedEditNotEquals(PreparedEdit $edit, PreparedEdit $edit2, $message='')
testHasViewableContent( $title, $viewable, $create=false)
provideHasViewableContent WikiPage::hasViewableContent
testDoEditContent()
WikiPage::doEditContent WikiPage::prepareContentForEdit.
createPage( $page, $content, $model=null, $user=null)
assertPreparedEditEquals(PreparedEdit $edit, PreparedEdit $edit2, $message='')
defineMockContentModelForUpdateTesting( $name)
testGetRevision()
WikiPage::getRevision.
testDoDeleteArticleReal_user0()
WikiPage::doDeleteArticleReal.
testIsCountable( $title, $model, $text, $mode, $expected)
provideIsCountable WikiPage::isCountable
testDoDeleteArticle()
Undeletion is covered in PageArchiveTest::testUndeleteRevisions() TODO: Revision deletion.
testDoDeleteArticleReal_suppress()
TODO: Test more stuff about suppression.
testDoEditUpdates()
WikiPage::doEditUpdates.
testDoDeleteArticleReal_userSysop()
WikiPage::doDeleteArticleReal.
testIsRedirect( $title, $model, $text, $target)
provideGetRedirectTarget WikiPage::isRedirect
testDoDeleteUpdates()
WikiPage::doDeleteUpdates.
__construct( $name=null, array $data=[], $dataName='')
createMockContent(ContentHandler $handler, $text)
testGetParserOutput( $model, $text, $expectedHtml)
provideGetParserOutput WikiPage::getParserOutput
Class representing a MediaWiki article and history.
Definition WikiPage.php:45
$res
Definition database.txt:21
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check set to true or false to override the $wgMaxImageArea check result gives extension the possibility to transform it themselves $handler
Definition hooks.txt:894
presenting them properly to the user as errors is done by the caller return true use this to change the list i e rollback
Definition hooks.txt:1776
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition hooks.txt:783
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return true
Definition hooks.txt:2004
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action, or null $user:User who performed the tagging when the tagging is subsequent to the action, or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy: boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'ContentSecurityPolicyDefaultSource':Modify the allowed CSP load sources. This affects all directives except for the script directive. If you want to add a script source, see ContentSecurityPolicyScriptSource hook. & $defaultSrc:Array of Content-Security-Policy allowed sources $policyConfig:Current configuration for the Content-Security-Policy header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyDirectives':Modify the content security policy directives. Use this only if ContentSecurityPolicyDefaultSource and ContentSecurityPolicyScriptSource do not meet your needs. & $directives:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyScriptSource':Modify the allowed CSP script sources. Note that you also have to use ContentSecurityPolicyDefaultSource if you want non-script sources to be loaded from whatever you add. & $scriptSrc:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition hooks.txt:1032
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition hooks.txt:1779
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
$data
Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
const EDIT_UPDATE
Definition Defines.php:162
const CONTENT_MODEL_WIKITEXT
Definition Defines.php:244
const CONTENT_FORMAT_WIKITEXT
Definition Defines.php:259
const EDIT_NEW
Definition Defines.php:161
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
$page->newPageUpdater($user) $updater
$content
The First
Definition primes.txt:1
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:26