00001 <?php
00008 if ( !defined( 'MEDIAWIKI' ) )
00009 die( 1 );
00010
00013 class CategoryPage extends Article {
00014 function view() {
00015 global $wgRequest, $wgUser;
00016
00017 $diff = $wgRequest->getVal( 'diff' );
00018 $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) );
00019
00020 if ( isset( $diff ) && $diffOnly )
00021 return Article::view();
00022
00023 if ( !wfRunHooks( 'CategoryPageView', array( &$this ) ) )
00024 return;
00025
00026 if ( NS_CATEGORY == $this->mTitle->getNamespace() ) {
00027 $this->openShowCategory();
00028 }
00029
00030 Article::view();
00031
00032 if ( NS_CATEGORY == $this->mTitle->getNamespace() ) {
00033 $this->closeShowCategory();
00034 }
00035 }
00036
00040 function hasViewableContent() {
00041 if ( parent::hasViewableContent() ) {
00042 return true;
00043 } else {
00044 $cat = Category::newFromTitle( $this->mTitle );
00045 return $cat->getId() != 0;
00046 }
00047 }
00048
00049 function openShowCategory() {
00050 # For overloading
00051 }
00052
00053 function closeShowCategory() {
00054 global $wgOut, $wgRequest;
00055 $from = $wgRequest->getVal( 'from' );
00056 $until = $wgRequest->getVal( 'until' );
00057
00058 $viewer = new CategoryViewer( $this->mTitle, $from, $until );
00059 $wgOut->addHTML( $viewer->getHTML() );
00060 }
00061 }
00062
00063 class CategoryViewer {
00064 var $title, $limit, $from, $until,
00065 $articles, $articles_start_char,
00066 $children, $children_start_char,
00067 $showGallery, $gallery,
00068 $skin;
00070 private $cat;
00071
00072 function __construct( $title, $from = '', $until = '' ) {
00073 global $wgCategoryPagingLimit;
00074 $this->title = $title;
00075 $this->from = $from;
00076 $this->until = $until;
00077 $this->limit = $wgCategoryPagingLimit;
00078 $this->cat = Category::newFromTitle( $title );
00079 }
00080
00087 function getHTML() {
00088 global $wgOut, $wgCategoryMagicGallery, $wgCategoryPagingLimit, $wgContLang;
00089 wfProfileIn( __METHOD__ );
00090
00091 $this->showGallery = $wgCategoryMagicGallery && !$wgOut->mNoGallery;
00092
00093 $this->clearCategoryState();
00094 $this->doCategoryQuery();
00095 $this->finaliseCategoryState();
00096
00097 $r = $this->getSubcategorySection() .
00098 $this->getPagesSection() .
00099 $this->getImageSection();
00100
00101 if ( $r == '' ) {
00102
00103
00104
00105
00106
00107 $r = $r . $this->getCategoryTop();
00108 } else {
00109 $r = $this->getCategoryTop() .
00110 $r .
00111 $this->getCategoryBottom();
00112 }
00113
00114
00115 if ( $r == '' ) {
00116 $r = wfMsgExt( 'category-empty', array( 'parse' ) );
00117 }
00118
00119 wfProfileOut( __METHOD__ );
00120 return $wgContLang->convert( $r );
00121 }
00122
00123 function clearCategoryState() {
00124 $this->articles = array();
00125 $this->articles_start_char = array();
00126 $this->children = array();
00127 $this->children_start_char = array();
00128 if ( $this->showGallery ) {
00129 $this->gallery = new ImageGallery();
00130 $this->gallery->setHideBadImages();
00131 }
00132 }
00133
00134 function getSkin() {
00135 if ( !$this->skin ) {
00136 global $wgUser;
00137 $this->skin = $wgUser->getSkin();
00138 }
00139 return $this->skin;
00140 }
00141
00145 function addSubcategoryObject( $cat, $sortkey, $pageLength ) {
00146 $title = $cat->getTitle();
00147 $this->addSubcategory( $title, $sortkey, $pageLength );
00148 }
00149
00154 function addSubcategory( $title, $sortkey, $pageLength ) {
00155
00156 $this->children[] = $this->getSkin()->link(
00157 $title,
00158 null,
00159 array(),
00160 array(),
00161 array( 'known', 'noclasses' )
00162 );
00163
00164 $this->children_start_char[] = $this->getSubcategorySortChar( $title, $sortkey );
00165 }
00166
00174 function getSubcategorySortChar( $title, $sortkey ) {
00175 global $wgContLang;
00176
00177 if ( $title->getPrefixedText() == $sortkey ) {
00178 $firstChar = $wgContLang->firstChar( $title->getDBkey() );
00179 } else {
00180 $firstChar = $wgContLang->firstChar( $sortkey );
00181 }
00182
00183 return $wgContLang->convert( $firstChar );
00184 }
00185
00189 function addImage( Title $title, $sortkey, $pageLength, $isRedirect = false ) {
00190 if ( $this->showGallery ) {
00191 if ( $this->flip ) {
00192 $this->gallery->insert( $title );
00193 } else {
00194 $this->gallery->add( $title );
00195 }
00196 } else {
00197 $this->addPage( $title, $sortkey, $pageLength, $isRedirect );
00198 }
00199 }
00200
00204 function addPage( $title, $sortkey, $pageLength, $isRedirect = false ) {
00205 global $wgContLang;
00206 $this->articles[] = $isRedirect
00207 ? '<span class="redirect-in-category">' .
00208 $this->getSkin()->link(
00209 $title,
00210 null,
00211 array(),
00212 array(),
00213 array( 'known', 'noclasses' )
00214 ) . '</span>'
00215 : $this->getSkin()->makeSizeLinkObj( $pageLength, $title );
00216 $this->articles_start_char[] = $wgContLang->convert( $wgContLang->firstChar( $sortkey ) );
00217 }
00218
00219 function finaliseCategoryState() {
00220 if ( $this->flip ) {
00221 $this->children = array_reverse( $this->children );
00222 $this->children_start_char = array_reverse( $this->children_start_char );
00223 $this->articles = array_reverse( $this->articles );
00224 $this->articles_start_char = array_reverse( $this->articles_start_char );
00225 }
00226 }
00227
00228 function doCategoryQuery() {
00229 $dbr = wfGetDB( DB_SLAVE, 'category' );
00230 if ( $this->from != '' ) {
00231 $pageCondition = 'cl_sortkey >= ' . $dbr->addQuotes( $this->from );
00232 $this->flip = false;
00233 } elseif ( $this->until != '' ) {
00234 $pageCondition = 'cl_sortkey < ' . $dbr->addQuotes( $this->until );
00235 $this->flip = true;
00236 } else {
00237 $pageCondition = '1 = 1';
00238 $this->flip = false;
00239 }
00240
00241 $res = $dbr->select(
00242 array( 'page', 'categorylinks', 'category' ),
00243 array( 'page_title', 'page_namespace', 'page_len', 'page_is_redirect', 'cl_sortkey',
00244 'cat_id', 'cat_title', 'cat_subcats', 'cat_pages', 'cat_files' ),
00245 array( $pageCondition, 'cl_to' => $this->title->getDBkey() ),
00246 __METHOD__,
00247 array( 'ORDER BY' => $this->flip ? 'cl_sortkey DESC' : 'cl_sortkey',
00248 'USE INDEX' => array( 'categorylinks' => 'cl_sortkey' ),
00249 'LIMIT' => $this->limit + 1 ),
00250 array( 'categorylinks' => array( 'INNER JOIN', 'cl_from = page_id' ),
00251 'category' => array( 'LEFT JOIN', 'cat_title = page_title AND page_namespace = ' . NS_CATEGORY ) )
00252 );
00253
00254 $count = 0;
00255 $this->nextPage = null;
00256
00257 while ( $x = $dbr->fetchObject ( $res ) ) {
00258 if ( ++$count > $this->limit ) {
00259
00260
00261 $this->nextPage = $x->cl_sortkey;
00262 break;
00263 }
00264
00265 $title = Title::makeTitle( $x->page_namespace, $x->page_title );
00266
00267 if ( $title->getNamespace() == NS_CATEGORY ) {
00268 $cat = Category::newFromRow( $x, $title );
00269 $this->addSubcategoryObject( $cat, $x->cl_sortkey, $x->page_len );
00270 } elseif ( $this->showGallery && $title->getNamespace() == NS_FILE ) {
00271 $this->addImage( $title, $x->cl_sortkey, $x->page_len, $x->page_is_redirect );
00272 } else {
00273 $this->addPage( $title, $x->cl_sortkey, $x->page_len, $x->page_is_redirect );
00274 }
00275 }
00276 }
00277
00278 function getCategoryTop() {
00279 $r = $this->getCategoryBottom();
00280 return $r === ''
00281 ? $r
00282 : "<br style=\"clear:both;\"/>\n" . $r;
00283 }
00284
00285 function getSubcategorySection() {
00286 # Don't show subcategories section if there are none.
00287 $r = '';
00288 $rescnt = count( $this->children );
00289 $dbcnt = $this->cat->getSubcatCount();
00290 $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'subcat' );
00291
00292 if ( $rescnt > 0 ) {
00293 # Showing subcategories
00294 $r .= "<div id=\"mw-subcategories\">\n";
00295 $r .= '<h2>' . wfMsg( 'subcategories' ) . "</h2>\n";
00296 $r .= $countmsg;
00297 $r .= $this->formatList( $this->children, $this->children_start_char );
00298 $r .= "\n</div>";
00299 }
00300 return $r;
00301 }
00302
00303 function getPagesSection() {
00304 $ti = htmlspecialchars( $this->title->getText() );
00305 # Don't show articles section if there are none.
00306 $r = '';
00307
00308 # FIXME, here and in the other two sections: we don't need to bother
00309 # with this rigamarole if the entire category contents fit on one page
00310 # and have already been retrieved. We can just use $rescnt in that
00311 # case and save a query and some logic.
00312 $dbcnt = $this->cat->getPageCount() - $this->cat->getSubcatCount()
00313 - $this->cat->getFileCount();
00314 $rescnt = count( $this->articles );
00315 $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'article' );
00316
00317 if ( $rescnt > 0 ) {
00318 $r = "<div id=\"mw-pages\">\n";
00319 $r .= '<h2>' . wfMsg( 'category_header', $ti ) . "</h2>\n";
00320 $r .= $countmsg;
00321 $r .= $this->formatList( $this->articles, $this->articles_start_char );
00322 $r .= "\n</div>";
00323 }
00324 return $r;
00325 }
00326
00327 function getImageSection() {
00328 if ( $this->showGallery && ! $this->gallery->isEmpty() ) {
00329 $dbcnt = $this->cat->getFileCount();
00330 $rescnt = $this->gallery->count();
00331 $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'file' );
00332
00333 return "<div id=\"mw-category-media\">\n" .
00334 '<h2>' . wfMsg( 'category-media-header', htmlspecialchars( $this->title->getText() ) ) . "</h2>\n" .
00335 $countmsg . $this->gallery->toHTML() . "\n</div>";
00336 } else {
00337 return '';
00338 }
00339 }
00340
00341 function getCategoryBottom() {
00342 if ( $this->until != '' ) {
00343 return $this->pagingLinks( $this->title, $this->nextPage, $this->until, $this->limit );
00344 } elseif ( $this->nextPage != '' || $this->from != '' ) {
00345 return $this->pagingLinks( $this->title, $this->from, $this->nextPage, $this->limit );
00346 } else {
00347 return '';
00348 }
00349 }
00350
00361 function formatList( $articles, $articles_start_char, $cutoff = 6 ) {
00362 if ( count ( $articles ) > $cutoff ) {
00363 return $this->columnList( $articles, $articles_start_char );
00364 } elseif ( count( $articles ) > 0 ) {
00365
00366 return $this->shortList( $articles, $articles_start_char );
00367 }
00368 return '';
00369 }
00370
00386 function columnList( $articles, $articles_start_char ) {
00387 $columns = array_combine( $articles, $articles_start_char );
00388 # Split into three columns
00389 $columns = array_chunk( $columns, ceil( count( $columns ) / 3 ), true );
00390
00391 $ret = '<table width="100%"><tr valign="top"><td>';
00392 $prevchar = null;
00393
00394 foreach ( $columns as $column ) {
00395 $colContents = array();
00396
00397 # Kind of like array_flip() here, but we keep duplicates in an
00398 # array instead of dropping them.
00399 foreach ( $column as $article => $char ) {
00400 if ( !isset( $colContents[$char] ) ) {
00401 $colContents[$char] = array();
00402 }
00403 $colContents[$char][] = $article;
00404 }
00405
00406 $first = true;
00407 foreach ( $colContents as $char => $articles ) {
00408 $ret .= '<h3>' . htmlspecialchars( $char );
00409 if ( $first && $char === $prevchar ) {
00410 # We're continuing a previous chunk at the top of a new
00411 # column, so add " cont." after the letter.
00412 $ret .= ' ' . wfMsgHtml( 'listingcontinuesabbrev' );
00413 }
00414 $ret .= "</h3>\n";
00415
00416 $ret .= '<ul><li>';
00417 $ret .= implode( "</li>\n<li>", $articles );
00418 $ret .= '</li></ul>';
00419
00420 $first = false;
00421 $prevchar = $char;
00422 }
00423
00424 $ret .= "</td>\n<td>";
00425 }
00426
00427 $ret .= '</td></tr></table>';
00428 return $ret;
00429 }
00430
00438 function shortList( $articles, $articles_start_char ) {
00439 $r = '<h3>' . htmlspecialchars( $articles_start_char[0] ) . "</h3>\n";
00440 $r .= '<ul><li>' . $articles[0] . '</li>';
00441 for ( $index = 1; $index < count( $articles ); $index++ )
00442 {
00443 if ( $articles_start_char[$index] != $articles_start_char[$index - 1] )
00444 {
00445 $r .= "</ul><h3>" . htmlspecialchars( $articles_start_char[$index] ) . "</h3>\n<ul>";
00446 }
00447
00448 $r .= "<li>{$articles[$index]}</li>";
00449 }
00450 $r .= '</ul>';
00451 return $r;
00452 }
00453
00463 function pagingLinks( $title, $first, $last, $limit, $query = array() ) {
00464 global $wgLang;
00465 $sk = $this->getSkin();
00466 $limitText = $wgLang->formatNum( $limit );
00467
00468 $prevLink = wfMsgExt( 'prevn', array( 'escape', 'parsemag' ), $limitText );
00469
00470 if ( $first != '' ) {
00471 $prevQuery = $query;
00472 $prevQuery['until'] = $first;
00473 $prevLink = $sk->linkKnown(
00474 $title,
00475 $prevLink,
00476 array(),
00477 $prevQuery
00478 );
00479 }
00480
00481 $nextLink = wfMsgExt( 'nextn', array( 'escape', 'parsemag' ), $limitText );
00482
00483 if ( $last != '' ) {
00484 $lastQuery = $query;
00485 $lastQuery['from'] = $last;
00486 $nextLink = $sk->linkKnown(
00487 $title,
00488 $nextLink,
00489 array(),
00490 $lastQuery
00491 );
00492 }
00493
00494 return "($prevLink) ($nextLink)";
00495 }
00496
00512 private function getCountMessage( $rescnt, $dbcnt, $type ) {
00513 global $wgLang;
00514 # There are three cases:
00515 # 1) The category table figure seems sane. It might be wrong, but
00516 # we can't do anything about it if we don't recalculate it on ev-
00517 # ery category view.
00518 # 2) The category table figure isn't sane, like it's smaller than the
00519 # number of actual results, *but* the number of results is less
00520 # than $this->limit and there's no offset. In this case we still
00521 # know the right figure.
00522 # 3) We have no idea.
00523 $totalrescnt = count( $this->articles ) + count( $this->children ) +
00524 ( $this->showGallery ? $this->gallery->count() : 0 );
00525
00526 if ( $dbcnt == $rescnt || ( ( $totalrescnt == $this->limit || $this->from
00527 || $this->until ) && $dbcnt > $rescnt ) )
00528 {
00529 # Case 1: seems sane.
00530 $totalcnt = $dbcnt;
00531 } elseif ( $totalrescnt < $this->limit && !$this->from && !$this->until ) {
00532 # Case 2: not sane, but salvageable. Use the number of results.
00533 # Since there are fewer than 200, we can also take this opportunity
00534 # to refresh the incorrect category table entry -- which should be
00535 # quick due to the small number of entries.
00536 $totalcnt = $rescnt;
00537 $this->cat->refreshCounts();
00538 } else {
00539 # Case 3: hopeless. Don't give a total count at all.
00540 return wfMsgExt( "category-$type-count-limited", 'parse',
00541 $wgLang->formatNum( $rescnt ) );
00542 }
00543 return wfMsgExt(
00544 "category-$type-count",
00545 'parse',
00546 $wgLang->formatNum( $rescnt ),
00547 $wgLang->formatNum( $totalcnt )
00548 );
00549 }
00550 }