CategoryPage.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. <?php
  2. /**
  3. * Special handling for category description pages
  4. * Modelled after ImagePage.php
  5. *
  6. */
  7. if( !defined( 'MEDIAWIKI' ) )
  8. die( 1 );
  9. /**
  10. */
  11. class CategoryPage extends Article {
  12. function view() {
  13. global $wgRequest, $wgUser;
  14. $diff = $wgRequest->getVal( 'diff' );
  15. $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) );
  16. if ( isset( $diff ) && $diffOnly )
  17. return Article::view();
  18. if( !wfRunHooks( 'CategoryPageView', array( &$this ) ) )
  19. return;
  20. if ( NS_CATEGORY == $this->mTitle->getNamespace() ) {
  21. $this->openShowCategory();
  22. }
  23. Article::view();
  24. if ( NS_CATEGORY == $this->mTitle->getNamespace() ) {
  25. $this->closeShowCategory();
  26. }
  27. }
  28. /**
  29. * Don't return a 404 for categories in use.
  30. */
  31. function hasViewableContent() {
  32. if( parent::hasViewableContent() ) {
  33. return true;
  34. } else {
  35. $cat = Category::newFromTitle( $this->mTitle );
  36. return $cat->getId() != 0;
  37. }
  38. }
  39. function openShowCategory() {
  40. # For overloading
  41. }
  42. function closeShowCategory() {
  43. global $wgOut, $wgRequest;
  44. $from = $wgRequest->getVal( 'from' );
  45. $until = $wgRequest->getVal( 'until' );
  46. $viewer = new CategoryViewer( $this->mTitle, $from, $until );
  47. $wgOut->addHTML( $viewer->getHTML() );
  48. }
  49. }
  50. class CategoryViewer {
  51. var $title, $limit, $from, $until,
  52. $articles, $articles_start_char,
  53. $children, $children_start_char,
  54. $showGallery, $gallery,
  55. $skin;
  56. /** Category object for this page */
  57. private $cat;
  58. function __construct( $title, $from = '', $until = '' ) {
  59. global $wgCategoryPagingLimit;
  60. $this->title = $title;
  61. $this->from = $from;
  62. $this->until = $until;
  63. $this->limit = $wgCategoryPagingLimit;
  64. $this->cat = Category::newFromTitle( $title );
  65. }
  66. /**
  67. * Format the category data list.
  68. *
  69. * @return string HTML output
  70. * @private
  71. */
  72. function getHTML() {
  73. global $wgOut, $wgCategoryMagicGallery, $wgCategoryPagingLimit;
  74. wfProfileIn( __METHOD__ );
  75. $this->showGallery = $wgCategoryMagicGallery && !$wgOut->mNoGallery;
  76. $this->clearCategoryState();
  77. $this->doCategoryQuery();
  78. $this->finaliseCategoryState();
  79. $r = $this->getCategoryTop() .
  80. $this->getSubcategorySection() .
  81. $this->getPagesSection() .
  82. $this->getImageSection() .
  83. $this->getCategoryBottom();
  84. // Give a proper message if category is empty
  85. if ( $r == '' ) {
  86. $r = wfMsgExt( 'category-empty', array( 'parse' ) );
  87. }
  88. wfProfileOut( __METHOD__ );
  89. return $r;
  90. }
  91. function clearCategoryState() {
  92. $this->articles = array();
  93. $this->articles_start_char = array();
  94. $this->children = array();
  95. $this->children_start_char = array();
  96. if( $this->showGallery ) {
  97. $this->gallery = new ImageGallery();
  98. $this->gallery->setHideBadImages();
  99. }
  100. }
  101. function getSkin() {
  102. if ( !$this->skin ) {
  103. global $wgUser;
  104. $this->skin = $wgUser->getSkin();
  105. }
  106. return $this->skin;
  107. }
  108. /**
  109. * Add a subcategory to the internal lists, using a Category object
  110. */
  111. function addSubcategoryObject( $cat, $sortkey, $pageLength ) {
  112. $title = $cat->getTitle();
  113. $this->addSubcategory( $title, $sortkey, $pageLength );
  114. }
  115. /**
  116. * Add a subcategory to the internal lists, using a title object
  117. * @deprecated kept for compatibility, please use addSubcategoryObject instead
  118. */
  119. function addSubcategory( $title, $sortkey, $pageLength ) {
  120. global $wgContLang;
  121. // Subcategory; strip the 'Category' namespace from the link text.
  122. $this->children[] = $this->getSkin()->makeKnownLinkObj(
  123. $title, $wgContLang->convertHtml( $title->getText() ) );
  124. $this->children_start_char[] = $this->getSubcategorySortChar( $title, $sortkey );
  125. }
  126. /**
  127. * Get the character to be used for sorting subcategories.
  128. * If there's a link from Category:A to Category:B, the sortkey of the resulting
  129. * entry in the categorylinks table is Category:A, not A, which it SHOULD be.
  130. * Workaround: If sortkey == "Category:".$title, than use $title for sorting,
  131. * else use sortkey...
  132. */
  133. function getSubcategorySortChar( $title, $sortkey ) {
  134. global $wgContLang;
  135. if( $title->getPrefixedText() == $sortkey ) {
  136. $firstChar = $wgContLang->firstChar( $title->getDBkey() );
  137. } else {
  138. $firstChar = $wgContLang->firstChar( $sortkey );
  139. }
  140. return $wgContLang->convert( $firstChar );
  141. }
  142. /**
  143. * Add a page in the image namespace
  144. */
  145. function addImage( Title $title, $sortkey, $pageLength, $isRedirect = false ) {
  146. if ( $this->showGallery ) {
  147. if( $this->flip ) {
  148. $this->gallery->insert( $title );
  149. } else {
  150. $this->gallery->add( $title );
  151. }
  152. } else {
  153. $this->addPage( $title, $sortkey, $pageLength, $isRedirect );
  154. }
  155. }
  156. /**
  157. * Add a miscellaneous page
  158. */
  159. function addPage( $title, $sortkey, $pageLength, $isRedirect = false ) {
  160. global $wgContLang;
  161. $titletext = $wgContLang->convert( $title->getPrefixedText() );
  162. $this->articles[] = $isRedirect
  163. ? '<span class="redirect-in-category">' . $this->getSkin()->makeKnownLinkObj( $title, $titletext ) . '</span>'
  164. : $this->getSkin()->makeSizeLinkObj( $pageLength, $title, $titletext );
  165. $this->articles_start_char[] = $wgContLang->convert( $wgContLang->firstChar( $sortkey ) );
  166. }
  167. function finaliseCategoryState() {
  168. if( $this->flip ) {
  169. $this->children = array_reverse( $this->children );
  170. $this->children_start_char = array_reverse( $this->children_start_char );
  171. $this->articles = array_reverse( $this->articles );
  172. $this->articles_start_char = array_reverse( $this->articles_start_char );
  173. }
  174. }
  175. function doCategoryQuery() {
  176. $dbr = wfGetDB( DB_SLAVE, 'category' );
  177. if( $this->from != '' ) {
  178. $pageCondition = 'cl_sortkey >= ' . $dbr->addQuotes( $this->from );
  179. $this->flip = false;
  180. } elseif( $this->until != '' ) {
  181. $pageCondition = 'cl_sortkey < ' . $dbr->addQuotes( $this->until );
  182. $this->flip = true;
  183. } else {
  184. $pageCondition = '1 = 1';
  185. $this->flip = false;
  186. }
  187. $res = $dbr->select(
  188. array( 'page', 'categorylinks', 'category' ),
  189. array( 'page_title', 'page_namespace', 'page_len', 'page_is_redirect', 'cl_sortkey',
  190. 'cat_id', 'cat_title', 'cat_subcats', 'cat_pages', 'cat_files' ),
  191. array( $pageCondition, 'cl_to' => $this->title->getDBkey() ),
  192. __METHOD__,
  193. array( 'ORDER BY' => $this->flip ? 'cl_sortkey DESC' : 'cl_sortkey',
  194. 'USE INDEX' => array( 'categorylinks' => 'cl_sortkey' ),
  195. 'LIMIT' => $this->limit + 1 ),
  196. array( 'categorylinks' => array( 'INNER JOIN', 'cl_from = page_id' ),
  197. 'category' => array( 'LEFT JOIN', 'cat_title = page_title AND page_namespace = ' . NS_CATEGORY ) )
  198. );
  199. $count = 0;
  200. $this->nextPage = null;
  201. while( $x = $dbr->fetchObject ( $res ) ) {
  202. if( ++$count > $this->limit ) {
  203. // We've reached the one extra which shows that there are
  204. // additional pages to be had. Stop here...
  205. $this->nextPage = $x->cl_sortkey;
  206. break;
  207. }
  208. $title = Title::makeTitle( $x->page_namespace, $x->page_title );
  209. if( $title->getNamespace() == NS_CATEGORY ) {
  210. $cat = Category::newFromRow( $x, $title );
  211. $this->addSubcategoryObject( $cat, $x->cl_sortkey, $x->page_len );
  212. } elseif( $this->showGallery && $title->getNamespace() == NS_FILE ) {
  213. $this->addImage( $title, $x->cl_sortkey, $x->page_len, $x->page_is_redirect );
  214. } else {
  215. $this->addPage( $title, $x->cl_sortkey, $x->page_len, $x->page_is_redirect );
  216. }
  217. }
  218. $dbr->freeResult( $res );
  219. }
  220. function getCategoryTop() {
  221. $r = '';
  222. if( $this->until != '' ) {
  223. $r .= $this->pagingLinks( $this->title, $this->nextPage, $this->until, $this->limit );
  224. } elseif( $this->nextPage != '' || $this->from != '' ) {
  225. $r .= $this->pagingLinks( $this->title, $this->from, $this->nextPage, $this->limit );
  226. }
  227. return $r == ''
  228. ? $r
  229. : "<br style=\"clear:both;\"/>\n" . $r;
  230. }
  231. function getSubcategorySection() {
  232. # Don't show subcategories section if there are none.
  233. $r = '';
  234. $rescnt = count( $this->children );
  235. $dbcnt = $this->cat->getSubcatCount();
  236. $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'subcat' );
  237. if( $rescnt > 0 ) {
  238. # Showing subcategories
  239. $r .= "<div id=\"mw-subcategories\">\n";
  240. $r .= '<h2>' . wfMsg( 'subcategories' ) . "</h2>\n";
  241. $r .= $countmsg;
  242. $r .= $this->formatList( $this->children, $this->children_start_char );
  243. $r .= "\n</div>";
  244. }
  245. return $r;
  246. }
  247. function getPagesSection() {
  248. $ti = htmlspecialchars( $this->title->getText() );
  249. # Don't show articles section if there are none.
  250. $r = '';
  251. # FIXME, here and in the other two sections: we don't need to bother
  252. # with this rigamarole if the entire category contents fit on one page
  253. # and have already been retrieved. We can just use $rescnt in that
  254. # case and save a query and some logic.
  255. $dbcnt = $this->cat->getPageCount() - $this->cat->getSubcatCount()
  256. - $this->cat->getFileCount();
  257. $rescnt = count( $this->articles );
  258. $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'article' );
  259. if( $rescnt > 0 ) {
  260. $r = "<div id=\"mw-pages\">\n";
  261. $r .= '<h2>' . wfMsg( 'category_header', $ti ) . "</h2>\n";
  262. $r .= $countmsg;
  263. $r .= $this->formatList( $this->articles, $this->articles_start_char );
  264. $r .= "\n</div>";
  265. }
  266. return $r;
  267. }
  268. function getImageSection() {
  269. if( $this->showGallery && ! $this->gallery->isEmpty() ) {
  270. $dbcnt = $this->cat->getFileCount();
  271. $rescnt = $this->gallery->count();
  272. $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'file' );
  273. return "<div id=\"mw-category-media\">\n" .
  274. '<h2>' . wfMsg( 'category-media-header', htmlspecialchars( $this->title->getText() ) ) . "</h2>\n" .
  275. $countmsg . $this->gallery->toHTML() . "\n</div>";
  276. } else {
  277. return '';
  278. }
  279. }
  280. function getCategoryBottom() {
  281. if( $this->until != '' ) {
  282. return $this->pagingLinks( $this->title, $this->nextPage, $this->until, $this->limit );
  283. } elseif( $this->nextPage != '' || $this->from != '' ) {
  284. return $this->pagingLinks( $this->title, $this->from, $this->nextPage, $this->limit );
  285. } else {
  286. return '';
  287. }
  288. }
  289. /**
  290. * Format a list of articles chunked by letter, either as a
  291. * bullet list or a columnar format, depending on the length.
  292. *
  293. * @param $articles Array
  294. * @param $articles_start_char Array
  295. * @param $cutoff Int
  296. * @return String
  297. * @private
  298. */
  299. function formatList( $articles, $articles_start_char, $cutoff = 6 ) {
  300. if ( count ( $articles ) > $cutoff ) {
  301. return $this->columnList( $articles, $articles_start_char );
  302. } elseif ( count($articles) > 0) {
  303. // for short lists of articles in categories.
  304. return $this->shortList( $articles, $articles_start_char );
  305. }
  306. return '';
  307. }
  308. /**
  309. * Format a list of articles chunked by letter in a three-column
  310. * list, ordered vertically.
  311. *
  312. * @param $articles Array
  313. * @param $articles_start_char Array
  314. * @return String
  315. * @private
  316. */
  317. function columnList( $articles, $articles_start_char ) {
  318. // divide list into three equal chunks
  319. $chunk = (int) (count ( $articles ) / 3);
  320. // get and display header
  321. $r = '<table width="100%"><tr valign="top">';
  322. $prev_start_char = 'none';
  323. // loop through the chunks
  324. for($startChunk = 0, $endChunk = $chunk, $chunkIndex = 0;
  325. $chunkIndex < 3;
  326. $chunkIndex++, $startChunk = $endChunk, $endChunk += $chunk + 1)
  327. {
  328. $r .= "<td>\n";
  329. $atColumnTop = true;
  330. // output all articles in category
  331. for ($index = $startChunk ;
  332. $index < $endChunk && $index < count($articles);
  333. $index++ )
  334. {
  335. // check for change of starting letter or begining of chunk
  336. if ( ($index == $startChunk) ||
  337. ($articles_start_char[$index] != $articles_start_char[$index - 1]) )
  338. {
  339. if( $atColumnTop ) {
  340. $atColumnTop = false;
  341. } else {
  342. $r .= "</ul>\n";
  343. }
  344. $cont_msg = "";
  345. if ( $articles_start_char[$index] == $prev_start_char )
  346. $cont_msg = ' ' . wfMsgHtml( 'listingcontinuesabbrev' );
  347. $r .= "<h3>" . htmlspecialchars( $articles_start_char[$index] ) . "$cont_msg</h3>\n<ul>";
  348. $prev_start_char = $articles_start_char[$index];
  349. }
  350. $r .= "<li>{$articles[$index]}</li>";
  351. }
  352. if( !$atColumnTop ) {
  353. $r .= "</ul>\n";
  354. }
  355. $r .= "</td>\n";
  356. }
  357. $r .= '</tr></table>';
  358. return $r;
  359. }
  360. /**
  361. * Format a list of articles chunked by letter in a bullet list.
  362. * @param $articles Array
  363. * @param $articles_start_char Array
  364. * @return String
  365. * @private
  366. */
  367. function shortList( $articles, $articles_start_char ) {
  368. $r = '<h3>' . htmlspecialchars( $articles_start_char[0] ) . "</h3>\n";
  369. $r .= '<ul><li>'.$articles[0].'</li>';
  370. for ($index = 1; $index < count($articles); $index++ )
  371. {
  372. if ($articles_start_char[$index] != $articles_start_char[$index - 1])
  373. {
  374. $r .= "</ul><h3>" . htmlspecialchars( $articles_start_char[$index] ) . "</h3>\n<ul>";
  375. }
  376. $r .= "<li>{$articles[$index]}</li>";
  377. }
  378. $r .= '</ul>';
  379. return $r;
  380. }
  381. /**
  382. * @param $title Title object
  383. * @param $first String
  384. * @param $last String
  385. * @param $limit Int
  386. * @param $query Array: additional query options to pass
  387. * @return String
  388. * @private
  389. */
  390. function pagingLinks( $title, $first, $last, $limit, $query = array() ) {
  391. global $wgLang;
  392. $sk = $this->getSkin();
  393. $limitText = $wgLang->formatNum( $limit );
  394. $prevLink = wfMsgExt( 'prevn', array( 'escape', 'parsemag' ), $limitText );
  395. if( $first != '' ) {
  396. $prevLink = $sk->makeLinkObj( $title, $prevLink,
  397. wfArrayToCGI( $query + array( 'until' => $first ) ) );
  398. }
  399. $nextLink = wfMsgExt( 'nextn', array( 'escape', 'parsemag' ), $limitText );
  400. if( $last != '' ) {
  401. $nextLink = $sk->makeLinkObj( $title, $nextLink,
  402. wfArrayToCGI( $query + array( 'from' => $last ) ) );
  403. }
  404. return "($prevLink) ($nextLink)";
  405. }
  406. /**
  407. * What to do if the category table conflicts with the number of results
  408. * returned? This function says what. It works the same whether the
  409. * things being counted are articles, subcategories, or files.
  410. *
  411. * Note for grepping: uses the messages category-article-count,
  412. * category-article-count-limited, category-subcat-count,
  413. * category-subcat-count-limited, category-file-count,
  414. * category-file-count-limited.
  415. *
  416. * @param $rescnt Int: The number of items returned by our database query.
  417. * @param $dbcnt Int: The number of items according to the category table.
  418. * @param $type String: 'subcat', 'article', or 'file'
  419. * @return String: A message giving the number of items, to output to HTML.
  420. */
  421. private function getCountMessage( $rescnt, $dbcnt, $type ) {
  422. global $wgLang;
  423. # There are three cases:
  424. # 1) The category table figure seems sane. It might be wrong, but
  425. # we can't do anything about it if we don't recalculate it on ev-
  426. # ery category view.
  427. # 2) The category table figure isn't sane, like it's smaller than the
  428. # number of actual results, *but* the number of results is less
  429. # than $this->limit and there's no offset. In this case we still
  430. # know the right figure.
  431. # 3) We have no idea.
  432. $totalrescnt = count( $this->articles ) + count( $this->children ) +
  433. ($this->showGallery ? $this->gallery->count() : 0);
  434. if($dbcnt == $rescnt || (($totalrescnt == $this->limit || $this->from
  435. || $this->until) && $dbcnt > $rescnt)){
  436. # Case 1: seems sane.
  437. $totalcnt = $dbcnt;
  438. } elseif($totalrescnt < $this->limit && !$this->from && !$this->until){
  439. # Case 2: not sane, but salvageable. Use the number of results.
  440. # Since there are fewer than 200, we can also take this opportunity
  441. # to refresh the incorrect category table entry -- which should be
  442. # quick due to the small number of entries.
  443. $totalcnt = $rescnt;
  444. $this->cat->refreshCounts();
  445. } else {
  446. # Case 3: hopeless. Don't give a total count at all.
  447. return wfMsgExt("category-$type-count-limited", 'parse',
  448. $wgLang->formatNum( $rescnt ) );
  449. }
  450. return wfMsgExt( "category-$type-count", 'parse', $wgLang->formatNum( $rescnt ),
  451. $wgLang->formatNum( $totalcnt ) );
  452. }
  453. }