34 use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
47 use ProtectedHookAccessorTrait;
105 private $languageConverter;
117 array
$until = [], array $query = []
134 'mediawiki.action.styles',
140 $this->query = $query;
143 ->getLanguageConverterFactory()->getLanguageConverter();
144 unset( $this->query[
'title'] );
179 $r = $this->
msg(
'category-empty' )->parseAsBlock();
184 'class' =>
'mw-category-generated',
185 'lang' =>
$lang->getHtmlCode(),
186 'dir' =>
$lang->getDir()
188 # put a div around the headings which are in the user language
195 $this->articles = [];
196 $this->articles_start_char = [];
197 $this->children = [];
198 $this->children_start_char = [];
199 if ( $this->showGallery ) {
201 $mode = $this->
getRequest()->getVal(
'gallerymode',
null );
204 }
catch ( ImageGalleryClassNotFoundException $e ) {
209 $this->gallery->setHideBadImages();
211 $this->imgsNoGallery = [];
212 $this->imgsNoGallery_start_char = [];
230 ->getPageByReference(
$page );
231 if ( !$pageRecord ) {
235 $this->children[] = $this->generateLink(
238 $pageRecord->isRedirect(),
239 htmlspecialchars( str_replace(
'_',
' ', $pageRecord->getDBkey() ) )
242 $this->children_start_char[] =
257 private function generateLink(
262 ->newFromPageReference(
$page );
264 $this->getHookRunner()->onCategoryViewer__generateLink(
$type, $legacyTitle, $html, $link );
265 if ( $link ===
null ) {
267 if ( $html !==
null ) {
270 $link = $linkRenderer->makeLink(
$page, $html );
275 [
'class' =>
'redirect-in-category' ],
296 ->getPrefixedText( $page );
297 if ( $titleText === $sortkey ) {
303 $firstChar = $this->collation->getFirstLetter( $word );
305 return $this->languageConverter->convert( $firstChar );
316 PageReference $page,
string $sortkey,
int $pageLength,
bool $isRedirect =
false
319 ->newFromPageReference( $page );
320 if ( $this->showGallery ) {
321 $flip = $this->flip[
'file'];
328 $this->imgsNoGallery[] = $this->generateLink(
'image', $page, $isRedirect );
330 $this->imgsNoGallery_start_char[] =
331 $this->languageConverter->convert( $this->collation->getFirstLetter( $sortkey ) );
346 bool $isRedirect =
false
348 $this->articles[] = $this->generateLink(
'page', $page, $isRedirect );
350 $this->articles_start_char[] =
351 $this->languageConverter->convert( $this->collation->getFirstLetter( $sortkey ) );
355 if ( $this->flip[
'subcat'] ) {
356 $this->children = array_reverse( $this->children );
357 $this->children_start_char = array_reverse( $this->children_start_char );
359 if ( $this->flip[
'page'] ) {
360 $this->articles = array_reverse( $this->articles );
361 $this->articles_start_char = array_reverse( $this->articles_start_char );
363 if ( !$this->showGallery && $this->flip[
'file'] ) {
364 $this->imgsNoGallery = array_reverse( $this->imgsNoGallery );
365 $this->imgsNoGallery_start_char = array_reverse( $this->imgsNoGallery_start_char );
383 $this->flip = [
'page' =>
false,
'subcat' =>
false,
'file' => false ];
385 foreach ( [
'page',
'subcat',
'file' ] as
$type ) {
386 # Get the sortkeys for start/end, if applicable. Note that if
387 # the collation in the database differs from the one
388 # set in $wgCategoryCollation, pagination might go totally haywire.
389 $extraConds = [
'cl_type' =>
$type ];
390 if ( isset( $this->from[
$type] ) ) {
391 $extraConds[] =
'cl_sortkey >= '
392 .
$dbr->addQuotes( $this->collation->getSortKey( $this->from[
$type] ) );
393 } elseif ( isset( $this->until[
$type] ) ) {
394 $extraConds[] =
'cl_sortkey < '
395 .
$dbr->addQuotes( $this->collation->getSortKey( $this->until[
$type] ) );
396 $this->flip[
$type] =
true;
399 $queryBuilder =
$dbr->newSelectQueryBuilder();
400 $queryBuilder->select( array_merge(
414 ->where( [
'cl_to' => $this->page->getDBkey() ] )
415 ->andWhere( $extraConds )
416 ->useIndex( [
'categorylinks' =>
'cl_sortkey' ] );
418 if ( $this->flip[
$type] ) {
419 $queryBuilder->orderBy(
'cl_sortkey', SelectQueryBuilder::SORT_DESC );
421 $queryBuilder->orderBy(
'cl_sortkey' );
425 ->join(
'categorylinks',
null, [
'cl_from = page_id' ] )
426 ->leftJoin(
'category',
null, [
427 'cat_title = page_title',
430 ->limit( $this->limit + 1 )
431 ->caller( __METHOD__ );
433 $res = $queryBuilder->fetchResultSet();
435 $this->getHookRunner()->onCategoryViewer__doCategoryQuery(
$type,
$res );
436 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
439 foreach (
$res as $row ) {
440 $title = Title::newFromRow( $row );
441 $linkCache->addGoodLinkObjFromRow(
$title, $row );
443 if ( $row->cl_collation ===
'' ) {
447 $humanSortkey = $row->cl_sortkey;
449 $humanSortkey =
$title->getCategorySortkey( $row->cl_sortkey_prefix );
452 if ( ++$count > $this->limit ) {
453 # We've reached the one extra which shows that there
454 # are additional pages to be had. Stop here...
455 $this->nextPage[
$type] = $humanSortkey;
458 if ( $count == $this->limit ) {
459 $this->prevPage[
$type] = $humanSortkey;
463 $cat = Category::newFromRow( $row,
$title );
464 $this->addSubcategoryObject( $cat, $humanSortkey, $row->page_len );
466 $this->addImage(
$title, $humanSortkey, $row->page_len, $row->page_is_redirect );
468 $this->addPage(
$title, $humanSortkey, $row->page_len, $row->page_is_redirect );
478 $r = $this->getCategoryBottom();
481 :
"<br style=\"clear:both;\"/>\n" . $r;
488 # Don't show subcategories section if there are none.
490 $rescnt = count( $this->children );
491 $dbcnt = $this->cat->getSubcatCount();
493 $countmsg = $this->getCountMessage( $rescnt, $dbcnt,
'subcat' );
496 # Showing subcategories
497 $r .= Html::openElement(
'div', [
'id' =>
'mw-subcategories' ] ) .
"\n";
498 $r .= Html::rawElement(
'h2', [], $this->msg(
'subcategories' )->parse() ) .
"\n";
500 $r .= $this->getSectionPagingLinks(
'subcat' );
501 $r .= $this->formatList( $this->children, $this->children_start_char );
502 $r .= $this->getSectionPagingLinks(
'subcat' );
503 $r .=
"\n" . Html::closeElement(
'div' );
512 $name = $this->getOutput()->getUnprefixedDisplayTitle();
513 # Don't show articles section if there are none.
516 # @todo FIXME: Here and in the other two sections: we don't need to bother
517 # with this rigmarole if the entire category contents fit on one page
518 # and have already been retrieved. We can just use $rescnt in that
519 # case and save a query and some logic.
520 $dbcnt = $this->cat->getPageCount( Category::COUNT_CONTENT_PAGES );
521 $rescnt = count( $this->articles );
523 $countmsg = $this->getCountMessage( $rescnt, $dbcnt,
'article' );
526 $r .= Html::openElement(
'div', [
'id' =>
'mw-pages' ] ) .
"\n";
527 $r .= Html::rawElement(
530 $this->msg(
'category_header' )->rawParams( $name )->parse()
533 $r .= $this->getSectionPagingLinks(
'page' );
534 $r .= $this->formatList( $this->articles, $this->articles_start_char );
535 $r .= $this->getSectionPagingLinks(
'page' );
536 $r .=
"\n" . Html::closeElement(
'div' );
545 $name = $this->getOutput()->getUnprefixedDisplayTitle();
547 $rescnt = $this->showGallery ? $this->gallery->count() : count( $this->imgsNoGallery );
548 $dbcnt = $this->cat->getFileCount();
550 $countmsg = $this->getCountMessage( $rescnt, $dbcnt,
'file' );
553 $r .= Html::openElement(
'div', [
'id' =>
'mw-category-media' ] ) .
"\n";
554 $r .= Html::rawElement(
557 $this->msg(
'category-media-header' )->rawParams( $name )->parse()
560 $r .= $this->getSectionPagingLinks(
'file' );
561 if ( $this->showGallery ) {
562 $r .= $this->gallery->toHTML();
564 $r .= $this->formatList( $this->imgsNoGallery, $this->imgsNoGallery_start_char );
566 $r .= $this->getSectionPagingLinks(
'file' );
567 $r .=
"\n" . Html::closeElement(
'div' );
579 private function getSectionPagingLinks(
$type ) {
580 if ( isset( $this->until[
$type] ) ) {
585 if ( $this->nextPage[
$type] !==
null ) {
586 return $this->pagingLinks(
587 $this->prevPage[
$type] ??
'',
595 return $this->pagingLinks(
600 } elseif ( $this->nextPage[
$type] !==
null || isset( $this->from[
$type] ) ) {
601 return $this->pagingLinks(
602 $this->from[
$type] ??
'',
603 $this->nextPage[
$type],
628 private function formatList( $articles, $articles_start_char, $cutoff = 6 ) {
630 if ( count( $articles ) > $cutoff ) {
631 $list = self::columnList( $articles, $articles_start_char );
632 } elseif ( count( $articles ) > 0 ) {
634 $list = self::shortList( $articles, $articles_start_char );
637 $pageLang = MediaWikiServices::getInstance()->getTitleFactory()
638 ->newFromPageIdentity( $this->page )
640 $attribs = [
'lang' => $pageLang->getHtmlCode(),
'dir' => $pageLang->getDir(),
641 'class' =>
'mw-content-' . $pageLang->getDir() ];
642 $list = Html::rawElement(
'div', $attribs, $list );
659 $articles_start_char,
660 $cssClasses =
'mw-category mw-category-columns'
662 $columns = array_combine( $articles, $articles_start_char );
664 $ret = Html::openElement(
'div', [
'class' => $cssClasses ] );
668 # Kind of like array_flip() here, but we keep duplicates in an
669 # array instead of dropping them.
670 foreach ( $columns as $article => $char ) {
671 $colContents[$char][] = $article;
674 foreach ( $colContents as $char => $articles ) {
675 # Change space to non-breaking space to keep headers aligned
676 $h3char = $char ===
' ' ?
"\u{00A0}" : htmlspecialchars( $char );
678 $ret .= Html::openElement(
'div', [
'class' =>
'mw-category-group' ] );
679 $ret .= Html::rawElement(
'h3', [], $h3char ) .
"\n";
680 $ret .= Html::openElement(
'ul' );
684 static function ( $article ) {
685 return Html::rawElement(
'li', [], $article );
690 $ret .= Html::closeElement(
'ul' ) . Html::closeElement(
'div' );
694 $ret .= Html::closeElement(
'div' );
706 public static function shortList( $articles, $articles_start_char ) {
707 return self::columnList( $articles, $articles_start_char,
'mw-category' );
719 private function pagingLinks( $first, $last,
$type =
'' ) {
720 $prevLink = $this->msg(
'prev-page' )->escaped();
722 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
723 if ( $first !=
'' ) {
724 $prevQuery = $this->query;
725 $prevQuery[
"{$type}until"] = $first;
726 unset( $prevQuery[
"{$type}from"] );
727 $prevLink = $linkRenderer->makeKnownLink(
728 $this->addFragmentToTitle( $this->page,
$type ),
735 $nextLink = $this->msg(
'next-page' )->escaped();
738 $lastQuery = $this->query;
739 $lastQuery[
"{$type}from"] = $last;
740 unset( $lastQuery[
"{$type}until"] );
741 $nextLink = $linkRenderer->makeKnownLink(
742 $this->addFragmentToTitle( $this->page,
$type ),
749 return $this->msg(
'categoryviewer-pagedlinks' )->rawParams( $prevLink, $nextLink )->escaped();
761 private function addFragmentToTitle( PageReference $page,
string $section ): LinkTarget {
762 switch ( $section ) {
764 $fragment =
'mw-pages';
767 $fragment =
'mw-subcategories';
770 $fragment =
'mw-category-media';
774 " Invalid section $section." );
791 private function getCountMessage( $rescnt, $dbcnt,
$type ) {
806 if (
$type ===
'article' ) {
807 $pagingType =
'page';
812 $fromOrUntil =
false;
813 if ( isset( $this->from[$pagingType] ) || isset( $this->until[$pagingType] ) ) {
817 if ( $dbcnt == $rescnt ||
818 ( ( $rescnt == $this->limit || $fromOrUntil ) && $dbcnt > $rescnt )
822 } elseif ( $rescnt < $this->limit && !$fromOrUntil ) {
829 return $this->msg(
"category-$type-count-limited" )->numParams( $rescnt )->parseAsBlock();
832 return $this->msg(
"category-$type-count" )->numParams( $rescnt, $totalcnt )->parseAsBlock();
836 class_alias( CategoryViewer::class,
'CategoryViewer' );
deprecatePublicPropertyFallback(string $property, string $version, $getter, $setter=null, $class=null, $component=null)
Mark a removed public property as deprecated and provide fallback getter and setter callables.
trait DeprecationHelper
Use this trait in classes which have properties for which public access is deprecated or implementati...
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
getContext()
Get the base IContextSource object.
setContext(IContextSource $context)
Marks HTML that shouldn't be escaped.
static factory( $mode=false, IContextSource $context=null)
Get a new image gallery.
Class for exceptions thrown by ImageGalleryBase::factory().
Cache for article titles (prefixed DB keys) and ids linked from one source.
static getSelectFields()
Fields that LinkCache needs to select.
A class containing constants representing the names of configuration variables.
const CategoryPagingLimit
Name constant for the CategoryPagingLimit setting, for use with Config::get()
const CategoryMagicGallery
Name constant for the CategoryMagicGallery setting, for use with Config::get()
Represents a page (or page fragment) title within MediaWiki.
Interface for objects which can provide a MediaWiki context on request.
getConfig()
Get the site configuration.
The shared interface for all language converters.
Interface for objects (potentially) representing an editable wiki page.
if(!isset( $args[0])) $lang