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