23use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
30 use ProtectedHookAccessorTrait;
108 return Title::castFromPageIdentity( $this->page );
117 'mediawiki.action.view.categoryPage.styles'
122 $this->cat = Category::newFromTitle(
$page );
124 $this->collation = MediaWikiServices::getInstance()->getCollationFactory()->getCategoryCollation();
125 $this->languageConverter = MediaWikiServices::getInstance()
126 ->getLanguageConverterFactory()->getLanguageConverter();
127 unset( $this->query[
'title'] );
136 $this->showGallery = $this->
getConfig()->get(
'CategoryMagicGallery' )
162 $r = $this->
msg(
'category-empty' )->parseAsBlock();
167 'class' =>
'mw-category-generated',
168 'lang' =>
$lang->getHtmlCode(),
169 'dir' =>
$lang->getDir()
171 # put a div around the headings which are in the user language
172 $r = Html::rawElement(
'div', $attribs, $r );
178 $this->articles = [];
179 $this->articles_start_char = [];
180 $this->children = [];
181 $this->children_start_char = [];
182 if ( $this->showGallery ) {
184 $mode = $this->
getRequest()->getVal(
'gallerymode',
null );
186 $this->gallery = ImageGalleryBase::factory( $mode, $this->
getContext() );
187 }
catch ( Exception $e ) {
189 $this->gallery = ImageGalleryBase::factory(
false, $this->
getContext() );
192 $this->gallery->setHideBadImages();
194 $this->imgsNoGallery = [];
195 $this->imgsNoGallery_start_char = [];
207 $pageRecord = MediaWikiServices::getInstance()->getPageStore()
213 $pageRecord->isRedirect(),
214 htmlspecialchars( str_replace(
'_',
' ', $pageRecord->getDBkey() ) )
217 $this->children_start_char[] =
236 $legacyTitle = MediaWikiServices::getInstance()->getTitleFactory()
237 ->castFromPageReference(
$page );
238 $this->getHookRunner()->onCategoryViewer__generateLink(
$type, $legacyTitle, $html, $link );
239 if ( $link ===
null ) {
240 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
241 if ( $html !==
null ) {
244 $link = $linkRenderer->makeLink(
$page, $html );
247 $link = Html::rawElement(
249 [
'class' =>
'redirect-in-category' ],
270 ->getPrefixedText( $page );
271 if ( $titleText === $sortkey ) {
277 $firstChar = $this->collation->getFirstLetter( $word );
279 return $this->languageConverter->convert( $firstChar );
290 PageReference $page,
string $sortkey,
int $pageLength,
bool $isRedirect =
false
293 ->castFromPageReference( $page );
294 if ( $this->showGallery ) {
295 $flip = $this->flip[
'file'];
297 $this->gallery->insert(
$title );
299 $this->gallery->add(
$title );
302 $this->imgsNoGallery[] = $this->generateLink(
'image', $page, $isRedirect );
304 $this->imgsNoGallery_start_char[] =
305 $this->languageConverter->convert( $this->collation->getFirstLetter( $sortkey ) );
317 PageReference $page,
string $sortkey,
int $pageLength,
bool $isRedirect =
false
319 $this->articles[] = $this->generateLink(
'page', $page, $isRedirect );
321 $this->articles_start_char[] =
322 $this->languageConverter->convert( $this->collation->getFirstLetter( $sortkey ) );
326 if ( $this->flip[
'subcat'] ) {
327 $this->children = array_reverse( $this->children );
328 $this->children_start_char = array_reverse( $this->children_start_char );
330 if ( $this->flip[
'page'] ) {
331 $this->articles = array_reverse( $this->articles );
332 $this->articles_start_char = array_reverse( $this->articles_start_char );
334 if ( !$this->showGallery && $this->flip[
'file'] ) {
335 $this->imgsNoGallery = array_reverse( $this->imgsNoGallery );
336 $this->imgsNoGallery_start_char = array_reverse( $this->imgsNoGallery_start_char );
354 $this->flip = [
'page' =>
false,
'subcat' =>
false,
'file' => false ];
356 foreach ( [
'page',
'subcat',
'file' ] as
$type ) {
357 # Get the sortkeys for start/end, if applicable. Note that if
358 # the collation in the database differs from the one
359 # set in $wgCategoryCollation, pagination might go totally haywire.
360 $extraConds = [
'cl_type' =>
$type ];
361 if ( isset( $this->from[
$type] ) ) {
362 $extraConds[] =
'cl_sortkey >= '
363 .
$dbr->addQuotes( $this->collation->getSortKey( $this->from[
$type] ) );
364 } elseif ( isset( $this->until[
$type] ) ) {
365 $extraConds[] =
'cl_sortkey < '
366 .
$dbr->addQuotes( $this->collation->getSortKey( $this->until[
$type] ) );
367 $this->flip[
$type] =
true;
371 [
'page',
'categorylinks',
'category' ],
373 LinkCache::getSelectFields(),
387 array_merge( [
'cl_to' => $this->page->getDBkey() ], $extraConds ),
390 'USE INDEX' => [
'categorylinks' =>
'cl_sortkey' ],
391 'LIMIT' => $this->limit + 1,
392 'ORDER BY' => $this->flip[
$type] ?
'cl_sortkey DESC' :
'cl_sortkey',
395 'categorylinks' => [
'JOIN',
'cl_from = page_id' ],
396 'category' => [
'LEFT JOIN', [
397 'cat_title = page_title',
403 $this->getHookRunner()->onCategoryViewer__doCategoryQuery(
$type,
$res );
404 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
407 foreach (
$res as $row ) {
408 $title = Title::newFromRow( $row );
409 $linkCache->addGoodLinkObjFromRow(
$title, $row );
411 if ( $row->cl_collation ===
'' ) {
415 $humanSortkey = $row->cl_sortkey;
417 $humanSortkey =
$title->getCategorySortkey( $row->cl_sortkey_prefix );
420 if ( ++$count > $this->limit ) {
421 # We've reached the one extra which shows that there
422 # are additional pages to be had. Stop here...
423 $this->nextPage[
$type] = $humanSortkey;
426 if ( $count == $this->limit ) {
427 $this->prevPage[
$type] = $humanSortkey;
431 $cat = Category::newFromRow( $row,
$title );
432 $this->addSubcategoryObject( $cat, $humanSortkey, $row->page_len );
434 $this->addImage(
$title, $humanSortkey, $row->page_len, $row->page_is_redirect );
436 $this->addPage(
$title, $humanSortkey, $row->page_len, $row->page_is_redirect );
446 $r = $this->getCategoryBottom();
449 :
"<br style=\"clear:both;\"/>\n" . $r;
456 # Don't show subcategories section if there are none.
458 $rescnt = count( $this->children );
459 $dbcnt = $this->cat->getSubcatCount();
461 $countmsg = $this->getCountMessage( $rescnt, $dbcnt,
'subcat' );
464 # Showing subcategories
465 $r .= Html::openElement(
'div', [
'id' =>
'mw-subcategories' ] ) .
"\n";
466 $r .= Html::rawElement(
'h2', [], $this->msg(
'subcategories' )->parse() ) .
"\n";
468 $r .= $this->getSectionPagingLinks(
'subcat' );
469 $r .= $this->formatList( $this->children, $this->children_start_char );
470 $r .= $this->getSectionPagingLinks(
'subcat' );
471 $r .=
"\n" . Html::closeElement(
'div' );
480 $name = $this->getOutput()->getUnprefixedDisplayTitle();
481 # Don't show articles section if there are none.
484 # @todo FIXME: Here and in the other two sections: we don't need to bother
485 # with this rigmarole if the entire category contents fit on one page
486 # and have already been retrieved. We can just use $rescnt in that
487 # case and save a query and some logic.
488 $dbcnt = $this->cat->getPageCount() - $this->cat->getSubcatCount()
489 - $this->cat->getFileCount();
490 $rescnt = count( $this->articles );
492 $countmsg = $this->getCountMessage( $rescnt, $dbcnt,
'article' );
495 $r .= Html::openElement(
'div', [
'id' =>
'mw-pages' ] ) .
"\n";
496 $r .= Html::rawElement(
499 $this->msg(
'category_header' )->rawParams( $name )->parse()
502 $r .= $this->getSectionPagingLinks(
'page' );
503 $r .= $this->formatList( $this->articles, $this->articles_start_char );
504 $r .= $this->getSectionPagingLinks(
'page' );
505 $r .=
"\n" . Html::closeElement(
'div' );
514 $name = $this->getOutput()->getUnprefixedDisplayTitle();
516 $rescnt = $this->showGallery ? $this->gallery->count() : count( $this->imgsNoGallery );
517 $dbcnt = $this->cat->getFileCount();
519 $countmsg = $this->getCountMessage( $rescnt, $dbcnt,
'file' );
522 $r .= Html::openElement(
'div', [
'id' =>
'mw-category-media' ] ) .
"\n";
523 $r .= Html::rawElement(
526 $this->msg(
'category-media-header' )->rawParams( $name )->parse()
529 $r .= $this->getSectionPagingLinks(
'file' );
530 if ( $this->showGallery ) {
531 $r .= $this->gallery->toHTML();
533 $r .= $this->formatList( $this->imgsNoGallery, $this->imgsNoGallery_start_char );
535 $r .= $this->getSectionPagingLinks(
'file' );
536 $r .=
"\n" . Html::closeElement(
'div' );
549 if ( isset( $this->until[
$type] ) ) {
554 if ( $this->nextPage[
$type] !==
null ) {
555 return $this->pagingLinks( $this->prevPage[
$type], $this->until[
$type],
$type );
559 return $this->pagingLinks(
null, $this->until[
$type],
$type );
561 } elseif ( $this->nextPage[
$type] !==
null || isset( $this->from[
$type] ) ) {
562 return $this->pagingLinks( $this->from[
$type], $this->nextPage[
$type],
$type );
585 private function formatList( $articles, $articles_start_char, $cutoff = 6 ) {
587 if ( count( $articles ) > $cutoff ) {
588 $list = self::columnList( $articles, $articles_start_char );
589 } elseif ( count( $articles ) > 0 ) {
591 $list = self::shortList( $articles, $articles_start_char );
594 $pageLang = MediaWikiServices::getInstance()->getTitleFactory()
595 ->castFromPageIdentity( $this->page )
597 $attribs = [
'lang' => $pageLang->getHtmlCode(),
'dir' => $pageLang->getDir(),
598 'class' =>
'mw-content-' . $pageLang->getDir() ];
599 $list = Html::rawElement(
'div', $attribs, $list );
618 public static function columnList( $articles, $articles_start_char ) {
619 $columns = array_combine( $articles, $articles_start_char );
621 $ret = Html::openElement(
'div', [
'class' =>
'mw-category' ] );
625 # Kind of like array_flip() here, but we keep duplicates in an
626 # array instead of dropping them.
627 foreach ( $columns as $article => $char ) {
628 if ( !isset( $colContents[$char] ) ) {
629 $colContents[$char] = [];
631 $colContents[$char][] = $article;
634 foreach ( $colContents as $char => $articles ) {
635 # Change space to non-breaking space to keep headers aligned
636 $h3char = $char ===
' ' ?
"\u{00A0}" : htmlspecialchars( $char );
638 $ret .= Html::openELement(
'div', [
'class' =>
'mw-category-group' ] );
639 $ret .= Html::rawElement(
'h3', [], $h3char ) .
"\n";
640 $ret .= Html::openElement(
'ul' );
644 static function ( $article ) {
645 return Html::rawElement(
'li', [], $article );
650 $ret .= Html::closeElement(
'ul' ) . Html::closeElement(
'div' );
654 $ret .= Html::closeElement(
'div' );
666 public static function shortList( $articles, $articles_start_char ) {
667 $r =
'<h3>' . htmlspecialchars( $articles_start_char[0] ) .
"</h3>\n";
668 $r .=
'<ul><li>' . $articles[0] .
'</li>';
669 $articleCount = count( $articles );
670 for ( $index = 1; $index < $articleCount; $index++ ) {
671 if ( $articles_start_char[$index] != $articles_start_char[$index - 1] ) {
672 $r .=
"</ul><h3>" . htmlspecialchars( $articles_start_char[$index] ) .
"</h3>\n<ul>";
675 $r .=
"<li>{$articles[$index]}</li>";
691 $prevLink = $this->msg(
'prev-page' )->escaped();
693 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
694 if ( $first !=
'' ) {
695 $prevQuery = $this->query;
696 $prevQuery[
"{$type}until"] = $first;
697 unset( $prevQuery[
"{$type}from"] );
698 $prevLink = $linkRenderer->makeKnownLink(
699 $this->addFragmentToTitle( $this->page,
$type ),
706 $nextLink = $this->msg(
'next-page' )->escaped();
709 $lastQuery = $this->query;
710 $lastQuery[
"{$type}from"] = $last;
711 unset( $lastQuery[
"{$type}until"] );
712 $nextLink = $linkRenderer->makeKnownLink(
713 $this->addFragmentToTitle( $this->page,
$type ),
720 return $this->msg(
'categoryviewer-pagedlinks' )->rawParams( $prevLink, $nextLink )->escaped();
733 switch ( $section ) {
735 $fragment =
'mw-pages';
738 $fragment =
'mw-subcategories';
741 $fragment =
'mw-category-media';
745 " Invalid section $section." );
777 if (
$type ===
'article' ) {
778 $pagingType =
'page';
783 $fromOrUntil =
false;
784 if ( isset( $this->from[$pagingType] ) || isset( $this->until[$pagingType] ) ) {
788 if ( $dbcnt == $rescnt ||
789 ( ( $rescnt == $this->limit || $fromOrUntil ) && $dbcnt > $rescnt )
793 } elseif ( $rescnt < $this->limit && !$fromOrUntil ) {
800 return $this->msg(
"category-$type-count-limited" )->numParams( $rescnt )->parseAsBlock();
803 return $this->msg(
"category-$type-count" )->numParams( $rescnt, $totalcnt )->parseAsBlock();
deprecatePublicPropertyFallback(string $property, string $version, callable $getter, ?callable $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.
getCountMessage( $rescnt, $dbcnt, $type)
What to do if the category table conflicts with the number of results returned? This function says wh...
array $articles_start_char
array $children_start_char
pagingLinks( $first, $last, $type='')
Create paging links, as a helper method to getSectionPagingLinks().
getSectionPagingLinks( $type)
Get the paging links for a section (subcats/pages/files), to go at the top and bottom of the output.
Category $cat
Category object for this page.
addFragmentToTitle(PageReference $page, string $section)
Takes a title, and adds the fragment identifier that corresponds to the correct segment of the catego...
__construct(PageIdentity $page, IContextSource $context, array $from=[], array $until=[], array $query=[])
generateLink(string $type, PageReference $page, bool $isRedirect, ?string $html=null)
getSubcategorySortChar(PageIdentity $page, string $sortkey)
Get the character to be used for sorting subcategories.
ImageGalleryBase $gallery
addImage(PageReference $page, string $sortkey, int $pageLength, bool $isRedirect=false)
Add a page in the image namespace.
addSubcategoryObject(Category $cat, $sortkey, $pageLength)
Add a subcategory to the internal lists, using a Category object.
addPage(PageReference $page, string $sortkey, int $pageLength, bool $isRedirect=false)
Add a miscellaneous page.
formatList( $articles, $articles_start_char, $cutoff=6)
Format a list of articles chunked by letter, either as a bullet list or a columnar format,...
ILanguageConverter $languageConverter
static columnList( $articles, $articles_start_char)
Format a list of articles chunked by letter in a three-column list, ordered vertically.
getHTML()
Format the category data list.
array $imgsNoGallery_start_char
array $query
The original query array, to be used in generating paging links.
static shortList( $articles, $articles_start_char)
Format a list of articles chunked by letter in a bullet list.
Category objects are immutable, strictly speaking.
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.
Represents a page (or page fragment) title within MediaWiki.
Represents a 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