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