2 use MediaWiki\MediaWikiServices
;
3 use MediaWiki\Storage\MutableRevisionRecord
;
4 use MediaWiki\Storage\RevisionRecord
;
5 use MediaWiki\Storage\SlotRecord
;
6 use PHPUnit\Framework\MockObject\MockObject
;
9 * @covers \Article::view()
11 class ArticleViewTest
extends MediaWikiTestCase
{
13 protected function setUp() {
16 $this->setUserLang( 'qqx' );
19 private function getHtml( OutputPage
$output ) {
20 return preg_replace( '/<!--.*?-->/s', '', $output->getHTML() );
24 * @param string|Title $title
25 * @param Content[]|string[] $revisionContents Content of the revisions to create
26 * (as Content or string).
27 * @param RevisionRecord[] &$revisions will be filled with the RevisionRecord for $content.
32 private function getPage( $title, array $revisionContents = [], array &$revisions = [] ) {
33 if ( is_string( $title ) ) {
34 $title = Title
::makeTitle( $this->getDefaultWikitextNS(), $title );
37 $page = WikiPage
::factory( $title );
39 $user = $this->getTestUser()->getUser();
41 foreach ( $revisionContents as $key => $cont ) {
42 if ( is_string( $cont ) ) {
43 $cont = new WikitextContent( $cont );
46 $u = $page->newPageUpdater( $user );
47 $u->setContent( SlotRecord
::MAIN
, $cont );
48 $rev = $u->saveRevision( CommentStoreComment
::newUnsavedComment( 'Rev ' . $key ) );
50 $revisions[ $key ] = $rev;
57 * @covers Article::getOldId()
58 * @covers Article::getRevIdFetched()
60 public function testGetOldId() {
62 $page = $this->getPage( __METHOD__
, [ 1 => 'Test A', 2 => 'Test B' ], $revisions );
64 $idA = $revisions[1]->getId();
65 $idB = $revisions[2]->getId();
67 // oldid in constructor
68 $article = new Article( $page->getTitle(), $idA );
69 $this->assertSame( $idA, $article->getOldID() );
70 $article->getRevisionFetched();
71 $this->assertSame( $idA, $article->getRevIdFetched() );
73 // oldid 0 in constructor
74 $article = new Article( $page->getTitle(), 0 );
75 $this->assertSame( 0, $article->getOldID() );
76 $article->getRevisionFetched();
77 $this->assertSame( $idB, $article->getRevIdFetched() );
80 $article = new Article( $page->getTitle() );
81 $context = new RequestContext();
82 $context->setRequest( new FauxRequest( [ 'oldid' => $idA ] ) );
83 $article->setContext( $context );
84 $this->assertSame( $idA, $article->getOldID() );
85 $article->getRevisionFetched();
86 $this->assertSame( $idA, $article->getRevIdFetched() );
89 $article = new Article( $page->getTitle() );
90 $context = new RequestContext();
91 $context->setRequest( new FauxRequest( [] ) );
92 $article->setContext( $context );
93 $this->assertSame( 0, $article->getOldID() );
94 $article->getRevisionFetched();
95 $this->assertSame( $idB, $article->getRevIdFetched() );
98 public function testView() {
99 $page = $this->getPage( __METHOD__
, [ 1 => 'Test A', 2 => 'Test B' ] );
101 $article = new Article( $page->getTitle(), 0 );
102 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
105 $output = $article->getContext()->getOutput();
106 $this->assertContains( 'Test B', $this->getHtml( $output ) );
107 $this->assertNotContains( 'id="mw-revision-info"', $this->getHtml( $output ) );
108 $this->assertNotContains( 'id="mw-revision-nav"', $this->getHtml( $output ) );
111 public function testViewCached() {
112 $page = $this->getPage( __METHOD__
, [ 1 => 'Test A', 2 => 'Test B' ] );
114 $po = new ParserOutput( 'Cached Text' );
116 $article = new Article( $page->getTitle(), 0 );
117 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
119 $cache = MediaWikiServices
::getInstance()->getParserCache();
120 $cache->save( $po, $page, $article->getParserOptions() );
124 $output = $article->getContext()->getOutput();
125 $this->assertContains( 'Cached Text', $this->getHtml( $output ) );
126 $this->assertNotContains( 'Test A', $this->getHtml( $output ) );
127 $this->assertNotContains( 'Test B', $this->getHtml( $output ) );
131 * @covers Article::getRedirectTarget()
133 public function testViewRedirect() {
134 $target = Title
::makeTitle( $this->getDefaultWikitextNS(), 'Test_Target' );
135 $redirectText = '#REDIRECT [[' . $target->getPrefixedText() . ']]';
137 $page = $this->getPage( __METHOD__
, [ $redirectText ] );
139 $article = new Article( $page->getTitle(), 0 );
140 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
143 $this->assertNotNull(
144 $article->getRedirectTarget()->getPrefixedDBkey()
147 $target->getPrefixedDBkey(),
148 $article->getRedirectTarget()->getPrefixedDBkey()
151 $output = $article->getContext()->getOutput();
152 $this->assertContains( 'class="redirectText"', $this->getHtml( $output ) );
153 $this->assertContains(
154 '>' . htmlspecialchars( $target->getPrefixedText() ) . '<',
155 $this->getHtml( $output )
159 public function testViewNonText() {
160 $dummy = $this->getPage( __METHOD__
, [ 'Dummy' ] );
161 $dummyRev = $dummy->getRevision()->getRevisionRecord();
162 $title = $dummy->getTitle();
164 /** @var MockObject|ContentHandler $mockHandler */
165 $mockHandler = $this->getMockBuilder( ContentHandler
::class )
168 'isParserCacheSupported',
170 'unserializeContent',
174 ->setConstructorArgs( [ 'NotText', [ 'application/frobnitz' ] ] )
177 $mockHandler->method( 'isParserCacheSupported' )
178 ->willReturn( false );
180 $this->setTemporaryHook(
181 'ContentHandlerForModelID',
182 function ( $id, &$handler ) use ( $mockHandler ) {
183 $handler = $mockHandler;
187 /** @var MockObject|Content $content */
188 $content = $this->getMock( Content
::class );
189 $content->method( 'getParserOutput' )
190 ->willReturn( new ParserOutput( 'Structured Output' ) );
191 $content->method( 'getModel' )
192 ->willReturn( 'NotText' );
193 $content->method( 'getNativeData' )
194 ->willReturn( [ (object)[ 'x' => 'stuff' ] ] );
195 $content->method( 'copy' )
196 ->willReturn( $content );
198 $rev = new MutableRevisionRecord( $title );
199 $rev->setId( $dummyRev->getId() );
200 $rev->setPageId( $title->getArticleID() );
201 $rev->setUser( $dummyRev->getUser() );
202 $rev->setComment( $dummyRev->getComment() );
203 $rev->setTimestamp( $dummyRev->getTimestamp() );
205 $rev->setContent( SlotRecord
::MAIN
, $content );
207 $rev = new Revision( $rev );
209 /** @var MockObject|WikiPage $page */
210 $page = $this->getMockBuilder( WikiPage
::class )
211 ->setMethods( [ 'getRevision', 'getLatest' ] )
212 ->setConstructorArgs( [ $title ] )
215 $page->method( 'getRevision' )
216 ->willReturn( $rev );
217 $page->method( 'getLatest' )
218 ->willReturn( $rev->getId() );
220 $article = Article
::newFromWikiPage( $page, RequestContext
::getMain() );
221 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
224 $output = $article->getContext()->getOutput();
225 $this->assertContains( 'Structured Output', $this->getHtml( $output ) );
226 $this->assertNotContains( 'Dummy', $this->getHtml( $output ) );
229 public function testViewOfOldRevision() {
231 $page = $this->getPage( __METHOD__
, [ 1 => 'Test A', 2 => 'Test B' ], $revisions );
232 $idA = $revisions[1]->getId();
234 $article = new Article( $page->getTitle(), $idA );
235 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
238 $output = $article->getContext()->getOutput();
239 $this->assertContains( 'Test A', $this->getHtml( $output ) );
240 $this->assertContains( 'id="mw-revision-info"', $output->getSubtitle() );
241 $this->assertContains( 'id="mw-revision-nav"', $output->getSubtitle() );
243 $this->assertNotContains( 'id="revision-info-current"', $output->getSubtitle() );
244 $this->assertNotContains( 'Test B', $this->getHtml( $output ) );
247 public function testViewOfCurrentRevision() {
249 $page = $this->getPage( __METHOD__
, [ 1 => 'Test A', 2 => 'Test B' ], $revisions );
250 $idB = $revisions[2]->getId();
252 $article = new Article( $page->getTitle(), $idB );
253 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
256 $output = $article->getContext()->getOutput();
257 $this->assertContains( 'Test B', $this->getHtml( $output ) );
258 $this->assertContains( 'id="mw-revision-info-current"', $output->getSubtitle() );
259 $this->assertContains( 'id="mw-revision-nav"', $output->getSubtitle() );
262 public function testViewOfMissingRevision() {
264 $page = $this->getPage( __METHOD__
, [ 1 => 'Test A' ], $revisions );
265 $badId = $revisions[1]->getId() +
100;
267 $article = new Article( $page->getTitle(), $badId );
268 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
271 $output = $article->getContext()->getOutput();
272 $this->assertContains( 'missing-revision: ' . $badId, $this->getHtml( $output ) );
274 $this->assertNotContains( 'Test A', $this->getHtml( $output ) );
277 public function testViewOfDeletedRevision() {
279 $page = $this->getPage( __METHOD__
, [ 1 => 'Test A', 2 => 'Test B' ], $revisions );
280 $idA = $revisions[1]->getId();
282 $revDelList = new RevDelRevisionList(
283 RequestContext
::getMain(), $page->getTitle(), [ $idA ]
285 $revDelList->setVisibility( [
286 'value' => [ RevisionRecord
::DELETED_TEXT
=> 1 ],
287 'comment' => "Testing",
290 $article = new Article( $page->getTitle(), $idA );
291 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
294 $output = $article->getContext()->getOutput();
295 $this->assertContains( '(rev-deleted-text-permission)', $this->getHtml( $output ) );
297 $this->assertNotContains( 'Test A', $this->getHtml( $output ) );
298 $this->assertNotContains( 'Test B', $this->getHtml( $output ) );
301 public function testUnhiddenViewOfDeletedRevision() {
303 $page = $this->getPage( __METHOD__
, [ 1 => 'Test A', 2 => 'Test B' ], $revisions );
304 $idA = $revisions[1]->getId();
306 $revDelList = new RevDelRevisionList(
307 RequestContext
::getMain(), $page->getTitle(), [ $idA ]
309 $revDelList->setVisibility( [
310 'value' => [ RevisionRecord
::DELETED_TEXT
=> 1 ],
311 'comment' => "Testing",
314 $article = new Article( $page->getTitle(), $idA );
315 $context = new DerivativeContext( $article->getContext() );
316 $article->setContext( $context );
317 $context->getOutput()->setTitle( $page->getTitle() );
318 $context->getRequest()->setVal( 'unhide', 1 );
319 $context->setUser( $this->getTestUser( [ 'sysop' ] )->getUser() );
322 $output = $article->getContext()->getOutput();
323 $this->assertContains( '(rev-deleted-text-view)', $this->getHtml( $output ) );
325 $this->assertContains( 'Test A', $this->getHtml( $output ) );
326 $this->assertNotContains( 'Test B', $this->getHtml( $output ) );
329 public function testViewMissingPage() {
330 $page = $this->getPage( __METHOD__
);
332 $article = new Article( $page->getTitle() );
333 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
336 $output = $article->getContext()->getOutput();
337 $this->assertContains( '(noarticletextanon)', $this->getHtml( $output ) );
340 public function testViewDeletedPage() {
341 $page = $this->getPage( __METHOD__
, [ 1 => 'Test A', 2 => 'Test B' ] );
342 $page->doDeleteArticle( 'Test' );
344 $article = new Article( $page->getTitle() );
345 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
348 $output = $article->getContext()->getOutput();
349 $this->assertContains( 'moveddeleted', $this->getHtml( $output ) );
350 $this->assertContains( 'logentry-delete-delete', $this->getHtml( $output ) );
351 $this->assertContains( '(noarticletextanon)', $this->getHtml( $output ) );
353 $this->assertNotContains( 'Test A', $this->getHtml( $output ) );
354 $this->assertNotContains( 'Test B', $this->getHtml( $output ) );
357 public function testViewMessagePage() {
358 $title = Title
::makeTitle( NS_MEDIAWIKI
, 'Mainpage' );
359 $page = $this->getPage( $title );
361 $article = new Article( $page->getTitle() );
362 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
365 $output = $article->getContext()->getOutput();
366 $this->assertContains(
367 wfMessage( 'mainpage' )->inContentLanguage()->parse(),
368 $this->getHtml( $output )
370 $this->assertNotContains( '(noarticletextanon)', $this->getHtml( $output ) );
373 public function testViewMissingUserPage() {
374 $user = $this->getTestUser()->getUser();
375 $user->addToDatabase();
377 $title = Title
::makeTitle( NS_USER
, $user->getName() );
379 $page = $this->getPage( $title );
381 $article = new Article( $page->getTitle() );
382 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
385 $output = $article->getContext()->getOutput();
386 $this->assertContains( '(noarticletextanon)', $this->getHtml( $output ) );
387 $this->assertNotContains( '(userpage-userdoesnotexist-view)', $this->getHtml( $output ) );
390 public function testViewUserPageOfNonexistingUser() {
391 $user = User
::newFromName( 'Testing ' . __METHOD__
);
393 $title = Title
::makeTitle( NS_USER
, $user->getName() );
395 $page = $this->getPage( $title );
397 $article = new Article( $page->getTitle() );
398 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
401 $output = $article->getContext()->getOutput();
402 $this->assertContains( '(noarticletextanon)', $this->getHtml( $output ) );
403 $this->assertContains( '(userpage-userdoesnotexist-view:', $this->getHtml( $output ) );
406 public function testArticleViewHeaderHook() {
407 $page = $this->getPage( __METHOD__
, [ 1 => 'Test A' ] );
409 $article = new Article( $page->getTitle(), 0 );
410 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
412 $this->setTemporaryHook(
414 function ( Article
$articlePage, &$outputDone, &$useParserCache ) use ( $article ) {
415 $this->assertSame( $article, $articlePage, '$articlePage' );
417 $outputDone = new ParserOutput( 'Hook Text' );
418 $outputDone->setTitleText( 'Hook Title' );
420 $articlePage->getContext()->getOutput()->addParserOutput( $outputDone );
426 $output = $article->getContext()->getOutput();
427 $this->assertNotContains( 'Test A', $this->getHtml( $output ) );
428 $this->assertContains( 'Hook Text', $this->getHtml( $output ) );
429 $this->assertSame( 'Hook Title', $output->getPageTitle() );
432 public function testArticleContentViewCustomHook() {
433 $page = $this->getPage( __METHOD__
, [ 1 => 'Test A' ] );
435 $article = new Article( $page->getTitle(), 0 );
436 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
438 // use ArticleViewHeader hook to bypass the parser cache
439 $this->setTemporaryHook(
441 function ( Article
$articlePage, &$outputDone, &$useParserCache ) use ( $article ) {
442 $useParserCache = false;
446 $this->setTemporaryHook(
447 'ArticleContentViewCustom',
448 function ( Content
$content, Title
$title, OutputPage
$output ) use ( $page ) {
449 $this->assertSame( $page->getTitle(), $title, '$title' );
450 $this->assertSame( 'Test A', $content->getNativeData(), '$content' );
452 $output->addHTML( 'Hook Text' );
457 $this->hideDeprecated(
458 'ArticleContentViewCustom hook (used in hook-ArticleContentViewCustom-closure)'
463 $output = $article->getContext()->getOutput();
464 $this->assertNotContains( 'Test A', $this->getHtml( $output ) );
465 $this->assertContains( 'Hook Text', $this->getHtml( $output ) );
468 public function testArticleRevisionViewCustomHook() {
469 $page = $this->getPage( __METHOD__
, [ 1 => 'Test A' ] );
471 $article = new Article( $page->getTitle(), 0 );
472 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
474 // use ArticleViewHeader hook to bypass the parser cache
475 $this->setTemporaryHook(
477 function ( Article
$articlePage, &$outputDone, &$useParserCache ) use ( $article ) {
478 $useParserCache = false;
482 $this->setTemporaryHook(
483 'ArticleRevisionViewCustom',
484 function ( RevisionRecord
$rev, Title
$title, $oldid, OutputPage
$output ) use ( $page ) {
485 $content = $rev->getContent( SlotRecord
::MAIN
);
487 $this->assertSame( $page->getTitle(), $title, '$title' );
488 $this->assertSame( 'Test A', $content->getNativeData(), '$content' );
490 $output->addHTML( 'Hook Text' );
497 $output = $article->getContext()->getOutput();
498 $this->assertNotContains( 'Test A', $this->getHtml( $output ) );
499 $this->assertContains( 'Hook Text', $this->getHtml( $output ) );
502 public function testArticleAfterFetchContentObjectHook() {
503 $page = $this->getPage( __METHOD__
, [ 1 => 'Test A' ] );
505 $article = new Article( $page->getTitle(), 0 );
506 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
508 // use ArticleViewHeader hook to bypass the parser cache
509 $this->setTemporaryHook(
511 function ( Article
$articlePage, &$outputDone, &$useParserCache ) use ( $article ) {
512 $useParserCache = false;
516 $this->setTemporaryHook(
517 'ArticleAfterFetchContentObject',
518 function ( Article
&$articlePage, Content
&$content ) use ( $page, $article ) {
519 $this->assertSame( $article, $articlePage, '$articlePage' );
520 $this->assertSame( 'Test A', $content->getNativeData(), '$content' );
522 $content = new WikitextContent( 'Hook Text' );
526 $this->hideDeprecated(
527 'ArticleAfterFetchContentObject hook'
528 . ' (used in hook-ArticleAfterFetchContentObject-closure)'
533 $output = $article->getContext()->getOutput();
534 $this->assertNotContains( 'Test A', $this->getHtml( $output ) );
535 $this->assertContains( 'Hook Text', $this->getHtml( $output ) );
538 public function testShowMissingArticleHook() {
539 $page = $this->getPage( __METHOD__
);
541 $article = new Article( $page->getTitle() );
542 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
544 $this->setTemporaryHook(
545 'ShowMissingArticle',
546 function ( Article
$articlePage ) use ( $article ) {
547 $this->assertSame( $article, $articlePage, '$articlePage' );
549 $articlePage->getContext()->getOutput()->addHTML( 'Hook Text' );
555 $output = $article->getContext()->getOutput();
556 $this->assertContains( '(noarticletextanon)', $this->getHtml( $output ) );
557 $this->assertContains( 'Hook Text', $this->getHtml( $output ) );