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