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