CategoryViewer.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742
  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 = $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. if ( $title->getPrefixedText() == $sortkey ) {
  191. $word = $title->getDBkey();
  192. } else {
  193. $word = $sortkey;
  194. }
  195. $firstChar = $this->collation->getFirstLetter( $word );
  196. return MediaWikiServices::getInstance()->getContentLanguage()->convert( $firstChar );
  197. }
  198. /**
  199. * Add a page in the image namespace
  200. * @param Title $title
  201. * @param string $sortkey
  202. * @param int $pageLength
  203. * @param bool $isRedirect
  204. */
  205. function addImage( Title $title, $sortkey, $pageLength, $isRedirect = false ) {
  206. if ( $this->showGallery ) {
  207. $flip = $this->flip['file'];
  208. if ( $flip ) {
  209. $this->gallery->insert( $title );
  210. } else {
  211. $this->gallery->add( $title );
  212. }
  213. } else {
  214. $this->imgsNoGallery[] = $this->generateLink( 'image', $title, $isRedirect );
  215. $this->imgsNoGallery_start_char[] = MediaWikiServices::getInstance()->
  216. getContentLanguage()->convert( $this->collation->getFirstLetter( $sortkey ) );
  217. }
  218. }
  219. /**
  220. * Add a miscellaneous page
  221. * @param Title $title
  222. * @param string $sortkey
  223. * @param int $pageLength
  224. * @param bool $isRedirect
  225. */
  226. function addPage( $title, $sortkey, $pageLength, $isRedirect = false ) {
  227. $this->articles[] = $this->generateLink( 'page', $title, $isRedirect );
  228. $this->articles_start_char[] = MediaWikiServices::getInstance()->
  229. getContentLanguage()->convert( $this->collation->getFirstLetter( $sortkey ) );
  230. }
  231. function finaliseCategoryState() {
  232. if ( $this->flip['subcat'] ) {
  233. $this->children = array_reverse( $this->children );
  234. $this->children_start_char = array_reverse( $this->children_start_char );
  235. }
  236. if ( $this->flip['page'] ) {
  237. $this->articles = array_reverse( $this->articles );
  238. $this->articles_start_char = array_reverse( $this->articles_start_char );
  239. }
  240. if ( !$this->showGallery && $this->flip['file'] ) {
  241. $this->imgsNoGallery = array_reverse( $this->imgsNoGallery );
  242. $this->imgsNoGallery_start_char = array_reverse( $this->imgsNoGallery_start_char );
  243. }
  244. }
  245. function doCategoryQuery() {
  246. $dbr = wfGetDB( DB_REPLICA, 'category' );
  247. $this->nextPage = [
  248. 'page' => null,
  249. 'subcat' => null,
  250. 'file' => null,
  251. ];
  252. $this->prevPage = [
  253. 'page' => null,
  254. 'subcat' => null,
  255. 'file' => null,
  256. ];
  257. $this->flip = [ 'page' => false, 'subcat' => false, 'file' => false ];
  258. foreach ( [ 'page', 'subcat', 'file' ] as $type ) {
  259. # Get the sortkeys for start/end, if applicable. Note that if
  260. # the collation in the database differs from the one
  261. # set in $wgCategoryCollation, pagination might go totally haywire.
  262. $extraConds = [ 'cl_type' => $type ];
  263. if ( isset( $this->from[$type] ) && $this->from[$type] !== null ) {
  264. $extraConds[] = 'cl_sortkey >= '
  265. . $dbr->addQuotes( $this->collation->getSortKey( $this->from[$type] ) );
  266. } elseif ( isset( $this->until[$type] ) && $this->until[$type] !== null ) {
  267. $extraConds[] = 'cl_sortkey < '
  268. . $dbr->addQuotes( $this->collation->getSortKey( $this->until[$type] ) );
  269. $this->flip[$type] = true;
  270. }
  271. $res = $dbr->select(
  272. [ 'page', 'categorylinks', 'category' ],
  273. array_merge(
  274. LinkCache::getSelectFields(),
  275. [
  276. 'page_namespace',
  277. 'page_title',
  278. 'cl_sortkey',
  279. 'cat_id',
  280. 'cat_title',
  281. 'cat_subcats',
  282. 'cat_pages',
  283. 'cat_files',
  284. 'cl_sortkey_prefix',
  285. 'cl_collation'
  286. ]
  287. ),
  288. array_merge( [ 'cl_to' => $this->title->getDBkey() ], $extraConds ),
  289. __METHOD__,
  290. [
  291. 'USE INDEX' => [ 'categorylinks' => 'cl_sortkey' ],
  292. 'LIMIT' => $this->limit + 1,
  293. 'ORDER BY' => $this->flip[$type] ? 'cl_sortkey DESC' : 'cl_sortkey',
  294. ],
  295. [
  296. 'categorylinks' => [ 'JOIN', 'cl_from = page_id' ],
  297. 'category' => [ 'LEFT JOIN', [
  298. 'cat_title = page_title',
  299. 'page_namespace' => NS_CATEGORY
  300. ] ]
  301. ]
  302. );
  303. Hooks::run( 'CategoryViewer::doCategoryQuery', [ $type, $res ] );
  304. $linkCache = MediaWikiServices::getInstance()->getLinkCache();
  305. $count = 0;
  306. foreach ( $res as $row ) {
  307. $title = Title::newFromRow( $row );
  308. $linkCache->addGoodLinkObjFromRow( $title, $row );
  309. if ( $row->cl_collation === '' ) {
  310. // Hack to make sure that while updating from 1.16 schema
  311. // and db is inconsistent, that the sky doesn't fall.
  312. // See r83544. Could perhaps be removed in a couple decades...
  313. $humanSortkey = $row->cl_sortkey;
  314. } else {
  315. $humanSortkey = $title->getCategorySortkey( $row->cl_sortkey_prefix );
  316. }
  317. if ( ++$count > $this->limit ) {
  318. # We've reached the one extra which shows that there
  319. # are additional pages to be had. Stop here...
  320. $this->nextPage[$type] = $humanSortkey;
  321. break;
  322. }
  323. if ( $count == $this->limit ) {
  324. $this->prevPage[$type] = $humanSortkey;
  325. }
  326. if ( $title->getNamespace() == NS_CATEGORY ) {
  327. $cat = Category::newFromRow( $row, $title );
  328. $this->addSubcategoryObject( $cat, $humanSortkey, $row->page_len );
  329. } elseif ( $title->getNamespace() == NS_FILE ) {
  330. $this->addImage( $title, $humanSortkey, $row->page_len, $row->page_is_redirect );
  331. } else {
  332. $this->addPage( $title, $humanSortkey, $row->page_len, $row->page_is_redirect );
  333. }
  334. }
  335. }
  336. }
  337. /**
  338. * @return string
  339. */
  340. function getCategoryTop() {
  341. $r = $this->getCategoryBottom();
  342. return $r === ''
  343. ? $r
  344. : "<br style=\"clear:both;\"/>\n" . $r;
  345. }
  346. /**
  347. * @return string
  348. */
  349. function getSubcategorySection() {
  350. # Don't show subcategories section if there are none.
  351. $r = '';
  352. $rescnt = count( $this->children );
  353. $dbcnt = $this->cat->getSubcatCount();
  354. // This function should be called even if the result isn't used, it has side-effects
  355. $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'subcat' );
  356. if ( $rescnt > 0 ) {
  357. # Showing subcategories
  358. $r .= "<div id=\"mw-subcategories\">\n";
  359. $r .= '<h2>' . $this->msg( 'subcategories' )->parse() . "</h2>\n";
  360. $r .= $countmsg;
  361. $r .= $this->getSectionPagingLinks( 'subcat' );
  362. $r .= $this->formatList( $this->children, $this->children_start_char );
  363. $r .= $this->getSectionPagingLinks( 'subcat' );
  364. $r .= "\n</div>";
  365. }
  366. return $r;
  367. }
  368. /**
  369. * @return string
  370. */
  371. function getPagesSection() {
  372. $name = $this->getOutput()->getUnprefixedDisplayTitle();
  373. # Don't show articles section if there are none.
  374. $r = '';
  375. # @todo FIXME: Here and in the other two sections: we don't need to bother
  376. # with this rigmarole if the entire category contents fit on one page
  377. # and have already been retrieved. We can just use $rescnt in that
  378. # case and save a query and some logic.
  379. $dbcnt = $this->cat->getPageCount() - $this->cat->getSubcatCount()
  380. - $this->cat->getFileCount();
  381. $rescnt = count( $this->articles );
  382. // This function should be called even if the result isn't used, it has side-effects
  383. $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'article' );
  384. if ( $rescnt > 0 ) {
  385. $r = "<div id=\"mw-pages\">\n";
  386. $r .= '<h2>' . $this->msg( 'category_header' )->rawParams( $name )->parse() . "</h2>\n";
  387. $r .= $countmsg;
  388. $r .= $this->getSectionPagingLinks( 'page' );
  389. $r .= $this->formatList( $this->articles, $this->articles_start_char );
  390. $r .= $this->getSectionPagingLinks( 'page' );
  391. $r .= "\n</div>";
  392. }
  393. return $r;
  394. }
  395. /**
  396. * @return string
  397. */
  398. function getImageSection() {
  399. $name = $this->getOutput()->getUnprefixedDisplayTitle();
  400. $r = '';
  401. $rescnt = $this->showGallery ? $this->gallery->count() : count( $this->imgsNoGallery );
  402. $dbcnt = $this->cat->getFileCount();
  403. // This function should be called even if the result isn't used, it has side-effects
  404. $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'file' );
  405. if ( $rescnt > 0 ) {
  406. $r .= "<div id=\"mw-category-media\">\n";
  407. $r .= '<h2>' .
  408. $this->msg( 'category-media-header' )->rawParams( $name )->parse() .
  409. "</h2>\n";
  410. $r .= $countmsg;
  411. $r .= $this->getSectionPagingLinks( 'file' );
  412. if ( $this->showGallery ) {
  413. $r .= $this->gallery->toHTML();
  414. } else {
  415. $r .= $this->formatList( $this->imgsNoGallery, $this->imgsNoGallery_start_char );
  416. }
  417. $r .= $this->getSectionPagingLinks( 'file' );
  418. $r .= "\n</div>";
  419. }
  420. return $r;
  421. }
  422. /**
  423. * Get the paging links for a section (subcats/pages/files), to go at the top and bottom
  424. * of the output.
  425. *
  426. * @param string $type 'page', 'subcat', or 'file'
  427. * @return string HTML output, possibly empty if there are no other pages
  428. */
  429. private function getSectionPagingLinks( $type ) {
  430. if ( isset( $this->until[$type] ) && $this->until[$type] !== null ) {
  431. // The new value for the until parameter should be pointing to the first
  432. // result displayed on the page which is the second last result retrieved
  433. // from the database.The next link should have a from parameter pointing
  434. // to the until parameter of the current page.
  435. if ( $this->nextPage[$type] !== null ) {
  436. return $this->pagingLinks( $this->prevPage[$type], $this->until[$type], $type );
  437. } else {
  438. // If the nextPage variable is null, it means that we have reached the first page
  439. // and therefore the previous link should be disabled.
  440. return $this->pagingLinks( null, $this->until[$type], $type );
  441. }
  442. } elseif ( $this->nextPage[$type] !== null
  443. || ( isset( $this->from[$type] ) && $this->from[$type] !== null )
  444. ) {
  445. return $this->pagingLinks( $this->from[$type], $this->nextPage[$type], $type );
  446. } else {
  447. return '';
  448. }
  449. }
  450. /**
  451. * @return string
  452. */
  453. function getCategoryBottom() {
  454. return '';
  455. }
  456. /**
  457. * Format a list of articles chunked by letter, either as a
  458. * bullet list or a columnar format, depending on the length.
  459. *
  460. * @param array $articles
  461. * @param array $articles_start_char
  462. * @param int $cutoff
  463. * @return string
  464. * @private
  465. */
  466. function formatList( $articles, $articles_start_char, $cutoff = 6 ) {
  467. $list = '';
  468. if ( count( $articles ) > $cutoff ) {
  469. $list = self::columnList( $articles, $articles_start_char );
  470. } elseif ( count( $articles ) > 0 ) {
  471. // for short lists of articles in categories.
  472. $list = self::shortList( $articles, $articles_start_char );
  473. }
  474. $pageLang = $this->title->getPageLanguage();
  475. $attribs = [ 'lang' => $pageLang->getHtmlCode(), 'dir' => $pageLang->getDir(),
  476. 'class' => 'mw-content-' . $pageLang->getDir() ];
  477. $list = Html::rawElement( 'div', $attribs, $list );
  478. return $list;
  479. }
  480. /**
  481. * Format a list of articles chunked by letter in a three-column list, ordered
  482. * vertically. This is used for categories with a significant number of pages.
  483. *
  484. * TODO: Take the headers into account when creating columns, so they're
  485. * more visually equal.
  486. *
  487. * TODO: shortList and columnList are similar, need merging
  488. *
  489. * @param string[] $articles HTML links to each article
  490. * @param string[] $articles_start_char The header characters for each article
  491. * @return string HTML to output
  492. * @private
  493. */
  494. static function columnList( $articles, $articles_start_char ) {
  495. $columns = array_combine( $articles, $articles_start_char );
  496. $ret = Html::openElement( 'div', [ 'class' => 'mw-category' ] );
  497. $colContents = [];
  498. # Kind of like array_flip() here, but we keep duplicates in an
  499. # array instead of dropping them.
  500. foreach ( $columns as $article => $char ) {
  501. if ( !isset( $colContents[$char] ) ) {
  502. $colContents[$char] = [];
  503. }
  504. $colContents[$char][] = $article;
  505. }
  506. foreach ( $colContents as $char => $articles ) {
  507. # Change space to non-breaking space to keep headers aligned
  508. $h3char = $char === ' ' ? "\u{00A0}" : htmlspecialchars( $char );
  509. $ret .= '<div class="mw-category-group"><h3>' . $h3char;
  510. $ret .= "</h3>\n";
  511. $ret .= '<ul><li>';
  512. $ret .= implode( "</li>\n<li>", $articles );
  513. $ret .= '</li></ul></div>';
  514. }
  515. $ret .= Html::closeElement( 'div' );
  516. return $ret;
  517. }
  518. /**
  519. * Format a list of articles chunked by letter in a bullet list. This is used
  520. * for categories with a small number of pages (when columns aren't needed).
  521. * @param string[] $articles HTML links to each article
  522. * @param string[] $articles_start_char The header characters for each article
  523. * @return string HTML to output
  524. * @private
  525. */
  526. static function shortList( $articles, $articles_start_char ) {
  527. $r = '<h3>' . htmlspecialchars( $articles_start_char[0] ) . "</h3>\n";
  528. $r .= '<ul><li>' . $articles[0] . '</li>';
  529. $articleCount = count( $articles );
  530. for ( $index = 1; $index < $articleCount; $index++ ) {
  531. if ( $articles_start_char[$index] != $articles_start_char[$index - 1] ) {
  532. $r .= "</ul><h3>" . htmlspecialchars( $articles_start_char[$index] ) . "</h3>\n<ul>";
  533. }
  534. $r .= "<li>{$articles[$index]}</li>";
  535. }
  536. $r .= '</ul>';
  537. return $r;
  538. }
  539. /**
  540. * Create paging links, as a helper method to getSectionPagingLinks().
  541. *
  542. * @param string $first The 'until' parameter for the generated URL
  543. * @param string $last The 'from' parameter for the generated URL
  544. * @param string $type A prefix for parameters, 'page' or 'subcat' or
  545. * 'file'
  546. * @return string HTML
  547. */
  548. private function pagingLinks( $first, $last, $type = '' ) {
  549. $prevLink = $this->msg( 'prev-page' )->escaped();
  550. $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
  551. if ( $first != '' ) {
  552. $prevQuery = $this->query;
  553. $prevQuery["{$type}until"] = $first;
  554. unset( $prevQuery["{$type}from"] );
  555. $prevLink = $linkRenderer->makeKnownLink(
  556. $this->addFragmentToTitle( $this->title, $type ),
  557. new HtmlArmor( $prevLink ),
  558. [],
  559. $prevQuery
  560. );
  561. }
  562. $nextLink = $this->msg( 'next-page' )->escaped();
  563. if ( $last != '' ) {
  564. $lastQuery = $this->query;
  565. $lastQuery["{$type}from"] = $last;
  566. unset( $lastQuery["{$type}until"] );
  567. $nextLink = $linkRenderer->makeKnownLink(
  568. $this->addFragmentToTitle( $this->title, $type ),
  569. new HtmlArmor( $nextLink ),
  570. [],
  571. $lastQuery
  572. );
  573. }
  574. return $this->msg( 'categoryviewer-pagedlinks' )->rawParams( $prevLink, $nextLink )->escaped();
  575. }
  576. /**
  577. * Takes a title, and adds the fragment identifier that
  578. * corresponds to the correct segment of the category.
  579. *
  580. * @param Title $title The title (usually $this->title)
  581. * @param string $section Which section
  582. * @throws MWException
  583. * @return Title
  584. */
  585. private function addFragmentToTitle( $title, $section ) {
  586. switch ( $section ) {
  587. case 'page':
  588. $fragment = 'mw-pages';
  589. break;
  590. case 'subcat':
  591. $fragment = 'mw-subcategories';
  592. break;
  593. case 'file':
  594. $fragment = 'mw-category-media';
  595. break;
  596. default:
  597. throw new MWException( __METHOD__ .
  598. " Invalid section $section." );
  599. }
  600. return Title::makeTitle( $title->getNamespace(),
  601. $title->getDBkey(), $fragment );
  602. }
  603. /**
  604. * What to do if the category table conflicts with the number of results
  605. * returned? This function says what. Each type is considered independently
  606. * of the other types.
  607. *
  608. * @param int $rescnt The number of items returned by our database query.
  609. * @param int $dbcnt The number of items according to the category table.
  610. * @param string $type 'subcat', 'article', or 'file'
  611. * @return string A message giving the number of items, to output to HTML.
  612. */
  613. private function getCountMessage( $rescnt, $dbcnt, $type ) {
  614. // There are three cases:
  615. // 1) The category table figure seems sane. It might be wrong, but
  616. // we can't do anything about it if we don't recalculate it on ev-
  617. // ery category view.
  618. // 2) The category table figure isn't sane, like it's smaller than the
  619. // number of actual results, *but* the number of results is less
  620. // than $this->limit and there's no offset. In this case we still
  621. // know the right figure.
  622. // 3) We have no idea.
  623. // Check if there's a "from" or "until" for anything
  624. // This is a little ugly, but we seem to use different names
  625. // for the paging types then for the messages.
  626. if ( $type === 'article' ) {
  627. $pagingType = 'page';
  628. } else {
  629. $pagingType = $type;
  630. }
  631. $fromOrUntil = false;
  632. if ( ( isset( $this->from[$pagingType] ) && $this->from[$pagingType] !== null ) ||
  633. ( isset( $this->until[$pagingType] ) && $this->until[$pagingType] !== null )
  634. ) {
  635. $fromOrUntil = true;
  636. }
  637. if ( $dbcnt == $rescnt ||
  638. ( ( $rescnt == $this->limit || $fromOrUntil ) && $dbcnt > $rescnt )
  639. ) {
  640. // Case 1: seems sane.
  641. $totalcnt = $dbcnt;
  642. } elseif ( $rescnt < $this->limit && !$fromOrUntil ) {
  643. // Case 2: not sane, but salvageable. Use the number of results.
  644. $totalcnt = $rescnt;
  645. } else {
  646. // Case 3: hopeless. Don't give a total count at all.
  647. // Messages: category-subcat-count-limited, category-article-count-limited,
  648. // category-file-count-limited
  649. return $this->msg( "category-$type-count-limited" )->numParams( $rescnt )->parseAsBlock();
  650. }
  651. // Messages: category-subcat-count, category-article-count, category-file-count
  652. return $this->msg( "category-$type-count" )->numParams( $rescnt, $totalcnt )->parseAsBlock();
  653. }
  654. }