MediaWiki  REL1_19
CategoryViewer.php
Go to the documentation of this file.
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 }