MediaWiki REL1_39
CategoryViewer.php
Go to the documentation of this file.
1<?php
23use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
30
32 use ProtectedHookAccessorTrait;
34
36 public $limit;
37
39 public $from;
40
42 public $until;
43
45 public $articles;
46
49
51 public $children;
52
55
58
61
64
66 public $nextPage;
67
69 protected $prevPage;
70
72 public $flip;
73
75 protected $page;
76
78 public $collation;
79
81 public $gallery;
82
84 private $cat;
85
87 private $query;
88
90 private $languageConverter;
91
101 public function __construct( PageIdentity $page, IContextSource $context, array $from = [],
102 array $until = [], array $query = []
103 ) {
104 $this->page = $page;
105
107 'title',
108 '1.37',
109 function (): Title {
110 // @phan-suppress-next-line PhanTypeMismatchReturnNullable castFrom does not return null here
111 return Title::castFromPageIdentity( $this->page );
112 },
113 function ( PageIdentity $page ) {
114 $this->page = $page;
115 }
116 );
117
118 $this->setContext( $context );
119 $this->getOutput()->addModuleStyles( [
120 'mediawiki.action.styles',
121 ] );
122 $this->from = $from;
123 $this->until = $until;
124 $this->limit = $context->getConfig()->get( MainConfigNames::CategoryPagingLimit );
125 $this->cat = Category::newFromTitle( $page );
126 $this->query = $query;
127 $this->collation = MediaWikiServices::getInstance()->getCollationFactory()->getCategoryCollation();
128 $this->languageConverter = MediaWikiServices::getInstance()
129 ->getLanguageConverterFactory()->getLanguageConverter();
130 unset( $this->query['title'] );
131 }
132
138 public function getHTML() {
139 $this->showGallery = $this->getConfig()->get( MainConfigNames::CategoryMagicGallery )
140 && !$this->getOutput()->mNoGallery;
141
142 $this->clearCategoryState();
143 $this->doCategoryQuery();
144 $this->finaliseCategoryState();
145
146 $r = $this->getSubcategorySection() .
147 $this->getPagesSection() .
148 $this->getImageSection();
149
150 if ( $r == '' ) {
151 // If there is no category content to display, only
152 // show the top part of the navigation links.
153 // @todo FIXME: Cannot be completely suppressed because it
154 // is unknown if 'until' or 'from' makes this
155 // give 0 results.
156 $r = $this->getCategoryTop();
157 } else {
158 $r = $this->getCategoryTop() .
159 $r .
160 $this->getCategoryBottom();
161 }
162
163 // Give a proper message if category is empty
164 if ( $r == '' ) {
165 $r = $this->msg( 'category-empty' )->parseAsBlock();
166 }
167
168 $lang = $this->getLanguage();
169 $attribs = [
170 'class' => 'mw-category-generated',
171 'lang' => $lang->getHtmlCode(),
172 'dir' => $lang->getDir()
173 ];
174 # put a div around the headings which are in the user language
175 $r = Html::rawElement( 'div', $attribs, $r );
176
177 return $r;
178 }
179
180 protected function clearCategoryState() {
181 $this->articles = [];
182 $this->articles_start_char = [];
183 $this->children = [];
184 $this->children_start_char = [];
185 if ( $this->showGallery ) {
186 // Note that null for mode is taken to mean use default.
187 $mode = $this->getRequest()->getVal( 'gallerymode', null );
188 try {
189 $this->gallery = ImageGalleryBase::factory( $mode, $this->getContext() );
191 // User specified something invalid, fallback to default.
192 $this->gallery = ImageGalleryBase::factory( false, $this->getContext() );
193 }
194
195 $this->gallery->setHideBadImages();
196 } else {
197 $this->imgsNoGallery = [];
198 $this->imgsNoGallery_start_char = [];
199 }
200 }
201
208 public function addSubcategoryObject( Category $cat, $sortkey, $pageLength ) {
209 $page = $cat->getPage();
210 if ( !$page ) {
211 return;
212 }
213
214 // Subcategory; strip the 'Category' namespace from the link text.
215 $pageRecord = MediaWikiServices::getInstance()->getPageStore()
216 ->getPageByReference( $page );
217 if ( !$pageRecord ) {
218 return;
219 }
220
221 $this->children[] = $this->generateLink(
222 'subcat',
223 $pageRecord,
224 $pageRecord->isRedirect(),
225 htmlspecialchars( str_replace( '_', ' ', $pageRecord->getDBkey() ) )
226 );
227
228 $this->children_start_char[] =
229 $this->getSubcategorySortChar( $page, $sortkey );
230 }
231
243 private function generateLink(
244 string $type, PageReference $page, bool $isRedirect, ?string $html = null
245 ): string {
246 $link = null;
247 $legacyTitle = MediaWikiServices::getInstance()->getTitleFactory()
248 ->castFromPageReference( $page );
249 // @phan-suppress-next-line PhanTypeMismatchArgument castFrom does not return null here
250 $this->getHookRunner()->onCategoryViewer__generateLink( $type, $legacyTitle, $html, $link );
251 if ( $link === null ) {
252 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
253 if ( $html !== null ) {
254 $html = new HtmlArmor( $html );
255 }
256 $link = $linkRenderer->makeLink( $page, $html );
257 }
258 if ( $isRedirect ) {
259 $link = Html::rawElement(
260 'span',
261 [ 'class' => 'redirect-in-category' ],
262 $link
263 );
264 }
265
266 return $link;
267 }
268
280 public function getSubcategorySortChar( PageIdentity $page, string $sortkey ): string {
281 $titleText = MediaWikiServices::getInstance()->getTitleFormatter()
282 ->getPrefixedText( $page );
283 if ( $titleText === $sortkey ) {
284 $word = $page->getDBkey();
285 } else {
286 $word = $sortkey;
287 }
288
289 $firstChar = $this->collation->getFirstLetter( $word );
290
291 return $this->languageConverter->convert( $firstChar );
292 }
293
301 public function addImage(
302 PageReference $page, string $sortkey, int $pageLength, bool $isRedirect = false
303 ): void {
304 $title = MediaWikiServices::getInstance()->getTitleFactory()
305 ->castFromPageReference( $page );
306 if ( $this->showGallery ) {
307 $flip = $this->flip['file'];
308 if ( $flip ) {
309 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable castFrom does not return null here
310 $this->gallery->insert( $title );
311 } else {
312 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable castFrom does not return null here
313 $this->gallery->add( $title );
314 }
315 } else {
316 $this->imgsNoGallery[] = $this->generateLink( 'image', $page, $isRedirect );
317
318 $this->imgsNoGallery_start_char[] =
319 $this->languageConverter->convert( $this->collation->getFirstLetter( $sortkey ) );
320 }
321 }
322
330 public function addPage(
331 PageReference $page, string $sortkey, int $pageLength, bool $isRedirect = false
332 ): void {
333 $this->articles[] = $this->generateLink( 'page', $page, $isRedirect );
334
335 $this->articles_start_char[] =
336 $this->languageConverter->convert( $this->collation->getFirstLetter( $sortkey ) );
337 }
338
339 protected function finaliseCategoryState() {
340 if ( $this->flip['subcat'] ) {
341 $this->children = array_reverse( $this->children );
342 $this->children_start_char = array_reverse( $this->children_start_char );
343 }
344 if ( $this->flip['page'] ) {
345 $this->articles = array_reverse( $this->articles );
346 $this->articles_start_char = array_reverse( $this->articles_start_char );
347 }
348 if ( !$this->showGallery && $this->flip['file'] ) {
349 $this->imgsNoGallery = array_reverse( $this->imgsNoGallery );
350 $this->imgsNoGallery_start_char = array_reverse( $this->imgsNoGallery_start_char );
351 }
352 }
353
354 protected function doCategoryQuery() {
355 $dbr = wfGetDB( DB_REPLICA, 'category' );
356
357 $this->nextPage = [
358 'page' => null,
359 'subcat' => null,
360 'file' => null,
361 ];
362 $this->prevPage = [
363 'page' => null,
364 'subcat' => null,
365 'file' => null,
366 ];
367
368 $this->flip = [ 'page' => false, 'subcat' => false, 'file' => false ];
369
370 foreach ( [ 'page', 'subcat', 'file' ] as $type ) {
371 # Get the sortkeys for start/end, if applicable. Note that if
372 # the collation in the database differs from the one
373 # set in $wgCategoryCollation, pagination might go totally haywire.
374 $extraConds = [ 'cl_type' => $type ];
375 if ( isset( $this->from[$type] ) ) {
376 $extraConds[] = 'cl_sortkey >= '
377 . $dbr->addQuotes( $this->collation->getSortKey( $this->from[$type] ) );
378 } elseif ( isset( $this->until[$type] ) ) {
379 $extraConds[] = 'cl_sortkey < '
380 . $dbr->addQuotes( $this->collation->getSortKey( $this->until[$type] ) );
381 $this->flip[$type] = true;
382 }
383
384 $queryBuilder = $dbr->newSelectQueryBuilder();
385 $queryBuilder->select( array_merge(
386 LinkCache::getSelectFields(),
387 [
388 'cl_sortkey',
389 'cat_id',
390 'cat_title',
391 'cat_subcats',
392 'cat_pages',
393 'cat_files',
394 'cl_sortkey_prefix',
395 'cl_collation'
396 ]
397 ) )
398 ->from( 'page' )
399 ->where( [ 'cl_to' => $this->page->getDBkey() ] )
400 ->andWhere( $extraConds )
401 ->useIndex( [ 'categorylinks' => 'cl_sortkey' ] );
402
403 if ( $this->flip[$type] ) {
404 $queryBuilder->orderBy( 'cl_sortkey', SelectQueryBuilder::SORT_DESC );
405 } else {
406 $queryBuilder->orderBy( 'cl_sortkey' );
407 }
408
409 $queryBuilder
410 ->join( 'categorylinks', null, [ 'cl_from = page_id' ] )
411 ->leftJoin( 'category', null, [
412 'cat_title = page_title',
413 'page_namespace' => NS_CATEGORY
414 ] )
415 ->limit( $this->limit + 1 )
416 ->caller( __METHOD__ );
417
418 $res = $queryBuilder->fetchResultSet();
419
420 $this->getHookRunner()->onCategoryViewer__doCategoryQuery( $type, $res );
421 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
422
423 $count = 0;
424 foreach ( $res as $row ) {
425 $title = Title::newFromRow( $row );
426 $linkCache->addGoodLinkObjFromRow( $title, $row );
427
428 if ( $row->cl_collation === '' ) {
429 // Hack to make sure that while updating from 1.16 schema
430 // and db is inconsistent, that the sky doesn't fall.
431 // See r83544. Could perhaps be removed in a couple decades...
432 $humanSortkey = $row->cl_sortkey;
433 } else {
434 $humanSortkey = $title->getCategorySortkey( $row->cl_sortkey_prefix );
435 }
436
437 if ( ++$count > $this->limit ) {
438 # We've reached the one extra which shows that there
439 # are additional pages to be had. Stop here...
440 $this->nextPage[$type] = $humanSortkey;
441 break;
442 }
443 if ( $count == $this->limit ) {
444 $this->prevPage[$type] = $humanSortkey;
445 }
446
447 if ( $title->getNamespace() === NS_CATEGORY ) {
448 $cat = Category::newFromRow( $row, $title );
449 $this->addSubcategoryObject( $cat, $humanSortkey, $row->page_len );
450 } elseif ( $title->getNamespace() === NS_FILE ) {
451 $this->addImage( $title, $humanSortkey, $row->page_len, $row->page_is_redirect );
452 } else {
453 $this->addPage( $title, $humanSortkey, $row->page_len, $row->page_is_redirect );
454 }
455 }
456 }
457 }
458
462 protected function getCategoryTop() {
463 $r = $this->getCategoryBottom();
464 return $r === ''
465 ? $r
466 : "<br style=\"clear:both;\"/>\n" . $r;
467 }
468
472 protected function getSubcategorySection() {
473 # Don't show subcategories section if there are none.
474 $r = '';
475 $rescnt = count( $this->children );
476 $dbcnt = $this->cat->getSubcatCount();
477 // This function should be called even if the result isn't used, it has side-effects
478 $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'subcat' );
479
480 if ( $rescnt > 0 ) {
481 # Showing subcategories
482 $r .= Html::openElement( 'div', [ 'id' => 'mw-subcategories' ] ) . "\n";
483 $r .= Html::rawElement( 'h2', [], $this->msg( 'subcategories' )->parse() ) . "\n";
484 $r .= $countmsg;
485 $r .= $this->getSectionPagingLinks( 'subcat' );
486 $r .= $this->formatList( $this->children, $this->children_start_char );
487 $r .= $this->getSectionPagingLinks( 'subcat' );
488 $r .= "\n" . Html::closeElement( 'div' );
489 }
490 return $r;
491 }
492
496 protected function getPagesSection() {
497 $name = $this->getOutput()->getUnprefixedDisplayTitle();
498 # Don't show articles section if there are none.
499 $r = '';
500
501 # @todo FIXME: Here and in the other two sections: we don't need to bother
502 # with this rigmarole if the entire category contents fit on one page
503 # and have already been retrieved. We can just use $rescnt in that
504 # case and save a query and some logic.
505 $dbcnt = $this->cat->getPageCount( Category::COUNT_CONTENT_PAGES );
506 $rescnt = count( $this->articles );
507 // This function should be called even if the result isn't used, it has side-effects
508 $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'article' );
509
510 if ( $rescnt > 0 ) {
511 $r .= Html::openElement( 'div', [ 'id' => 'mw-pages' ] ) . "\n";
512 $r .= Html::rawElement(
513 'h2',
514 [],
515 $this->msg( 'category_header' )->rawParams( $name )->parse()
516 ) . "\n";
517 $r .= $countmsg;
518 $r .= $this->getSectionPagingLinks( 'page' );
519 $r .= $this->formatList( $this->articles, $this->articles_start_char );
520 $r .= $this->getSectionPagingLinks( 'page' );
521 $r .= "\n" . Html::closeElement( 'div' );
522 }
523 return $r;
524 }
525
529 protected function getImageSection() {
530 $name = $this->getOutput()->getUnprefixedDisplayTitle();
531 $r = '';
532 $rescnt = $this->showGallery ? $this->gallery->count() : count( $this->imgsNoGallery );
533 $dbcnt = $this->cat->getFileCount();
534 // This function should be called even if the result isn't used, it has side-effects
535 $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'file' );
536
537 if ( $rescnt > 0 ) {
538 $r .= Html::openElement( 'div', [ 'id' => 'mw-category-media' ] ) . "\n";
539 $r .= Html::rawElement(
540 'h2',
541 [],
542 $this->msg( 'category-media-header' )->rawParams( $name )->parse()
543 ) . "\n";
544 $r .= $countmsg;
545 $r .= $this->getSectionPagingLinks( 'file' );
546 if ( $this->showGallery ) {
547 $r .= $this->gallery->toHTML();
548 } else {
549 $r .= $this->formatList( $this->imgsNoGallery, $this->imgsNoGallery_start_char );
550 }
551 $r .= $this->getSectionPagingLinks( 'file' );
552 $r .= "\n" . Html::closeElement( 'div' );
553 }
554 return $r;
555 }
556
564 private function getSectionPagingLinks( $type ) {
565 if ( isset( $this->until[$type] ) ) {
566 // The new value for the until parameter should be pointing to the first
567 // result displayed on the page which is the second last result retrieved
568 // from the database.The next link should have a from parameter pointing
569 // to the until parameter of the current page.
570 if ( $this->nextPage[$type] !== null ) {
571 return $this->pagingLinks( $this->prevPage[$type], $this->until[$type], $type );
572 } else {
573 // If the nextPage variable is null, it means that we have reached the first page
574 // and therefore the previous link should be disabled.
575 return $this->pagingLinks( '', $this->until[$type], $type );
576 }
577 } elseif ( $this->nextPage[$type] !== null || isset( $this->from[$type] ) ) {
578 return $this->pagingLinks( $this->from[$type], $this->nextPage[$type], $type );
579 } else {
580 return '';
581 }
582 }
583
587 protected function getCategoryBottom() {
588 return '';
589 }
590
601 private function formatList( $articles, $articles_start_char, $cutoff = 6 ) {
602 $list = '';
603 if ( count( $articles ) > $cutoff ) {
604 $list = self::columnList( $articles, $articles_start_char );
605 } elseif ( count( $articles ) > 0 ) {
606 // for short lists of articles in categories.
607 $list = self::shortList( $articles, $articles_start_char );
608 }
609
610 $pageLang = MediaWikiServices::getInstance()->getTitleFactory()
611 ->castFromPageIdentity( $this->page )
612 ->getPageLanguage();
613 $attribs = [ 'lang' => $pageLang->getHtmlCode(), 'dir' => $pageLang->getDir(),
614 'class' => 'mw-content-' . $pageLang->getDir() ];
615 $list = Html::rawElement( 'div', $attribs, $list );
616
617 return $list;
618 }
619
630 public static function columnList(
631 $articles,
632 $articles_start_char,
633 $cssClasses = 'mw-category mw-category-columns'
634 ) {
635 $columns = array_combine( $articles, $articles_start_char );
636
637 $ret = Html::openElement( 'div', [ 'class' => $cssClasses ] );
638
639 $colContents = [];
640
641 # Kind of like array_flip() here, but we keep duplicates in an
642 # array instead of dropping them.
643 foreach ( $columns as $article => $char ) {
644 if ( !isset( $colContents[$char] ) ) {
645 $colContents[$char] = [];
646 }
647 $colContents[$char][] = $article;
648 }
649
650 foreach ( $colContents as $char => $articles ) {
651 # Change space to non-breaking space to keep headers aligned
652 $h3char = $char === ' ' ? "\u{00A0}" : htmlspecialchars( $char );
653
654 $ret .= Html::openElement( 'div', [ 'class' => 'mw-category-group' ] );
655 $ret .= Html::rawElement( 'h3', [], $h3char ) . "\n";
656 $ret .= Html::openElement( 'ul' );
657 $ret .= implode(
658 "\n",
659 array_map(
660 static function ( $article ) {
661 return Html::rawElement( 'li', [], $article );
662 },
663 $articles
664 )
665 );
666 $ret .= Html::closeElement( 'ul' ) . Html::closeElement( 'div' );
667
668 }
669
670 $ret .= Html::closeElement( 'div' );
671 return $ret;
672 }
673
682 public static function shortList( $articles, $articles_start_char ) {
683 return self::columnList( $articles, $articles_start_char, 'mw-category' );
684 }
685
695 private function pagingLinks( $first, $last, $type = '' ) {
696 $prevLink = $this->msg( 'prev-page' )->escaped();
697
698 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
699 if ( $first != '' ) {
700 $prevQuery = $this->query;
701 $prevQuery["{$type}until"] = $first;
702 unset( $prevQuery["{$type}from"] );
703 $prevLink = $linkRenderer->makeKnownLink(
704 $this->addFragmentToTitle( $this->page, $type ),
705 new HtmlArmor( $prevLink ),
706 [],
707 $prevQuery
708 );
709 }
710
711 $nextLink = $this->msg( 'next-page' )->escaped();
712
713 if ( $last != '' ) {
714 $lastQuery = $this->query;
715 $lastQuery["{$type}from"] = $last;
716 unset( $lastQuery["{$type}until"] );
717 $nextLink = $linkRenderer->makeKnownLink(
718 $this->addFragmentToTitle( $this->page, $type ),
719 new HtmlArmor( $nextLink ),
720 [],
721 $lastQuery
722 );
723 }
724
725 return $this->msg( 'categoryviewer-pagedlinks' )->rawParams( $prevLink, $nextLink )->escaped();
726 }
727
737 private function addFragmentToTitle( PageReference $page, string $section ): LinkTarget {
738 switch ( $section ) {
739 case 'page':
740 $fragment = 'mw-pages';
741 break;
742 case 'subcat':
743 $fragment = 'mw-subcategories';
744 break;
745 case 'file':
746 $fragment = 'mw-category-media';
747 break;
748 default:
749 throw new MWException( __METHOD__ .
750 " Invalid section $section." );
751 }
752
753 return new TitleValue( $page->getNamespace(),
754 $page->getDBkey(), $fragment );
755 }
756
767 private function getCountMessage( $rescnt, $dbcnt, $type ) {
768 // There are three cases:
769 // 1) The category table figure seems good. It might be wrong, but
770 // we can't do anything about it if we don't recalculate it on ev-
771 // ery category view.
772 // 2) The category table figure isn't good, like it's smaller than the
773 // number of actual results, *but* the number of results is less
774 // than $this->limit and there's no offset. In this case we still
775 // know the right figure.
776 // 3) We have no idea.
777
778 // Check if there's a "from" or "until" for anything
779
780 // This is a little ugly, but we seem to use different names
781 // for the paging types then for the messages.
782 if ( $type === 'article' ) {
783 $pagingType = 'page';
784 } else {
785 $pagingType = $type;
786 }
787
788 $fromOrUntil = false;
789 if ( isset( $this->from[$pagingType] ) || isset( $this->until[$pagingType] ) ) {
790 $fromOrUntil = true;
791 }
792
793 if ( $dbcnt == $rescnt ||
794 ( ( $rescnt == $this->limit || $fromOrUntil ) && $dbcnt > $rescnt )
795 ) {
796 // Case 1: seems good.
797 $totalcnt = $dbcnt;
798 } elseif ( $rescnt < $this->limit && !$fromOrUntil ) {
799 // Case 2: not good, but salvageable. Use the number of results.
800 $totalcnt = $rescnt;
801 } else {
802 // Case 3: hopeless. Don't give a total count at all.
803 // Messages: category-subcat-count-limited, category-article-count-limited,
804 // category-file-count-limited
805 return $this->msg( "category-$type-count-limited" )->numParams( $rescnt )->parseAsBlock();
806 }
807 // Messages: category-subcat-count, category-article-count, category-file-count
808 return $this->msg( "category-$type-count" )->numParams( $rescnt, $totalcnt )->parseAsBlock();
809 }
810}
const NS_FILE
Definition Defines.php:70
const NS_CATEGORY
Definition Defines.php:78
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.
Collation $collation
__construct(PageIdentity $page, IContextSource $context, array $from=[], array $until=[], array $query=[])
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.
getHTML()
Format the category data list.
array $imgsNoGallery_start_char
static shortList( $articles, $articles_start_char)
Format a list of articles chunked by letter in a bullet list.
PageIdentity $page
static columnList( $articles, $articles_start_char, $cssClasses='mw-category mw-category-columns')
Format a list of articles chunked by letter in a three-column list, ordered vertically.
Category objects are immutable, strictly speaking.
Definition Category.php:33
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.
Definition HtmlArmor.php:30
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition Html.php:214
Class for exceptions thrown by ImageGalleryBase::factory().
MediaWiki exception.
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Represents a page (or page fragment) title within MediaWiki.
Represents a title within MediaWiki.
Definition Title.php:49
Note that none of the methods in this class are stable to override.
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.
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
getNamespace()
Returns the page's namespace number.
getDBkey()
Get the page title in DB key form.
const DB_REPLICA
Definition defines.php:26
if(!isset( $args[0])) $lang