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 WikiPage $p */
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  null,
487  '127.0.0.1',
488  (string)$page->getTitle()->getNamespace(),
489  $page->getTitle()->getDBkey(),
490  ] ],
491  [],
492  $actorQuery['joins'] + $commentQuery['joins']
493  );
494  }
495 
500  $page = $this->createPage(
501  __METHOD__,
502  "[[original text]] foo",
504  );
505  $id = $page->getId();
506 
507  $user = $this->getTestSysop()->getUser();
508  $errorStack = '';
509  $status = $page->doDeleteArticleReal(
510  /* reason */ "testing sysop deletion",
511  /* suppress */ false,
512  /* unused 1 */ null,
513  /* unused 2 */ null,
514  /* errorStack */ $errorStack,
515  $user
516  );
517  $logId = $status->getValue();
518  $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
519  $commentQuery = MediaWikiServices::getInstance()->getCommentStore()->getJoin( 'log_comment' );
520  $this->assertSelect(
521  [ 'logging' ] + $actorQuery['tables'] + $commentQuery['tables'], /* table */
522  [
523  'log_type',
524  'log_action',
525  'log_comment' => $commentQuery['fields']['log_comment_text'],
526  'log_user' => $actorQuery['fields']['log_user'],
527  'log_user_text' => $actorQuery['fields']['log_user_text'],
528  'log_namespace',
529  'log_title',
530  ],
531  [ 'log_id' => $logId ],
532  [ [
533  'delete',
534  'delete',
535  'testing sysop deletion',
536  (string)$user->getId(),
537  $user->getName(),
538  (string)$page->getTitle()->getNamespace(),
539  $page->getTitle()->getDBkey(),
540  ] ],
541  [],
542  $actorQuery['joins'] + $commentQuery['joins']
543  );
544  }
545 
552  $page = $this->createPage(
553  __METHOD__,
554  "[[original text]] foo",
556  );
557  $id = $page->getId();
558 
559  $user = $this->getTestSysop()->getUser();
560  $errorStack = '';
561  $status = $page->doDeleteArticleReal(
562  /* reason */ "testing deletion",
563  /* suppress */ true,
564  /* unused 1 */ null,
565  /* unused 2 */ null,
566  /* errorStack */ $errorStack,
567  $user
568  );
569  $logId = $status->getValue();
570  $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
571  $commentQuery = MediaWikiServices::getInstance()->getCommentStore()->getJoin( 'log_comment' );
572  $this->assertSelect(
573  [ 'logging' ] + $actorQuery['tables'] + $commentQuery['tables'], /* table */
574  [
575  'log_type',
576  'log_action',
577  'log_comment' => $commentQuery['fields']['log_comment_text'],
578  'log_user' => $actorQuery['fields']['log_user'],
579  'log_user_text' => $actorQuery['fields']['log_user_text'],
580  'log_namespace',
581  'log_title',
582  ],
583  [ 'log_id' => $logId ],
584  [ [
585  'suppress',
586  'delete',
587  'testing deletion',
588  (string)$user->getId(),
589  $user->getName(),
590  (string)$page->getTitle()->getNamespace(),
591  $page->getTitle()->getDBkey(),
592  ] ],
593  [],
594  $actorQuery['joins'] + $commentQuery['joins']
595  );
596 
597  $this->assertNull(
598  $page->getContent( Revision::FOR_PUBLIC ),
599  "WikiPage::getContent should return null after the page was suppressed for general users"
600  );
601 
602  $this->assertNull(
603  $page->getContent( Revision::FOR_THIS_USER, null ),
604  "WikiPage::getContent should return null after the page was suppressed for user zero"
605  );
606 
607  $this->assertNull(
608  $page->getContent( Revision::FOR_THIS_USER, $user ),
609  "WikiPage::getContent should return null after the page was suppressed even for a sysop"
610  );
611  }
612 
616  public function testDoDeleteUpdates() {
617  $user = $this->getTestUser()->getUser();
618  $page = $this->createPage(
619  __METHOD__,
620  "[[original text]] foo",
622  );
623  $id = $page->getId();
624  $page->loadPageData(); // make sure the current revision is cached.
625 
626  // Similar to MovePage logic
627  wfGetDB( DB_MASTER )->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
628  $page->doDeleteUpdates( $page->getId(), $page->getContent(), $page->getRevision(), $user );
629 
630  // Run the job queue
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 
673  $this->mergeMwGlobalArrayValue(
674  'wgContentHandlers', [
675  $name => function () use ( $handler ){
676  return $handler;
677  }
678  ]
679  );
680 
681  return $handler;
682  }
683 
690  protected function createMockContent( ContentHandler $handler, $text ) {
692  $content = $this->getMockBuilder( TextContent::class )
693  ->setConstructorArgs( [ $text ] )
694  ->setMethods( [ 'getModel', 'getContentHandler' ] )
695  ->getMock();
696 
697  $content->method( 'getModel' )->willReturn( $handler->getModelID() );
698  $content->method( 'getContentHandler' )->willReturn( $handler );
699 
700  return $content;
701  }
702 
703  public function testGetDeletionUpdates() {
704  $m1 = $this->defineMockContentModelForUpdateTesting( 'M1' );
705 
706  $mainContent1 = $this->createMockContent( $m1, 'main 1' );
707 
708  $page = new WikiPage( Title::newFromText( __METHOD__ ) );
709  $page = $this->createPage(
710  $page,
711  [ 'main' => $mainContent1 ]
712  );
713 
714  $dataUpdates = $page->getDeletionUpdates( $page->getRevisionRecord() );
715  $this->assertNotEmpty( $dataUpdates );
716 
717  $updateNames = array_map( function ( $du ) {
718  return isset( $du->_name ) ? $du->_name : get_class( $du );
719  }, $dataUpdates );
720 
721  $this->assertContains( LinksDeletionUpdate::class, $updateNames );
722  $this->assertContains( 'M1 deletion update', $updateNames );
723  }
724 
728  public function testGetRevision() {
729  $page = $this->newPage( __METHOD__ );
730 
731  $rev = $page->getRevision();
732  $this->assertNull( $rev );
733 
734  # -----------------
735  $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
736 
737  $rev = $page->getRevision();
738 
739  $this->assertEquals( $page->getLatest(), $rev->getId() );
740  $this->assertEquals( "some text", $rev->getContent()->getText() );
741  }
742 
746  public function testGetContent() {
747  $page = $this->newPage( __METHOD__ );
748 
749  $content = $page->getContent();
750  $this->assertNull( $content );
751 
752  # -----------------
753  $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
754 
755  $content = $page->getContent();
756  $this->assertEquals( "some text", $content->getText() );
757  }
758 
762  public function testExists() {
763  $page = $this->newPage( __METHOD__ );
764  $this->assertFalse( $page->exists() );
765 
766  # -----------------
767  $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
768  $this->assertTrue( $page->exists() );
769 
770  $page = new WikiPage( $page->getTitle() );
771  $this->assertTrue( $page->exists() );
772 
773  # -----------------
774  $page->doDeleteArticle( "done testing" );
775  $this->assertFalse( $page->exists() );
776 
777  $page = new WikiPage( $page->getTitle() );
778  $this->assertFalse( $page->exists() );
779  }
780 
781  public function provideHasViewableContent() {
782  return [
783  [ 'WikiPageTest_testHasViewableContent', false, true ],
784  [ 'Special:WikiPageTest_testHasViewableContent', false ],
785  [ 'MediaWiki:WikiPageTest_testHasViewableContent', false ],
786  [ 'Special:Userlogin', true ],
787  [ 'MediaWiki:help', true ],
788  ];
789  }
790 
795  public function testHasViewableContent( $title, $viewable, $create = false ) {
796  $page = $this->newPage( $title );
797  $this->assertEquals( $viewable, $page->hasViewableContent() );
798 
799  if ( $create ) {
800  $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
801  $this->assertTrue( $page->hasViewableContent() );
802 
803  $page = new WikiPage( $page->getTitle() );
804  $this->assertTrue( $page->hasViewableContent() );
805  }
806  }
807 
808  public function provideGetRedirectTarget() {
809  return [
810  [ 'WikiPageTest_testGetRedirectTarget_1', CONTENT_MODEL_WIKITEXT, "hello world", null ],
811  [
812  'WikiPageTest_testGetRedirectTarget_2',
814  "#REDIRECT [[hello world]]",
815  "Hello world"
816  ],
817  // The below added to protect against Media namespace
818  // redirects which throw a fatal: (T203942)
819  [
820  'WikiPageTest_testGetRedirectTarget_3',
822  "#REDIRECT [[Media:hello_world]]",
823  "File:Hello world"
824  ],
825  // Test fragments longer than 255 bytes (T207876)
826  [
827  'WikiPageTest_testGetRedirectTarget_4',
829  // phpcs:ignore Generic.Files.LineLength
830  '#REDIRECT [[Foobar#🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿]]',
831  // phpcs:ignore Generic.Files.LineLength
832  'Foobar#🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬󠁦󠁲󠁿🏴󠁮󠁬...'
833  ]
834  ];
835  }
836 
841  public function testGetRedirectTarget( $title, $model, $text, $target ) {
842  $this->setMwGlobals( [
843  'wgCapitalLinks' => true,
844  ] );
845 
846  $page = $this->createPage( $title, $text, $model );
847 
848  # sanity check, because this test seems to fail for no reason for some people.
849  $c = $page->getContent();
850  $this->assertEquals( WikitextContent::class, get_class( $c ) );
851 
852  # now, test the actual redirect
853  $t = $page->getRedirectTarget();
854  $this->assertEquals( $target, $t ? $t->getFullText() : null );
855  }
856 
861  public function testIsRedirect( $title, $model, $text, $target ) {
862  $page = $this->createPage( $title, $text, $model );
863  $this->assertEquals( !is_null( $target ), $page->isRedirect() );
864  }
865 
866  public function provideIsCountable() {
867  return [
868 
869  // any
870  [ 'WikiPageTest_testIsCountable',
872  '',
873  'any',
874  true
875  ],
876  [ 'WikiPageTest_testIsCountable',
878  'Foo',
879  'any',
880  true
881  ],
882 
883  // link
884  [ 'WikiPageTest_testIsCountable',
886  'Foo',
887  'link',
888  false
889  ],
890  [ 'WikiPageTest_testIsCountable',
892  'Foo [[bar]]',
893  'link',
894  true
895  ],
896 
897  // redirects
898  [ 'WikiPageTest_testIsCountable',
900  '#REDIRECT [[bar]]',
901  'any',
902  false
903  ],
904  [ 'WikiPageTest_testIsCountable',
906  '#REDIRECT [[bar]]',
907  'link',
908  false
909  ],
910 
911  // not a content namespace
912  [ 'Talk:WikiPageTest_testIsCountable',
914  'Foo',
915  'any',
916  false
917  ],
918  [ 'Talk:WikiPageTest_testIsCountable',
920  'Foo [[bar]]',
921  'link',
922  false
923  ],
924 
925  // not a content namespace, different model
926  [ 'MediaWiki:WikiPageTest_testIsCountable.js',
927  null,
928  'Foo',
929  'any',
930  false
931  ],
932  [ 'MediaWiki:WikiPageTest_testIsCountable.js',
933  null,
934  'Foo [[bar]]',
935  'link',
936  false
937  ],
938  ];
939  }
940 
945  public function testIsCountable( $title, $model, $text, $mode, $expected ) {
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() );
1113  $c = $page->replaceSectionContent( $section, $content, $sectionTitle );
1114 
1115  $this->assertEquals( $expected, $c ? trim( $c->getText() ) : null );
1116  }
1117 
1122  public function testReplaceSectionAtRev( $title, $model, $text, $section,
1123  $with, $sectionTitle, $expected
1124  ) {
1125  $page = $this->createPage( $title, $text, $model );
1126  $baseRevId = $page->getLatest();
1127 
1128  $content = ContentHandler::makeContent( $with, $page->getTitle(), $page->getContentModel() );
1130  $c = $page->replaceSectionAtRev( $section, $content, $sectionTitle, $baseRevId );
1131 
1132  $this->assertEquals( $expected, $c ? trim( $c->getText() ) : null );
1133  }
1134 
1138  public function testGetOldestRevision() {
1139  $page = $this->newPage( __METHOD__ );
1140  $page->doEditContent(
1141  new WikitextContent( 'one' ),
1142  "first edit",
1143  EDIT_NEW
1144  );
1145  $rev1 = $page->getRevision();
1146 
1147  $page = new WikiPage( $page->getTitle() );
1148  $page->doEditContent(
1149  new WikitextContent( 'two' ),
1150  "second edit",
1151  EDIT_UPDATE
1152  );
1153 
1154  $page = new WikiPage( $page->getTitle() );
1155  $page->doEditContent(
1156  new WikitextContent( 'three' ),
1157  "third edit",
1158  EDIT_UPDATE
1159  );
1160 
1161  // sanity check
1162  $this->assertNotEquals(
1163  $rev1->getId(),
1164  $page->getRevision()->getId(),
1165  '$page->getRevision()->getId()'
1166  );
1167 
1168  // actual test
1169  $this->assertEquals(
1170  $rev1->getId(),
1171  $page->getOldestRevision()->getId(),
1172  '$page->getOldestRevision()->getId()'
1173  );
1174  }
1175 
1180  public function testDoRollback() {
1181  // FIXME: fails under postgres
1182  $this->markTestSkippedIfDbType( 'postgres' );
1183 
1184  $admin = $this->getTestSysop()->getUser();
1185  $user1 = $this->getTestUser()->getUser();
1186  // Use the confirmed group for user2 to make sure the user is different
1187  $user2 = $this->getTestUser( [ 'confirmed' ] )->getUser();
1188 
1189  // make sure we can test autopatrolling
1190  $this->setMwGlobals( 'wgUseRCPatrol', true );
1191 
1192  // TODO: MCR: test rollback of multiple slots!
1193  $page = $this->newPage( __METHOD__ );
1194 
1195  // Make some edits
1196  $text = "one";
1197  $status1 = $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ),
1198  "section one", EDIT_NEW, false, $admin );
1199 
1200  $text .= "\n\ntwo";
1201  $status2 = $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ),
1202  "adding section two", 0, false, $user1 );
1203 
1204  $text .= "\n\nthree";
1205  $status3 = $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ),
1206  "adding section three", 0, false, $user2 );
1207 
1211  $rev1 = $status1->getValue()['revision'];
1212  $rev2 = $status2->getValue()['revision'];
1213  $rev3 = $status3->getValue()['revision'];
1214 
1220  $this->assertEquals( 3, Revision::countByPageId( wfGetDB( DB_REPLICA ), $page->getId() ) );
1221  $this->assertEquals( $admin->getName(), $rev1->getUserText() );
1222  $this->assertEquals( $user1->getName(), $rev2->getUserText() );
1223  $this->assertEquals( $user2->getName(), $rev3->getUserText() );
1224 
1225  // Now, try the actual rollback
1226  $token = $admin->getEditToken( 'rollback' );
1227  $rollbackErrors = $page->doRollback(
1228  $user2->getName(),
1229  "testing rollback",
1230  $token,
1231  false,
1232  $resultDetails,
1233  $admin
1234  );
1235 
1236  if ( $rollbackErrors ) {
1237  $this->fail(
1238  "Rollback failed:\n" .
1239  print_r( $rollbackErrors, true ) . ";\n" .
1240  print_r( $resultDetails, true )
1241  );
1242  }
1243 
1244  $page = new WikiPage( $page->getTitle() );
1245  $this->assertEquals( $rev2->getSha1(), $page->getRevision()->getSha1(),
1246  "rollback did not revert to the correct revision" );
1247  $this->assertEquals( "one\n\ntwo", $page->getContent()->getText() );
1248 
1249  $rc = MediaWikiServices::getInstance()->getRevisionStore()->getRecentChange(
1250  $page->getRevision()->getRevisionRecord()
1251  );
1252 
1253  $this->assertNotNull( $rc, 'RecentChanges entry' );
1254  $this->assertEquals(
1255  RecentChange::PRC_AUTOPATROLLED,
1256  $rc->getAttribute( 'rc_patrolled' ),
1257  'rc_patrolled'
1258  );
1259 
1260  // TODO: MCR: assert origin once we write slot data
1261  // $mainSlot = $page->getRevision()->getRevisionRecord()->getSlot( SlotRecord::MAIN );
1262  // $this->assertTrue( $mainSlot->isInherited(), 'isInherited' );
1263  // $this->assertSame( $rev2->getId(), $mainSlot->getOrigin(), 'getOrigin' );
1264  }
1265 
1270  public function testDoRollbackFailureSameContent() {
1271  $admin = $this->getTestSysop()->getUser();
1272 
1273  $text = "one";
1274  $page = $this->newPage( __METHOD__ );
1275  $page->doEditContent(
1276  ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
1277  "section one",
1278  EDIT_NEW,
1279  false,
1280  $admin
1281  );
1282  $rev1 = $page->getRevision();
1283 
1284  $user1 = $this->getTestUser( [ 'sysop' ] )->getUser();
1285  $text .= "\n\ntwo";
1286  $page = new WikiPage( $page->getTitle() );
1287  $page->doEditContent(
1288  ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
1289  "adding section two",
1290  0,
1291  false,
1292  $user1
1293  );
1294 
1295  # now, do a the rollback from the same user was doing the edit before
1296  $resultDetails = [];
1297  $token = $user1->getEditToken( 'rollback' );
1298  $errors = $page->doRollback(
1299  $user1->getName(),
1300  "testing revert same user",
1301  $token,
1302  false,
1303  $resultDetails,
1304  $admin
1305  );
1306 
1307  $this->assertEquals( [], $errors, "Rollback failed same user" );
1308 
1309  # now, try the rollback
1310  $resultDetails = [];
1311  $token = $admin->getEditToken( 'rollback' );
1312  $errors = $page->doRollback(
1313  $user1->getName(),
1314  "testing revert",
1315  $token,
1316  false,
1317  $resultDetails,
1318  $admin
1319  );
1320 
1321  $this->assertEquals(
1322  [
1323  [
1324  'alreadyrolled',
1325  __METHOD__,
1326  $user1->getName(),
1327  $admin->getName(),
1328  ],
1329  ],
1330  $errors,
1331  "Rollback not failed"
1332  );
1333 
1334  $page = new WikiPage( $page->getTitle() );
1335  $this->assertEquals( $rev1->getSha1(), $page->getRevision()->getSha1(),
1336  "rollback did not revert to the correct revision" );
1337  $this->assertEquals( "one", $page->getContent()->getText() );
1338  }
1339 
1344  public function testDoRollbackTagging() {
1345  if ( !in_array( 'mw-rollback', ChangeTags::getSoftwareTags() ) ) {
1346  $this->markTestSkipped( 'Rollback tag deactivated, skipped the test.' );
1347  }
1348 
1349  $admin = new User();
1350  $admin->setName( 'Administrator' );
1351  $admin->addToDatabase();
1352 
1353  $text = 'First line';
1354  $page = $this->newPage( 'WikiPageTest_testDoRollbackTagging' );
1355  $page->doEditContent(
1356  ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
1357  'Added first line',
1358  EDIT_NEW,
1359  false,
1360  $admin
1361  );
1362 
1363  $secondUser = new User();
1364  $secondUser->setName( '92.65.217.32' );
1365  $text .= '\n\nSecond line';
1366  $page = new WikiPage( $page->getTitle() );
1367  $page->doEditContent(
1368  ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
1369  'Adding second line',
1370  0,
1371  false,
1372  $secondUser
1373  );
1374 
1375  // Now, try the rollback
1376  $admin->addGroup( 'sysop' ); // Make the test user a sysop
1377  MediaWikiServices::getInstance()->getPermissionManager()->invalidateUsersRightsCache();
1378  $token = $admin->getEditToken( 'rollback' );
1379  $errors = $page->doRollback(
1380  $secondUser->getName(),
1381  'testing rollback',
1382  $token,
1383  false,
1384  $resultDetails,
1385  $admin
1386  );
1387 
1388  // If doRollback completed without errors
1389  if ( $errors === [] ) {
1390  $tags = $resultDetails[ 'tags' ];
1391  $this->assertContains( 'mw-rollback', $tags );
1392  }
1393  }
1394 
1395  public function provideGetAutoDeleteReason() {
1396  return [
1397  [
1398  [],
1399  false,
1400  false
1401  ],
1402 
1403  [
1404  [
1405  [ "first edit", null ],
1406  ],
1407  "/first edit.*only contributor/",
1408  false
1409  ],
1410 
1411  [
1412  [
1413  [ "first edit", null ],
1414  [ "second edit", null ],
1415  ],
1416  "/second edit.*only contributor/",
1417  true
1418  ],
1419 
1420  [
1421  [
1422  [ "first edit", "127.0.2.22" ],
1423  [ "second edit", "127.0.3.33" ],
1424  ],
1425  "/second edit/",
1426  true
1427  ],
1428 
1429  [
1430  [
1431  [
1432  "first edit: "
1433  . "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam "
1434  . " nonumy eirmod tempor invidunt ut labore et dolore magna "
1435  . "aliquyam erat, sed diam voluptua. At vero eos et accusam "
1436  . "et justo duo dolores et ea rebum. Stet clita kasd gubergren, "
1437  . "no sea takimata sanctus est Lorem ipsum dolor sit amet. "
1438  . " this here is some more filler content added to try and "
1439  . "reach the maximum automatic summary length so that this is"
1440  . " truncated ipot sodit colrad ut ad olve amit basul dat"
1441  . "Dorbet romt crobit trop bri. DannyS712 put me here lor pe"
1442  . " ode quob zot bozro see also T22281 for background pol sup"
1443  . "Lorem ipsum dolor sit amet'",
1444  null
1445  ],
1446  ],
1447  '/first edit:.*\.\.\."/',
1448  false
1449  ],
1450 
1451  [
1452  [
1453  [ "first edit", "127.0.2.22" ],
1454  [ "", "127.0.3.33" ],
1455  ],
1456  "/before blanking.*first edit/",
1457  true
1458  ],
1459 
1460  ];
1461  }
1462 
1467  public function testGetAutoDeleteReason( $edits, $expectedResult, $expectedHistory ) {
1468  global $wgUser;
1469 
1470  // NOTE: assume Help namespace to contain wikitext
1471  $page = $this->newPage( "Help:WikiPageTest_testGetAutoDeleteReason" );
1472 
1473  $c = 1;
1474 
1475  foreach ( $edits as $edit ) {
1476  $user = new User();
1477 
1478  if ( !empty( $edit[1] ) ) {
1479  $user->setName( $edit[1] );
1480  } else {
1481  $user = $wgUser;
1482  }
1483 
1484  $content = ContentHandler::makeContent( $edit[0], $page->getTitle(), $page->getContentModel() );
1485 
1486  $page->doEditContent( $content, "test edit $c", $c < 2 ? EDIT_NEW : 0, false, $user );
1487 
1488  $c += 1;
1489  }
1490 
1491  $reason = $page->getAutoDeleteReason( $hasHistory );
1492 
1493  if ( is_bool( $expectedResult ) || is_null( $expectedResult ) ) {
1494  $this->assertEquals( $expectedResult, $reason );
1495  } else {
1496  $this->assertTrue( (bool)preg_match( $expectedResult, $reason ),
1497  "Autosummary didn't match expected pattern $expectedResult: $reason" );
1498  }
1499 
1500  $this->assertEquals( $expectedHistory, $hasHistory,
1501  "expected \$hasHistory to be " . var_export( $expectedHistory, true ) );
1502 
1503  $page->doDeleteArticle( "done" );
1504  }
1505 
1506  public function providePreSaveTransform() {
1507  return [
1508  [ 'hello this is ~~~',
1509  "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]",
1510  ],
1511  [ 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
1512  'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
1513  ],
1514  ];
1515  }
1516 
1520  public function testWikiPageFactory() {
1521  $title = Title::makeTitle( NS_FILE, 'Someimage.png' );
1522  $page = WikiPage::factory( $title );
1523  $this->assertEquals( WikiFilePage::class, get_class( $page ) );
1524 
1525  $title = Title::makeTitle( NS_CATEGORY, 'SomeCategory' );
1526  $page = WikiPage::factory( $title );
1527  $this->assertEquals( WikiCategoryPage::class, get_class( $page ) );
1528 
1529  $title = Title::makeTitle( NS_MAIN, 'SomePage' );
1530  $page = WikiPage::factory( $title );
1531  $this->assertEquals( WikiPage::class, get_class( $page ) );
1532  }
1533 
1538  public function testLoadPageData() {
1539  $title = Title::makeTitle( NS_MAIN, 'SomePage' );
1540  $page = WikiPage::factory( $title );
1541 
1542  $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
1543  $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
1544  $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
1545  $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
1546 
1547  $page->loadPageData( IDBAccessObject::READ_NORMAL );
1548  $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
1549  $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
1550  $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
1551  $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
1552 
1553  $page->loadPageData( IDBAccessObject::READ_LATEST );
1554  $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
1555  $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
1556  $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
1557  $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
1558 
1559  $page->loadPageData( IDBAccessObject::READ_LOCKING );
1560  $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
1561  $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
1562  $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
1563  $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
1564 
1565  $page->loadPageData( IDBAccessObject::READ_EXCLUSIVE );
1566  $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
1567  $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
1568  $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
1569  $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
1570  }
1571 
1575  public function testUpdateCategoryCounts() {
1576  $page = new WikiPage( Title::newFromText( __METHOD__ ) );
1577 
1578  // Add an initial category
1579  $page->updateCategoryCounts( [ 'A' ], [], 0 );
1580 
1581  $this->assertEquals( 1, Category::newFromName( 'A' )->getPageCount() );
1582  $this->assertEquals( 0, Category::newFromName( 'B' )->getPageCount() );
1583  $this->assertEquals( 0, Category::newFromName( 'C' )->getPageCount() );
1584 
1585  // Add a new category
1586  $page->updateCategoryCounts( [ 'B' ], [], 0 );
1587 
1588  $this->assertEquals( 1, Category::newFromName( 'A' )->getPageCount() );
1589  $this->assertEquals( 1, Category::newFromName( 'B' )->getPageCount() );
1590  $this->assertEquals( 0, Category::newFromName( 'C' )->getPageCount() );
1591 
1592  // Add and remove a category
1593  $page->updateCategoryCounts( [ 'C' ], [ 'A' ], 0 );
1594 
1595  $this->assertEquals( 0, Category::newFromName( 'A' )->getPageCount() );
1596  $this->assertEquals( 1, Category::newFromName( 'B' )->getPageCount() );
1597  $this->assertEquals( 1, Category::newFromName( 'C' )->getPageCount() );
1598  }
1599 
1600  public function provideUpdateRedirectOn() {
1601  yield [ '#REDIRECT [[Foo]]', true, null, true, true, 0 ];
1602  yield [ '#REDIRECT [[Foo]]', true, 'Foo', true, true, 1 ];
1603  yield [ 'SomeText', false, null, false, true, 0 ];
1604  yield [ 'SomeText', false, 'Foo', false, true, 1 ];
1605  }
1606 
1618  public function testUpdateRedirectOn(
1619  $initialText,
1620  $initialRedirectState,
1621  $redirectTitle,
1622  $lastRevIsRedirect,
1623  $expectedSuccess,
1624  $expectedRowCount
1625  ) {
1626  // FIXME: fails under sqlite and postgres
1627  $this->markTestSkippedIfDbType( 'sqlite' );
1628  $this->markTestSkippedIfDbType( 'postgres' );
1629  static $pageCounter = 0;
1630  $pageCounter++;
1631 
1632  $page = $this->createPage( Title::newFromText( __METHOD__ . $pageCounter ), $initialText );
1633  $this->assertSame( $initialRedirectState, $page->isRedirect() );
1634 
1635  $redirectTitle = is_string( $redirectTitle )
1636  ? Title::newFromText( $redirectTitle )
1637  : $redirectTitle;
1638 
1639  $success = $page->updateRedirectOn( $this->db, $redirectTitle, $lastRevIsRedirect );
1640  $this->assertSame( $expectedSuccess, $success, 'Success assertion' );
1646  $this->assertRedirectTableCountForPageId( $page->getId(), $expectedRowCount );
1647  }
1648 
1649  private function assertRedirectTableCountForPageId( $pageId, $expected ) {
1650  $this->assertSelect(
1651  'redirect',
1652  'COUNT(*)',
1653  [ 'rd_from' => $pageId ],
1654  [ [ strval( $expected ) ] ]
1655  );
1656  }
1657 
1661  public function testInsertRedirectEntry_insertsRedirectEntry() {
1662  $page = $this->createPage( Title::newFromText( __METHOD__ ), 'A' );
1663  $this->assertRedirectTableCountForPageId( $page->getId(), 0 );
1664 
1665  $targetTitle = Title::newFromText( 'SomeTarget#Frag' );
1666  $targetTitle->mInterwiki = 'eninter';
1667  $page->insertRedirectEntry( $targetTitle, null );
1668 
1669  $this->assertSelect(
1670  'redirect',
1671  [ 'rd_from', 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
1672  [ 'rd_from' => $page->getId() ],
1673  [ [
1674  strval( $page->getId() ),
1675  strval( $targetTitle->getNamespace() ),
1676  strval( $targetTitle->getDBkey() ),
1677  strval( $targetTitle->getFragment() ),
1678  strval( $targetTitle->getInterwiki() ),
1679  ] ]
1680  );
1681  }
1682 
1686  public function testInsertRedirectEntry_insertsRedirectEntryWithPageLatest() {
1687  $page = $this->createPage( Title::newFromText( __METHOD__ ), 'A' );
1688  $this->assertRedirectTableCountForPageId( $page->getId(), 0 );
1689 
1690  $targetTitle = Title::newFromText( 'SomeTarget#Frag' );
1691  $targetTitle->mInterwiki = 'eninter';
1692  $page->insertRedirectEntry( $targetTitle, $page->getLatest() );
1693 
1694  $this->assertSelect(
1695  'redirect',
1696  [ 'rd_from', 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
1697  [ 'rd_from' => $page->getId() ],
1698  [ [
1699  strval( $page->getId() ),
1700  strval( $targetTitle->getNamespace() ),
1701  strval( $targetTitle->getDBkey() ),
1702  strval( $targetTitle->getFragment() ),
1703  strval( $targetTitle->getInterwiki() ),
1704  ] ]
1705  );
1706  }
1707 
1711  public function testInsertRedirectEntry_doesNotInsertIfPageLatestIncorrect() {
1712  $page = $this->createPage( Title::newFromText( __METHOD__ ), 'A' );
1713  $this->assertRedirectTableCountForPageId( $page->getId(), 0 );
1714 
1715  $targetTitle = Title::newFromText( 'SomeTarget#Frag' );
1716  $targetTitle->mInterwiki = 'eninter';
1717  $page->insertRedirectEntry( $targetTitle, 215251 );
1718 
1719  $this->assertRedirectTableCountForPageId( $page->getId(), 0 );
1720  }
1721 
1722  private function getRow( array $overrides = [] ) {
1723  $row = [
1724  'page_id' => '44',
1725  'page_len' => '76',
1726  'page_is_redirect' => '1',
1727  'page_latest' => '99',
1728  'page_namespace' => '3',
1729  'page_title' => 'JaJaTitle',
1730  'page_restrictions' => 'edit=autoconfirmed,sysop:move=sysop',
1731  'page_touched' => '20120101020202',
1732  'page_links_updated' => '20140101020202',
1733  ];
1734  foreach ( $overrides as $key => $value ) {
1735  $row[$key] = $value;
1736  }
1737  return (object)$row;
1738  }
1739 
1740  public function provideNewFromRowSuccess() {
1741  yield 'basic row' => [
1742  $this->getRow(),
1743  function ( WikiPage $wikiPage, self $test ) {
1744  $test->assertSame( 44, $wikiPage->getId() );
1745  $test->assertSame( 76, $wikiPage->getTitle()->getLength() );
1746  $test->assertTrue( $wikiPage->isRedirect() );
1747  $test->assertSame( 99, $wikiPage->getLatest() );
1748  $test->assertSame( 3, $wikiPage->getTitle()->getNamespace() );
1749  $test->assertSame( 'JaJaTitle', $wikiPage->getTitle()->getDBkey() );
1750  $test->assertSame(
1751  [
1752  'edit' => [ 'autoconfirmed', 'sysop' ],
1753  'move' => [ 'sysop' ],
1754  ],
1755  $wikiPage->getTitle()->getAllRestrictions()
1756  );
1757  $test->assertSame( '20120101020202', $wikiPage->getTouched() );
1758  $test->assertSame( '20140101020202', $wikiPage->getLinksTimestamp() );
1759  }
1760  ];
1761  yield 'different timestamp formats' => [
1762  $this->getRow( [
1763  'page_touched' => '2012-01-01 02:02:02',
1764  'page_links_updated' => '2014-01-01 02:02:02',
1765  ] ),
1766  function ( WikiPage $wikiPage, self $test ) {
1767  $test->assertSame( '20120101020202', $wikiPage->getTouched() );
1768  $test->assertSame( '20140101020202', $wikiPage->getLinksTimestamp() );
1769  }
1770  ];
1771  yield 'no restrictions' => [
1772  $this->getRow( [
1773  'page_restrictions' => '',
1774  ] ),
1775  function ( WikiPage $wikiPage, self $test ) {
1776  $test->assertSame(
1777  [
1778  'edit' => [],
1779  'move' => [],
1780  ],
1781  $wikiPage->getTitle()->getAllRestrictions()
1782  );
1783  }
1784  ];
1785  yield 'not redirect' => [
1786  $this->getRow( [
1787  'page_is_redirect' => '0',
1788  ] ),
1789  function ( WikiPage $wikiPage, self $test ) {
1790  $test->assertFalse( $wikiPage->isRedirect() );
1791  }
1792  ];
1793  }
1794 
1803  public function testNewFromRow( $row, $assertions ) {
1804  $page = WikiPage::newFromRow( $row, 'fromdb' );
1805  $assertions( $page, $this );
1806  }
1807 
1808  public function provideTestNewFromId_returnsNullOnBadPageId() {
1809  yield[ 0 ];
1810  yield[ -11 ];
1811  }
1812 
1817  public function testNewFromId_returnsNullOnBadPageId( $pageId ) {
1818  $this->assertNull( WikiPage::newFromID( $pageId ) );
1819  }
1820 
1824  public function testNewFromId_appearsToFetchCorrectRow() {
1825  $createdPage = $this->createPage( __METHOD__, 'Xsfaij09' );
1826  $fetchedPage = WikiPage::newFromID( $createdPage->getId() );
1827  $this->assertSame( $createdPage->getId(), $fetchedPage->getId() );
1828  $this->assertEquals(
1829  $createdPage->getContent()->getText(),
1830  $fetchedPage->getContent()->getText()
1831  );
1832  }
1833 
1837  public function testNewFromId_returnsNullOnNonExistingId() {
1838  $this->assertNull( WikiPage::newFromID( 2147483647 ) );
1839  }
1840 
1841  public function provideTestInsertProtectNullRevision() {
1842  // phpcs:disable Generic.Files.LineLength
1843  yield [
1844  'goat-message-key',
1845  [ 'edit' => 'sysop' ],
1846  [ 'edit' => '20200101040404' ],
1847  false,
1848  'Goat Reason',
1849  true,
1850  '(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)))'
1851  ];
1852  yield [
1853  'goat-key',
1854  [ 'edit' => 'sysop', 'move' => 'something' ],
1855  [ 'edit' => '20200101040404', 'move' => '20210101050505' ],
1856  false,
1857  'Goat Goat',
1858  true,
1859  '(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)))'
1860  ];
1861  // phpcs:enable
1862  }
1863 
1877  public function testInsertProtectNullRevision(
1878  $revCommentMsg,
1879  array $limit,
1880  array $expiry,
1881  $cascade,
1882  $reason,
1883  $user,
1884  $expectedComment
1885  ) {
1886  $this->setContentLang( 'qqx' );
1887 
1888  $page = $this->createPage( __METHOD__, 'Goat' );
1889 
1890  $user = $user === null ? $user : $this->getTestSysop()->getUser();
1891 
1892  $result = $page->insertProtectNullRevision(
1893  $revCommentMsg,
1894  $limit,
1895  $expiry,
1896  $cascade,
1897  $reason,
1898  $user
1899  );
1900 
1901  $this->assertTrue( $result instanceof Revision );
1902  $this->assertSame( $expectedComment, $result->getComment( Revision::RAW ) );
1903  }
1904 
1908  public function testUpdateRevisionOn_existingPage() {
1909  $user = $this->getTestSysop()->getUser();
1910  $page = $this->createPage( __METHOD__, 'StartText' );
1911 
1912  $revision = new Revision(
1913  [
1914  'id' => 9989,
1915  'page' => $page->getId(),
1916  'title' => $page->getTitle(),
1917  'comment' => __METHOD__,
1918  'minor_edit' => true,
1919  'text' => __METHOD__ . '-text',
1920  'len' => strlen( __METHOD__ . '-text' ),
1921  'user' => $user->getId(),
1922  'user_text' => $user->getName(),
1923  'timestamp' => '20170707040404',
1924  'content_model' => CONTENT_MODEL_WIKITEXT,
1925  'content_format' => CONTENT_FORMAT_WIKITEXT,
1926  ]
1927  );
1928 
1929  $result = $page->updateRevisionOn( $this->db, $revision );
1930  $this->assertTrue( $result );
1931  $this->assertSame( 9989, $page->getLatest() );
1932  $this->assertEquals( $revision, $page->getRevision() );
1933  }
1934 
1938  public function testUpdateRevisionOn_NonExistingPage() {
1939  $user = $this->getTestSysop()->getUser();
1940  $page = $this->createPage( __METHOD__, 'StartText' );
1941  $page->doDeleteArticle( 'reason' );
1942 
1943  $revision = new Revision(
1944  [
1945  'id' => 9989,
1946  'page' => $page->getId(),
1947  'title' => $page->getTitle(),
1948  'comment' => __METHOD__,
1949  'minor_edit' => true,
1950  'text' => __METHOD__ . '-text',
1951  'len' => strlen( __METHOD__ . '-text' ),
1952  'user' => $user->getId(),
1953  'user_text' => $user->getName(),
1954  'timestamp' => '20170707040404',
1955  'content_model' => CONTENT_MODEL_WIKITEXT,
1956  'content_format' => CONTENT_FORMAT_WIKITEXT,
1957  ]
1958  );
1959 
1960  $result = $page->updateRevisionOn( $this->db, $revision );
1961  $this->assertFalse( $result );
1962  }
1963 
1967  public function testUpdateIfNewerOn_olderRevision() {
1968  $user = $this->getTestSysop()->getUser();
1969  $page = $this->createPage( __METHOD__, 'StartText' );
1970  $initialRevision = $page->getRevision();
1971 
1972  $olderTimeStamp = wfTimestamp(
1973  TS_MW,
1974  wfTimestamp( TS_UNIX, $initialRevision->getTimestamp() ) - 1
1975  );
1976 
1977  $olderRevision = new Revision(
1978  [
1979  'id' => 9989,
1980  'page' => $page->getId(),
1981  'title' => $page->getTitle(),
1982  'comment' => __METHOD__,
1983  'minor_edit' => true,
1984  'text' => __METHOD__ . '-text',
1985  'len' => strlen( __METHOD__ . '-text' ),
1986  'user' => $user->getId(),
1987  'user_text' => $user->getName(),
1988  'timestamp' => $olderTimeStamp,
1989  'content_model' => CONTENT_MODEL_WIKITEXT,
1990  'content_format' => CONTENT_FORMAT_WIKITEXT,
1991  ]
1992  );
1993 
1994  $result = $page->updateIfNewerOn( $this->db, $olderRevision );
1995  $this->assertFalse( $result );
1996  }
1997 
2001  public function testUpdateIfNewerOn_newerRevision() {
2002  $user = $this->getTestSysop()->getUser();
2003  $page = $this->createPage( __METHOD__, 'StartText' );
2004  $initialRevision = $page->getRevision();
2005 
2006  $newerTimeStamp = wfTimestamp(
2007  TS_MW,
2008  wfTimestamp( TS_UNIX, $initialRevision->getTimestamp() ) + 1
2009  );
2010 
2011  $newerRevision = new Revision(
2012  [
2013  'id' => 9989,
2014  'page' => $page->getId(),
2015  'title' => $page->getTitle(),
2016  'comment' => __METHOD__,
2017  'minor_edit' => true,
2018  'text' => __METHOD__ . '-text',
2019  'len' => strlen( __METHOD__ . '-text' ),
2020  'user' => $user->getId(),
2021  'user_text' => $user->getName(),
2022  'timestamp' => $newerTimeStamp,
2023  'content_model' => CONTENT_MODEL_WIKITEXT,
2024  'content_format' => CONTENT_FORMAT_WIKITEXT,
2025  ]
2026  );
2027  $result = $page->updateIfNewerOn( $this->db, $newerRevision );
2028  $this->assertTrue( $result );
2029  }
2030 
2034  public function testInsertOn() {
2035  $title = Title::newFromText( __METHOD__ );
2036  $page = new WikiPage( $title );
2037 
2038  $startTimeStamp = wfTimestampNow();
2039  $result = $page->insertOn( $this->db );
2040  $endTimeStamp = wfTimestampNow();
2041 
2042  $this->assertInternalType( 'int', $result );
2043  $this->assertTrue( $result > 0 );
2044 
2045  $condition = [ 'page_id' => $result ];
2046 
2047  // Check the default fields have been filled
2048  $this->assertSelect(
2049  'page',
2050  [
2051  'page_namespace',
2052  'page_title',
2053  'page_restrictions',
2054  'page_is_redirect',
2055  'page_is_new',
2056  'page_latest',
2057  'page_len',
2058  ],
2059  $condition,
2060  [ [
2061  '0',
2062  __METHOD__,
2063  '',
2064  '0',
2065  '1',
2066  '0',
2067  '0',
2068  ] ]
2069  );
2070 
2071  // Check the page_random field has been filled
2072  $pageRandom = $this->db->selectField( 'page', 'page_random', $condition );
2073  $this->assertTrue( (float)$pageRandom < 1 && (float)$pageRandom > 0 );
2074 
2075  // Assert the touched timestamp in the DB is roughly when we inserted the page
2076  $pageTouched = $this->db->selectField( 'page', 'page_touched', $condition );
2077  $this->assertTrue(
2078  wfTimestamp( TS_UNIX, $startTimeStamp )
2079  <= wfTimestamp( TS_UNIX, $pageTouched )
2080  );
2081  $this->assertTrue(
2082  wfTimestamp( TS_UNIX, $endTimeStamp )
2083  >= wfTimestamp( TS_UNIX, $pageTouched )
2084  );
2085 
2086  // Try inserting the same page again and checking the result is false (no change)
2087  $result = $page->insertOn( $this->db );
2088  $this->assertFalse( $result );
2089  }
2090 
2094  public function testInsertOn_idSpecified() {
2095  $title = Title::newFromText( __METHOD__ );
2096  $page = new WikiPage( $title );
2097  $id = 1478952189;
2098 
2099  $result = $page->insertOn( $this->db, $id );
2100 
2101  $this->assertSame( $id, $result );
2102 
2103  $condition = [ 'page_id' => $result ];
2104 
2105  // Check there is actually a row in the db
2106  $this->assertSelect(
2107  'page',
2108  [ 'page_title' ],
2109  $condition,
2110  [ [ __METHOD__ ] ]
2111  );
2112  }
2113 
2114  public function provideTestDoUpdateRestrictions_setBasicRestrictions() {
2115  // Note: Once the current dates passes the date in these tests they will fail.
2116  yield 'move something' => [
2117  true,
2118  [ 'move' => 'something' ],
2119  [],
2120  [ 'edit' => [], 'move' => [ 'something' ] ],
2121  [],
2122  ];
2123  yield 'move something, edit blank' => [
2124  true,
2125  [ 'move' => 'something', 'edit' => '' ],
2126  [],
2127  [ 'edit' => [], 'move' => [ 'something' ] ],
2128  [],
2129  ];
2130  yield 'edit sysop, with expiry' => [
2131  true,
2132  [ 'edit' => 'sysop' ],
2133  [ 'edit' => '21330101020202' ],
2134  [ 'edit' => [ 'sysop' ], 'move' => [] ],
2135  [ 'edit' => '21330101020202' ],
2136  ];
2137  yield 'move and edit, move with expiry' => [
2138  true,
2139  [ 'move' => 'something', 'edit' => 'another' ],
2140  [ 'move' => '22220202010101' ],
2141  [ 'edit' => [ 'another' ], 'move' => [ 'something' ] ],
2142  [ 'move' => '22220202010101' ],
2143  ];
2144  yield 'move and edit, edit with infinity expiry' => [
2145  true,
2146  [ 'move' => 'something', 'edit' => 'another' ],
2147  [ 'edit' => 'infinity' ],
2148  [ 'edit' => [ 'another' ], 'move' => [ 'something' ] ],
2149  [ 'edit' => 'infinity' ],
2150  ];
2151  yield 'non existing, create something' => [
2152  false,
2153  [ 'create' => 'something' ],
2154  [],
2155  [ 'create' => [ 'something' ] ],
2156  [],
2157  ];
2158  yield 'non existing, create something with expiry' => [
2159  false,
2160  [ 'create' => 'something' ],
2161  [ 'create' => '23451212112233' ],
2162  [ 'create' => [ 'something' ] ],
2163  [ 'create' => '23451212112233' ],
2164  ];
2165  }
2166 
2171  public function testDoUpdateRestrictions_setBasicRestrictions(
2172  $pageExists,
2173  array $limit,
2174  array $expiry,
2175  array $expectedRestrictions,
2176  array $expectedRestrictionExpiries
2177  ) {
2178  if ( $pageExists ) {
2179  $page = $this->createPage( __METHOD__, 'ABC' );
2180  } else {
2181  $page = new WikiPage( Title::newFromText( __METHOD__ . '-nonexist' ) );
2182  }
2183  $user = $this->getTestSysop()->getUser();
2184  $cascade = false;
2185 
2186  $status = $page->doUpdateRestrictions( $limit, $expiry, $cascade, 'aReason', $user, [] );
2187 
2188  $logId = $status->getValue();
2189  $allRestrictions = $page->getTitle()->getAllRestrictions();
2190 
2191  $this->assertTrue( $status->isGood() );
2192  $this->assertInternalType( 'int', $logId );
2193  $this->assertSame( $expectedRestrictions, $allRestrictions );
2194  foreach ( $expectedRestrictionExpiries as $key => $value ) {
2195  $this->assertSame( $value, $page->getTitle()->getRestrictionExpiry( $key ) );
2196  }
2197 
2198  // Make sure the log entry looks good
2199  // log_params is not checked here
2200  $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
2201  $commentQuery = MediaWikiServices::getInstance()->getCommentStore()->getJoin( 'log_comment' );
2202  $this->assertSelect(
2203  [ 'logging' ] + $actorQuery['tables'] + $commentQuery['tables'],
2204  [
2205  'log_comment' => $commentQuery['fields']['log_comment_text'],
2206  'log_user' => $actorQuery['fields']['log_user'],
2207  'log_user_text' => $actorQuery['fields']['log_user_text'],
2208  'log_namespace',
2209  'log_title',
2210  ],
2211  [ 'log_id' => $logId ],
2212  [ [
2213  'aReason',
2214  (string)$user->getId(),
2215  $user->getName(),
2216  (string)$page->getTitle()->getNamespace(),
2217  $page->getTitle()->getDBkey(),
2218  ] ],
2219  [],
2220  $actorQuery['joins'] + $commentQuery['joins']
2221  );
2222  }
2223 
2227  public function testDoUpdateRestrictions_failsOnReadOnly() {
2228  $page = $this->createPage( __METHOD__, 'ABC' );
2229  $user = $this->getTestSysop()->getUser();
2230  $cascade = false;
2231 
2232  // Set read only
2233  $readOnly = $this->getMockBuilder( ReadOnlyMode::class )
2234  ->disableOriginalConstructor()
2235  ->setMethods( [ 'isReadOnly', 'getReason' ] )
2236  ->getMock();
2237  $readOnly->expects( $this->once() )
2238  ->method( 'isReadOnly' )
2239  ->will( $this->returnValue( true ) );
2240  $readOnly->expects( $this->once() )
2241  ->method( 'getReason' )
2242  ->will( $this->returnValue( 'Some Read Only Reason' ) );
2243  $this->setService( 'ReadOnlyMode', $readOnly );
2244 
2245  $status = $page->doUpdateRestrictions( [], [], $cascade, 'aReason', $user, [] );
2246  $this->assertFalse( $status->isOK() );
2247  $this->assertSame( 'readonlytext', $status->getMessage()->getKey() );
2248  }
2249 
2253  public function testDoUpdateRestrictions_returnsGoodIfNothingChanged() {
2254  $page = $this->createPage( __METHOD__, 'ABC' );
2255  $user = $this->getTestSysop()->getUser();
2256  $cascade = false;
2257  $limit = [ 'edit' => 'sysop' ];
2258 
2259  $status = $page->doUpdateRestrictions(
2260  $limit,
2261  [],
2262  $cascade,
2263  'aReason',
2264  $user,
2265  []
2266  );
2267 
2268  // The first entry should have a logId as it did something
2269  $this->assertTrue( $status->isGood() );
2270  $this->assertInternalType( 'int', $status->getValue() );
2271 
2272  $status = $page->doUpdateRestrictions(
2273  $limit,
2274  [],
2275  $cascade,
2276  'aReason',
2277  $user,
2278  []
2279  );
2280 
2281  // The second entry should not have a logId as nothing changed
2282  $this->assertTrue( $status->isGood() );
2283  $this->assertNull( $status->getValue() );
2284  }
2285 
2289  public function testDoUpdateRestrictions_logEntryTypeAndAction() {
2290  $page = $this->createPage( __METHOD__, 'ABC' );
2291  $user = $this->getTestSysop()->getUser();
2292  $cascade = false;
2293 
2294  // Protect the page
2295  $status = $page->doUpdateRestrictions(
2296  [ 'edit' => 'sysop' ],
2297  [],
2298  $cascade,
2299  'aReason',
2300  $user,
2301  []
2302  );
2303  $this->assertTrue( $status->isGood() );
2304  $this->assertInternalType( 'int', $status->getValue() );
2305  $this->assertSelect(
2306  'logging',
2307  [ 'log_type', 'log_action' ],
2308  [ 'log_id' => $status->getValue() ],
2309  [ [ 'protect', 'protect' ] ]
2310  );
2311 
2312  // Modify the protection
2313  $status = $page->doUpdateRestrictions(
2314  [ 'edit' => 'somethingElse' ],
2315  [],
2316  $cascade,
2317  'aReason',
2318  $user,
2319  []
2320  );
2321  $this->assertTrue( $status->isGood() );
2322  $this->assertInternalType( 'int', $status->getValue() );
2323  $this->assertSelect(
2324  'logging',
2325  [ 'log_type', 'log_action' ],
2326  [ 'log_id' => $status->getValue() ],
2327  [ [ 'protect', 'modify' ] ]
2328  );
2329 
2330  // Remove the protection
2331  $status = $page->doUpdateRestrictions(
2332  [],
2333  [],
2334  $cascade,
2335  'aReason',
2336  $user,
2337  []
2338  );
2339  $this->assertTrue( $status->isGood() );
2340  $this->assertInternalType( 'int', $status->getValue() );
2341  $this->assertSelect(
2342  'logging',
2343  [ 'log_type', 'log_action' ],
2344  [ 'log_id' => $status->getValue() ],
2345  [ [ 'protect', 'unprotect' ] ]
2346  );
2347  }
2348 
2353  public function testNewPageUpdater() {
2354  $user = $this->getTestUser()->getUser();
2355  $page = $this->newPage( __METHOD__, __METHOD__ );
2356 
2358  $content = $this->getMockBuilder( WikitextContent::class )
2359  ->setConstructorArgs( [ 'Hello World' ] )
2360  ->setMethods( [ 'getParserOutput' ] )
2361  ->getMock();
2362  $content->expects( $this->once() )
2363  ->method( 'getParserOutput' )
2364  ->willReturn( new ParserOutput( 'HTML' ) );
2365 
2366  $preparedEditBefore = $page->prepareContentForEdit( $content, null, $user );
2367 
2368  // provide context, so the cache can be kept in place
2369  $slotsUpdate = new revisionSlotsUpdate();
2370  $slotsUpdate->modifyContent( SlotRecord::MAIN, $content );
2371 
2372  $updater = $page->newPageUpdater( $user, $slotsUpdate );
2373  $updater->setContent( SlotRecord::MAIN, $content );
2374  $revision = $updater->saveRevision(
2375  CommentStoreComment::newUnsavedComment( 'test' ),
2376  EDIT_NEW
2377  );
2378 
2379  $preparedEditAfter = $page->prepareContentForEdit( $content, $revision, $user );
2380 
2381  $this->assertSame( $revision->getId(), $page->getLatest() );
2382 
2383  // Parsed output must remain cached throughout.
2384  $this->assertSame( $preparedEditBefore->output, $preparedEditAfter->output );
2385  }
2386 
2391  public function testGetDerivedDataUpdater() {
2392  $admin = $this->getTestSysop()->getUser();
2393 
2395  $page = $this->createPage( __METHOD__, __METHOD__ );
2396  $page = TestingAccessWrapper::newFromObject( $page );
2397 
2398  $revision = $page->getRevision()->getRevisionRecord();
2399  $user = $revision->getUser();
2400 
2401  $slotsUpdate = new RevisionSlotsUpdate();
2402  $slotsUpdate->modifyContent( SlotRecord::MAIN, new WikitextContent( 'Hello World' ) );
2403 
2404  // get a virgin updater
2405  $updater1 = $page->getDerivedDataUpdater( $user );
2406  $this->assertFalse( $updater1->isUpdatePrepared() );
2407 
2408  $updater1->prepareUpdate( $revision );
2409 
2410  // Re-use updater with same revision or content, even if base changed
2411  $this->assertSame( $updater1, $page->getDerivedDataUpdater( $user, $revision ) );
2412 
2413  $slotsUpdate = RevisionSlotsUpdate::newFromContent(
2414  [ SlotRecord::MAIN => $revision->getContent( SlotRecord::MAIN ) ]
2415  );
2416  $this->assertSame( $updater1, $page->getDerivedDataUpdater( $user, null, $slotsUpdate ) );
2417 
2418  // Don't re-use for edit if base revision ID changed
2419  $this->assertNotSame(
2420  $updater1,
2421  $page->getDerivedDataUpdater( $user, null, $slotsUpdate, true )
2422  );
2423 
2424  // Don't re-use with different user
2425  $updater2a = $page->getDerivedDataUpdater( $admin, null, $slotsUpdate );
2426  $updater2a->prepareContent( $admin, $slotsUpdate, false );
2427 
2428  $updater2b = $page->getDerivedDataUpdater( $user, null, $slotsUpdate );
2429  $updater2b->prepareContent( $user, $slotsUpdate, false );
2430  $this->assertNotSame( $updater2a, $updater2b );
2431 
2432  // Don't re-use with different content
2433  $updater3 = $page->getDerivedDataUpdater( $admin, null, $slotsUpdate );
2434  $updater3->prepareUpdate( $revision );
2435  $this->assertNotSame( $updater2b, $updater3 );
2436 
2437  // Don't re-use if no context given
2438  $updater4 = $page->getDerivedDataUpdater( $admin );
2439  $updater4->prepareUpdate( $revision );
2440  $this->assertNotSame( $updater3, $updater4 );
2441 
2442  // Don't re-use if AGAIN no context given
2443  $updater5 = $page->getDerivedDataUpdater( $admin );
2444  $this->assertNotSame( $updater4, $updater5 );
2445 
2446  // Don't re-use cached "virgin" unprepared updater
2447  $updater6 = $page->getDerivedDataUpdater( $admin, $revision );
2448  $this->assertNotSame( $updater5, $updater6 );
2449  }
2450 
2451  protected function assertPreparedEditEquals(
2452  PreparedEdit $edit, PreparedEdit $edit2, $message = ''
2453  ) {
2454  // suppress differences caused by a clock tick between generating the two PreparedEdits
2455  if ( abs( $edit->timestamp - $edit2->timestamp ) < 3 ) {
2456  $edit2 = clone $edit2;
2457  $edit2->timestamp = $edit->timestamp;
2458  }
2459  $this->assertEquals( $edit, $edit2, $message );
2460  }
2461 
2462  protected function assertPreparedEditNotEquals(
2463  PreparedEdit $edit, PreparedEdit $edit2, $message = ''
2464  ) {
2465  if ( abs( $edit->timestamp - $edit2->timestamp ) < 3 ) {
2466  $edit2 = clone $edit2;
2467  $edit2->timestamp = $edit->timestamp;
2468  }
2469  $this->assertNotEquals( $edit, $edit2, $message );
2470  }
2471 
2472 }
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:138
const FOR_THIS_USER
Definition: Revision.php:55
testIsRedirect( $title, $model, $text, $target)
provideGetRedirectTarget WikiPage::isRedirect
const CONTENT_MODEL_WIKITEXT
Definition: Defines.php:215
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:193
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)
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:181
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:1759
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
const EDIT_UPDATE
Definition: Defines.php:133
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...
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check set to true or false to override the $wgMaxImageArea check result gives extension the possibility to transform it themselves $handler
Definition: hooks.txt:773
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:1983
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 '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:1250
static newMigration()
Static constructor.
testPrepareContentForEdit()
WikiPage::prepareContentForEdit.
const FOR_PUBLIC
Definition: Revision.php:54
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:39
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:773
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:918
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:1759
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
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:132
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:277
const CONTENT_FORMAT_WIKITEXT
Definition: Defines.php:230
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
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:1460
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:322