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