ArticleViewTest.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. <?php
  2. use MediaWiki\MediaWikiServices;
  3. use MediaWiki\Storage\MutableRevisionRecord;
  4. use MediaWiki\Storage\RevisionRecord;
  5. use PHPUnit\Framework\MockObject\MockObject;
  6. /**
  7. * @covers \Article::view()
  8. */
  9. class ArticleViewTest extends MediaWikiTestCase {
  10. protected function setUp() {
  11. parent::setUp();
  12. $this->setUserLang( 'qqx' );
  13. }
  14. private function getHtml( OutputPage $output ) {
  15. return preg_replace( '/<!--.*?-->/s', '', $output->getHTML() );
  16. }
  17. /**
  18. * @param string|Title $title
  19. * @param Content[]|string[] $revisionContents Content of the revisions to create
  20. * (as Content or string).
  21. * @param RevisionRecord[] &$revisions will be filled with the RevisionRecord for $content.
  22. *
  23. * @return WikiPage
  24. * @throws MWException
  25. */
  26. private function getPage( $title, array $revisionContents = [], array &$revisions = [] ) {
  27. if ( is_string( $title ) ) {
  28. $title = Title::makeTitle( $this->getDefaultWikitextNS(), $title );
  29. }
  30. $page = WikiPage::factory( $title );
  31. $user = $this->getTestUser()->getUser();
  32. foreach ( $revisionContents as $key => $cont ) {
  33. if ( is_string( $cont ) ) {
  34. $cont = new WikitextContent( $cont );
  35. }
  36. $u = $page->newPageUpdater( $user );
  37. $u->setContent( 'main', $cont );
  38. $rev = $u->saveRevision( CommentStoreComment::newUnsavedComment( 'Rev ' . $key ) );
  39. $revisions[ $key ] = $rev;
  40. }
  41. return $page;
  42. }
  43. /**
  44. * @covers Article::getOldId()
  45. * @covers Article::getRevIdFetched()
  46. */
  47. public function testGetOldId() {
  48. $revisions = [];
  49. $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ], $revisions );
  50. $idA = $revisions[1]->getId();
  51. $idB = $revisions[2]->getId();
  52. // oldid in constructor
  53. $article = new Article( $page->getTitle(), $idA );
  54. $this->assertSame( $idA, $article->getOldID() );
  55. $article->getRevisionFetched();
  56. $this->assertSame( $idA, $article->getRevIdFetched() );
  57. // oldid 0 in constructor
  58. $article = new Article( $page->getTitle(), 0 );
  59. $this->assertSame( 0, $article->getOldID() );
  60. $article->getRevisionFetched();
  61. $this->assertSame( $idB, $article->getRevIdFetched() );
  62. // oldid in request
  63. $article = new Article( $page->getTitle() );
  64. $context = new RequestContext();
  65. $context->setRequest( new FauxRequest( [ 'oldid' => $idA ] ) );
  66. $article->setContext( $context );
  67. $this->assertSame( $idA, $article->getOldID() );
  68. $article->getRevisionFetched();
  69. $this->assertSame( $idA, $article->getRevIdFetched() );
  70. // no oldid
  71. $article = new Article( $page->getTitle() );
  72. $context = new RequestContext();
  73. $context->setRequest( new FauxRequest( [] ) );
  74. $article->setContext( $context );
  75. $this->assertSame( 0, $article->getOldID() );
  76. $article->getRevisionFetched();
  77. $this->assertSame( $idB, $article->getRevIdFetched() );
  78. }
  79. public function testView() {
  80. $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ] );
  81. $article = new Article( $page->getTitle(), 0 );
  82. $article->getContext()->getOutput()->setTitle( $page->getTitle() );
  83. $article->view();
  84. $output = $article->getContext()->getOutput();
  85. $this->assertContains( 'Test B', $this->getHtml( $output ) );
  86. $this->assertNotContains( 'id="mw-revision-info"', $this->getHtml( $output ) );
  87. $this->assertNotContains( 'id="mw-revision-nav"', $this->getHtml( $output ) );
  88. }
  89. public function testViewCached() {
  90. $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ] );
  91. $po = new ParserOutput( 'Cached Text' );
  92. $article = new Article( $page->getTitle(), 0 );
  93. $article->getContext()->getOutput()->setTitle( $page->getTitle() );
  94. $cache = MediaWikiServices::getInstance()->getParserCache();
  95. $cache->save( $po, $page, $article->getParserOptions() );
  96. $article->view();
  97. $output = $article->getContext()->getOutput();
  98. $this->assertContains( 'Cached Text', $this->getHtml( $output ) );
  99. $this->assertNotContains( 'Test A', $this->getHtml( $output ) );
  100. $this->assertNotContains( 'Test B', $this->getHtml( $output ) );
  101. }
  102. /**
  103. * @covers Article::getRedirectTarget()
  104. */
  105. public function testViewRedirect() {
  106. $target = Title::makeTitle( $this->getDefaultWikitextNS(), 'Test_Target' );
  107. $redirectText = '#REDIRECT [[' . $target->getPrefixedText() . ']]';
  108. $page = $this->getPage( __METHOD__, [ $redirectText ] );
  109. $article = new Article( $page->getTitle(), 0 );
  110. $article->getContext()->getOutput()->setTitle( $page->getTitle() );
  111. $article->view();
  112. $this->assertNotNull(
  113. $article->getRedirectTarget()->getPrefixedDBkey()
  114. );
  115. $this->assertSame(
  116. $target->getPrefixedDBkey(),
  117. $article->getRedirectTarget()->getPrefixedDBkey()
  118. );
  119. $output = $article->getContext()->getOutput();
  120. $this->assertContains( 'class="redirectText"', $this->getHtml( $output ) );
  121. $this->assertContains(
  122. '>' . htmlspecialchars( $target->getPrefixedText() ) . '<',
  123. $this->getHtml( $output )
  124. );
  125. }
  126. public function testViewNonText() {
  127. $dummy = $this->getPage( __METHOD__, [ 'Dummy' ] );
  128. $dummyRev = $dummy->getRevision()->getRevisionRecord();
  129. $title = $dummy->getTitle();
  130. /** @var MockObject|ContentHandler $mockHandler */
  131. $mockHandler = $this->getMockBuilder( ContentHandler::class )
  132. ->setMethods(
  133. [
  134. 'isParserCacheSupported',
  135. 'serializeContent',
  136. 'unserializeContent',
  137. 'makeEmptyContent',
  138. ]
  139. )
  140. ->setConstructorArgs( [ 'NotText', [ 'application/frobnitz' ] ] )
  141. ->getMock();
  142. $mockHandler->method( 'isParserCacheSupported' )
  143. ->willReturn( false );
  144. $this->setTemporaryHook(
  145. 'ContentHandlerForModelID',
  146. function ( $id, &$handler ) use ( $mockHandler ) {
  147. $handler = $mockHandler;
  148. }
  149. );
  150. /** @var MockObject|Content $content */
  151. $content = $this->getMock( Content::class );
  152. $content->method( 'getParserOutput' )
  153. ->willReturn( new ParserOutput( 'Structured Output' ) );
  154. $content->method( 'getModel' )
  155. ->willReturn( 'NotText' );
  156. $content->method( 'getNativeData' )
  157. ->willReturn( [ (object)[ 'x' => 'stuff' ] ] );
  158. $content->method( 'copy' )
  159. ->willReturn( $content );
  160. $rev = new MutableRevisionRecord( $title );
  161. $rev->setId( $dummyRev->getId() );
  162. $rev->setPageId( $title->getArticleID() );
  163. $rev->setUser( $dummyRev->getUser() );
  164. $rev->setComment( $dummyRev->getComment() );
  165. $rev->setTimestamp( $dummyRev->getTimestamp() );
  166. $rev->setContent( 'main', $content );
  167. $rev = new Revision( $rev );
  168. /** @var MockObject|WikiPage $page */
  169. $page = $this->getMockBuilder( WikiPage::class )
  170. ->setMethods( [ 'getRevision', 'getLatest' ] )
  171. ->setConstructorArgs( [ $title ] )
  172. ->getMock();
  173. $page->method( 'getRevision' )
  174. ->willReturn( $rev );
  175. $page->method( 'getLatest' )
  176. ->willReturn( $rev->getId() );
  177. $article = Article::newFromWikiPage( $page, RequestContext::getMain() );
  178. $article->getContext()->getOutput()->setTitle( $page->getTitle() );
  179. $article->view();
  180. $output = $article->getContext()->getOutput();
  181. $this->assertContains( 'Structured Output', $this->getHtml( $output ) );
  182. $this->assertNotContains( 'Dummy', $this->getHtml( $output ) );
  183. }
  184. public function testViewOfOldRevision() {
  185. $revisions = [];
  186. $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ], $revisions );
  187. $idA = $revisions[1]->getId();
  188. $article = new Article( $page->getTitle(), $idA );
  189. $article->getContext()->getOutput()->setTitle( $page->getTitle() );
  190. $article->view();
  191. $output = $article->getContext()->getOutput();
  192. $this->assertContains( 'Test A', $this->getHtml( $output ) );
  193. $this->assertContains( 'id="mw-revision-info"', $output->getSubtitle() );
  194. $this->assertContains( 'id="mw-revision-nav"', $output->getSubtitle() );
  195. $this->assertNotContains( 'id="revision-info-current"', $output->getSubtitle() );
  196. $this->assertNotContains( 'Test B', $this->getHtml( $output ) );
  197. }
  198. public function testViewOfCurrentRevision() {
  199. $revisions = [];
  200. $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ], $revisions );
  201. $idB = $revisions[2]->getId();
  202. $article = new Article( $page->getTitle(), $idB );
  203. $article->getContext()->getOutput()->setTitle( $page->getTitle() );
  204. $article->view();
  205. $output = $article->getContext()->getOutput();
  206. $this->assertContains( 'Test B', $this->getHtml( $output ) );
  207. $this->assertContains( 'id="mw-revision-info-current"', $output->getSubtitle() );
  208. $this->assertContains( 'id="mw-revision-nav"', $output->getSubtitle() );
  209. }
  210. public function testViewOfMissingRevision() {
  211. $revisions = [];
  212. $page = $this->getPage( __METHOD__, [ 1 => 'Test A' ], $revisions );
  213. $badId = $revisions[1]->getId() + 100;
  214. $article = new Article( $page->getTitle(), $badId );
  215. $article->getContext()->getOutput()->setTitle( $page->getTitle() );
  216. $article->view();
  217. $output = $article->getContext()->getOutput();
  218. $this->assertContains( 'missing-revision: ' . $badId, $this->getHtml( $output ) );
  219. $this->assertNotContains( 'Test A', $this->getHtml( $output ) );
  220. }
  221. public function testViewOfDeletedRevision() {
  222. $revisions = [];
  223. $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ], $revisions );
  224. $idA = $revisions[1]->getId();
  225. $revDelList = new RevDelRevisionList(
  226. RequestContext::getMain(), $page->getTitle(), [ $idA ]
  227. );
  228. $revDelList->setVisibility( [
  229. 'value' => [ RevisionRecord::DELETED_TEXT => 1 ],
  230. 'comment' => "Testing",
  231. ] );
  232. $article = new Article( $page->getTitle(), $idA );
  233. $article->getContext()->getOutput()->setTitle( $page->getTitle() );
  234. $article->view();
  235. $output = $article->getContext()->getOutput();
  236. $this->assertContains( '(rev-deleted-text-permission)', $this->getHtml( $output ) );
  237. $this->assertNotContains( 'Test A', $this->getHtml( $output ) );
  238. $this->assertNotContains( 'Test B', $this->getHtml( $output ) );
  239. }
  240. public function testViewMissingPage() {
  241. $page = $this->getPage( __METHOD__ );
  242. $article = new Article( $page->getTitle() );
  243. $article->getContext()->getOutput()->setTitle( $page->getTitle() );
  244. $article->view();
  245. $output = $article->getContext()->getOutput();
  246. $this->assertContains( '(noarticletextanon)', $this->getHtml( $output ) );
  247. }
  248. public function testViewDeletedPage() {
  249. $page = $this->getPage( __METHOD__, [ 1 => 'Test A', 2 => 'Test B' ] );
  250. $page->doDeleteArticle( 'Test' );
  251. $article = new Article( $page->getTitle() );
  252. $article->getContext()->getOutput()->setTitle( $page->getTitle() );
  253. $article->view();
  254. $output = $article->getContext()->getOutput();
  255. $this->assertContains( 'moveddeleted', $this->getHtml( $output ) );
  256. $this->assertContains( 'logentry-delete-delete', $this->getHtml( $output ) );
  257. $this->assertContains( '(noarticletextanon)', $this->getHtml( $output ) );
  258. $this->assertNotContains( 'Test A', $this->getHtml( $output ) );
  259. $this->assertNotContains( 'Test B', $this->getHtml( $output ) );
  260. }
  261. public function testViewMessagePage() {
  262. $title = Title::makeTitle( NS_MEDIAWIKI, 'Mainpage' );
  263. $page = $this->getPage( $title );
  264. $article = new Article( $page->getTitle() );
  265. $article->getContext()->getOutput()->setTitle( $page->getTitle() );
  266. $article->view();
  267. $output = $article->getContext()->getOutput();
  268. $this->assertContains(
  269. wfMessage( 'mainpage' )->inContentLanguage()->parse(),
  270. $this->getHtml( $output )
  271. );
  272. $this->assertNotContains( '(noarticletextanon)', $this->getHtml( $output ) );
  273. }
  274. public function testViewMissingUserPage() {
  275. $user = $this->getTestUser()->getUser();
  276. $user->addToDatabase();
  277. $title = Title::makeTitle( NS_USER, $user->getName() );
  278. $page = $this->getPage( $title );
  279. $article = new Article( $page->getTitle() );
  280. $article->getContext()->getOutput()->setTitle( $page->getTitle() );
  281. $article->view();
  282. $output = $article->getContext()->getOutput();
  283. $this->assertContains( '(noarticletextanon)', $this->getHtml( $output ) );
  284. $this->assertNotContains( '(userpage-userdoesnotexist-view)', $this->getHtml( $output ) );
  285. }
  286. public function testViewUserPageOfNonexistingUser() {
  287. $user = User::newFromName( 'Testing ' . __METHOD__ );
  288. $title = Title::makeTitle( NS_USER, $user->getName() );
  289. $page = $this->getPage( $title );
  290. $article = new Article( $page->getTitle() );
  291. $article->getContext()->getOutput()->setTitle( $page->getTitle() );
  292. $article->view();
  293. $output = $article->getContext()->getOutput();
  294. $this->assertContains( '(noarticletextanon)', $this->getHtml( $output ) );
  295. $this->assertContains( '(userpage-userdoesnotexist-view:', $this->getHtml( $output ) );
  296. }
  297. public function testArticleViewHeaderHook() {
  298. $page = $this->getPage( __METHOD__, [ 1 => 'Test A' ] );
  299. $article = new Article( $page->getTitle(), 0 );
  300. $article->getContext()->getOutput()->setTitle( $page->getTitle() );
  301. $this->setTemporaryHook(
  302. 'ArticleViewHeader',
  303. function ( Article $articlePage, &$outputDone, &$useParserCache ) use ( $article ) {
  304. $this->assertSame( $article, $articlePage, '$articlePage' );
  305. $outputDone = new ParserOutput( 'Hook Text' );
  306. $outputDone->setTitleText( 'Hook Title' );
  307. $articlePage->getContext()->getOutput()->addParserOutput( $outputDone );
  308. }
  309. );
  310. $article->view();
  311. $output = $article->getContext()->getOutput();
  312. $this->assertNotContains( 'Test A', $this->getHtml( $output ) );
  313. $this->assertContains( 'Hook Text', $this->getHtml( $output ) );
  314. $this->assertSame( 'Hook Title', $output->getPageTitle() );
  315. }
  316. public function testArticleContentViewCustomHook() {
  317. $page = $this->getPage( __METHOD__, [ 1 => 'Test A' ] );
  318. $article = new Article( $page->getTitle(), 0 );
  319. $article->getContext()->getOutput()->setTitle( $page->getTitle() );
  320. // use ArticleViewHeader hook to bypass the parser cache
  321. $this->setTemporaryHook(
  322. 'ArticleViewHeader',
  323. function ( Article $articlePage, &$outputDone, &$useParserCache ) use ( $article ) {
  324. $useParserCache = false;
  325. }
  326. );
  327. $this->setTemporaryHook(
  328. 'ArticleContentViewCustom',
  329. function ( Content $content, Title $title, OutputPage $output ) use ( $page ) {
  330. $this->assertSame( $page->getTitle(), $title, '$title' );
  331. $this->assertSame( 'Test A', $content->getNativeData(), '$content' );
  332. $output->addHTML( 'Hook Text' );
  333. return false;
  334. }
  335. );
  336. $this->hideDeprecated(
  337. 'ArticleContentViewCustom hook (used in hook-ArticleContentViewCustom-closure)'
  338. );
  339. $article->view();
  340. $output = $article->getContext()->getOutput();
  341. $this->assertNotContains( 'Test A', $this->getHtml( $output ) );
  342. $this->assertContains( 'Hook Text', $this->getHtml( $output ) );
  343. }
  344. public function testArticleRevisionViewCustomHook() {
  345. $page = $this->getPage( __METHOD__, [ 1 => 'Test A' ] );
  346. $article = new Article( $page->getTitle(), 0 );
  347. $article->getContext()->getOutput()->setTitle( $page->getTitle() );
  348. // use ArticleViewHeader hook to bypass the parser cache
  349. $this->setTemporaryHook(
  350. 'ArticleViewHeader',
  351. function ( Article $articlePage, &$outputDone, &$useParserCache ) use ( $article ) {
  352. $useParserCache = false;
  353. }
  354. );
  355. $this->setTemporaryHook(
  356. 'ArticleRevisionViewCustom',
  357. function ( RevisionRecord $rev, Title $title, $oldid, OutputPage $output ) use ( $page ) {
  358. $content = $rev->getContent( 'main' );
  359. $this->assertSame( $page->getTitle(), $title, '$title' );
  360. $this->assertSame( 'Test A', $content->getNativeData(), '$content' );
  361. $output->addHTML( 'Hook Text' );
  362. return false;
  363. }
  364. );
  365. $article->view();
  366. $output = $article->getContext()->getOutput();
  367. $this->assertNotContains( 'Test A', $this->getHtml( $output ) );
  368. $this->assertContains( 'Hook Text', $this->getHtml( $output ) );
  369. }
  370. public function testArticleAfterFetchContentObjectHook() {
  371. $page = $this->getPage( __METHOD__, [ 1 => 'Test A' ] );
  372. $article = new Article( $page->getTitle(), 0 );
  373. $article->getContext()->getOutput()->setTitle( $page->getTitle() );
  374. // use ArticleViewHeader hook to bypass the parser cache
  375. $this->setTemporaryHook(
  376. 'ArticleViewHeader',
  377. function ( Article $articlePage, &$outputDone, &$useParserCache ) use ( $article ) {
  378. $useParserCache = false;
  379. }
  380. );
  381. $this->setTemporaryHook(
  382. 'ArticleAfterFetchContentObject',
  383. function ( Article &$articlePage, Content &$content ) use ( $page, $article ) {
  384. $this->assertSame( $article, $articlePage, '$articlePage' );
  385. $this->assertSame( 'Test A', $content->getNativeData(), '$content' );
  386. $content = new WikitextContent( 'Hook Text' );
  387. }
  388. );
  389. $this->hideDeprecated(
  390. 'ArticleAfterFetchContentObject hook'
  391. . ' (used in hook-ArticleAfterFetchContentObject-closure)'
  392. );
  393. $article->view();
  394. $output = $article->getContext()->getOutput();
  395. $this->assertNotContains( 'Test A', $this->getHtml( $output ) );
  396. $this->assertContains( 'Hook Text', $this->getHtml( $output ) );
  397. }
  398. public function testShowMissingArticleHook() {
  399. $page = $this->getPage( __METHOD__ );
  400. $article = new Article( $page->getTitle() );
  401. $article->getContext()->getOutput()->setTitle( $page->getTitle() );
  402. $this->setTemporaryHook(
  403. 'ShowMissingArticle',
  404. function ( Article $articlePage ) use ( $article ) {
  405. $this->assertSame( $article, $articlePage, '$articlePage' );
  406. $articlePage->getContext()->getOutput()->addHTML( 'Hook Text' );
  407. }
  408. );
  409. $article->view();
  410. $output = $article->getContext()->getOutput();
  411. $this->assertContains( '(noarticletextanon)', $this->getHtml( $output ) );
  412. $this->assertContains( 'Hook Text', $this->getHtml( $output ) );
  413. }
  414. }