Merge "Improve return types in class MagicWordArray"
[lhc/web/wiklou.git] / tests / phpunit / includes / page / ArticleViewTest.php
1 <?php
2
3 use MediaWiki\MediaWikiServices;
4 use MediaWiki\Revision\MutableRevisionRecord;
5 use MediaWiki\Revision\RevisionRecord;
6 use MediaWiki\Revision\SlotRecord;
7 use PHPUnit\Framework\MockObject\MockObject;
8
9 /**
10 * @covers \Article::view()
11 */
12 class ArticleViewTest extends MediaWikiTestCase {
13
14 protected function setUp() {
15 parent::setUp();
16
17 $this->setUserLang( 'qqx' );
18 }
19
20 private function getHtml( OutputPage $output ) {
21 return preg_replace( '/<!--.*?-->/s', '', $output->getHTML() );
22 }
23
24 /**
25 * @param string|Title $title
26 * @param Content[]|string[] $revisionContents Content of the revisions to create
27 * (as Content or string).
28 * @param RevisionRecord[] &$revisions will be filled with the RevisionRecord for $content.
29 *
30 * @return WikiPage
31 * @throws MWException
32 */
33 private function getPage( $title, array $revisionContents = [], array &$revisions = [] ) {
34 if ( is_string( $title ) ) {
35 $title = Title::makeTitle( $this->getDefaultWikitextNS(), $title );
36 }
37
38 $page = WikiPage::factory( $title );
39
40 $user = $this->getTestUser()->getUser();
41
42 foreach ( $revisionContents as $key => $cont ) {
43 if ( is_string( $cont ) ) {
44 $cont = new WikitextContent( $cont );
45 }
46
47 $u = $page->newPageUpdater( $user );
48 $u->setContent( SlotRecord::MAIN, $cont );
49 $rev = $u->saveRevision( CommentStoreComment::newUnsavedComment( 'Rev ' . $key ) );
50
51 $revisions[ $key ] = $rev;
52 }
53
54 return $page;
55 }
56
57 /**
58 * @covers Article::getOldId()
59 * @covers Article::getRevIdFetched()
60 */
61 public function testGetOldId() {
62 $revisions = [];
63 $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ], $revisions );
64
65 $idA = $revisions[1]->getId();
66 $idB = $revisions[2]->getId();
67
68 // oldid in constructor
69 $article = new Article( $page->getTitle(), $idA );
70 $this->assertSame( $idA, $article->getOldID() );
71 $article->getRevisionFetched();
72 $this->assertSame( $idA, $article->getRevIdFetched() );
73
74 // oldid 0 in constructor
75 $article = new Article( $page->getTitle(), 0 );
76 $this->assertSame( 0, $article->getOldID() );
77 $article->getRevisionFetched();
78 $this->assertSame( $idB, $article->getRevIdFetched() );
79
80 // oldid in request
81 $article = new Article( $page->getTitle() );
82 $context = new RequestContext();
83 $context->setRequest( new FauxRequest( [ 'oldid' => $idA ] ) );
84 $article->setContext( $context );
85 $this->assertSame( $idA, $article->getOldID() );
86 $article->getRevisionFetched();
87 $this->assertSame( $idA, $article->getRevIdFetched() );
88
89 // no oldid
90 $article = new Article( $page->getTitle() );
91 $context = new RequestContext();
92 $context->setRequest( new FauxRequest( [] ) );
93 $article->setContext( $context );
94 $this->assertSame( 0, $article->getOldID() );
95 $article->getRevisionFetched();
96 $this->assertSame( $idB, $article->getRevIdFetched() );
97 }
98
99 public function testView() {
100 $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ] );
101
102 $article = new Article( $page->getTitle(), 0 );
103 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
104 $article->view();
105
106 $output = $article->getContext()->getOutput();
107 $this->assertContains( 'Test B', $this->getHtml( $output ) );
108 $this->assertNotContains( 'id="mw-revision-info"', $this->getHtml( $output ) );
109 $this->assertNotContains( 'id="mw-revision-nav"', $this->getHtml( $output ) );
110 }
111
112 public function testViewCached() {
113 $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ] );
114
115 $po = new ParserOutput( 'Cached Text' );
116
117 $article = new Article( $page->getTitle(), 0 );
118 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
119
120 $cache = MediaWikiServices::getInstance()->getParserCache();
121 $cache->save( $po, $page, $article->getParserOptions() );
122
123 $article->view();
124
125 $output = $article->getContext()->getOutput();
126 $this->assertContains( 'Cached Text', $this->getHtml( $output ) );
127 $this->assertNotContains( 'Test A', $this->getHtml( $output ) );
128 $this->assertNotContains( 'Test B', $this->getHtml( $output ) );
129 }
130
131 /**
132 * @covers Article::getRedirectTarget()
133 */
134 public function testViewRedirect() {
135 $target = Title::makeTitle( $this->getDefaultWikitextNS(), 'Test_Target' );
136 $redirectText = '#REDIRECT [[' . $target->getPrefixedText() . ']]';
137
138 $page = $this->getPage( __METHOD__, [ $redirectText ] );
139
140 $article = new Article( $page->getTitle(), 0 );
141 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
142 $article->view();
143
144 $this->assertNotNull(
145 $article->getRedirectTarget()->getPrefixedDBkey()
146 );
147 $this->assertSame(
148 $target->getPrefixedDBkey(),
149 $article->getRedirectTarget()->getPrefixedDBkey()
150 );
151
152 $output = $article->getContext()->getOutput();
153 $this->assertContains( 'class="redirectText"', $this->getHtml( $output ) );
154 $this->assertContains(
155 '>' . htmlspecialchars( $target->getPrefixedText() ) . '<',
156 $this->getHtml( $output )
157 );
158 }
159
160 public function testViewNonText() {
161 $dummy = $this->getPage( __METHOD__, [ 'Dummy' ] );
162 $dummyRev = $dummy->getRevision()->getRevisionRecord();
163 $title = $dummy->getTitle();
164
165 /** @var MockObject|ContentHandler $mockHandler */
166 $mockHandler = $this->getMockBuilder( ContentHandler::class )
167 ->setMethods(
168 [
169 'isParserCacheSupported',
170 'serializeContent',
171 'unserializeContent',
172 'makeEmptyContent',
173 ]
174 )
175 ->setConstructorArgs( [ 'NotText', [ 'application/frobnitz' ] ] )
176 ->getMock();
177
178 $mockHandler->method( 'isParserCacheSupported' )
179 ->willReturn( false );
180
181 $this->setTemporaryHook(
182 'ContentHandlerForModelID',
183 function ( $id, &$handler ) use ( $mockHandler ) {
184 $handler = $mockHandler;
185 }
186 );
187
188 /** @var MockObject|Content $content */
189 $content = $this->getMock( Content::class );
190 $content->method( 'getParserOutput' )
191 ->willReturn( new ParserOutput( 'Structured Output' ) );
192 $content->method( 'getModel' )
193 ->willReturn( 'NotText' );
194 $content->expects( $this->never() )->method( 'getNativeData' );
195 $content->method( 'copy' )
196 ->willReturn( $content );
197
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() );
204
205 $rev->setContent( SlotRecord::MAIN, $content );
206
207 $rev = new Revision( $rev );
208
209 /** @var MockObject|WikiPage $page */
210 $page = $this->getMockBuilder( WikiPage::class )
211 ->setMethods( [ 'getRevision', 'getLatest' ] )
212 ->setConstructorArgs( [ $title ] )
213 ->getMock();
214
215 $page->method( 'getRevision' )
216 ->willReturn( $rev );
217 $page->method( 'getLatest' )
218 ->willReturn( $rev->getId() );
219
220 $article = Article::newFromWikiPage( $page, RequestContext::getMain() );
221 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
222 $article->view();
223
224 $output = $article->getContext()->getOutput();
225 $this->assertContains( 'Structured Output', $this->getHtml( $output ) );
226 $this->assertNotContains( 'Dummy', $this->getHtml( $output ) );
227 }
228
229 public function testViewOfOldRevision() {
230 $revisions = [];
231 $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ], $revisions );
232 $idA = $revisions[1]->getId();
233
234 $article = new Article( $page->getTitle(), $idA );
235 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
236 $article->view();
237
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() );
242
243 $this->assertNotContains( 'id="revision-info-current"', $output->getSubtitle() );
244 $this->assertNotContains( 'Test B', $this->getHtml( $output ) );
245 }
246
247 public function testViewOfCurrentRevision() {
248 $revisions = [];
249 $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ], $revisions );
250 $idB = $revisions[2]->getId();
251
252 $article = new Article( $page->getTitle(), $idB );
253 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
254 $article->view();
255
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() );
260 }
261
262 public function testViewOfMissingRevision() {
263 $revisions = [];
264 $page = $this->getPage( __METHOD__, [ 1 => 'Test A' ], $revisions );
265 $badId = $revisions[1]->getId() + 100;
266
267 $article = new Article( $page->getTitle(), $badId );
268 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
269 $article->view();
270
271 $output = $article->getContext()->getOutput();
272 $this->assertContains( 'missing-revision: ' . $badId, $this->getHtml( $output ) );
273
274 $this->assertNotContains( 'Test A', $this->getHtml( $output ) );
275 }
276
277 public function testViewOfDeletedRevision() {
278 $revisions = [];
279 $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ], $revisions );
280 $idA = $revisions[1]->getId();
281
282 $revDelList = new RevDelRevisionList(
283 RequestContext::getMain(), $page->getTitle(), [ $idA ]
284 );
285 $revDelList->setVisibility( [
286 'value' => [ RevisionRecord::DELETED_TEXT => 1 ],
287 'comment' => "Testing",
288 ] );
289
290 $article = new Article( $page->getTitle(), $idA );
291 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
292 $article->view();
293
294 $output = $article->getContext()->getOutput();
295 $this->assertContains( '(rev-deleted-text-permission)', $this->getHtml( $output ) );
296
297 $this->assertNotContains( 'Test A', $this->getHtml( $output ) );
298 $this->assertNotContains( 'Test B', $this->getHtml( $output ) );
299 }
300
301 public function testUnhiddenViewOfDeletedRevision() {
302 $revisions = [];
303 $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ], $revisions );
304 $idA = $revisions[1]->getId();
305
306 $revDelList = new RevDelRevisionList(
307 RequestContext::getMain(), $page->getTitle(), [ $idA ]
308 );
309 $revDelList->setVisibility( [
310 'value' => [ RevisionRecord::DELETED_TEXT => 1 ],
311 'comment' => "Testing",
312 ] );
313
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() );
320 $article->view();
321
322 $output = $article->getContext()->getOutput();
323 $this->assertContains( '(rev-deleted-text-view)', $this->getHtml( $output ) );
324
325 $this->assertContains( 'Test A', $this->getHtml( $output ) );
326 $this->assertNotContains( 'Test B', $this->getHtml( $output ) );
327 }
328
329 public function testViewMissingPage() {
330 $page = $this->getPage( __METHOD__ );
331
332 $article = new Article( $page->getTitle() );
333 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
334 $article->view();
335
336 $output = $article->getContext()->getOutput();
337 $this->assertContains( '(noarticletextanon)', $this->getHtml( $output ) );
338 }
339
340 public function testViewDeletedPage() {
341 $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ] );
342 $page->doDeleteArticle( 'Test' );
343
344 $article = new Article( $page->getTitle() );
345 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
346 $article->view();
347
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 ) );
352
353 $this->assertNotContains( 'Test A', $this->getHtml( $output ) );
354 $this->assertNotContains( 'Test B', $this->getHtml( $output ) );
355 }
356
357 public function testViewMessagePage() {
358 $title = Title::makeTitle( NS_MEDIAWIKI, 'Mainpage' );
359 $page = $this->getPage( $title );
360
361 $article = new Article( $page->getTitle() );
362 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
363 $article->view();
364
365 $output = $article->getContext()->getOutput();
366 $this->assertContains(
367 wfMessage( 'mainpage' )->inContentLanguage()->parse(),
368 $this->getHtml( $output )
369 );
370 $this->assertNotContains( '(noarticletextanon)', $this->getHtml( $output ) );
371 }
372
373 public function testViewMissingUserPage() {
374 $user = $this->getTestUser()->getUser();
375 $user->addToDatabase();
376
377 $title = Title::makeTitle( NS_USER, $user->getName() );
378
379 $page = $this->getPage( $title );
380
381 $article = new Article( $page->getTitle() );
382 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
383 $article->view();
384
385 $output = $article->getContext()->getOutput();
386 $this->assertContains( '(noarticletextanon)', $this->getHtml( $output ) );
387 $this->assertNotContains( '(userpage-userdoesnotexist-view)', $this->getHtml( $output ) );
388 }
389
390 public function testViewUserPageOfNonexistingUser() {
391 $user = User::newFromName( 'Testing ' . __METHOD__ );
392
393 $title = Title::makeTitle( NS_USER, $user->getName() );
394
395 $page = $this->getPage( $title );
396
397 $article = new Article( $page->getTitle() );
398 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
399 $article->view();
400
401 $output = $article->getContext()->getOutput();
402 $this->assertContains( '(noarticletextanon)', $this->getHtml( $output ) );
403 $this->assertContains( '(userpage-userdoesnotexist-view:', $this->getHtml( $output ) );
404 }
405
406 public function testArticleViewHeaderHook() {
407 $page = $this->getPage( __METHOD__, [ 1 => 'Test A' ] );
408
409 $article = new Article( $page->getTitle(), 0 );
410 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
411
412 $this->setTemporaryHook(
413 'ArticleViewHeader',
414 function ( Article $articlePage, &$outputDone, &$useParserCache ) use ( $article ) {
415 $this->assertSame( $article, $articlePage, '$articlePage' );
416
417 $outputDone = new ParserOutput( 'Hook Text' );
418 $outputDone->setTitleText( 'Hook Title' );
419
420 $articlePage->getContext()->getOutput()->addParserOutput( $outputDone );
421 }
422 );
423
424 $article->view();
425
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() );
430 }
431
432 public function testArticleContentViewCustomHook() {
433 $page = $this->getPage( __METHOD__, [ 1 => 'Test A' ] );
434
435 $article = new Article( $page->getTitle(), 0 );
436 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
437
438 // use ArticleViewHeader hook to bypass the parser cache
439 $this->setTemporaryHook(
440 'ArticleViewHeader',
441 function ( Article $articlePage, &$outputDone, &$useParserCache ) use ( $article ) {
442 $useParserCache = false;
443 }
444 );
445
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->getText(), '$content' );
451
452 $output->addHTML( 'Hook Text' );
453 return false;
454 }
455 );
456
457 $this->hideDeprecated(
458 'ArticleContentViewCustom hook (used in hook-ArticleContentViewCustom-closure)'
459 );
460
461 $article->view();
462
463 $output = $article->getContext()->getOutput();
464 $this->assertNotContains( 'Test A', $this->getHtml( $output ) );
465 $this->assertContains( 'Hook Text', $this->getHtml( $output ) );
466 }
467
468 public function testArticleRevisionViewCustomHook() {
469 $page = $this->getPage( __METHOD__, [ 1 => 'Test A' ] );
470
471 $article = new Article( $page->getTitle(), 0 );
472 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
473
474 // use ArticleViewHeader hook to bypass the parser cache
475 $this->setTemporaryHook(
476 'ArticleViewHeader',
477 function ( Article $articlePage, &$outputDone, &$useParserCache ) use ( $article ) {
478 $useParserCache = false;
479 }
480 );
481
482 $this->setTemporaryHook(
483 'ArticleRevisionViewCustom',
484 function ( RevisionRecord $rev, Title $title, $oldid, OutputPage $output ) use ( $page ) {
485 $content = $rev->getContent( SlotRecord::MAIN );
486 $this->assertSame( $page->getTitle(), $title, '$title' );
487 $this->assertSame( 'Test A', $content->getText(), '$content' );
488
489 $output->addHTML( 'Hook Text' );
490 return false;
491 }
492 );
493
494 $article->view();
495
496 $output = $article->getContext()->getOutput();
497 $this->assertNotContains( 'Test A', $this->getHtml( $output ) );
498 $this->assertContains( 'Hook Text', $this->getHtml( $output ) );
499 }
500
501 public function testArticleAfterFetchContentObjectHook() {
502 $page = $this->getPage( __METHOD__, [ 1 => 'Test A' ] );
503
504 $article = new Article( $page->getTitle(), 0 );
505 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
506
507 // use ArticleViewHeader hook to bypass the parser cache
508 $this->setTemporaryHook(
509 'ArticleViewHeader',
510 function ( Article $articlePage, &$outputDone, &$useParserCache ) use ( $article ) {
511 $useParserCache = false;
512 }
513 );
514
515 $this->setTemporaryHook(
516 'ArticleAfterFetchContentObject',
517 function ( Article &$articlePage, Content &$content ) use ( $page, $article ) {
518 $this->assertSame( $article, $articlePage, '$articlePage' );
519 $this->assertSame( 'Test A', $content->getText(), '$content' );
520
521 $content = new WikitextContent( 'Hook Text' );
522 }
523 );
524
525 $this->hideDeprecated(
526 'ArticleAfterFetchContentObject hook'
527 . ' (used in hook-ArticleAfterFetchContentObject-closure)'
528 );
529
530 $article->view();
531
532 $output = $article->getContext()->getOutput();
533 $this->assertNotContains( 'Test A', $this->getHtml( $output ) );
534 $this->assertContains( 'Hook Text', $this->getHtml( $output ) );
535 }
536
537 public function testShowMissingArticleHook() {
538 $page = $this->getPage( __METHOD__ );
539
540 $article = new Article( $page->getTitle() );
541 $article->getContext()->getOutput()->setTitle( $page->getTitle() );
542
543 $this->setTemporaryHook(
544 'ShowMissingArticle',
545 function ( Article $articlePage ) use ( $article ) {
546 $this->assertSame( $article, $articlePage, '$articlePage' );
547
548 $articlePage->getContext()->getOutput()->addHTML( 'Hook Text' );
549 }
550 );
551
552 $article->view();
553
554 $output = $article->getContext()->getOutput();
555 $this->assertContains( '(noarticletextanon)', $this->getHtml( $output ) );
556 $this->assertContains( 'Hook Text', $this->getHtml( $output ) );
557 }
558
559 }