3 * Special handling for category description pages.
4 * Modelled after ImagePage.php.
9 if ( !defined( 'MEDIAWIKI' ) )
13 * Special handling for category description pages, showing pages,
14 * subcategories and file that belong to the category
16 class CategoryPage
extends Article
{
17 # Subclasses can change this to override the viewer class.
18 protected $mCategoryViewerClass = 'CategoryViewer';
21 global $wgRequest, $wgUser;
23 $diff = $wgRequest->getVal( 'diff' );
24 $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) );
26 if ( isset( $diff ) && $diffOnly ) {
27 return parent
::view();
30 if ( !wfRunHooks( 'CategoryPageView', array( &$this ) ) ) {
34 if ( NS_CATEGORY
== $this->mTitle
->getNamespace() ) {
35 $this->openShowCategory();
40 if ( NS_CATEGORY
== $this->mTitle
->getNamespace() ) {
41 $this->closeShowCategory();
46 * Don't return a 404 for categories in use.
47 * In use defined as: either the actual page exists
48 * or the category currently has members.
50 function hasViewableContent() {
51 if ( parent
::hasViewableContent() ) {
54 $cat = Category
::newFromTitle( $this->mTitle
);
55 // If any of these are not 0, then has members
56 if ( $cat->getPageCount()
57 ||
$cat->getSubcatCount()
58 ||
$cat->getFileCount()
66 function openShowCategory() {
70 function closeShowCategory() {
73 $from = $until = array();
74 foreach ( array( 'page', 'subcat', 'file' ) as $type ) {
75 # Use $_GET instead of $wgRequest, because the latter helpfully
76 # normalizes Unicode, which removes nulls. TODO: do something
77 # smarter than passing nulls in URLs. :/
78 $from[$type] = isset( $_GET["{$type}from"] ) ?
$_GET["{$type}from"] : null;
79 $until[$type] = isset( $_GET["{$type}until"] ) ?
$_GET["{$type}until"] : null;
82 $viewer = new $this->mCategoryViewerClass( $this->mTitle
, $from, $until, $_GET );
83 $wgOut->addHTML( $viewer->getHTML() );
87 class CategoryViewer
{
88 var $title, $limit, $from, $until,
89 $articles, $articles_start_char,
90 $children, $children_start_char,
91 $showGallery, $gallery,
92 $imgsNoGalley, $imgsNoGallery_start_char,
94 # Category object for this page
96 # The original query array, to be used in generating paging links.
99 function __construct( $title, $from = '', $until = '', $query = array() ) {
100 global $wgCategoryPagingLimit;
101 $this->title
= $title;
103 $this->until
= $until;
104 $this->limit
= $wgCategoryPagingLimit;
105 $this->cat
= Category
::newFromTitle( $title );
106 $this->query
= $query;
107 $this->collation
= Collation
::singleton();
108 unset( $this->query
['title'] );
112 * Format the category data list.
114 * @return string HTML output
116 public function getHTML() {
117 global $wgOut, $wgCategoryMagicGallery, $wgContLang;
118 wfProfileIn( __METHOD__
);
120 $this->showGallery
= $wgCategoryMagicGallery && !$wgOut->mNoGallery
;
122 $this->clearCategoryState();
123 $this->doCategoryQuery();
124 $this->finaliseCategoryState();
126 $r = $this->getSubcategorySection() .
127 $this->getPagesSection() .
128 $this->getImageSection();
131 // If there is no category content to display, only
132 // show the top part of the navigation links.
133 // FIXME: cannot be completely suppressed because it
134 // is unknown if 'until' or 'from' makes this
136 $r = $r . $this->getCategoryTop();
138 $r = $this->getCategoryTop() .
140 $this->getCategoryBottom();
143 // Give a proper message if category is empty
145 $r = wfMsgExt( 'category-empty', array( 'parse' ) );
148 wfProfileOut( __METHOD__
);
149 return $wgContLang->convert( $r );
152 function clearCategoryState() {
153 $this->articles
= array();
154 $this->articles_start_char
= array();
155 $this->children
= array();
156 $this->children_start_char
= array();
157 if ( $this->showGallery
) {
158 $this->gallery
= new ImageGallery();
159 $this->gallery
->setHideBadImages();
161 $this->imgsNoGallery
= array();
162 $this->imgsNoGallery_start_char
= array();
167 if ( !$this->skin
) {
169 $this->skin
= $wgUser->getSkin();
175 * Add a subcategory to the internal lists, using a Category object
177 function addSubcategoryObject( Category
$cat, $sortkey, $pageLength ) {
178 // Subcategory; strip the 'Category' namespace from the link text.
179 $title = $cat->getTitle();
180 $this->children
[] = $this->getSkin()->link(
185 array( 'known', 'noclasses' )
188 $this->children_start_char
[] =
189 $this->getSubcategorySortChar( $cat->getTitle(), $sortkey );
193 * Add a subcategory to the internal lists, using a title object
194 * @deprecated kept for compatibility, please use addSubcategoryObject instead
196 function addSubcategory( Title
$title, $sortkey, $pageLength ) {
197 $this->addSubcategoryObject( Category
::newFromTitle( $title ), $sortkey, $pageLength );
201 * Get the character to be used for sorting subcategories.
202 * If there's a link from Category:A to Category:B, the sortkey of the resulting
203 * entry in the categorylinks table is Category:A, not A, which it SHOULD be.
204 * Workaround: If sortkey == "Category:".$title, than use $title for sorting,
205 * else use sortkey...
207 function getSubcategorySortChar( $title, $sortkey ) {
210 if ( $title->getPrefixedText() == $sortkey ) {
211 $word = $title->getDBkey();
216 $firstChar = $this->collation
->getFirstLetter( $word );
218 return $wgContLang->convert( $firstChar );
222 * Add a page in the image namespace
224 function addImage( Title
$title, $sortkey, $pageLength, $isRedirect = false ) {
226 if ( $this->showGallery
) {
227 $flip = $this->flip
['file'];
229 $this->gallery
->insert( $title );
231 $this->gallery
->add( $title );
234 $this->imgsNoGallery
[] = $isRedirect
235 ?
'<span class="redirect-in-category">' .
236 $this->getSkin()->link(
241 array( 'known', 'noclasses' )
243 : $this->getSkin()->link( $title );
245 $this->imgsNoGallery_start_char
[] = $wgContLang->convert(
246 $this->collation
->getFirstLetter( $sortkey ) );
251 * Add a miscellaneous page
253 function addPage( $title, $sortkey, $pageLength, $isRedirect = false ) {
255 $this->articles
[] = $isRedirect
256 ?
'<span class="redirect-in-category">' .
257 $this->getSkin()->link(
262 array( 'known', 'noclasses' )
264 : $this->getSkin()->link( $title );
266 $this->articles_start_char
[] = $wgContLang->convert(
267 $this->collation
->getFirstLetter( $sortkey ) );
270 function finaliseCategoryState() {
271 if ( $this->flip
['subcat'] ) {
272 $this->children
= array_reverse( $this->children
);
273 $this->children_start_char
= array_reverse( $this->children_start_char
);
275 if ( $this->flip
['page'] ) {
276 $this->articles
= array_reverse( $this->articles
);
277 $this->articles_start_char
= array_reverse( $this->articles_start_char
);
279 if ( !$this->showGallery
&& $this->flip
['file'] ) {
280 $this->imgsNoGallery
= array_reverse( $this->imgsNoGallery
);
281 $this->imgsNoGallery_start_char
= array_reverse( $this->imgsNoGallery_start_char
);
285 function doCategoryQuery() {
286 $dbr = wfGetDB( DB_SLAVE
, 'category' );
288 $this->nextPage
= array(
293 $this->flip
= array( 'page' => false, 'subcat' => false, 'file' => false );
295 foreach ( array( 'page', 'subcat', 'file' ) as $type ) {
296 # Get the sortkeys for start/end, if applicable. Note that if
297 # the collation in the database differs from the one
298 # set in $wgCategoryCollation, pagination might go totally haywire.
299 $extraConds = array( 'cl_type' => $type );
300 if ( $this->from
[$type] !== null ) {
301 $extraConds[] = 'cl_sortkey >= '
302 . $dbr->addQuotes( $this->collation
->getSortKey( $this->from
[$type] ) );
303 } elseif ( $this->until
[$type] !== null ) {
304 $extraConds[] = 'cl_sortkey < '
305 . $dbr->addQuotes( $this->collation
->getSortKey( $this->until
[$type] ) );
306 $this->flip
[$type] = true;
310 array( 'page', 'categorylinks', 'category' ),
311 array( 'page_id', 'page_title', 'page_namespace', 'page_len',
312 'page_is_redirect', 'cl_sortkey', 'cat_id', 'cat_title',
313 'cat_subcats', 'cat_pages', 'cat_files', 'cl_sortkey_prefix' ),
314 array( 'cl_to' => $this->title
->getDBkey() ) +
$extraConds,
317 'USE INDEX' => array( 'categorylinks' => 'cl_sortkey' ),
318 'LIMIT' => $this->limit +
1,
319 'ORDER BY' => $this->flip
[$type] ?
'cl_sortkey DESC' : 'cl_sortkey',
322 'categorylinks' => array( 'INNER JOIN', 'cl_from = page_id' ),
323 'category' => array( 'LEFT JOIN', 'cat_title = page_title AND page_namespace = ' . NS_CATEGORY
)
328 foreach ( $res as $row ) {
329 $title = Title
::newFromRow( $row );
330 $rawSortkey = $title->getCategorySortkey( $row->cl_sortkey_prefix
);
332 if ( ++
$count > $this->limit
) {
333 # We've reached the one extra which shows that there
334 # are additional pages to be had. Stop here...
335 $this->nextPage
[$type] = $rawSortkey;
339 if ( $title->getNamespace() == NS_CATEGORY
) {
340 $cat = Category
::newFromRow( $row, $title );
341 $this->addSubcategoryObject( $cat, $rawSortkey, $row->page_len
);
342 } elseif ( $title->getNamespace() == NS_FILE
) {
343 $this->addImage( $title, $rawSortkey, $row->page_len
, $row->page_is_redirect
);
345 $this->addPage( $title, $rawSortkey, $row->page_len
, $row->page_is_redirect
);
351 function getCategoryTop() {
352 $r = $this->getCategoryBottom();
355 : "<br style=\"clear:both;\"/>\n" . $r;
358 function getSubcategorySection() {
359 # Don't show subcategories section if there are none.
361 $rescnt = count( $this->children
);
362 $dbcnt = $this->cat
->getSubcatCount();
363 $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'subcat' );
366 # Showing subcategories
367 $r .= "<div id=\"mw-subcategories\">\n";
368 $r .= '<h2>' . wfMsg( 'subcategories' ) . "</h2>\n";
370 $r .= $this->getSectionPagingLinks( 'subcat' );
371 $r .= $this->formatList( $this->children
, $this->children_start_char
);
372 $r .= $this->getSectionPagingLinks( 'subcat' );
378 function getPagesSection() {
379 $ti = htmlspecialchars( $this->title
->getText() );
380 # Don't show articles section if there are none.
383 # FIXME, here and in the other two sections: we don't need to bother
384 # with this rigamarole if the entire category contents fit on one page
385 # and have already been retrieved. We can just use $rescnt in that
386 # case and save a query and some logic.
387 $dbcnt = $this->cat
->getPageCount() - $this->cat
->getSubcatCount()
388 - $this->cat
->getFileCount();
389 $rescnt = count( $this->articles
);
390 $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'article' );
393 $r = "<div id=\"mw-pages\">\n";
394 $r .= '<h2>' . wfMsg( 'category_header', $ti ) . "</h2>\n";
396 $r .= $this->getSectionPagingLinks( 'page' );
397 $r .= $this->formatList( $this->articles
, $this->articles_start_char
);
398 $r .= $this->getSectionPagingLinks( 'page' );
404 function getImageSection() {
406 $rescnt = $this->showGallery ?
$this->gallery
->count() : count( $this->imgsNoGallery
);
408 $dbcnt = $this->cat
->getFileCount();
409 $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'file' );
411 $r .= "<div id=\"mw-category-media\">\n";
412 $r .= '<h2>' . wfMsg( 'category-media-header', htmlspecialchars( $this->title
->getText() ) ) . "</h2>\n";
414 $r .= $this->getSectionPagingLinks( 'file' );
415 if ( $this->showGallery
) {
416 $r .= $this->gallery
->toHTML();
418 $r .= $this->formatList( $this->imgsNoGallery
, $this->imgsNoGallery_start_char
);
420 $r .= $this->getSectionPagingLinks( 'file' );
427 * Get the paging links for a section (subcats/pages/files), to go at the top and bottom
430 * @param $type String: 'page', 'subcat', or 'file'
431 * @return String: HTML output, possibly empty if there are no other pages
433 private function getSectionPagingLinks( $type ) {
434 if ( $this->until
[$type] !== null ) {
435 return $this->pagingLinks( $this->nextPage
[$type], $this->until
[$type], $type );
436 } elseif ( $this->nextPage
[$type] !== null ||
$this->from
[$type] !== null ) {
437 return $this->pagingLinks( $this->from
[$type], $this->nextPage
[$type], $type );
443 function getCategoryBottom() {
448 * Format a list of articles chunked by letter, either as a
449 * bullet list or a columnar format, depending on the length.
451 * @param $articles Array
452 * @param $articles_start_char Array
457 function formatList( $articles, $articles_start_char, $cutoff = 6 ) {
458 if ( count ( $articles ) > $cutoff ) {
459 return self
::columnList( $articles, $articles_start_char );
460 } elseif ( count( $articles ) > 0 ) {
461 // for short lists of articles in categories.
462 return self
::shortList( $articles, $articles_start_char );
468 * Format a list of articles chunked by letter in a three-column
469 * list, ordered vertically.
471 * TODO: Take the headers into account when creating columns, so they're
472 * more visually equal.
474 * More distant TODO: Scrap this and use CSS columns, whenever IE finally
477 * @param $articles Array
478 * @param $articles_start_char Array
482 static function columnList( $articles, $articles_start_char ) {
483 $columns = array_combine( $articles, $articles_start_char );
484 # Split into three columns
485 $columns = array_chunk( $columns, ceil( count( $columns ) / 3 ), true /* preserve keys */ );
487 $ret = '<table width="100%"><tr valign="top"><td>';
490 foreach ( $columns as $column ) {
491 $colContents = array();
493 # Kind of like array_flip() here, but we keep duplicates in an
494 # array instead of dropping them.
495 foreach ( $column as $article => $char ) {
496 if ( !isset( $colContents[$char] ) ) {
497 $colContents[$char] = array();
499 $colContents[$char][] = $article;
503 foreach ( $colContents as $char => $articles ) {
504 $ret .= '<h3>' . htmlspecialchars( $char );
505 if ( $first && $char === $prevchar ) {
506 # We're continuing a previous chunk at the top of a new
507 # column, so add " cont." after the letter.
508 $ret .= ' ' . wfMsgHtml( 'listingcontinuesabbrev' );
513 $ret .= implode( "</li>\n<li>", $articles );
514 $ret .= '</li></ul>';
520 $ret .= "</td>\n<td>";
523 $ret .= '</td></tr></table>';
528 * Format a list of articles chunked by letter in a bullet list.
529 * @param $articles Array
530 * @param $articles_start_char Array
534 static function shortList( $articles, $articles_start_char ) {
535 $r = '<h3>' . htmlspecialchars( $articles_start_char[0] ) . "</h3>\n";
536 $r .= '<ul><li>' . $articles[0] . '</li>';
537 for ( $index = 1; $index < count( $articles ); $index++
)
539 if ( $articles_start_char[$index] != $articles_start_char[$index - 1] )
541 $r .= "</ul><h3>" . htmlspecialchars( $articles_start_char[$index] ) . "</h3>\n<ul>";
544 $r .= "<li>{$articles[$index]}</li>";
551 * Create paging links, as a helper method to getSectionPagingLinks().
553 * @param $first String The 'until' parameter for the generated URL
554 * @param $last String The 'from' parameter for the genererated URL
555 * @param $type String A prefix for parameters, 'page' or 'subcat' or
557 * @return String HTML
559 private function pagingLinks( $first, $last, $type = '' ) {
561 $sk = $this->getSkin();
562 $limitText = $wgLang->formatNum( $this->limit
);
564 $prevLink = wfMsgExt( 'prevn', array( 'escape', 'parsemag' ), $limitText );
566 if ( $first != '' ) {
567 $prevQuery = $this->query
;
568 $prevQuery["{$type}until"] = $first;
569 unset( $prevQuery["{$type}from"] );
570 $prevLink = $sk->linkKnown(
578 $nextLink = wfMsgExt( 'nextn', array( 'escape', 'parsemag' ), $limitText );
581 $lastQuery = $this->query
;
582 $lastQuery["{$type}from"] = $last;
583 unset( $lastQuery["{$type}until"] );
584 $nextLink = $sk->linkKnown(
592 return "($prevLink) ($nextLink)";
596 * What to do if the category table conflicts with the number of results
597 * returned? This function says what. Each type is considered independantly
598 * of the other types.
600 * Note for grepping: uses the messages category-article-count,
601 * category-article-count-limited, category-subcat-count,
602 * category-subcat-count-limited, category-file-count,
603 * category-file-count-limited.
605 * @param $rescnt Int: The number of items returned by our database query.
606 * @param $dbcnt Int: The number of items according to the category table.
607 * @param $type String: 'subcat', 'article', or 'file'
608 * @return String: A message giving the number of items, to output to HTML.
610 private function getCountMessage( $rescnt, $dbcnt, $type ) {
612 # There are three cases:
613 # 1) The category table figure seems sane. It might be wrong, but
614 # we can't do anything about it if we don't recalculate it on ev-
616 # 2) The category table figure isn't sane, like it's smaller than the
617 # number of actual results, *but* the number of results is less
618 # than $this->limit and there's no offset. In this case we still
619 # know the right figure.
620 # 3) We have no idea.
622 # 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';
632 $fromOrUntil = false;
633 if ( $this->from
[$pagingType] !== null ||
$this->until
[$pagingType] !== null ) {
637 if ( $dbcnt == $rescnt ||
( ( $rescnt == $this->limit ||
$fromOrUntil )
638 && $dbcnt > $rescnt ) )
640 # Case 1: seems sane.
642 } elseif ( $rescnt < $this->limit
&& !$fromOrUntil ) {
643 # Case 2: not sane, but salvageable. Use the number of results.
644 # Since there are fewer than 200, we can also take this opportunity
645 # to refresh the incorrect category table entry -- which should be
646 # quick due to the small number of entries.
648 $this->cat
->refreshCounts();
650 # Case 3: hopeless. Don't give a total count at all.
651 return wfMsgExt( "category-$type-count-limited", 'parse',
652 $wgLang->formatNum( $rescnt ) );
655 "category-$type-count",
657 $wgLang->formatNum( $rescnt ),
658 $wgLang->formatNum( $totalcnt )