1<?php 2 3use MediaWiki\MediaWikiServices; 4use MediaWiki\Revision\MutableRevisionRecord; 5use MediaWiki\Revision\RevisionRecord; 6use MediaWiki\Revision\SlotRecord; 7use PHPUnit\Framework\MockObject\MockObject; 8use Wikimedia\TestingAccessWrapper; 9 10/** 11 * @covers \Article::view() 12 */ 13class ArticleViewTest extends MediaWikiIntegrationTestCase { 14 15 protected function setUp(): void { 16 parent::setUp(); 17 18 $this->setUserLang( 'qqx' ); 19 } 20 21 private function getHtml( OutputPage $output ) { 22 return preg_replace( '/<!--.*?-->/s', '', $output->getHTML() ); 23 } 24 25 /** 26 * @param string|Title $title 27 * @param Content[]|string[] $revisionContents Content of the revisions to create 28 * (as Content or string). 29 * @param RevisionRecord[] &$revisions will be filled with the RevisionRecord for $content. 30 * 31 * @return WikiPage 32 * @throws MWException 33 */ 34 private function getPage( $title, array $revisionContents = [], array &$revisions = [] ) { 35 if ( is_string( $title ) ) { 36 $title = Title::makeTitle( $this->getDefaultWikitextNS(), $title ); 37 } 38 39 $page = WikiPage::factory( $title ); 40 41 $user = $this->getTestUser()->getUser(); 42 43 // Make sure all revision have different timestamps all the time, 44 // to make timestamp asserts below deterministic. 45 $time = time() - 86400; 46 MWTimestamp::setFakeTime( $time ); 47 48 foreach ( $revisionContents as $key => $cont ) { 49 if ( is_string( $cont ) ) { 50 $cont = new WikitextContent( $cont ); 51 } 52 53 $u = $page->newPageUpdater( $user ); 54 $u->setContent( SlotRecord::MAIN, $cont ); 55 $rev = $u->saveRevision( CommentStoreComment::newUnsavedComment( 'Rev ' . $key ) ); 56 57 $revisions[ $key ] = $rev; 58 MWTimestamp::setFakeTime( ++$time ); 59 } 60 MWTimestamp::setFakeTime( false ); 61 62 // Clear content model cache to support tests that mock the revision 63 $this->getServiceContainer()->getMainWANObjectCache()->clearProcessCache(); 64 65 return $page; 66 } 67 68 /** 69 * @covers Article::getOldId() 70 * @covers Article::getRevIdFetched() 71 */ 72 public function testGetOldId() { 73 $revisions = []; 74 $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ], $revisions ); 75 76 $idA = $revisions[1]->getId(); 77 $idB = $revisions[2]->getId(); 78 79 // oldid in constructor 80 $article = new Article( $page->getTitle(), $idA ); 81 $this->assertSame( $idA, $article->getOldID() ); 82 $article->fetchRevisionRecord(); 83 $this->assertSame( $idA, $article->getRevIdFetched() ); 84 85 // oldid 0 in constructor 86 $article = new Article( $page->getTitle(), 0 ); 87 $this->assertSame( 0, $article->getOldID() ); 88 $article->fetchRevisionRecord(); 89 $this->assertSame( $idB, $article->getRevIdFetched() ); 90 91 // oldid in request 92 $article = new Article( $page->getTitle() ); 93 $context = new RequestContext(); 94 $context->setRequest( new FauxRequest( [ 'oldid' => $idA ] ) ); 95 $article->setContext( $context ); 96 $this->assertSame( $idA, $article->getOldID() ); 97 $article->fetchRevisionRecord(); 98 $this->assertSame( $idA, $article->getRevIdFetched() ); 99 100 // no oldid 101 $article = new Article( $page->getTitle() ); 102 $context = new RequestContext(); 103 $context->setRequest( new FauxRequest( [] ) ); 104 $article->setContext( $context ); 105 $this->assertSame( 0, $article->getOldID() ); 106 $article->fetchRevisionRecord(); 107 $this->assertSame( $idB, $article->getRevIdFetched() ); 108 } 109 110 public function testView() { 111 $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ] ); 112 113 $article = new Article( $page->getTitle(), 0 ); 114 $article->getContext()->getOutput()->setTitle( $page->getTitle() ); 115 $article->view(); 116 117 $output = $article->getContext()->getOutput(); 118 $this->assertStringContainsString( 'Test B', $this->getHtml( $output ) ); 119 $this->assertStringNotContainsString( 'id="mw-revision-info"', $this->getHtml( $output ) ); 120 $this->assertStringNotContainsString( 'id="mw-revision-nav"', $this->getHtml( $output ) ); 121 } 122 123 public function testViewCached() { 124 $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ] ); 125 126 $po = new ParserOutput( 'Cached Text' ); 127 128 $article = new Article( $page->getTitle(), 0 ); 129 $article->getContext()->getOutput()->setTitle( $page->getTitle() ); 130 131 $cache = MediaWikiServices::getInstance()->getParserCache(); 132 $cache->save( $po, $page, $article->getParserOptions() ); 133 134 $article->view(); 135 136 $output = $article->getContext()->getOutput(); 137 $this->assertStringContainsString( 'Cached Text', $this->getHtml( $output ) ); 138 $this->assertStringNotContainsString( 'Test A', $this->getHtml( $output ) ); 139 $this->assertStringNotContainsString( 'Test B', $this->getHtml( $output ) ); 140 } 141 142 /** 143 * @covers Article::getPage() 144 * @covers WikiPage::getRedirectTarget() 145 */ 146 public function testViewRedirect() { 147 $target = Title::makeTitle( $this->getDefaultWikitextNS(), 'Test_Target' ); 148 $redirectText = '#REDIRECT [[' . $target->getPrefixedText() . ']]'; 149 150 $page = $this->getPage( __METHOD__, [ $redirectText ] ); 151 152 $article = new Article( $page->getTitle(), 0 ); 153 $article->getContext()->getOutput()->setTitle( $page->getTitle() ); 154 $article->view(); 155 156 $this->assertNotNull( 157 $article->getPage()->getRedirectTarget()->getPrefixedDBkey() 158 ); 159 $this->assertSame( 160 $target->getPrefixedDBkey(), 161 $article->getPage()->getRedirectTarget()->getPrefixedDBkey() 162 ); 163 164 $output = $article->getContext()->getOutput(); 165 $this->assertStringContainsString( 'class="redirectText"', $this->getHtml( $output ) ); 166 $this->assertStringContainsString( 167 '>' . htmlspecialchars( $target->getPrefixedText() ) . '<', 168 $this->getHtml( $output ) 169 ); 170 } 171 172 public function testViewNonText() { 173 $dummy = $this->getPage( __METHOD__, [ 'Dummy' ] ); 174 $dummyRev = $dummy->getRevisionRecord(); 175 $title = $dummy->getTitle(); 176 177 /** @var MockObject|ContentHandler $mockHandler */ 178 $mockHandler = $this->getMockBuilder( ContentHandler::class ) 179 ->onlyMethods( 180 [ 181 'isParserCacheSupported', 182 'serializeContent', 183 'unserializeContent', 184 'makeEmptyContent', 185 ] 186 ) 187 ->setConstructorArgs( [ 'NotText', [ 'application/frobnitz' ] ] ) 188 ->getMock(); 189 190 $mockHandler->method( 'isParserCacheSupported' ) 191 ->willReturn( false ); 192 193 $this->setTemporaryHook( 194 'ContentHandlerForModelID', 195 static function ( $id, &$handler ) use ( $mockHandler ) { 196 $handler = $mockHandler; 197 } 198 ); 199 200 /** @var MockObject|Content $content */ 201 $content = $this->createMock( Content::class ); 202 $content->method( 'getParserOutput' ) 203 ->willReturn( new ParserOutput( 'Structured Output' ) ); 204 $content->method( 'getModel' ) 205 ->willReturn( 'NotText' ); 206 $content->expects( $this->never() )->method( 'getNativeData' ); 207 $content->method( 'copy' ) 208 ->willReturn( $content ); 209 210 $rev = new MutableRevisionRecord( $title ); 211 $rev->setId( $dummyRev->getId() ); 212 $rev->setPageId( $title->getArticleID() ); 213 $rev->setUser( $dummyRev->getUser() ); 214 $rev->setComment( $dummyRev->getComment() ); 215 $rev->setTimestamp( $dummyRev->getTimestamp() ); 216 217 $rev->setContent( SlotRecord::MAIN, $content ); 218 219 /** @var MockObject|WikiPage $page */ 220 $page = $this->getMockBuilder( WikiPage::class ) 221 ->onlyMethods( [ 'getRevisionRecord', 'getLatest' ] ) 222 ->setConstructorArgs( [ $title ] ) 223 ->getMock(); 224 225 $page->method( 'getRevisionRecord' ) 226 ->willReturn( $rev ); 227 $page->method( 'getLatest' ) 228 ->willReturn( $rev->getId() ); 229 230 $article = Article::newFromWikiPage( $page, RequestContext::getMain() ); 231 $article->getContext()->getOutput()->setTitle( $page->getTitle() ); 232 $article->view(); 233 234 $output = $article->getContext()->getOutput(); 235 $this->assertStringContainsString( 'Structured Output', $this->getHtml( $output ) ); 236 $this->assertStringNotContainsString( 'Dummy', $this->getHtml( $output ) ); 237 } 238 239 public function testViewOfOldRevision() { 240 $revisions = []; 241 $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ], $revisions ); 242 $idA = $revisions[1]->getId(); 243 244 $article = new Article( $page->getTitle(), $idA ); 245 $article->getContext()->getOutput()->setTitle( $page->getTitle() ); 246 $article->view(); 247 248 $output = $article->getContext()->getOutput(); 249 $this->assertStringContainsString( 'Test A', $this->getHtml( $output ) ); 250 $this->assertStringContainsString( 'id="mw-revision-info"', $output->getSubtitle() ); 251 $this->assertStringContainsString( 'id="mw-revision-nav"', $output->getSubtitle() ); 252 253 $this->assertStringNotContainsString( 'id="revision-info-current"', $output->getSubtitle() ); 254 $this->assertStringNotContainsString( 'Test B', $this->getHtml( $output ) ); 255 $this->assertSame( $idA, $output->getRevisionId() ); 256 $this->assertSame( $revisions[1]->getTimestamp(), $output->getRevisionTimestamp() ); 257 } 258 259 public function testViewOfOldRevisionFromCache() { 260 $this->setMwGlobals( [ 261 'wgOldRevisionParserCacheExpireTime' => 100500, 262 'wgMainWANCache' => 'main', 263 'wgWANObjectCaches' => [ 264 'main' => [ 265 'class' => WANObjectCache::class, 266 'cacheId' => 'hash', 267 ], 268 ], 269 ] ); 270 271 $revisions = []; 272 $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ], $revisions ); 273 $idA = $revisions[1]->getId(); 274 275 // View the revision once (to get it into the cache) 276 $article = new Article( $page->getTitle(), $idA ); 277 $article->view(); 278 279 // Reset the output page and view the revision again (from ParserCache) 280 $article = new Article( $page->getTitle(), $idA ); 281 $context = RequestContext::getMain(); 282 $context->setOutput( new OutputPage( $context ) ); 283 $article->setContext( $context ); 284 285 $outputPageBeforeHTMLRevisionId = null; 286 $this->setTemporaryHook( 'OutputPageBeforeHTML', 287 static function ( OutputPage $out ) use ( &$outputPageBeforeHTMLRevisionId ) { 288 $outputPageBeforeHTMLRevisionId = $out->getRevisionId(); 289 } 290 ); 291 292 $article->view(); 293 $output = $article->getContext()->getOutput(); 294 $this->assertStringContainsString( 'Test A', $this->getHtml( $output ) ); 295 $this->assertSame( 1, substr_count( $output->getSubtitle(), 'class="mw-revision warningbox"' ) ); 296 $this->assertSame( $idA, $output->getRevisionId() ); 297 $this->assertSame( $idA, $outputPageBeforeHTMLRevisionId ); 298 $this->assertSame( $revisions[1]->getTimestamp(), $output->getRevisionTimestamp() ); 299 } 300 301 public function testViewOfCurrentRevision() { 302 $revisions = []; 303 $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ], $revisions ); 304 $idB = $revisions[2]->getId(); 305 306 $article = new Article( $page->getTitle(), $idB ); 307 $article->getContext()->getOutput()->setTitle( $page->getTitle() ); 308 $article->view(); 309 310 $output = $article->getContext()->getOutput(); 311 $this->assertStringContainsString( 'Test B', $this->getHtml( $output ) ); 312 $this->assertStringContainsString( 'id="mw-revision-info-current"', $output->getSubtitle() ); 313 $this->assertStringContainsString( 'id="mw-revision-nav"', $output->getSubtitle() ); 314 } 315 316 public function testViewOfMissingRevision() { 317 $revisions = []; 318 $page = $this->getPage( __METHOD__, [ 1 => 'Test A' ], $revisions ); 319 $badId = $revisions[1]->getId() + 100; 320 321 $article = new Article( $page->getTitle(), $badId ); 322 $article->getContext()->getOutput()->setTitle( $page->getTitle() ); 323 $article->view(); 324 325 $output = $article->getContext()->getOutput(); 326 $this->assertStringContainsString( 'missing-revision: ' . $badId, $this->getHtml( $output ) ); 327 328 $this->assertStringNotContainsString( 'Test A', $this->getHtml( $output ) ); 329 } 330 331 public function testViewOfDeletedRevision() { 332 $revisions = []; 333 $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ], $revisions ); 334 $idA = $revisions[1]->getId(); 335 336 $revDelList = $this->getRevDelRevisionList( $page->getTitle(), $idA ); 337 $revDelList->setVisibility( [ 338 'value' => [ RevisionRecord::DELETED_TEXT => 1 ], 339 'comment' => "Testing", 340 ] ); 341 342 $article = new Article( $page->getTitle(), $idA ); 343 $article->getContext()->getOutput()->setTitle( $page->getTitle() ); 344 $article->view(); 345 346 $output = $article->getContext()->getOutput(); 347 $this->assertStringContainsString( 'rev-deleted-text-permission', $this->getHtml( $output ) ); 348 349 $this->assertStringNotContainsString( 'Test A', $this->getHtml( $output ) ); 350 $this->assertStringNotContainsString( 'Test B', $this->getHtml( $output ) ); 351 } 352 353 public function testUnhiddenViewOfDeletedRevision() { 354 $revisions = []; 355 $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ], $revisions ); 356 $idA = $revisions[1]->getId(); 357 358 $revDelList = $this->getRevDelRevisionList( $page->getTitle(), $idA ); 359 $revDelList->setVisibility( [ 360 'value' => [ RevisionRecord::DELETED_TEXT => 1 ], 361 'comment' => "Testing", 362 ] ); 363 364 $article = new Article( $page->getTitle(), $idA ); 365 $context = new DerivativeContext( $article->getContext() ); 366 $article->setContext( $context ); 367 $context->getOutput()->setTitle( $page->getTitle() ); 368 $context->getRequest()->setVal( 'unhide', 1 ); 369 $context->setUser( $this->getTestUser( [ 'sysop' ] )->getUser() ); 370 $article->view(); 371 372 $output = $article->getContext()->getOutput(); 373 $this->assertStringContainsString( 'rev-deleted-text-view', $this->getHtml( $output ) ); 374 375 $this->assertStringContainsString( 'Test A', $this->getHtml( $output ) ); 376 $this->assertStringNotContainsString( 'Test B', $this->getHtml( $output ) ); 377 } 378 379 public function testViewMissingPage() { 380 $page = $this->getPage( __METHOD__ ); 381 382 $article = new Article( $page->getTitle() ); 383 $article->getContext()->getOutput()->setTitle( $page->getTitle() ); 384 $article->view(); 385 386 $output = $article->getContext()->getOutput(); 387 $this->assertStringContainsString( '(noarticletextanon)', $this->getHtml( $output ) ); 388 } 389 390 public function testViewDeletedPage() { 391 $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ] ); 392 $page->doDeleteArticleReal( 'Test', $this->getTestSysop()->getUser() ); 393 394 $article = new Article( $page->getTitle() ); 395 $article->getContext()->getOutput()->setTitle( $page->getTitle() ); 396 $article->view(); 397 398 $output = $article->getContext()->getOutput(); 399 $this->assertStringContainsString( 'moveddeleted', $this->getHtml( $output ) ); 400 $this->assertStringContainsString( 'logentry-delete-delete', $this->getHtml( $output ) ); 401 $this->assertStringContainsString( '(noarticletextanon)', $this->getHtml( $output ) ); 402 403 $this->assertStringNotContainsString( 'Test A', $this->getHtml( $output ) ); 404 $this->assertStringNotContainsString( 'Test B', $this->getHtml( $output ) ); 405 } 406 407 public function testViewMessagePage() { 408 $title = Title::makeTitle( NS_MEDIAWIKI, 'Mainpage' ); 409 $page = $this->getPage( $title ); 410 411 $article = new Article( $page->getTitle() ); 412 $article->getContext()->getOutput()->setTitle( $page->getTitle() ); 413 $article->view(); 414 415 $output = $article->getContext()->getOutput(); 416 $this->assertStringContainsString( 417 wfMessage( 'mainpage' )->inContentLanguage()->parse(), 418 $this->getHtml( $output ) 419 ); 420 $this->assertStringNotContainsString( '(noarticletextanon)', $this->getHtml( $output ) ); 421 } 422 423 public function testViewMissingUserPage() { 424 $user = $this->getTestUser()->getUser(); 425 $user->addToDatabase(); 426 427 $title = Title::makeTitle( NS_USER, $user->getName() ); 428 429 $page = $this->getPage( $title ); 430 431 $article = new Article( $page->getTitle() ); 432 $article->getContext()->getOutput()->setTitle( $page->getTitle() ); 433 $article->view(); 434 435 $output = $article->getContext()->getOutput(); 436 $this->assertStringContainsString( '(noarticletextanon)', $this->getHtml( $output ) ); 437 $this->assertStringNotContainsString( 438 '(userpage-userdoesnotexist-view)', 439 $this->getHtml( $output ) 440 ); 441 } 442 443 public function testViewUserPageOfNonexistingUser() { 444 $user = User::newFromName( 'Testing ' . __METHOD__ ); 445 446 $title = Title::makeTitle( NS_USER, $user->getName() ); 447 448 $page = $this->getPage( $title ); 449 450 $article = new Article( $page->getTitle() ); 451 $article->getContext()->getOutput()->setTitle( $page->getTitle() ); 452 $article->view(); 453 454 $output = $article->getContext()->getOutput(); 455 $this->assertStringContainsString( '(noarticletextanon)', $this->getHtml( $output ) ); 456 $this->assertStringContainsString( 457 '(userpage-userdoesnotexist-view:', 458 $this->getHtml( $output ) 459 ); 460 } 461 462 public function testArticleViewHeaderHook() { 463 $page = $this->getPage( __METHOD__, [ 1 => 'Test A' ] ); 464 465 $article = new Article( $page->getTitle(), 0 ); 466 $article->getContext()->getOutput()->setTitle( $page->getTitle() ); 467 468 $this->setTemporaryHook( 469 'ArticleViewHeader', 470 function ( Article $articlePage, &$outputDone, &$useParserCache ) use ( $article ) { 471 $this->assertSame( $article, $articlePage, '$articlePage' ); 472 473 $outputDone = new ParserOutput( 'Hook Text' ); 474 $outputDone->setTitleText( 'Hook Title' ); 475 476 $articlePage->getContext()->getOutput()->addParserOutput( $outputDone ); 477 } 478 ); 479 480 $article->view(); 481 482 $output = $article->getContext()->getOutput(); 483 $this->assertStringNotContainsString( 'Test A', $this->getHtml( $output ) ); 484 $this->assertStringContainsString( 'Hook Text', $this->getHtml( $output ) ); 485 $this->assertSame( 'Hook Title', $output->getPageTitle() ); 486 } 487 488 public function testArticleRevisionViewCustomHook() { 489 $page = $this->getPage( __METHOD__, [ 1 => 'Test A' ] ); 490 491 $article = new Article( $page->getTitle(), 0 ); 492 $article->getContext()->getOutput()->setTitle( $page->getTitle() ); 493 494 // use ArticleViewHeader hook to bypass the parser cache 495 $this->setTemporaryHook( 496 'ArticleViewHeader', 497 static function ( Article $articlePage, &$outputDone, &$useParserCache ) { 498 $useParserCache = false; 499 } 500 ); 501 502 $this->setTemporaryHook( 503 'ArticleRevisionViewCustom', 504 function ( RevisionRecord $rev, Title $title, $oldid, OutputPage $output ) use ( $page ) { 505 $content = $rev->getContent( SlotRecord::MAIN ); 506 $this->assertSame( $page->getTitle(), $title, '$title' ); 507 $this->assertSame( 'Test A', $content->getText(), '$content' ); 508 509 $output->addHTML( 'Hook Text' ); 510 return false; 511 } 512 ); 513 514 $article->view(); 515 516 $output = $article->getContext()->getOutput(); 517 $this->assertStringNotContainsString( 'Test A', $this->getHtml( $output ) ); 518 $this->assertStringContainsString( 'Hook Text', $this->getHtml( $output ) ); 519 } 520 521 public function testShowMissingArticleHook() { 522 $page = $this->getPage( __METHOD__ ); 523 524 $article = new Article( $page->getTitle() ); 525 $article->getContext()->getOutput()->setTitle( $page->getTitle() ); 526 527 $this->setTemporaryHook( 528 'ShowMissingArticle', 529 function ( Article $articlePage ) use ( $article ) { 530 $this->assertSame( $article, $articlePage, '$articlePage' ); 531 532 $articlePage->getContext()->getOutput()->addHTML( 'Hook Text' ); 533 } 534 ); 535 536 $article->view(); 537 538 $output = $article->getContext()->getOutput(); 539 $this->assertStringContainsString( '(noarticletextanon)', $this->getHtml( $output ) ); 540 $this->assertStringContainsString( 'Hook Text', $this->getHtml( $output ) ); 541 } 542 543 /** 544 * @covers \Article::showViewError() 545 */ 546 public function testViewLatestError() { 547 $page = $this->getPage( __METHOD__, [ 1 => 'Test A' ] ); 548 549 $article = new Article( $page->getTitle(), 0 ); 550 $output = $article->getContext()->getOutput(); 551 $output->setTitle( $page->getTitle() ); 552 553 // use ArticleViewHeader hook to bypass the parser cache 554 $this->setTemporaryHook( 555 'ArticleViewHeader', 556 static function ( Article $articlePage, &$outputDone, &$useParserCache ) { 557 $useParserCache = false; 558 } 559 ); 560 561 $article = TestingAccessWrapper::newFromObject( $article ); 562 $article->fetchResult = Status::newFatal( 563 'rev-deleted-text-permission', 564 $page->getTitle()->getPrefixedDBkey() 565 ); 566 567 $article->view(); 568 569 $this->assertStringContainsString( 570 'rev-deleted-text-permission: ArticleViewTest::testViewLatestError', 571 $this->getHtml( $output ) 572 ); 573 } 574 575 /** 576 * @covers \Article::showViewError() 577 */ 578 public function testViewOldError() { 579 $revisions = []; 580 $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ], $revisions ); 581 $idA = $revisions[1]->getId(); 582 583 $article = new Article( $page->getTitle(), $idA ); 584 $output = $article->getContext()->getOutput(); 585 $output->setTitle( $page->getTitle() ); 586 587 $article = TestingAccessWrapper::newFromObject( $article ); 588 $article->fetchResult = Status::newFatal( 589 'rev-deleted-text-permission', 590 $page->getTitle()->getPrefixedDBkey() 591 ); 592 593 $article->view(); 594 595 $this->assertStringContainsString( 596 'rev-deleted-text-permission: ArticleViewTest::testViewOldError', 597 $this->getHtml( $output ) 598 ); 599 } 600 601 private function getRevDelRevisionList( $title, $revisionId ) { 602 $services = MediaWikiServices::getInstance(); 603 return new RevDelRevisionList( 604 RequestContext::getMain(), 605 $title, 606 [ $revisionId ], 607 $services->getDBLoadBalancerFactory(), 608 $services->getHookContainer(), 609 $services->getHtmlCacheUpdater(), 610 $services->getRevisionStore(), 611 $services->getMainWANObjectCache() 612 ); 613 } 614} 615