CategoryViewer.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753
  1. <?php
  2. /**
  3. * List and paging of category members.
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. */
  22. use MediaWiki\MediaWikiServices;
  23. class CategoryViewer extends ContextSource {
  24. /** @var int */
  25. public $limit;
  26. /** @var array */
  27. public $from;
  28. /** @var array */
  29. public $until;
  30. /** @var string[] */
  31. public $articles;
  32. /** @var array */
  33. public $articles_start_char;
  34. /** @var array */
  35. public $children;
  36. /** @var array */
  37. public $children_start_char;
  38. /** @var bool */
  39. public $showGallery;
  40. /** @var array */
  41. public $imgsNoGallery_start_char;
  42. /** @var array */
  43. public $imgsNoGallery;
  44. /** @var array */
  45. public $nextPage;
  46. /** @var array */
  47. protected $prevPage;
  48. /** @var array */
  49. public $flip;
  50. /** @var Title */
  51. public $title;
  52. /** @var Collation */
  53. public $collation;
  54. /** @var ImageGalleryBase */
  55. public $gallery;
  56. /** @var Category Category object for this page. */
  57. private $cat;
  58. /** @var array The original query array, to be used in generating paging links. */
  59. private $query;
  60. /**
  61. * @since 1.19 $context is a second, required parameter
  62. * @param Title $title
  63. * @param IContextSource $context
  64. * @param array $from An array with keys page, subcat,
  65. * and file for offset of results of each section (since 1.17)
  66. * @param array $until An array with 3 keys for until of each section (since 1.17)
  67. * @param array $query
  68. */
  69. function __construct( $title, IContextSource $context, $from = [],
  70. $until = [], $query = []
  71. ) {
  72. $this->title = $title;
  73. $this->setContext( $context );
  74. $this->getOutput()->addModuleStyles( [
  75. 'mediawiki.action.view.categoryPage.styles'
  76. ] );
  77. $this->from = $from;
  78. $this->until = $until;
  79. $this->limit = $context->getConfig()->get( 'CategoryPagingLimit' );
  80. $this->cat = Category::newFromTitle( $title );
  81. $this->query = $query;
  82. $this->collation = Collation::singleton();
  83. unset( $this->query['title'] );
  84. }
  85. /**
  86. * Format the category data list.
  87. *
  88. * @return string HTML output
  89. */
  90. public function getHTML() {
  91. $this->showGallery = $this->getConfig()->get( 'CategoryMagicGallery' )
  92. && !$this->getOutput()->mNoGallery;
  93. $this->clearCategoryState();
  94. $this->doCategoryQuery();
  95. $this->finaliseCategoryState();
  96. $r = $this->getSubcategorySection() .
  97. $this->getPagesSection() .
  98. $this->getImageSection();
  99. if ( $r == '' ) {
  100. // If there is no category content to display, only
  101. // show the top part of the navigation links.
  102. // @todo FIXME: Cannot be completely suppressed because it
  103. // is unknown if 'until' or 'from' makes this
  104. // give 0 results.
  105. $r = $r . $this->getCategoryTop();
  106. } else {
  107. $r = $this->getCategoryTop() .
  108. $r .
  109. $this->getCategoryBottom();
  110. }
  111. // Give a proper message if category is empty
  112. if ( $r == '' ) {
  113. $r = $this->msg( 'category-empty' )->parseAsBlock();
  114. }
  115. $lang = $this->getLanguage();
  116. $attribs = [
  117. 'class' => 'mw-category-generated',
  118. 'lang' => $lang->getHtmlCode(),
  119. 'dir' => $lang->getDir()
  120. ];
  121. # put a div around the headings which are in the user language
  122. $r = Html::openElement( 'div', $attribs ) . $r . '</div>';
  123. return $r;
  124. }
  125. function clearCategoryState() {
  126. $this->articles = [];
  127. $this->articles_start_char = [];
  128. $this->children = [];
  129. $this->children_start_char = [];
  130. if ( $this->showGallery ) {
  131. // Note that null for mode is taken to mean use default.
  132. $mode = $this->getRequest()->getVal( 'gallerymode', null );
  133. try {
  134. $this->gallery = ImageGalleryBase::factory( $mode, $this->getContext() );
  135. } catch ( Exception $e ) {
  136. // User specified something invalid, fallback to default.
  137. $this->gallery = ImageGalleryBase::factory( false, $this->getContext() );
  138. }
  139. $this->gallery->setHideBadImages();
  140. } else {
  141. $this->imgsNoGallery = [];
  142. $this->imgsNoGallery_start_char = [];
  143. }
  144. }
  145. /**
  146. * Add a subcategory to the internal lists, using a Category object
  147. * @param Category $cat
  148. * @param string $sortkey
  149. * @param int $pageLength
  150. */
  151. function addSubcategoryObject( Category $cat, $sortkey, $pageLength ) {
  152. // Subcategory; strip the 'Category' namespace from the link text.
  153. $title = $cat->getTitle();
  154. $this->children[] = $this->generateLink(
  155. 'subcat',
  156. $title,
  157. $title->isRedirect(),
  158. htmlspecialchars( $title->getText() )
  159. );
  160. $this->children_start_char[] =
  161. $this->getSubcategorySortChar( $cat->getTitle(), $sortkey );
  162. }
  163. function generateLink( $type, Title $title, $isRedirect, $html = null ) {
  164. $link = null;
  165. Hooks::run( 'CategoryViewer::generateLink', [ $type, $title, $html, &$link ] );
  166. if ( $link === null ) {
  167. $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
  168. if ( $html !== null ) {
  169. $html = new HtmlArmor( $html );
  170. }
  171. $link = $linkRenderer->makeLink( $title, $html );
  172. }
  173. if ( $isRedirect ) {
  174. $link = '<span class="redirect-in-category">' . $link . '</span>';
  175. }
  176. return $link;
  177. }
  178. /**
  179. * Get the character to be used for sorting subcategories.
  180. * If there's a link from Category:A to Category:B, the sortkey of the resulting
  181. * entry in the categorylinks table is Category:A, not A, which it SHOULD be.
  182. * Workaround: If sortkey == "Category:".$title, than use $title for sorting,
  183. * else use sortkey...
  184. *
  185. * @param Title $title
  186. * @param string $sortkey The human-readable sortkey (before transforming to icu or whatever).
  187. * @return string
  188. */
  189. function getSubcategorySortChar( $title, $sortkey ) {
  190. global $wgContLang;
  191. if ( $title->getPrefixedText() == $sortkey ) {
  192. $word = $title->getDBkey();
  193. } else {
  194. $word = $sortkey;
  195. }
  196. $firstChar = $this->collation->getFirstLetter( $word );
  197. return $wgContLang->convert( $firstChar );
  198. }
  199. /**
  200. * Add a page in the image namespace
  201. * @param Title $title
  202. * @param string $sortkey
  203. * @param int $pageLength
  204. * @param bool $isRedirect
  205. */
  206. function addImage( Title $title, $sortkey, $pageLength, $isRedirect = false ) {
  207. global $wgContLang;
  208. if ( $this->showGallery ) {
  209. $flip = $this->flip['file'];
  210. if ( $flip ) {
  211. $this->gallery->insert( $title );
  212. } else {
  213. $this->gallery->add( $title );
  214. }
  215. } else {
  216. $this->imgsNoGallery[] = $this->generateLink( 'image', $title, $isRedirect );
  217. $this->imgsNoGallery_start_char[] = $wgContLang->convert(
  218. $this->collation->getFirstLetter( $sortkey ) );
  219. }
  220. }
  221. /**
  222. * Add a miscellaneous page
  223. * @param Title $title
  224. * @param string $sortkey
  225. * @param int $pageLength
  226. * @param bool $isRedirect
  227. */
  228. function addPage( $title, $sortkey, $pageLength, $isRedirect = false ) {
  229. global $wgContLang;
  230. $this->articles[] = $this->generateLink( 'page', $title, $isRedirect );
  231. $this->articles_start_char[] = $wgContLang->convert(
  232. $this->collation->getFirstLetter( $sortkey ) );
  233. }
  234. function finaliseCategoryState() {
  235. if ( $this->flip['subcat'] ) {
  236. $this->children = array_reverse( $this->children );
  237. $this->children_start_char = array_reverse( $this->children_start_char );
  238. }
  239. if ( $this->flip['page'] ) {
  240. $this->articles = array_reverse( $this->articles );
  241. $this->articles_start_char = array_reverse( $this->articles_start_char );
  242. }
  243. if ( !$this->showGallery && $this->flip['file'] ) {
  244. $this->imgsNoGallery = array_reverse( $this->imgsNoGallery );
  245. $this->imgsNoGallery_start_char = array_reverse( $this->imgsNoGallery_start_char );
  246. }
  247. }
  248. function doCategoryQuery() {
  249. $dbr = wfGetDB( DB_REPLICA, 'category' );
  250. $this->nextPage = [
  251. 'page' => null,
  252. 'subcat' => null,
  253. 'file' => null,
  254. ];
  255. $this->prevPage = [
  256. 'page' => null,
  257. 'subcat' => null,
  258. 'file' => null,
  259. ];
  260. $this->flip = [ 'page' => false, 'subcat' => false, 'file' => false ];
  261. foreach ( [ 'page', 'subcat', 'file' ] as $type ) {
  262. # Get the sortkeys for start/end, if applicable. Note that if
  263. # the collation in the database differs from the one
  264. # set in $wgCategoryCollation, pagination might go totally haywire.
  265. $extraConds = [ 'cl_type' => $type ];
  266. if ( isset( $this->from[$type] ) && $this->from[$type] !== null ) {
  267. $extraConds[] = 'cl_sortkey >= '
  268. . $dbr->addQuotes( $this->collation->getSortKey( $this->from[$type] ) );
  269. } elseif ( isset( $this->until[$type] ) && $this->until[$type] !== null ) {
  270. $extraConds[] = 'cl_sortkey < '
  271. . $dbr->addQuotes( $this->collation->getSortKey( $this->until[$type] ) );
  272. $this->flip[$type] = true;
  273. }
  274. $res = $dbr->select(
  275. [ 'page', 'categorylinks', 'category' ],
  276. array_merge(
  277. LinkCache::getSelectFields(),
  278. [
  279. 'page_namespace',
  280. 'page_title',
  281. 'cl_sortkey',
  282. 'cat_id',
  283. 'cat_title',
  284. 'cat_subcats',
  285. 'cat_pages',
  286. 'cat_files',
  287. 'cl_sortkey_prefix',
  288. 'cl_collation'
  289. ]
  290. ),
  291. array_merge( [ 'cl_to' => $this->title->getDBkey() ], $extraConds ),
  292. __METHOD__,
  293. [
  294. 'USE INDEX' => [ 'categorylinks' => 'cl_sortkey' ],
  295. 'LIMIT' => $this->limit + 1,
  296. 'ORDER BY' => $this->flip[$type] ? 'cl_sortkey DESC' : 'cl_sortkey',
  297. ],
  298. [
  299. 'categorylinks' => [ 'INNER JOIN', 'cl_from = page_id' ],
  300. 'category' => [ 'LEFT JOIN', [
  301. 'cat_title = page_title',
  302. 'page_namespace' => NS_CATEGORY
  303. ] ]
  304. ]
  305. );
  306. Hooks::run( 'CategoryViewer::doCategoryQuery', [ $type, $res ] );
  307. $linkCache = MediaWikiServices::getInstance()->getLinkCache();
  308. $count = 0;
  309. foreach ( $res as $row ) {
  310. $title = Title::newFromRow( $row );
  311. $linkCache->addGoodLinkObjFromRow( $title, $row );
  312. if ( $row->cl_collation === '' ) {
  313. // Hack to make sure that while updating from 1.16 schema
  314. // and db is inconsistent, that the sky doesn't fall.
  315. // See r83544. Could perhaps be removed in a couple decades...
  316. $humanSortkey = $row->cl_sortkey;
  317. } else {
  318. $humanSortkey = $title->getCategorySortkey( $row->cl_sortkey_prefix );
  319. }
  320. if ( ++$count > $this->limit ) {
  321. # We've reached the one extra which shows that there
  322. # are additional pages to be had. Stop here...
  323. $this->nextPage[$type] = $humanSortkey;
  324. break;
  325. }
  326. if ( $count == $this->limit ) {
  327. $this->prevPage[$type] = $humanSortkey;
  328. }
  329. if ( $title->getNamespace() == NS_CATEGORY ) {
  330. $cat = Category::newFromRow( $row, $title );
  331. $this->addSubcategoryObject( $cat, $humanSortkey, $row->page_len );
  332. } elseif ( $title->getNamespace() == NS_FILE ) {
  333. $this->addImage( $title, $humanSortkey, $row->page_len, $row->page_is_redirect );
  334. } else {
  335. $this->addPage( $title, $humanSortkey, $row->page_len, $row->page_is_redirect );
  336. }
  337. }
  338. }
  339. }
  340. /**
  341. * @return string
  342. */
  343. function getCategoryTop() {
  344. $r = $this->getCategoryBottom();
  345. return $r === ''
  346. ? $r
  347. : "<br style=\"clear:both;\"/>\n" . $r;
  348. }
  349. /**
  350. * @return string
  351. */
  352. function getSubcategorySection() {
  353. # Don't show subcategories section if there are none.
  354. $r = '';
  355. $rescnt = count( $this->children );
  356. $dbcnt = $this->cat->getSubcatCount();
  357. // This function should be called even if the result isn't used, it has side-effects
  358. $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'subcat' );
  359. if ( $rescnt > 0 ) {
  360. # Showing subcategories
  361. $r .= "<div id=\"mw-subcategories\">\n";
  362. $r .= '<h2>' . $this->msg( 'subcategories' )->parse() . "</h2>\n";
  363. $r .= $countmsg;
  364. $r .= $this->getSectionPagingLinks( 'subcat' );
  365. $r .= $this->formatList( $this->children, $this->children_start_char );
  366. $r .= $this->getSectionPagingLinks( 'subcat' );
  367. $r .= "\n</div>";
  368. }
  369. return $r;
  370. }
  371. /**
  372. * @return string
  373. */
  374. function getPagesSection() {
  375. $ti = wfEscapeWikiText( $this->title->getText() );
  376. # Don't show articles section if there are none.
  377. $r = '';
  378. # @todo FIXME: Here and in the other two sections: we don't need to bother
  379. # with this rigmarole if the entire category contents fit on one page
  380. # and have already been retrieved. We can just use $rescnt in that
  381. # case and save a query and some logic.
  382. $dbcnt = $this->cat->getPageCount() - $this->cat->getSubcatCount()
  383. - $this->cat->getFileCount();
  384. $rescnt = count( $this->articles );
  385. // This function should be called even if the result isn't used, it has side-effects
  386. $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'article' );
  387. if ( $rescnt > 0 ) {
  388. $r = "<div id=\"mw-pages\">\n";
  389. $r .= '<h2>' . $this->msg( 'category_header', $ti )->parse() . "</h2>\n";
  390. $r .= $countmsg;
  391. $r .= $this->getSectionPagingLinks( 'page' );
  392. $r .= $this->formatList( $this->articles, $this->articles_start_char );
  393. $r .= $this->getSectionPagingLinks( 'page' );
  394. $r .= "\n</div>";
  395. }
  396. return $r;
  397. }
  398. /**
  399. * @return string
  400. */
  401. function getImageSection() {
  402. $r = '';
  403. $rescnt = $this->showGallery ? $this->gallery->count() : count( $this->imgsNoGallery );
  404. $dbcnt = $this->cat->getFileCount();
  405. // This function should be called even if the result isn't used, it has side-effects
  406. $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'file' );
  407. if ( $rescnt > 0 ) {
  408. $r .= "<div id=\"mw-category-media\">\n";
  409. $r .= '<h2>' .
  410. $this->msg(
  411. 'category-media-header',
  412. wfEscapeWikiText( $this->title->getText() )
  413. )->text() .
  414. "</h2>\n";
  415. $r .= $countmsg;
  416. $r .= $this->getSectionPagingLinks( 'file' );
  417. if ( $this->showGallery ) {
  418. $r .= $this->gallery->toHTML();
  419. } else {
  420. $r .= $this->formatList( $this->imgsNoGallery, $this->imgsNoGallery_start_char );
  421. }
  422. $r .= $this->getSectionPagingLinks( 'file' );
  423. $r .= "\n</div>";
  424. }
  425. return $r;
  426. }
  427. /**
  428. * Get the paging links for a section (subcats/pages/files), to go at the top and bottom
  429. * of the output.
  430. *
  431. * @param string $type 'page', 'subcat', or 'file'
  432. * @return string HTML output, possibly empty if there are no other pages
  433. */
  434. private function getSectionPagingLinks( $type ) {
  435. if ( isset( $this->until[$type] ) && $this->until[$type] !== null ) {
  436. // The new value for the until parameter should be pointing to the first
  437. // result displayed on the page which is the second last result retrieved
  438. // from the database.The next link should have a from parameter pointing
  439. // to the until parameter of the current page.
  440. if ( $this->nextPage[$type] !== null ) {
  441. return $this->pagingLinks( $this->prevPage[$type], $this->until[$type], $type );
  442. } else {
  443. // If the nextPage variable is null, it means that we have reached the first page
  444. // and therefore the previous link should be disabled.
  445. return $this->pagingLinks( null, $this->until[$type], $type );
  446. }
  447. } elseif ( $this->nextPage[$type] !== null
  448. || ( isset( $this->from[$type] ) && $this->from[$type] !== null )
  449. ) {
  450. return $this->pagingLinks( $this->from[$type], $this->nextPage[$type], $type );
  451. } else {
  452. return '';
  453. }
  454. }
  455. /**
  456. * @return string
  457. */
  458. function getCategoryBottom() {
  459. return '';
  460. }
  461. /**
  462. * Format a list of articles chunked by letter, either as a
  463. * bullet list or a columnar format, depending on the length.
  464. *
  465. * @param array $articles
  466. * @param array $articles_start_char
  467. * @param int $cutoff
  468. * @return string
  469. * @private
  470. */
  471. function formatList( $articles, $articles_start_char, $cutoff = 6 ) {
  472. $list = '';
  473. if ( count( $articles ) > $cutoff ) {
  474. $list = self::columnList( $articles, $articles_start_char );
  475. } elseif ( count( $articles ) > 0 ) {
  476. // for short lists of articles in categories.
  477. $list = self::shortList( $articles, $articles_start_char );
  478. }
  479. $pageLang = $this->title->getPageLanguage();
  480. $attribs = [ 'lang' => $pageLang->getHtmlCode(), 'dir' => $pageLang->getDir(),
  481. 'class' => 'mw-content-' . $pageLang->getDir() ];
  482. $list = Html::rawElement( 'div', $attribs, $list );
  483. return $list;
  484. }
  485. /**
  486. * Format a list of articles chunked by letter in a three-column list, ordered
  487. * vertically. This is used for categories with a significant number of pages.
  488. *
  489. * TODO: Take the headers into account when creating columns, so they're
  490. * more visually equal.
  491. *
  492. * TODO: shortList and columnList are similar, need merging
  493. *
  494. * @param string[] $articles HTML links to each article
  495. * @param string[] $articles_start_char The header characters for each article
  496. * @return string HTML to output
  497. * @private
  498. */
  499. static function columnList( $articles, $articles_start_char ) {
  500. $columns = array_combine( $articles, $articles_start_char );
  501. $ret = Html::openElement( 'div', [ 'class' => 'mw-category' ] );
  502. $colContents = [];
  503. # Kind of like array_flip() here, but we keep duplicates in an
  504. # array instead of dropping them.
  505. foreach ( $columns as $article => $char ) {
  506. if ( !isset( $colContents[$char] ) ) {
  507. $colContents[$char] = [];
  508. }
  509. $colContents[$char][] = $article;
  510. }
  511. foreach ( $colContents as $char => $articles ) {
  512. # Change space to non-breaking space to keep headers aligned
  513. $h3char = $char === ' ' ? '&#160;' : htmlspecialchars( $char );
  514. $ret .= '<div class="mw-category-group"><h3>' . $h3char;
  515. $ret .= "</h3>\n";
  516. $ret .= '<ul><li>';
  517. $ret .= implode( "</li>\n<li>", $articles );
  518. $ret .= '</li></ul></div>';
  519. }
  520. $ret .= Html::closeElement( 'div' );
  521. return $ret;
  522. }
  523. /**
  524. * Format a list of articles chunked by letter in a bullet list. This is used
  525. * for categories with a small number of pages (when columns aren't needed).
  526. * @param string[] $articles HTML links to each article
  527. * @param string[] $articles_start_char The header characters for each article
  528. * @return string HTML to output
  529. * @private
  530. */
  531. static function shortList( $articles, $articles_start_char ) {
  532. $r = '<h3>' . htmlspecialchars( $articles_start_char[0] ) . "</h3>\n";
  533. $r .= '<ul><li>' . $articles[0] . '</li>';
  534. $articleCount = count( $articles );
  535. for ( $index = 1; $index < $articleCount; $index++ ) {
  536. if ( $articles_start_char[$index] != $articles_start_char[$index - 1] ) {
  537. $r .= "</ul><h3>" . htmlspecialchars( $articles_start_char[$index] ) . "</h3>\n<ul>";
  538. }
  539. $r .= "<li>{$articles[$index]}</li>";
  540. }
  541. $r .= '</ul>';
  542. return $r;
  543. }
  544. /**
  545. * Create paging links, as a helper method to getSectionPagingLinks().
  546. *
  547. * @param string $first The 'until' parameter for the generated URL
  548. * @param string $last The 'from' parameter for the generated URL
  549. * @param string $type A prefix for parameters, 'page' or 'subcat' or
  550. * 'file'
  551. * @return string HTML
  552. */
  553. private function pagingLinks( $first, $last, $type = '' ) {
  554. $prevLink = $this->msg( 'prev-page' )->escaped();
  555. $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
  556. if ( $first != '' ) {
  557. $prevQuery = $this->query;
  558. $prevQuery["{$type}until"] = $first;
  559. unset( $prevQuery["{$type}from"] );
  560. $prevLink = $linkRenderer->makeKnownLink(
  561. $this->addFragmentToTitle( $this->title, $type ),
  562. new HtmlArmor( $prevLink ),
  563. [],
  564. $prevQuery
  565. );
  566. }
  567. $nextLink = $this->msg( 'next-page' )->escaped();
  568. if ( $last != '' ) {
  569. $lastQuery = $this->query;
  570. $lastQuery["{$type}from"] = $last;
  571. unset( $lastQuery["{$type}until"] );
  572. $nextLink = $linkRenderer->makeKnownLink(
  573. $this->addFragmentToTitle( $this->title, $type ),
  574. new HtmlArmor( $nextLink ),
  575. [],
  576. $lastQuery
  577. );
  578. }
  579. return $this->msg( 'categoryviewer-pagedlinks' )->rawParams( $prevLink, $nextLink )->escaped();
  580. }
  581. /**
  582. * Takes a title, and adds the fragment identifier that
  583. * corresponds to the correct segment of the category.
  584. *
  585. * @param Title $title The title (usually $this->title)
  586. * @param string $section Which section
  587. * @throws MWException
  588. * @return Title
  589. */
  590. private function addFragmentToTitle( $title, $section ) {
  591. switch ( $section ) {
  592. case 'page':
  593. $fragment = 'mw-pages';
  594. break;
  595. case 'subcat':
  596. $fragment = 'mw-subcategories';
  597. break;
  598. case 'file':
  599. $fragment = 'mw-category-media';
  600. break;
  601. default:
  602. throw new MWException( __METHOD__ .
  603. " Invalid section $section." );
  604. }
  605. return Title::makeTitle( $title->getNamespace(),
  606. $title->getDBkey(), $fragment );
  607. }
  608. /**
  609. * What to do if the category table conflicts with the number of results
  610. * returned? This function says what. Each type is considered independently
  611. * of the other types.
  612. *
  613. * @param int $rescnt The number of items returned by our database query.
  614. * @param int $dbcnt The number of items according to the category table.
  615. * @param string $type 'subcat', 'article', or 'file'
  616. * @return string A message giving the number of items, to output to HTML.
  617. */
  618. private function getCountMessage( $rescnt, $dbcnt, $type ) {
  619. // There are three cases:
  620. // 1) The category table figure seems sane. It might be wrong, but
  621. // we can't do anything about it if we don't recalculate it on ev-
  622. // ery category view.
  623. // 2) The category table figure isn't sane, like it's smaller than the
  624. // number of actual results, *but* the number of results is less
  625. // than $this->limit and there's no offset. In this case we still
  626. // know the right figure.
  627. // 3) We have no idea.
  628. // Check if there's a "from" or "until" for anything
  629. // This is a little ugly, but we seem to use different names
  630. // for the paging types then for the messages.
  631. if ( $type === 'article' ) {
  632. $pagingType = 'page';
  633. } else {
  634. $pagingType = $type;
  635. }
  636. $fromOrUntil = false;
  637. if ( ( isset( $this->from[$pagingType] ) && $this->from[$pagingType] !== null ) ||
  638. ( isset( $this->until[$pagingType] ) && $this->until[$pagingType] !== null )
  639. ) {
  640. $fromOrUntil = true;
  641. }
  642. if ( $dbcnt == $rescnt ||
  643. ( ( $rescnt == $this->limit || $fromOrUntil ) && $dbcnt > $rescnt )
  644. ) {
  645. // Case 1: seems sane.
  646. $totalcnt = $dbcnt;
  647. } elseif ( $rescnt < $this->limit && !$fromOrUntil ) {
  648. // Case 2: not sane, but salvageable. Use the number of results.
  649. // Since there are fewer than 200, we can also take this opportunity
  650. // to refresh the incorrect category table entry -- which should be
  651. // quick due to the small number of entries.
  652. $totalcnt = $rescnt;
  653. DeferredUpdates::addCallableUpdate( [ $this->cat, 'refreshCounts' ] );
  654. } else {
  655. // Case 3: hopeless. Don't give a total count at all.
  656. // Messages: category-subcat-count-limited, category-article-count-limited,
  657. // category-file-count-limited
  658. return $this->msg( "category-$type-count-limited" )->numParams( $rescnt )->parseAsBlock();
  659. }
  660. // Messages: category-subcat-count, category-article-count, category-file-count
  661. return $this->msg( "category-$type-count" )->numParams( $rescnt, $totalcnt )->parseAsBlock();
  662. }
  663. }