MediaWiki
REL1_19
|
00001 <?php 00002 00003 if ( !defined( 'MEDIAWIKI' ) ) 00004 die( 1 ); 00005 00006 class CategoryViewer extends ContextSource { 00007 var $limit, $from, $until, 00008 $articles, $articles_start_char, 00009 $children, $children_start_char, 00010 $showGallery, $imgsNoGalley, 00011 $imgsNoGallery_start_char, 00012 $imgsNoGallery; 00013 00017 var $nextPage; 00018 00022 var $flip; 00023 00027 var $title; 00028 00032 var $collation; 00033 00037 var $gallery; 00038 00043 private $cat; 00044 00049 private $query; 00050 00061 function __construct( $title, IContextSource $context, $from = '', $until = '', $query = array() ) { 00062 global $wgCategoryPagingLimit; 00063 $this->title = $title; 00064 $this->setContext( $context ); 00065 $this->from = $from; 00066 $this->until = $until; 00067 $this->limit = $wgCategoryPagingLimit; 00068 $this->cat = Category::newFromTitle( $title ); 00069 $this->query = $query; 00070 $this->collation = Collation::singleton(); 00071 unset( $this->query['title'] ); 00072 } 00073 00079 public function getHTML() { 00080 global $wgCategoryMagicGallery; 00081 wfProfileIn( __METHOD__ ); 00082 00083 $this->showGallery = $wgCategoryMagicGallery && !$this->getOutput()->mNoGallery; 00084 00085 $this->clearCategoryState(); 00086 $this->doCategoryQuery(); 00087 $this->finaliseCategoryState(); 00088 00089 $r = $this->getSubcategorySection() . 00090 $this->getPagesSection() . 00091 $this->getImageSection(); 00092 00093 if ( $r == '' ) { 00094 // If there is no category content to display, only 00095 // show the top part of the navigation links. 00096 // @todo FIXME: Cannot be completely suppressed because it 00097 // is unknown if 'until' or 'from' makes this 00098 // give 0 results. 00099 $r = $r . $this->getCategoryTop(); 00100 } else { 00101 $r = $this->getCategoryTop() . 00102 $r . 00103 $this->getCategoryBottom(); 00104 } 00105 00106 // Give a proper message if category is empty 00107 if ( $r == '' ) { 00108 $r = wfMsgExt( 'category-empty', array( 'parse' ) ); 00109 } 00110 00111 $lang = $this->getLanguage(); 00112 $langAttribs = array( 'lang' => $lang->getCode(), 'dir' => $lang->getDir() ); 00113 # put a div around the headings which are in the user language 00114 $r = Html::openElement( 'div', $langAttribs ) . $r . '</div>'; 00115 00116 wfProfileOut( __METHOD__ ); 00117 return $r; 00118 } 00119 00120 function clearCategoryState() { 00121 $this->articles = array(); 00122 $this->articles_start_char = array(); 00123 $this->children = array(); 00124 $this->children_start_char = array(); 00125 if ( $this->showGallery ) { 00126 $this->gallery = new ImageGallery(); 00127 $this->gallery->setHideBadImages(); 00128 } else { 00129 $this->imgsNoGallery = array(); 00130 $this->imgsNoGallery_start_char = array(); 00131 } 00132 } 00133 00140 function addSubcategoryObject( Category $cat, $sortkey, $pageLength ) { 00141 // Subcategory; strip the 'Category' namespace from the link text. 00142 $title = $cat->getTitle(); 00143 00144 $link = Linker::link( $title, htmlspecialchars( $title->getText() ) ); 00145 if ( $title->isRedirect() ) { 00146 // This didn't used to add redirect-in-category, but might 00147 // as well be consistent with the rest of the sections 00148 // on a category page. 00149 $link = '<span class="redirect-in-category">' . $link . '</span>'; 00150 } 00151 $this->children[] = $link; 00152 00153 $this->children_start_char[] = 00154 $this->getSubcategorySortChar( $cat->getTitle(), $sortkey ); 00155 } 00156 00161 function addSubcategory( Title $title, $sortkey, $pageLength ) { 00162 wfDeprecated( __METHOD__, '1.17' ); 00163 $this->addSubcategoryObject( Category::newFromTitle( $title ), $sortkey, $pageLength ); 00164 } 00165 00176 function getSubcategorySortChar( $title, $sortkey ) { 00177 global $wgContLang; 00178 00179 if ( $title->getPrefixedText() == $sortkey ) { 00180 $word = $title->getDBkey(); 00181 } else { 00182 $word = $sortkey; 00183 } 00184 00185 $firstChar = $this->collation->getFirstLetter( $word ); 00186 00187 return $wgContLang->convert( $firstChar ); 00188 } 00189 00197 function addImage( Title $title, $sortkey, $pageLength, $isRedirect = false ) { 00198 global $wgContLang; 00199 if ( $this->showGallery ) { 00200 $flip = $this->flip['file']; 00201 if ( $flip ) { 00202 $this->gallery->insert( $title ); 00203 } else { 00204 $this->gallery->add( $title ); 00205 } 00206 } else { 00207 $link = Linker::link( $title ); 00208 if ( $isRedirect ) { 00209 // This seems kind of pointless given 'mw-redirect' class, 00210 // but keeping for back-compatibility with user css. 00211 $link = '<span class="redirect-in-category">' . $link . '</span>'; 00212 } 00213 $this->imgsNoGallery[] = $link; 00214 00215 $this->imgsNoGallery_start_char[] = $wgContLang->convert( 00216 $this->collation->getFirstLetter( $sortkey ) ); 00217 } 00218 } 00219 00227 function addPage( $title, $sortkey, $pageLength, $isRedirect = false ) { 00228 global $wgContLang; 00229 00230 $link = Linker::link( $title ); 00231 if ( $isRedirect ) { 00232 // This seems kind of pointless given 'mw-redirect' class, 00233 // but keeping for back-compatiability with user css. 00234 $link = '<span class="redirect-in-category">' . $link . '</span>'; 00235 } 00236 $this->articles[] = $link; 00237 00238 $this->articles_start_char[] = $wgContLang->convert( 00239 $this->collation->getFirstLetter( $sortkey ) ); 00240 } 00241 00242 function finaliseCategoryState() { 00243 if ( $this->flip['subcat'] ) { 00244 $this->children = array_reverse( $this->children ); 00245 $this->children_start_char = array_reverse( $this->children_start_char ); 00246 } 00247 if ( $this->flip['page'] ) { 00248 $this->articles = array_reverse( $this->articles ); 00249 $this->articles_start_char = array_reverse( $this->articles_start_char ); 00250 } 00251 if ( !$this->showGallery && $this->flip['file'] ) { 00252 $this->imgsNoGallery = array_reverse( $this->imgsNoGallery ); 00253 $this->imgsNoGallery_start_char = array_reverse( $this->imgsNoGallery_start_char ); 00254 } 00255 } 00256 00257 function doCategoryQuery() { 00258 $dbr = wfGetDB( DB_SLAVE, 'category' ); 00259 00260 $this->nextPage = array( 00261 'page' => null, 00262 'subcat' => null, 00263 'file' => null, 00264 ); 00265 $this->flip = array( 'page' => false, 'subcat' => false, 'file' => false ); 00266 00267 foreach ( array( 'page', 'subcat', 'file' ) as $type ) { 00268 # Get the sortkeys for start/end, if applicable. Note that if 00269 # the collation in the database differs from the one 00270 # set in $wgCategoryCollation, pagination might go totally haywire. 00271 $extraConds = array( 'cl_type' => $type ); 00272 if ( $this->from[$type] !== null ) { 00273 $extraConds[] = 'cl_sortkey >= ' 00274 . $dbr->addQuotes( $this->collation->getSortKey( $this->from[$type] ) ); 00275 } elseif ( $this->until[$type] !== null ) { 00276 $extraConds[] = 'cl_sortkey < ' 00277 . $dbr->addQuotes( $this->collation->getSortKey( $this->until[$type] ) ); 00278 $this->flip[$type] = true; 00279 } 00280 00281 $res = $dbr->select( 00282 array( 'page', 'categorylinks', 'category' ), 00283 array( 'page_id', 'page_title', 'page_namespace', 'page_len', 00284 'page_is_redirect', 'cl_sortkey', 'cat_id', 'cat_title', 00285 'cat_subcats', 'cat_pages', 'cat_files', 00286 'cl_sortkey_prefix', 'cl_collation' ), 00287 array_merge( array( 'cl_to' => $this->title->getDBkey() ), $extraConds ), 00288 __METHOD__, 00289 array( 00290 'USE INDEX' => array( 'categorylinks' => 'cl_sortkey' ), 00291 'LIMIT' => $this->limit + 1, 00292 'ORDER BY' => $this->flip[$type] ? 'cl_sortkey DESC' : 'cl_sortkey', 00293 ), 00294 array( 00295 'categorylinks' => array( 'INNER JOIN', 'cl_from = page_id' ), 00296 'category' => array( 'LEFT JOIN', 'cat_title = page_title AND page_namespace = ' . NS_CATEGORY ) 00297 ) 00298 ); 00299 00300 $count = 0; 00301 foreach ( $res as $row ) { 00302 $title = Title::newFromRow( $row ); 00303 if ( $row->cl_collation === '' ) { 00304 // Hack to make sure that while updating from 1.16 schema 00305 // and db is inconsistent, that the sky doesn't fall. 00306 // See r83544. Could perhaps be removed in a couple decades... 00307 $humanSortkey = $row->cl_sortkey; 00308 } else { 00309 $humanSortkey = $title->getCategorySortkey( $row->cl_sortkey_prefix ); 00310 } 00311 00312 if ( ++$count > $this->limit ) { 00313 # We've reached the one extra which shows that there 00314 # are additional pages to be had. Stop here... 00315 $this->nextPage[$type] = $humanSortkey; 00316 break; 00317 } 00318 00319 if ( $title->getNamespace() == NS_CATEGORY ) { 00320 $cat = Category::newFromRow( $row, $title ); 00321 $this->addSubcategoryObject( $cat, $humanSortkey, $row->page_len ); 00322 } elseif ( $title->getNamespace() == NS_FILE ) { 00323 $this->addImage( $title, $humanSortkey, $row->page_len, $row->page_is_redirect ); 00324 } else { 00325 $this->addPage( $title, $humanSortkey, $row->page_len, $row->page_is_redirect ); 00326 } 00327 } 00328 } 00329 } 00330 00334 function getCategoryTop() { 00335 $r = $this->getCategoryBottom(); 00336 return $r === '' 00337 ? $r 00338 : "<br style=\"clear:both;\"/>\n" . $r; 00339 } 00340 00344 function getSubcategorySection() { 00345 # Don't show subcategories section if there are none. 00346 $r = ''; 00347 $rescnt = count( $this->children ); 00348 $dbcnt = $this->cat->getSubcatCount(); 00349 $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'subcat' ); 00350 00351 if ( $rescnt > 0 ) { 00352 # Showing subcategories 00353 $r .= "<div id=\"mw-subcategories\">\n"; 00354 $r .= '<h2>' . wfMsg( 'subcategories' ) . "</h2>\n"; 00355 $r .= $countmsg; 00356 $r .= $this->getSectionPagingLinks( 'subcat' ); 00357 $r .= $this->formatList( $this->children, $this->children_start_char ); 00358 $r .= $this->getSectionPagingLinks( 'subcat' ); 00359 $r .= "\n</div>"; 00360 } 00361 return $r; 00362 } 00363 00367 function getPagesSection() { 00368 $ti = htmlspecialchars( $this->title->getText() ); 00369 # Don't show articles section if there are none. 00370 $r = ''; 00371 00372 # @todo FIXME: Here and in the other two sections: we don't need to bother 00373 # with this rigamarole if the entire category contents fit on one page 00374 # and have already been retrieved. We can just use $rescnt in that 00375 # case and save a query and some logic. 00376 $dbcnt = $this->cat->getPageCount() - $this->cat->getSubcatCount() 00377 - $this->cat->getFileCount(); 00378 $rescnt = count( $this->articles ); 00379 $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'article' ); 00380 00381 if ( $rescnt > 0 ) { 00382 $r = "<div id=\"mw-pages\">\n"; 00383 $r .= '<h2>' . wfMsg( 'category_header', $ti ) . "</h2>\n"; 00384 $r .= $countmsg; 00385 $r .= $this->getSectionPagingLinks( 'page' ); 00386 $r .= $this->formatList( $this->articles, $this->articles_start_char ); 00387 $r .= $this->getSectionPagingLinks( 'page' ); 00388 $r .= "\n</div>"; 00389 } 00390 return $r; 00391 } 00392 00396 function getImageSection() { 00397 $r = ''; 00398 $rescnt = $this->showGallery ? $this->gallery->count() : count( $this->imgsNoGallery ); 00399 if ( $rescnt > 0 ) { 00400 $dbcnt = $this->cat->getFileCount(); 00401 $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'file' ); 00402 00403 $r .= "<div id=\"mw-category-media\">\n"; 00404 $r .= '<h2>' . wfMsg( 'category-media-header', htmlspecialchars( $this->title->getText() ) ) . "</h2>\n"; 00405 $r .= $countmsg; 00406 $r .= $this->getSectionPagingLinks( 'file' ); 00407 if ( $this->showGallery ) { 00408 $r .= $this->gallery->toHTML(); 00409 } else { 00410 $r .= $this->formatList( $this->imgsNoGallery, $this->imgsNoGallery_start_char ); 00411 } 00412 $r .= $this->getSectionPagingLinks( 'file' ); 00413 $r .= "\n</div>"; 00414 } 00415 return $r; 00416 } 00417 00425 private function getSectionPagingLinks( $type ) { 00426 if ( $this->until[$type] !== null ) { 00427 return $this->pagingLinks( $this->nextPage[$type], $this->until[$type], $type ); 00428 } elseif ( $this->nextPage[$type] !== null || $this->from[$type] !== null ) { 00429 return $this->pagingLinks( $this->from[$type], $this->nextPage[$type], $type ); 00430 } else { 00431 return ''; 00432 } 00433 } 00434 00438 function getCategoryBottom() { 00439 return ''; 00440 } 00441 00452 function formatList( $articles, $articles_start_char, $cutoff = 6 ) { 00453 $list = ''; 00454 if ( count ( $articles ) > $cutoff ) { 00455 $list = self::columnList( $articles, $articles_start_char ); 00456 } elseif ( count( $articles ) > 0 ) { 00457 // for short lists of articles in categories. 00458 $list = self::shortList( $articles, $articles_start_char ); 00459 } 00460 00461 $pageLang = $this->title->getPageLanguage(); 00462 $attribs = array( 'lang' => $pageLang->getCode(), 'dir' => $pageLang->getDir(), 00463 'class' => 'mw-content-'.$pageLang->getDir() ); 00464 $list = Html::rawElement( 'div', $attribs, $list ); 00465 00466 return $list; 00467 } 00468 00484 static function columnList( $articles, $articles_start_char ) { 00485 $columns = array_combine( $articles, $articles_start_char ); 00486 # Split into three columns 00487 $columns = array_chunk( $columns, ceil( count( $columns ) / 3 ), true /* preserve keys */ ); 00488 00489 $ret = '<table width="100%"><tr valign="top">'; 00490 $prevchar = null; 00491 00492 foreach ( $columns as $column ) { 00493 $ret .= '<td width="33.3%">'; 00494 $colContents = array(); 00495 00496 # Kind of like array_flip() here, but we keep duplicates in an 00497 # array instead of dropping them. 00498 foreach ( $column as $article => $char ) { 00499 if ( !isset( $colContents[$char] ) ) { 00500 $colContents[$char] = array(); 00501 } 00502 $colContents[$char][] = $article; 00503 } 00504 00505 $first = true; 00506 foreach ( $colContents as $char => $articles ) { 00507 $ret .= '<h3>' . htmlspecialchars( $char ); 00508 if ( $first && $char === $prevchar ) { 00509 # We're continuing a previous chunk at the top of a new 00510 # column, so add " cont." after the letter. 00511 $ret .= ' ' . wfMsgHtml( 'listingcontinuesabbrev' ); 00512 } 00513 $ret .= "</h3>\n"; 00514 00515 $ret .= '<ul><li>'; 00516 $ret .= implode( "</li>\n<li>", $articles ); 00517 $ret .= '</li></ul>'; 00518 00519 $first = false; 00520 $prevchar = $char; 00521 } 00522 00523 $ret .= "</td>\n"; 00524 } 00525 00526 $ret .= '</tr></table>'; 00527 return $ret; 00528 } 00529 00537 static function shortList( $articles, $articles_start_char ) { 00538 $r = '<h3>' . htmlspecialchars( $articles_start_char[0] ) . "</h3>\n"; 00539 $r .= '<ul><li>' . $articles[0] . '</li>'; 00540 for ( $index = 1; $index < count( $articles ); $index++ ) { 00541 if ( $articles_start_char[$index] != $articles_start_char[$index - 1] ) { 00542 $r .= "</ul><h3>" . htmlspecialchars( $articles_start_char[$index] ) . "</h3>\n<ul>"; 00543 } 00544 00545 $r .= "<li>{$articles[$index]}</li>"; 00546 } 00547 $r .= '</ul>'; 00548 return $r; 00549 } 00550 00560 private function pagingLinks( $first, $last, $type = '' ) { 00561 $prevLink = wfMessage( 'prevn' )->numParams( $this->limit )->escaped(); 00562 00563 if ( $first != '' ) { 00564 $prevQuery = $this->query; 00565 $prevQuery["{$type}until"] = $first; 00566 unset( $prevQuery["{$type}from"] ); 00567 $prevLink = Linker::linkKnown( 00568 $this->addFragmentToTitle( $this->title, $type ), 00569 $prevLink, 00570 array(), 00571 $prevQuery 00572 ); 00573 } 00574 00575 $nextLink = wfMessage( 'nextn' )->numParams( $this->limit )->escaped(); 00576 00577 if ( $last != '' ) { 00578 $lastQuery = $this->query; 00579 $lastQuery["{$type}from"] = $last; 00580 unset( $lastQuery["{$type}until"] ); 00581 $nextLink = Linker::linkKnown( 00582 $this->addFragmentToTitle( $this->title, $type ), 00583 $nextLink, 00584 array(), 00585 $lastQuery 00586 ); 00587 } 00588 00589 return "($prevLink) ($nextLink)"; 00590 } 00591 00600 private function addFragmentToTitle( $title, $section ) { 00601 switch ( $section ) { 00602 case 'page': 00603 $fragment = 'mw-pages'; 00604 break; 00605 case 'subcat': 00606 $fragment = 'mw-subcategories'; 00607 break; 00608 case 'file': 00609 $fragment = 'mw-category-media'; 00610 break; 00611 default: 00612 throw new MWException( __METHOD__ . 00613 " Invalid section $section." ); 00614 } 00615 00616 return Title::makeTitle( $title->getNamespace(), 00617 $title->getDBkey(), $fragment ); 00618 } 00634 private function getCountMessage( $rescnt, $dbcnt, $type ) { 00635 # There are three cases: 00636 # 1) The category table figure seems sane. It might be wrong, but 00637 # we can't do anything about it if we don't recalculate it on ev- 00638 # ery category view. 00639 # 2) The category table figure isn't sane, like it's smaller than the 00640 # number of actual results, *but* the number of results is less 00641 # than $this->limit and there's no offset. In this case we still 00642 # know the right figure. 00643 # 3) We have no idea. 00644 00645 # Check if there's a "from" or "until" for anything 00646 00647 // This is a little ugly, but we seem to use different names 00648 // for the paging types then for the messages. 00649 if ( $type === 'article' ) { 00650 $pagingType = 'page'; 00651 } else { 00652 $pagingType = $type; 00653 } 00654 00655 $fromOrUntil = false; 00656 if ( $this->from[$pagingType] !== null || $this->until[$pagingType] !== null ) { 00657 $fromOrUntil = true; 00658 } 00659 00660 if ( $dbcnt == $rescnt || ( ( $rescnt == $this->limit || $fromOrUntil ) 00661 && $dbcnt > $rescnt ) ) { 00662 # Case 1: seems sane. 00663 $totalcnt = $dbcnt; 00664 } elseif ( $rescnt < $this->limit && !$fromOrUntil ) { 00665 # Case 2: not sane, but salvageable. Use the number of results. 00666 # Since there are fewer than 200, we can also take this opportunity 00667 # to refresh the incorrect category table entry -- which should be 00668 # quick due to the small number of entries. 00669 $totalcnt = $rescnt; 00670 $this->cat->refreshCounts(); 00671 } else { 00672 # Case 3: hopeless. Don't give a total count at all. 00673 return wfMessage( "category-$type-count-limited" )->numParams( $rescnt )->parseAsBlock(); 00674 } 00675 return wfMessage( "category-$type-count" )->numParams( $rescnt, $totalcnt )->parseAsBlock(); 00676 } 00677 }