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 
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  $token = $admin->getEditToken( 'rollback' );
1378  $errors = $page->doRollback(
1379  $secondUser->getName(),
1380  'testing rollback',
1381  $token,
1382  false,
1383  $resultDetails,
1384  $admin
1385  );
1386 
1387  // If doRollback completed without errors
1388  if ( $errors === [] ) {
1389  $tags = $resultDetails[ 'tags' ];
1390  $this->assertContains( 'mw-rollback', $tags );
1391  }
1392  }
1393 
1394  public function provideGetAutoDeleteReason() {
1395  return [
1396  [
1397  [],
1398  false,
1399  false
1400  ],
1401 
1402  [
1403  [
1404  [ "first edit", null ],
1405  ],
1406  "/first edit.*only contributor/",
1407  false
1408  ],
1409 
1410  [
1411  [
1412  [ "first edit", null ],
1413  [ "second edit", null ],
1414  ],
1415  "/second edit.*only contributor/",
1416  true
1417  ],
1418 
1419  [
1420  [
1421  [ "first edit", "127.0.2.22" ],
1422  [ "second edit", "127.0.3.33" ],
1423  ],
1424  "/second edit/",
1425  true
1426  ],
1427 
1428  [
1429  [
1430  [
1431  "first edit: "
1432  . "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam "
1433  . " nonumy eirmod tempor invidunt ut labore et dolore magna "
1434  . "aliquyam erat, sed diam voluptua. At vero eos et accusam "
1435  . "et justo duo dolores et ea rebum. Stet clita kasd gubergren, "
1436  . "no sea takimata sanctus est Lorem ipsum dolor sit amet.'",
1437  null
1438  ],
1439  ],
1440  '/first edit:.*\.\.\."/',
1441  false
1442  ],
1443 
1444  [
1445  [
1446  [ "first edit", "127.0.2.22" ],
1447  [ "", "127.0.3.33" ],
1448  ],
1449  "/before blanking.*first edit/",
1450  true
1451  ],
1452 
1453  ];
1454  }
1455 
1460  public function testGetAutoDeleteReason( $edits, $expectedResult, $expectedHistory ) {
1461  global $wgUser;
1462 
1463  // NOTE: assume Help namespace to contain wikitext
1464  $page = $this->newPage( "Help:WikiPageTest_testGetAutoDeleteReason" );
1465 
1466  $c = 1;
1467 
1468  foreach ( $edits as $edit ) {
1469  $user = new User();
1470 
1471  if ( !empty( $edit[1] ) ) {
1472  $user->setName( $edit[1] );
1473  } else {
1474  $user = $wgUser;
1475  }
1476 
1477  $content = ContentHandler::makeContent( $edit[0], $page->getTitle(), $page->getContentModel() );
1478 
1479  $page->doEditContent( $content, "test edit $c", $c < 2 ? EDIT_NEW : 0, false, $user );
1480 
1481  $c += 1;
1482  }
1483 
1484  $reason = $page->getAutoDeleteReason( $hasHistory );
1485 
1486  if ( is_bool( $expectedResult ) || is_null( $expectedResult ) ) {
1487  $this->assertEquals( $expectedResult, $reason );
1488  } else {
1489  $this->assertTrue( (bool)preg_match( $expectedResult, $reason ),
1490  "Autosummary didn't match expected pattern $expectedResult: $reason" );
1491  }
1492 
1493  $this->assertEquals( $expectedHistory, $hasHistory,
1494  "expected \$hasHistory to be " . var_export( $expectedHistory, true ) );
1495 
1496  $page->doDeleteArticle( "done" );
1497  }
1498 
1499  public function providePreSaveTransform() {
1500  return [
1501  [ 'hello this is ~~~',
1502  "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]",
1503  ],
1504  [ 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
1505  'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
1506  ],
1507  ];
1508  }
1509 
1513  public function testWikiPageFactory() {
1514  $title = Title::makeTitle( NS_FILE, 'Someimage.png' );
1515  $page = WikiPage::factory( $title );
1516  $this->assertEquals( WikiFilePage::class, get_class( $page ) );
1517 
1518  $title = Title::makeTitle( NS_CATEGORY, 'SomeCategory' );
1519  $page = WikiPage::factory( $title );
1520  $this->assertEquals( WikiCategoryPage::class, get_class( $page ) );
1521 
1522  $title = Title::makeTitle( NS_MAIN, 'SomePage' );
1523  $page = WikiPage::factory( $title );
1524  $this->assertEquals( WikiPage::class, get_class( $page ) );
1525  }
1526 
1531  public function testLoadPageData() {
1532  $title = Title::makeTitle( NS_MAIN, 'SomePage' );
1533  $page = WikiPage::factory( $title );
1534 
1535  $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
1536  $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
1537  $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
1538  $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
1539 
1540  $page->loadPageData( IDBAccessObject::READ_NORMAL );
1541  $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
1542  $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
1543  $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
1544  $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
1545 
1546  $page->loadPageData( IDBAccessObject::READ_LATEST );
1547  $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
1548  $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
1549  $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
1550  $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
1551 
1552  $page->loadPageData( IDBAccessObject::READ_LOCKING );
1553  $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
1554  $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
1555  $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
1556  $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
1557 
1558  $page->loadPageData( IDBAccessObject::READ_EXCLUSIVE );
1559  $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
1560  $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
1561  $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
1562  $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
1563  }
1564 
1568  public function testUpdateCategoryCounts() {
1569  $page = new WikiPage( Title::newFromText( __METHOD__ ) );
1570 
1571  // Add an initial category
1572  $page->updateCategoryCounts( [ 'A' ], [], 0 );
1573 
1574  $this->assertEquals( 1, Category::newFromName( 'A' )->getPageCount() );
1575  $this->assertEquals( 0, Category::newFromName( 'B' )->getPageCount() );
1576  $this->assertEquals( 0, Category::newFromName( 'C' )->getPageCount() );
1577 
1578  // Add a new category
1579  $page->updateCategoryCounts( [ 'B' ], [], 0 );
1580 
1581  $this->assertEquals( 1, Category::newFromName( 'A' )->getPageCount() );
1582  $this->assertEquals( 1, Category::newFromName( 'B' )->getPageCount() );
1583  $this->assertEquals( 0, Category::newFromName( 'C' )->getPageCount() );
1584 
1585  // Add and remove a category
1586  $page->updateCategoryCounts( [ 'C' ], [ 'A' ], 0 );
1587 
1588  $this->assertEquals( 0, Category::newFromName( 'A' )->getPageCount() );
1589  $this->assertEquals( 1, Category::newFromName( 'B' )->getPageCount() );
1590  $this->assertEquals( 1, Category::newFromName( 'C' )->getPageCount() );
1591  }
1592 
1593  public function provideUpdateRedirectOn() {
1594  yield [ '#REDIRECT [[Foo]]', true, null, true, true, 0 ];
1595  yield [ '#REDIRECT [[Foo]]', true, 'Foo', true, false, 1 ];
1596  yield [ 'SomeText', false, null, false, true, 0 ];
1597  yield [ 'SomeText', false, 'Foo', false, false, 1 ];
1598  }
1599 
1611  public function testUpdateRedirectOn(
1612  $initialText,
1613  $initialRedirectState,
1614  $redirectTitle,
1615  $lastRevIsRedirect,
1616  $expectedSuccess,
1617  $expectedRowCount
1618  ) {
1619  // FIXME: fails under sqlite and postgres
1620  $this->markTestSkippedIfDbType( 'sqlite' );
1621  $this->markTestSkippedIfDbType( 'postgres' );
1622  static $pageCounter = 0;
1623  $pageCounter++;
1624 
1625  $page = $this->createPage( Title::newFromText( __METHOD__ . $pageCounter ), $initialText );
1626  $this->assertSame( $initialRedirectState, $page->isRedirect() );
1627 
1628  $redirectTitle = is_string( $redirectTitle )
1629  ? Title::newFromText( $redirectTitle )
1630  : $redirectTitle;
1631 
1632  $success = $page->updateRedirectOn( $this->db, $redirectTitle, $lastRevIsRedirect );
1633  $this->assertSame( $expectedSuccess, $success, 'Success assertion' );
1639  $this->assertRedirectTableCountForPageId( $page->getId(), $expectedRowCount );
1640  }
1641 
1642  private function assertRedirectTableCountForPageId( $pageId, $expected ) {
1643  $this->assertSelect(
1644  'redirect',
1645  'COUNT(*)',
1646  [ 'rd_from' => $pageId ],
1647  [ [ strval( $expected ) ] ]
1648  );
1649  }
1650 
1654  public function testInsertRedirectEntry_insertsRedirectEntry() {
1655  $page = $this->createPage( Title::newFromText( __METHOD__ ), 'A' );
1656  $this->assertRedirectTableCountForPageId( $page->getId(), 0 );
1657 
1658  $targetTitle = Title::newFromText( 'SomeTarget#Frag' );
1659  $targetTitle->mInterwiki = 'eninter';
1660  $page->insertRedirectEntry( $targetTitle, null );
1661 
1662  $this->assertSelect(
1663  'redirect',
1664  [ 'rd_from', 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
1665  [ 'rd_from' => $page->getId() ],
1666  [ [
1667  strval( $page->getId() ),
1668  strval( $targetTitle->getNamespace() ),
1669  strval( $targetTitle->getDBkey() ),
1670  strval( $targetTitle->getFragment() ),
1671  strval( $targetTitle->getInterwiki() ),
1672  ] ]
1673  );
1674  }
1675 
1679  public function testInsertRedirectEntry_insertsRedirectEntryWithPageLatest() {
1680  $page = $this->createPage( Title::newFromText( __METHOD__ ), 'A' );
1681  $this->assertRedirectTableCountForPageId( $page->getId(), 0 );
1682 
1683  $targetTitle = Title::newFromText( 'SomeTarget#Frag' );
1684  $targetTitle->mInterwiki = 'eninter';
1685  $page->insertRedirectEntry( $targetTitle, $page->getLatest() );
1686 
1687  $this->assertSelect(
1688  'redirect',
1689  [ 'rd_from', 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
1690  [ 'rd_from' => $page->getId() ],
1691  [ [
1692  strval( $page->getId() ),
1693  strval( $targetTitle->getNamespace() ),
1694  strval( $targetTitle->getDBkey() ),
1695  strval( $targetTitle->getFragment() ),
1696  strval( $targetTitle->getInterwiki() ),
1697  ] ]
1698  );
1699  }
1700 
1704  public function testInsertRedirectEntry_doesNotInsertIfPageLatestIncorrect() {
1705  $page = $this->createPage( Title::newFromText( __METHOD__ ), 'A' );
1706  $this->assertRedirectTableCountForPageId( $page->getId(), 0 );
1707 
1708  $targetTitle = Title::newFromText( 'SomeTarget#Frag' );
1709  $targetTitle->mInterwiki = 'eninter';
1710  $page->insertRedirectEntry( $targetTitle, 215251 );
1711 
1712  $this->assertRedirectTableCountForPageId( $page->getId(), 0 );
1713  }
1714 
1715  private function getRow( array $overrides = [] ) {
1716  $row = [
1717  'page_id' => '44',
1718  'page_len' => '76',
1719  'page_is_redirect' => '1',
1720  'page_latest' => '99',
1721  'page_namespace' => '3',
1722  'page_title' => 'JaJaTitle',
1723  'page_restrictions' => 'edit=autoconfirmed,sysop:move=sysop',
1724  'page_touched' => '20120101020202',
1725  'page_links_updated' => '20140101020202',
1726  ];
1727  foreach ( $overrides as $key => $value ) {
1728  $row[$key] = $value;
1729  }
1730  return (object)$row;
1731  }
1732 
1733  public function provideNewFromRowSuccess() {
1734  yield 'basic row' => [
1735  $this->getRow(),
1736  function ( WikiPage $wikiPage, self $test ) {
1737  $test->assertSame( 44, $wikiPage->getId() );
1738  $test->assertSame( 76, $wikiPage->getTitle()->getLength() );
1739  $test->assertTrue( $wikiPage->isRedirect() );
1740  $test->assertSame( 99, $wikiPage->getLatest() );
1741  $test->assertSame( 3, $wikiPage->getTitle()->getNamespace() );
1742  $test->assertSame( 'JaJaTitle', $wikiPage->getTitle()->getDBkey() );
1743  $test->assertSame(
1744  [
1745  'edit' => [ 'autoconfirmed', 'sysop' ],
1746  'move' => [ 'sysop' ],
1747  ],
1748  $wikiPage->getTitle()->getAllRestrictions()
1749  );
1750  $test->assertSame( '20120101020202', $wikiPage->getTouched() );
1751  $test->assertSame( '20140101020202', $wikiPage->getLinksTimestamp() );
1752  }
1753  ];
1754  yield 'different timestamp formats' => [
1755  $this->getRow( [
1756  'page_touched' => '2012-01-01 02:02:02',
1757  'page_links_updated' => '2014-01-01 02:02:02',
1758  ] ),
1759  function ( WikiPage $wikiPage, self $test ) {
1760  $test->assertSame( '20120101020202', $wikiPage->getTouched() );
1761  $test->assertSame( '20140101020202', $wikiPage->getLinksTimestamp() );
1762  }
1763  ];
1764  yield 'no restrictions' => [
1765  $this->getRow( [
1766  'page_restrictions' => '',
1767  ] ),
1768  function ( WikiPage $wikiPage, self $test ) {
1769  $test->assertSame(
1770  [
1771  'edit' => [],
1772  'move' => [],
1773  ],
1774  $wikiPage->getTitle()->getAllRestrictions()
1775  );
1776  }
1777  ];
1778  yield 'not redirect' => [
1779  $this->getRow( [
1780  'page_is_redirect' => '0',
1781  ] ),
1782  function ( WikiPage $wikiPage, self $test ) {
1783  $test->assertFalse( $wikiPage->isRedirect() );
1784  }
1785  ];
1786  }
1787 
1796  public function testNewFromRow( $row, $assertions ) {
1797  $page = WikiPage::newFromRow( $row, 'fromdb' );
1798  $assertions( $page, $this );
1799  }
1800 
1801  public function provideTestNewFromId_returnsNullOnBadPageId() {
1802  yield[ 0 ];
1803  yield[ -11 ];
1804  }
1805 
1810  public function testNewFromId_returnsNullOnBadPageId( $pageId ) {
1811  $this->assertNull( WikiPage::newFromID( $pageId ) );
1812  }
1813 
1817  public function testNewFromId_appearsToFetchCorrectRow() {
1818  $createdPage = $this->createPage( __METHOD__, 'Xsfaij09' );
1819  $fetchedPage = WikiPage::newFromID( $createdPage->getId() );
1820  $this->assertSame( $createdPage->getId(), $fetchedPage->getId() );
1821  $this->assertEquals(
1822  $createdPage->getContent()->getText(),
1823  $fetchedPage->getContent()->getText()
1824  );
1825  }
1826 
1830  public function testNewFromId_returnsNullOnNonExistingId() {
1831  $this->assertNull( WikiPage::newFromID( 2147483647 ) );
1832  }
1833 
1834  public function provideTestInsertProtectNullRevision() {
1835  // phpcs:disable Generic.Files.LineLength
1836  yield [
1837  'goat-message-key',
1838  [ 'edit' => 'sysop' ],
1839  [ 'edit' => '20200101040404' ],
1840  false,
1841  'Goat Reason',
1842  true,
1843  '(goat-message-key: WikiPageDbTestBase::testInsertProtectNullRevision, UTSysop)(colon-separator)Goat Reason(word-separator)(parentheses: (protect-summary-desc: (restriction-edit), (protect-level-sysop), (protect-expiring: 04:04, 1 (january) 2020, 1 (january) 2020, 04:04)))'
1844  ];
1845  yield [
1846  'goat-key',
1847  [ 'edit' => 'sysop', 'move' => 'something' ],
1848  [ 'edit' => '20200101040404', 'move' => '20210101050505' ],
1849  false,
1850  'Goat Goat',
1851  true,
1852  '(goat-key: WikiPageDbTestBase::testInsertProtectNullRevision, UTSysop)(colon-separator)Goat Goat(word-separator)(parentheses: (protect-summary-desc: (restriction-edit), (protect-level-sysop), (protect-expiring: 04:04, 1 (january) 2020, 1 (january) 2020, 04:04))(word-separator)(protect-summary-desc: (restriction-move), (protect-level-something), (protect-expiring: 05:05, 1 (january) 2021, 1 (january) 2021, 05:05)))'
1853  ];
1854  // phpcs:enable
1855  }
1856 
1870  public function testInsertProtectNullRevision(
1871  $revCommentMsg,
1872  array $limit,
1873  array $expiry,
1874  $cascade,
1875  $reason,
1876  $user,
1877  $expectedComment
1878  ) {
1879  $this->setContentLang( 'qqx' );
1880 
1881  $page = $this->createPage( __METHOD__, 'Goat' );
1882 
1883  $user = $user === null ? $user : $this->getTestSysop()->getUser();
1884 
1885  $result = $page->insertProtectNullRevision(
1886  $revCommentMsg,
1887  $limit,
1888  $expiry,
1889  $cascade,
1890  $reason,
1891  $user
1892  );
1893 
1894  $this->assertTrue( $result instanceof Revision );
1895  $this->assertSame( $expectedComment, $result->getComment( Revision::RAW ) );
1896  }
1897 
1901  public function testUpdateRevisionOn_existingPage() {
1902  $user = $this->getTestSysop()->getUser();
1903  $page = $this->createPage( __METHOD__, 'StartText' );
1904 
1905  $revision = new Revision(
1906  [
1907  'id' => 9989,
1908  'page' => $page->getId(),
1909  'title' => $page->getTitle(),
1910  'comment' => __METHOD__,
1911  'minor_edit' => true,
1912  'text' => __METHOD__ . '-text',
1913  'len' => strlen( __METHOD__ . '-text' ),
1914  'user' => $user->getId(),
1915  'user_text' => $user->getName(),
1916  'timestamp' => '20170707040404',
1917  'content_model' => CONTENT_MODEL_WIKITEXT,
1918  'content_format' => CONTENT_FORMAT_WIKITEXT,
1919  ]
1920  );
1921 
1922  $result = $page->updateRevisionOn( $this->db, $revision );
1923  $this->assertTrue( $result );
1924  $this->assertSame( 9989, $page->getLatest() );
1925  $this->assertEquals( $revision, $page->getRevision() );
1926  }
1927 
1931  public function testUpdateRevisionOn_NonExistingPage() {
1932  $user = $this->getTestSysop()->getUser();
1933  $page = $this->createPage( __METHOD__, 'StartText' );
1934  $page->doDeleteArticle( 'reason' );
1935 
1936  $revision = new Revision(
1937  [
1938  'id' => 9989,
1939  'page' => $page->getId(),
1940  'title' => $page->getTitle(),
1941  'comment' => __METHOD__,
1942  'minor_edit' => true,
1943  'text' => __METHOD__ . '-text',
1944  'len' => strlen( __METHOD__ . '-text' ),
1945  'user' => $user->getId(),
1946  'user_text' => $user->getName(),
1947  'timestamp' => '20170707040404',
1948  'content_model' => CONTENT_MODEL_WIKITEXT,
1949  'content_format' => CONTENT_FORMAT_WIKITEXT,
1950  ]
1951  );
1952 
1953  $result = $page->updateRevisionOn( $this->db, $revision );
1954  $this->assertFalse( $result );
1955  }
1956 
1960  public function testUpdateIfNewerOn_olderRevision() {
1961  $user = $this->getTestSysop()->getUser();
1962  $page = $this->createPage( __METHOD__, 'StartText' );
1963  $initialRevision = $page->getRevision();
1964 
1965  $olderTimeStamp = wfTimestamp(
1966  TS_MW,
1967  wfTimestamp( TS_UNIX, $initialRevision->getTimestamp() ) - 1
1968  );
1969 
1970  $olderRevision = new Revision(
1971  [
1972  'id' => 9989,
1973  'page' => $page->getId(),
1974  'title' => $page->getTitle(),
1975  'comment' => __METHOD__,
1976  'minor_edit' => true,
1977  'text' => __METHOD__ . '-text',
1978  'len' => strlen( __METHOD__ . '-text' ),
1979  'user' => $user->getId(),
1980  'user_text' => $user->getName(),
1981  'timestamp' => $olderTimeStamp,
1982  'content_model' => CONTENT_MODEL_WIKITEXT,
1983  'content_format' => CONTENT_FORMAT_WIKITEXT,
1984  ]
1985  );
1986 
1987  $result = $page->updateIfNewerOn( $this->db, $olderRevision );
1988  $this->assertFalse( $result );
1989  }
1990 
1994  public function testUpdateIfNewerOn_newerRevision() {
1995  $user = $this->getTestSysop()->getUser();
1996  $page = $this->createPage( __METHOD__, 'StartText' );
1997  $initialRevision = $page->getRevision();
1998 
1999  $newerTimeStamp = wfTimestamp(
2000  TS_MW,
2001  wfTimestamp( TS_UNIX, $initialRevision->getTimestamp() ) + 1
2002  );
2003 
2004  $newerRevision = new Revision(
2005  [
2006  'id' => 9989,
2007  'page' => $page->getId(),
2008  'title' => $page->getTitle(),
2009  'comment' => __METHOD__,
2010  'minor_edit' => true,
2011  'text' => __METHOD__ . '-text',
2012  'len' => strlen( __METHOD__ . '-text' ),
2013  'user' => $user->getId(),
2014  'user_text' => $user->getName(),
2015  'timestamp' => $newerTimeStamp,
2016  'content_model' => CONTENT_MODEL_WIKITEXT,
2017  'content_format' => CONTENT_FORMAT_WIKITEXT,
2018  ]
2019  );
2020  $result = $page->updateIfNewerOn( $this->db, $newerRevision );
2021  $this->assertTrue( $result );
2022  }
2023 
2027  public function testInsertOn() {
2028  $title = Title::newFromText( __METHOD__ );
2029  $page = new WikiPage( $title );
2030 
2031  $startTimeStamp = wfTimestampNow();
2032  $result = $page->insertOn( $this->db );
2033  $endTimeStamp = wfTimestampNow();
2034 
2035  $this->assertInternalType( 'int', $result );
2036  $this->assertTrue( $result > 0 );
2037 
2038  $condition = [ 'page_id' => $result ];
2039 
2040  // Check the default fields have been filled
2041  $this->assertSelect(
2042  'page',
2043  [
2044  'page_namespace',
2045  'page_title',
2046  'page_restrictions',
2047  'page_is_redirect',
2048  'page_is_new',
2049  'page_latest',
2050  'page_len',
2051  ],
2052  $condition,
2053  [ [
2054  '0',
2055  __METHOD__,
2056  '',
2057  '0',
2058  '1',
2059  '0',
2060  '0',
2061  ] ]
2062  );
2063 
2064  // Check the page_random field has been filled
2065  $pageRandom = $this->db->selectField( 'page', 'page_random', $condition );
2066  $this->assertTrue( (float)$pageRandom < 1 && (float)$pageRandom > 0 );
2067 
2068  // Assert the touched timestamp in the DB is roughly when we inserted the page
2069  $pageTouched = $this->db->selectField( 'page', 'page_touched', $condition );
2070  $this->assertTrue(
2071  wfTimestamp( TS_UNIX, $startTimeStamp )
2072  <= wfTimestamp( TS_UNIX, $pageTouched )
2073  );
2074  $this->assertTrue(
2075  wfTimestamp( TS_UNIX, $endTimeStamp )
2076  >= wfTimestamp( TS_UNIX, $pageTouched )
2077  );
2078 
2079  // Try inserting the same page again and checking the result is false (no change)
2080  $result = $page->insertOn( $this->db );
2081  $this->assertFalse( $result );
2082  }
2083 
2087  public function testInsertOn_idSpecified() {
2088  $title = Title::newFromText( __METHOD__ );
2089  $page = new WikiPage( $title );
2090  $id = 1478952189;
2091 
2092  $result = $page->insertOn( $this->db, $id );
2093 
2094  $this->assertSame( $id, $result );
2095 
2096  $condition = [ 'page_id' => $result ];
2097 
2098  // Check there is actually a row in the db
2099  $this->assertSelect(
2100  'page',
2101  [ 'page_title' ],
2102  $condition,
2103  [ [ __METHOD__ ] ]
2104  );
2105  }
2106 
2107  public function provideTestDoUpdateRestrictions_setBasicRestrictions() {
2108  // Note: Once the current dates passes the date in these tests they will fail.
2109  yield 'move something' => [
2110  true,
2111  [ 'move' => 'something' ],
2112  [],
2113  [ 'edit' => [], 'move' => [ 'something' ] ],
2114  [],
2115  ];
2116  yield 'move something, edit blank' => [
2117  true,
2118  [ 'move' => 'something', 'edit' => '' ],
2119  [],
2120  [ 'edit' => [], 'move' => [ 'something' ] ],
2121  [],
2122  ];
2123  yield 'edit sysop, with expiry' => [
2124  true,
2125  [ 'edit' => 'sysop' ],
2126  [ 'edit' => '21330101020202' ],
2127  [ 'edit' => [ 'sysop' ], 'move' => [] ],
2128  [ 'edit' => '21330101020202' ],
2129  ];
2130  yield 'move and edit, move with expiry' => [
2131  true,
2132  [ 'move' => 'something', 'edit' => 'another' ],
2133  [ 'move' => '22220202010101' ],
2134  [ 'edit' => [ 'another' ], 'move' => [ 'something' ] ],
2135  [ 'move' => '22220202010101' ],
2136  ];
2137  yield 'move and edit, edit with infinity expiry' => [
2138  true,
2139  [ 'move' => 'something', 'edit' => 'another' ],
2140  [ 'edit' => 'infinity' ],
2141  [ 'edit' => [ 'another' ], 'move' => [ 'something' ] ],
2142  [ 'edit' => 'infinity' ],
2143  ];
2144  yield 'non existing, create something' => [
2145  false,
2146  [ 'create' => 'something' ],
2147  [],
2148  [ 'create' => [ 'something' ] ],
2149  [],
2150  ];
2151  yield 'non existing, create something with expiry' => [
2152  false,
2153  [ 'create' => 'something' ],
2154  [ 'create' => '23451212112233' ],
2155  [ 'create' => [ 'something' ] ],
2156  [ 'create' => '23451212112233' ],
2157  ];
2158  }
2159 
2164  public function testDoUpdateRestrictions_setBasicRestrictions(
2165  $pageExists,
2166  array $limit,
2167  array $expiry,
2168  array $expectedRestrictions,
2169  array $expectedRestrictionExpiries
2170  ) {
2171  if ( $pageExists ) {
2172  $page = $this->createPage( __METHOD__, 'ABC' );
2173  } else {
2174  $page = new WikiPage( Title::newFromText( __METHOD__ . '-nonexist' ) );
2175  }
2176  $user = $this->getTestSysop()->getUser();
2177  $cascade = false;
2178 
2179  $status = $page->doUpdateRestrictions( $limit, $expiry, $cascade, 'aReason', $user, [] );
2180 
2181  $logId = $status->getValue();
2182  $allRestrictions = $page->getTitle()->getAllRestrictions();
2183 
2184  $this->assertTrue( $status->isGood() );
2185  $this->assertInternalType( 'int', $logId );
2186  $this->assertSame( $expectedRestrictions, $allRestrictions );
2187  foreach ( $expectedRestrictionExpiries as $key => $value ) {
2188  $this->assertSame( $value, $page->getTitle()->getRestrictionExpiry( $key ) );
2189  }
2190 
2191  // Make sure the log entry looks good
2192  // log_params is not checked here
2193  $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
2194  $commentQuery = MediaWikiServices::getInstance()->getCommentStore()->getJoin( 'log_comment' );
2195  $this->assertSelect(
2196  [ 'logging' ] + $actorQuery['tables'] + $commentQuery['tables'],
2197  [
2198  'log_comment' => $commentQuery['fields']['log_comment_text'],
2199  'log_user' => $actorQuery['fields']['log_user'],
2200  'log_user_text' => $actorQuery['fields']['log_user_text'],
2201  'log_namespace',
2202  'log_title',
2203  ],
2204  [ 'log_id' => $logId ],
2205  [ [
2206  'aReason',
2207  (string)$user->getId(),
2208  $user->getName(),
2209  (string)$page->getTitle()->getNamespace(),
2210  $page->getTitle()->getDBkey(),
2211  ] ],
2212  [],
2213  $actorQuery['joins'] + $commentQuery['joins']
2214  );
2215  }
2216 
2220  public function testDoUpdateRestrictions_failsOnReadOnly() {
2221  $page = $this->createPage( __METHOD__, 'ABC' );
2222  $user = $this->getTestSysop()->getUser();
2223  $cascade = false;
2224 
2225  // Set read only
2226  $readOnly = $this->getMockBuilder( ReadOnlyMode::class )
2227  ->disableOriginalConstructor()
2228  ->setMethods( [ 'isReadOnly', 'getReason' ] )
2229  ->getMock();
2230  $readOnly->expects( $this->once() )
2231  ->method( 'isReadOnly' )
2232  ->will( $this->returnValue( true ) );
2233  $readOnly->expects( $this->once() )
2234  ->method( 'getReason' )
2235  ->will( $this->returnValue( 'Some Read Only Reason' ) );
2236  $this->setService( 'ReadOnlyMode', $readOnly );
2237 
2238  $status = $page->doUpdateRestrictions( [], [], $cascade, 'aReason', $user, [] );
2239  $this->assertFalse( $status->isOK() );
2240  $this->assertSame( 'readonlytext', $status->getMessage()->getKey() );
2241  }
2242 
2246  public function testDoUpdateRestrictions_returnsGoodIfNothingChanged() {
2247  $page = $this->createPage( __METHOD__, 'ABC' );
2248  $user = $this->getTestSysop()->getUser();
2249  $cascade = false;
2250  $limit = [ 'edit' => 'sysop' ];
2251 
2252  $status = $page->doUpdateRestrictions(
2253  $limit,
2254  [],
2255  $cascade,
2256  'aReason',
2257  $user,
2258  []
2259  );
2260 
2261  // The first entry should have a logId as it did something
2262  $this->assertTrue( $status->isGood() );
2263  $this->assertInternalType( 'int', $status->getValue() );
2264 
2265  $status = $page->doUpdateRestrictions(
2266  $limit,
2267  [],
2268  $cascade,
2269  'aReason',
2270  $user,
2271  []
2272  );
2273 
2274  // The second entry should not have a logId as nothing changed
2275  $this->assertTrue( $status->isGood() );
2276  $this->assertNull( $status->getValue() );
2277  }
2278 
2282  public function testDoUpdateRestrictions_logEntryTypeAndAction() {
2283  $page = $this->createPage( __METHOD__, 'ABC' );
2284  $user = $this->getTestSysop()->getUser();
2285  $cascade = false;
2286 
2287  // Protect the page
2288  $status = $page->doUpdateRestrictions(
2289  [ 'edit' => 'sysop' ],
2290  [],
2291  $cascade,
2292  'aReason',
2293  $user,
2294  []
2295  );
2296  $this->assertTrue( $status->isGood() );
2297  $this->assertInternalType( 'int', $status->getValue() );
2298  $this->assertSelect(
2299  'logging',
2300  [ 'log_type', 'log_action' ],
2301  [ 'log_id' => $status->getValue() ],
2302  [ [ 'protect', 'protect' ] ]
2303  );
2304 
2305  // Modify the protection
2306  $status = $page->doUpdateRestrictions(
2307  [ 'edit' => 'somethingElse' ],
2308  [],
2309  $cascade,
2310  'aReason',
2311  $user,
2312  []
2313  );
2314  $this->assertTrue( $status->isGood() );
2315  $this->assertInternalType( 'int', $status->getValue() );
2316  $this->assertSelect(
2317  'logging',
2318  [ 'log_type', 'log_action' ],
2319  [ 'log_id' => $status->getValue() ],
2320  [ [ 'protect', 'modify' ] ]
2321  );
2322 
2323  // Remove the protection
2324  $status = $page->doUpdateRestrictions(
2325  [],
2326  [],
2327  $cascade,
2328  'aReason',
2329  $user,
2330  []
2331  );
2332  $this->assertTrue( $status->isGood() );
2333  $this->assertInternalType( 'int', $status->getValue() );
2334  $this->assertSelect(
2335  'logging',
2336  [ 'log_type', 'log_action' ],
2337  [ 'log_id' => $status->getValue() ],
2338  [ [ 'protect', 'unprotect' ] ]
2339  );
2340  }
2341 
2346  public function testNewPageUpdater() {
2347  $user = $this->getTestUser()->getUser();
2348  $page = $this->newPage( __METHOD__, __METHOD__ );
2349 
2351  $content = $this->getMockBuilder( WikitextContent::class )
2352  ->setConstructorArgs( [ 'Hello World' ] )
2353  ->setMethods( [ 'getParserOutput' ] )
2354  ->getMock();
2355  $content->expects( $this->once() )
2356  ->method( 'getParserOutput' )
2357  ->willReturn( new ParserOutput( 'HTML' ) );
2358 
2359  $preparedEditBefore = $page->prepareContentForEdit( $content, null, $user );
2360 
2361  // provide context, so the cache can be kept in place
2362  $slotsUpdate = new revisionSlotsUpdate();
2363  $slotsUpdate->modifyContent( SlotRecord::MAIN, $content );
2364 
2365  $updater = $page->newPageUpdater( $user, $slotsUpdate );
2366  $updater->setContent( SlotRecord::MAIN, $content );
2367  $revision = $updater->saveRevision(
2368  CommentStoreComment::newUnsavedComment( 'test' ),
2369  EDIT_NEW
2370  );
2371 
2372  $preparedEditAfter = $page->prepareContentForEdit( $content, $revision, $user );
2373 
2374  $this->assertSame( $revision->getId(), $page->getLatest() );
2375 
2376  // Parsed output must remain cached throughout.
2377  $this->assertSame( $preparedEditBefore->output, $preparedEditAfter->output );
2378  }
2379 
2384  public function testGetDerivedDataUpdater() {
2385  $admin = $this->getTestSysop()->getUser();
2386 
2388  $page = $this->createPage( __METHOD__, __METHOD__ );
2389  $page = TestingAccessWrapper::newFromObject( $page );
2390 
2391  $revision = $page->getRevision()->getRevisionRecord();
2392  $user = $revision->getUser();
2393 
2394  $slotsUpdate = new RevisionSlotsUpdate();
2395  $slotsUpdate->modifyContent( SlotRecord::MAIN, new WikitextContent( 'Hello World' ) );
2396 
2397  // get a virgin updater
2398  $updater1 = $page->getDerivedDataUpdater( $user );
2399  $this->assertFalse( $updater1->isUpdatePrepared() );
2400 
2401  $updater1->prepareUpdate( $revision );
2402 
2403  // Re-use updater with same revision or content, even if base changed
2404  $this->assertSame( $updater1, $page->getDerivedDataUpdater( $user, $revision ) );
2405 
2406  $slotsUpdate = RevisionSlotsUpdate::newFromContent(
2407  [ SlotRecord::MAIN => $revision->getContent( SlotRecord::MAIN ) ]
2408  );
2409  $this->assertSame( $updater1, $page->getDerivedDataUpdater( $user, null, $slotsUpdate ) );
2410 
2411  // Don't re-use for edit if base revision ID changed
2412  $this->assertNotSame(
2413  $updater1,
2414  $page->getDerivedDataUpdater( $user, null, $slotsUpdate, true )
2415  );
2416 
2417  // Don't re-use with different user
2418  $updater2a = $page->getDerivedDataUpdater( $admin, null, $slotsUpdate );
2419  $updater2a->prepareContent( $admin, $slotsUpdate, false );
2420 
2421  $updater2b = $page->getDerivedDataUpdater( $user, null, $slotsUpdate );
2422  $updater2b->prepareContent( $user, $slotsUpdate, false );
2423  $this->assertNotSame( $updater2a, $updater2b );
2424 
2425  // Don't re-use with different content
2426  $updater3 = $page->getDerivedDataUpdater( $admin, null, $slotsUpdate );
2427  $updater3->prepareUpdate( $revision );
2428  $this->assertNotSame( $updater2b, $updater3 );
2429 
2430  // Don't re-use if no context given
2431  $updater4 = $page->getDerivedDataUpdater( $admin );
2432  $updater4->prepareUpdate( $revision );
2433  $this->assertNotSame( $updater3, $updater4 );
2434 
2435  // Don't re-use if AGAIN no context given
2436  $updater5 = $page->getDerivedDataUpdater( $admin );
2437  $this->assertNotSame( $updater4, $updater5 );
2438 
2439  // Don't re-use cached "virgin" unprepared updater
2440  $updater6 = $page->getDerivedDataUpdater( $admin, $revision );
2441  $this->assertNotSame( $updater5, $updater6 );
2442  }
2443 
2444  protected function assertPreparedEditEquals(
2445  PreparedEdit $edit, PreparedEdit $edit2, $message = ''
2446  ) {
2447  // suppress differences caused by a clock tick between generating the two PreparedEdits
2448  if ( abs( $edit->timestamp - $edit2->timestamp ) < 3 ) {
2449  $edit2 = clone $edit2;
2450  $edit2->timestamp = $edit->timestamp;
2451  }
2452  $this->assertEquals( $edit, $edit2, $message );
2453  }
2454 
2455  protected function assertPreparedEditNotEquals(
2456  PreparedEdit $edit, PreparedEdit $edit2, $message = ''
2457  ) {
2458  if ( abs( $edit->timestamp - $edit2->timestamp ) < 3 ) {
2459  $edit2 = clone $edit2;
2460  $edit2->timestamp = $edit->timestamp;
2461  }
2462  $this->assertNotEquals( $edit, $edit2, $message );
2463  }
2464 
2465 }
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:138
const FOR_THIS_USER
Definition: Revision.php:55
testIsRedirect( $title, $model, $text, $target)
provideGetRedirectTarget WikiPage::isRedirect
$data
Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
const CONTENT_MODEL_WIKITEXT
Definition: Defines.php:235
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
assertPreparedEditEquals(PreparedEdit $edit, PreparedEdit $edit2, $message='')
testDoDeleteUpdates()
WikiPage::doDeleteUpdates.
wiki Special
defineMockContentModelForUpdateTesting( $name)
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
static getDefaultModelFor(Title $title)
Returns the name of the default content model to be used for the page with the given title...
This code would result in ircNotify being run twice when an article is and once for brion Hooks can return three possible true was required This is the default since MediaWiki *some string
Definition: hooks.txt:175
getModelID()
Returns the model id that identifies the content model this ContentHandler can handle.
The First
Definition: primes.txt:1
presenting them properly to the user as errors is done by the caller return true use this to change the list i e rollback
Definition: hooks.txt:1766
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency MediaWikiServices
Definition: injection.txt:23
mergeMwGlobalArrayValue( $name, $values)
Merges the given values into a MW global array variable.
const EDIT_UPDATE
Definition: Defines.php:153
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
const DB_MASTER
Definition: defines.php:26
testGetRevision()
WikiPage::getRevision.
This document provides an overview of the usage of PageUpdater and that is
Definition: pageupdater.txt:3
$wgContentHandlerUseDB
Set to false to disable use of the database fields introduced by the ContentHandler facility...
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:780
getDefaultWikitextNS()
Returns the ID of a namespace that defaults to Wikitext.
newPage( $title, $model=null)
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return true
Definition: hooks.txt:1982
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:1263
static getTestSysop()
Convenience method for getting an immutable admin test user.
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:780
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:925
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:1766
overrideMwServices(Config $configOverrides=null, array $services=[])
Stashes the global instance of MediaWikiServices, and installs a new one, allowing test cases to over...
setMwGlobals( $pairs, $value=null)
Sets a global, maintaining a stashed version of the previous global to be restored in tearDown...
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
assertSelect( $table, $fields, $condition, array $expectedRows, array $options=[], array $join_conds=[])
Asserts that the given database query yields the rows given by $expectedRows.
testGetRedirectTarget( $title, $model, $text, $target)
provideGetRedirectTarget WikiPage::getRedirectTarget
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
testDoEditContent_twice()
WikiPage::doEditContent.
testExists()
WikiPage::exists.
testDoDeleteArticleReal_user0()
WikiPage::doDeleteArticleReal.
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
const EDIT_NEW
Definition: Defines.php:152
testDoEditUpdates()
WikiPage::doEditUpdates.
$page->newPageUpdater($user) $updater
Definition: pageupdater.txt:63
In both all secondary updates will be triggered handle like object that caches derived data representing a revision
Definition: pageupdater.txt:78
testDoDeleteArticle()
Undeletion is covered in PageArchiveTest::testUndeleteRevisions() TODO: Revision deletion.
testDoDeleteArticleReal_userSysop()
WikiPage::doDeleteArticleReal.
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:271
const CONTENT_FORMAT_WIKITEXT
Definition: Defines.php:250
const DB_REPLICA
Definition: defines.php:25
testDoEditContent()
WikiPage::doEditContent WikiPage::prepareContentForEdit.
static makeContent( $text, Title $title=null, $modelId=null, $format=null)
Convenience function for creating a Content object from a given textual representation.
$content
Definition: pageupdater.txt:72
static getTestUser( $groups=[])
Convenience method for getting an immutable test user.
loadParamsAndArgs( $self=null, $opts=null, $args=null)
Process command line arguments $mOptions becomes an array with keys set to the option names $mArgs be...
createMockContent(ContentHandler $handler, $text)
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1473
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:319