41 private $languageConverter;
43 private $linkBatchFactory;
45 private $namespaceInfo;
47 private $titleFactory;
49 private $titleFormatter;
51 private $watchedItemStore;
53 private $restrictionStore;
55 private $linksMigration;
57 private $fld_protection =
false, $fld_talkid =
false,
58 $fld_subjectid =
false, $fld_url =
false,
59 $fld_readable =
false, $fld_watched =
false,
60 $fld_watchers =
false, $fld_visitingwatchers =
false,
61 $fld_notificationtimestamp =
false,
62 $fld_preload =
false, $fld_displaytitle =
false, $fld_varianttitles =
false;
68 private $fld_linkclasses =
false;
73 private $fld_associatedpage =
false;
84 private $pageIsRedir, $pageIsNew, $pageTouched,
85 $pageLatest, $pageLength;
87 private $protections, $restrictionTypes, $watched, $watchers, $visitingwatchers,
88 $notificationtimestamps, $talkids, $subjectids, $displaytitles, $variantTitles;
94 private $watchlistExpiries;
100 private $linkClasses;
102 private $showZeroWatchers =
false;
104 private $countTestedActions = 0;
132 parent::__construct( $queryModule, $moduleName,
'in' );
134 $this->linkBatchFactory = $linkBatchFactory;
135 $this->namespaceInfo = $namespaceInfo;
136 $this->titleFactory = $titleFactory;
137 $this->titleFormatter = $titleFormatter;
138 $this->watchedItemStore = $watchedItemStore;
139 $this->restrictionStore = $restrictionStore;
140 $this->linksMigration = $linksMigration;
151 $pageSet->requestField(
'page_is_redirect' );
152 $pageSet->requestField(
'page_is_new' );
154 $pageSet->requestField(
'page_touched' );
155 $pageSet->requestField(
'page_latest' );
156 $pageSet->requestField(
'page_len' );
157 $pageSet->requestField(
'page_content_model' );
158 if ( $config->get( MainConfigNames::PageLanguageUseDB ) ) {
159 $pageSet->requestField(
'page_lang' );
165 if ( $this->params[
'prop'] !==
null ) {
166 $prop = array_fill_keys( $this->params[
'prop'],
true );
167 $this->fld_protection = isset( $prop[
'protection'] );
168 $this->fld_watched = isset( $prop[
'watched'] );
169 $this->fld_watchers = isset( $prop[
'watchers'] );
170 $this->fld_visitingwatchers = isset( $prop[
'visitingwatchers'] );
171 $this->fld_notificationtimestamp = isset( $prop[
'notificationtimestamp'] );
172 $this->fld_talkid = isset( $prop[
'talkid'] );
173 $this->fld_subjectid = isset( $prop[
'subjectid'] );
174 $this->fld_url = isset( $prop[
'url'] );
175 $this->fld_readable = isset( $prop[
'readable'] );
176 $this->fld_preload = isset( $prop[
'preload'] );
177 $this->fld_displaytitle = isset( $prop[
'displaytitle'] );
178 $this->fld_varianttitles = isset( $prop[
'varianttitles'] );
179 $this->fld_linkclasses = isset( $prop[
'linkclasses'] );
180 $this->fld_associatedpage = isset( $prop[
'associatedpage'] );
184 $this->titles = $pageSet->getGoodTitles();
185 $this->missing = $pageSet->getMissingTitles();
186 $this->everything = $this->titles + $this->missing;
189 uasort( $this->everything, [ Title::class,
'compare' ] );
190 if ( $this->params[
'continue'] !==
null ) {
193 $cont = explode(
'|', $this->params[
'continue'] );
195 $conttitle = $this->titleFactory->makeTitleSafe( (
int)$cont[0], $cont[1] );
197 foreach ( $this->everything as $pageid =>
$title ) {
198 if ( Title::compare(
$title, $conttitle ) >= 0 ) {
201 unset( $this->titles[$pageid] );
202 unset( $this->missing[$pageid] );
203 unset( $this->everything[$pageid] );
208 $this->pageIsRedir = !$pageSet->isResolvingRedirects()
209 ? $pageSet->getCustomField(
'page_is_redirect' )
211 $this->pageIsNew = $pageSet->getCustomField(
'page_is_new' );
213 $this->pageTouched = $pageSet->getCustomField(
'page_touched' );
214 $this->pageLatest = $pageSet->getCustomField(
'page_latest' );
215 $this->pageLength = $pageSet->getCustomField(
'page_len' );
218 if ( $this->fld_protection ) {
219 $this->getProtectionInfo();
222 if ( $this->fld_watched || $this->fld_notificationtimestamp ) {
223 $this->getWatchedInfo();
226 if ( $this->fld_watchers ) {
227 $this->getWatcherInfo();
230 if ( $this->fld_visitingwatchers ) {
231 $this->getVisitingWatcherInfo();
235 if ( $this->fld_talkid || $this->fld_subjectid ) {
239 if ( $this->fld_displaytitle ) {
240 $this->getDisplayTitle();
243 if ( $this->fld_varianttitles ) {
244 $this->getVariantTitles();
247 if ( $this->fld_linkclasses ) {
248 $this->getLinkClasses( $this->params[
'linkcontext'] );
252 foreach ( $this->everything as $pageid =>
$title ) {
253 $pageInfo = $this->extractPageInfo( $pageid,
$title );
254 $fit = $pageInfo !==
null && $result->addValue( [
257 ], $pageid, $pageInfo );
260 $title->getNamespace() .
'|' .
273 private function extractPageInfo( $pageid,
$title ) {
276 $titleExists = $pageid > 0;
277 $ns =
$title->getNamespace();
278 $dbkey =
$title->getDBkey();
280 $pageInfo[
'contentmodel'] =
$title->getContentModel();
282 $pageLanguage =
$title->getPageLanguage();
283 $pageInfo[
'pagelanguage'] = $pageLanguage->getCode();
284 $pageInfo[
'pagelanguagehtmlcode'] = $pageLanguage->getHtmlCode();
285 $pageInfo[
'pagelanguagedir'] = $pageLanguage->getDir();
287 if ( $titleExists ) {
288 $pageInfo[
'touched'] =
wfTimestamp( TS_ISO_8601, $this->pageTouched[$pageid] );
289 $pageInfo[
'lastrevid'] = (int)$this->pageLatest[$pageid];
290 $pageInfo[
'length'] = (int)$this->pageLength[$pageid];
292 if ( isset( $this->pageIsRedir[$pageid] ) && $this->pageIsRedir[$pageid] ) {
293 $pageInfo[
'redirect'] =
true;
295 if ( $this->pageIsNew[$pageid] ) {
296 $pageInfo[
'new'] =
true;
300 if ( $this->fld_protection ) {
301 $pageInfo[
'protection'] = [];
302 if ( isset( $this->protections[$ns][$dbkey] ) ) {
303 $pageInfo[
'protection'] =
304 $this->protections[$ns][$dbkey];
308 $pageInfo[
'restrictiontypes'] = [];
309 if ( isset( $this->restrictionTypes[$ns][$dbkey] ) ) {
310 $pageInfo[
'restrictiontypes'] =
311 $this->restrictionTypes[$ns][$dbkey];
316 if ( $this->fld_watched ) {
317 $pageInfo[
'watched'] =
false;
319 if ( isset( $this->watched[$ns][$dbkey] ) ) {
320 $pageInfo[
'watched'] = $this->watched[$ns][$dbkey];
323 if ( isset( $this->watchlistExpiries[$ns][$dbkey] ) ) {
324 $pageInfo[
'watchlistexpiry'] = $this->watchlistExpiries[$ns][$dbkey];
328 if ( $this->fld_watchers ) {
329 if ( $this->watchers !==
null && $this->watchers[$ns][$dbkey] !== 0 ) {
330 $pageInfo[
'watchers'] = $this->watchers[$ns][$dbkey];
331 } elseif ( $this->showZeroWatchers ) {
332 $pageInfo[
'watchers'] = 0;
336 if ( $this->fld_visitingwatchers ) {
337 if ( $this->visitingwatchers !==
null && $this->visitingwatchers[$ns][$dbkey] !== 0 ) {
338 $pageInfo[
'visitingwatchers'] = $this->visitingwatchers[$ns][$dbkey];
339 } elseif ( $this->showZeroWatchers ) {
340 $pageInfo[
'visitingwatchers'] = 0;
344 if ( $this->fld_notificationtimestamp ) {
345 $pageInfo[
'notificationtimestamp'] =
'';
346 if ( isset( $this->notificationtimestamps[$ns][$dbkey] ) ) {
347 $pageInfo[
'notificationtimestamp'] =
348 wfTimestamp( TS_ISO_8601, $this->notificationtimestamps[$ns][$dbkey] );
352 if ( $this->fld_talkid && isset( $this->talkids[$ns][$dbkey] ) ) {
353 $pageInfo[
'talkid'] = $this->talkids[$ns][$dbkey];
356 if ( $this->fld_subjectid && isset( $this->subjectids[$ns][$dbkey] ) ) {
357 $pageInfo[
'subjectid'] = $this->subjectids[$ns][$dbkey];
360 if ( $this->fld_associatedpage && $ns >=
NS_MAIN ) {
361 $pageInfo[
'associatedpage'] = $this->titleFormatter->getPrefixedText(
362 $this->namespaceInfo->getAssociatedPage(
$title )
366 if ( $this->fld_url ) {
371 if ( $this->fld_readable ) {
372 $pageInfo[
'readable'] = $this->
getAuthority()->definitelyCan(
'read', $title );
375 if ( $this->fld_preload ) {
376 if ( $titleExists ) {
377 $pageInfo[
'preload'] =
'';
383 $pageInfo[
'preload'] = $text;
387 if ( $this->fld_displaytitle ) {
388 $pageInfo[
'displaytitle'] = $this->displaytitles[$pageid] ??
389 htmlspecialchars(
$title->getPrefixedText(), ENT_NOQUOTES );
392 if ( $this->fld_varianttitles && isset( $this->variantTitles[$pageid] ) ) {
393 $pageInfo[
'varianttitles'] = $this->variantTitles[$pageid];
396 if ( $this->fld_linkclasses && isset( $this->linkClasses[$pageid] ) ) {
397 $pageInfo[
'linkclasses'] = $this->linkClasses[$pageid];
400 if ( $this->params[
'testactions'] ) {
402 if ( $this->countTestedActions >= $limit ) {
406 $detailLevel = $this->params[
'testactionsdetail'];
408 if ( $errorFormatter->getFormat() ===
'bc' ) {
410 $errorFormatter = $errorFormatter->newWithFormat(
'plaintext' );
413 $pageInfo[
'actions'] = [];
414 foreach ( $this->params[
'testactions'] as $action ) {
415 $this->countTestedActions++;
417 if ( $detailLevel ===
'boolean' ) {
418 $pageInfo[
'actions'][$action] = $this->
getAuthority()->authorizeRead( $action,
$title );
421 if ( $detailLevel ===
'quick' ) {
427 $pageInfo[
'actions'][$action] = $errorFormatter->arrayFromStatus( $status );
438 private function getProtectionInfo() {
439 $this->protections = [];
440 $db = $this->
getDB();
443 if ( count( $this->titles ) ) {
446 $this->
addFields( [
'pr_page',
'pr_type',
'pr_level',
447 'pr_expiry',
'pr_cascade' ] );
448 $this->
addWhereFld(
'pr_page', array_keys( $this->titles ) );
451 foreach (
$res as $row ) {
453 $title = $this->titles[$row->pr_page];
455 'type' => $row->pr_type,
456 'level' => $row->pr_level,
459 if ( $row->pr_cascade ) {
460 $a[
'cascade'] =
true;
462 $this->protections[
$title->getNamespace()][
$title->getDBkey()][] = $a;
467 if ( count( $this->missing ) ) {
469 $lb = $this->linkBatchFactory->newLinkBatch( $this->missing );
471 $this->
addFields( [
'pt_title',
'pt_namespace',
'pt_create_perm',
'pt_expiry' ] );
472 $this->
addWhere( $lb->constructSet(
'pt', $db ) );
474 foreach (
$res as $row ) {
475 $this->protections[$row->pt_namespace][$row->pt_title][] = [
477 'level' => $row->pt_create_perm,
485 $images = $others = [];
486 foreach ( $this->everything as
$title ) {
488 $images[] =
$title->getDBkey();
493 $this->restrictionTypes[
$title->getNamespace()][
$title->getDBkey()] =
494 array_values( $this->restrictionStore->listApplicableRestrictionTypes(
$title ) );
497 list( $blNamespace, $blTitle ) = $this->linksMigration->getTitleFields(
'templatelinks' );
498 $queryInfo = $this->linksMigration->getQueryInfo(
'templatelinks' );
500 if ( count( $others ) ) {
502 $lb = $this->linkBatchFactory->newLinkBatch( $others );
504 $this->
addTables( array_merge( [
'page_restrictions',
'page' ], $queryInfo[
'tables'] ) );
506 $this->
addOption(
'USE INDEX', [
'templatelinks' =>
'PRIMARY' ] );
507 $this->
addFields( [
'pr_type',
'pr_level',
'pr_expiry',
508 'page_title',
'page_namespace',
509 $blNamespace, $blTitle ] );
510 $this->
addWhere( $lb->constructSet(
'tl', $db ) );
511 $this->
addWhere(
'pr_page = page_id' );
512 $this->
addWhere(
'pr_page = tl_from' );
517 foreach (
$res as $row ) {
518 $this->protections[$row->$blNamespace][$row->$blTitle][] = [
519 'type' => $row->pr_type,
520 'level' => $row->pr_level,
522 'source' => $this->titleFormatter->formatTitle( $row->page_namespace, $row->page_title ),
527 if ( count( $images ) ) {
530 $this->
addTables( [
'page_restrictions',
'page',
'imagelinks' ] );
531 $this->
addFields( [
'pr_type',
'pr_level',
'pr_expiry',
532 'page_title',
'page_namespace',
'il_to' ] );
533 $this->
addWhere(
'pr_page = page_id' );
534 $this->
addWhere(
'pr_page = il_from' );
539 foreach (
$res as $row ) {
540 $this->protections[
NS_FILE][$row->il_to][] = [
541 'type' => $row->pr_type,
542 'level' => $row->pr_level,
544 'source' => $this->titleFormatter->formatTitle( $row->page_namespace, $row->page_title ),
554 private function getTSIDs() {
555 $getTitles = $this->talkids = $this->subjectids = [];
556 $nsInfo = $this->namespaceInfo;
559 foreach ( $this->everything as
$t ) {
560 if ( $nsInfo->isTalk(
$t->getNamespace() ) ) {
561 if ( $this->fld_subjectid ) {
562 $getTitles[] =
$t->getSubjectPage();
564 } elseif ( $this->fld_talkid ) {
565 $getTitles[] =
$t->getTalkPage();
568 if ( $getTitles === [] ) {
572 $db = $this->
getDB();
576 $lb = $this->linkBatchFactory->newLinkBatch( $getTitles );
579 $this->
addFields( [
'page_title',
'page_namespace',
'page_id' ] );
580 $this->
addWhere( $lb->constructSet(
'page', $db ) );
582 foreach (
$res as $row ) {
583 if ( $nsInfo->isTalk( $row->page_namespace ) ) {
584 $this->talkids[$nsInfo->getSubject( $row->page_namespace )][$row->page_title] =
585 (int)( $row->page_id );
587 $this->subjectids[$nsInfo->getTalk( $row->page_namespace )][$row->page_title] =
588 (int)( $row->page_id );
593 private function getDisplayTitle() {
594 $this->displaytitles = [];
596 $pageIds = array_keys( $this->titles );
598 if ( $pageIds === [] ) {
604 $this->
addFields( [
'pp_page',
'pp_value' ] );
606 $this->
addWhereFld(
'pp_propname',
'displaytitle' );
609 foreach (
$res as $row ) {
610 $this->displaytitles[$row->pp_page] = $row->pp_value;
621 private function getLinkClasses( ?
LinkTarget $context_title =
null ) {
622 if ( $this->titles === [] ) {
633 foreach ( $this->titles as $pageId =>
$title ) {
634 $pdbk =
$title->getPrefixedDBkey();
635 $pagemap[$pageId] = $pdbk;
636 $classes[$pdbk] =
$title->isRedirect() ?
'mw-redirect' :
'';
639 $context_title = $this->titleFactory->newFromLinkTarget(
640 $context_title ?? $this->titleFactory->newMainPage()
643 $pagemap, $classes, $context_title
650 $this->linkClasses = [];
651 foreach ( $this->titles as $pageId =>
$title ) {
652 $pdbk =
$title->getPrefixedDBkey();
653 $this->linkClasses[$pageId] = preg_split(
654 '/\s+/', $classes[$pdbk] ??
'', -1, PREG_SPLIT_NO_EMPTY
659 private function getVariantTitles() {
660 if ( $this->titles === [] ) {
663 $this->variantTitles = [];
664 foreach ( $this->titles as $pageId =>
$t ) {
665 $this->variantTitles[$pageId] = isset( $this->displaytitles[$pageId] )
666 ? $this->getAllVariants( $this->displaytitles[$pageId] )
667 : $this->getAllVariants(
$t->getText(),
$t->getNamespace() );
671 private function getAllVariants( $text, $ns =
NS_MAIN ) {
673 foreach ( $this->languageConverter->getVariants() as $variant ) {
674 $convertTitle = $this->languageConverter->autoConvert( $text, $variant );
676 $convertNs = $this->languageConverter->convertNamespace( $ns, $variant );
677 $convertTitle = $convertNs .
':' . $convertTitle;
679 $result[$variant] = $convertTitle;
688 private function getWatchedInfo() {
691 if ( !$user->isRegistered() || count( $this->everything ) == 0
692 || !$this->
getAuthority()->isAllowed(
'viewmywatchlist' )
698 $this->watchlistExpiries = [];
699 $this->notificationtimestamps = [];
702 $items = $this->watchedItemStore->loadWatchedItemsBatch( $user, $this->everything );
704 foreach ( $items as $item ) {
705 $nsId = $item->getTarget()->getNamespace();
706 $dbKey = $item->getTarget()->getDBkey();
708 if ( $this->fld_watched ) {
709 $this->watched[$nsId][$dbKey] =
true;
711 $expiry = $item->getExpiry( TS_ISO_8601 );
713 $this->watchlistExpiries[$nsId][$dbKey] = $expiry;
717 if ( $this->fld_notificationtimestamp ) {
718 $this->notificationtimestamps[$nsId][$dbKey] = $item->getNotificationTimestamp();
726 private function getWatcherInfo() {
727 if ( count( $this->everything ) == 0 ) {
731 $canUnwatchedpages = $this->
getAuthority()->isAllowed(
'unwatchedpages' );
732 $unwatchedPageThreshold =
733 $this->
getConfig()->get( MainConfigNames::UnwatchedPageThreshold );
734 if ( !$canUnwatchedpages && !is_int( $unwatchedPageThreshold ) ) {
738 $this->showZeroWatchers = $canUnwatchedpages;
741 if ( !$canUnwatchedpages ) {
742 $countOptions[
'minimumWatchers'] = $unwatchedPageThreshold;
745 $this->watchers = $this->watchedItemStore->countWatchersMultiple(
757 private function getVisitingWatcherInfo() {
759 $db = $this->
getDB();
761 $canUnwatchedpages = $this->
getAuthority()->isAllowed(
'unwatchedpages' );
762 $unwatchedPageThreshold = $config->get( MainConfigNames::UnwatchedPageThreshold );
763 if ( !$canUnwatchedpages && !is_int( $unwatchedPageThreshold ) ) {
767 $this->showZeroWatchers = $canUnwatchedpages;
769 $titlesWithThresholds = [];
770 if ( $this->titles ) {
771 $lb = $this->linkBatchFactory->newLinkBatch( $this->titles );
775 $this->
addTables( [
'page',
'revision' ] );
776 $this->
addFields( [
'page_namespace',
'page_title',
'rev_timestamp' ] );
778 'page_latest = rev_id',
779 $lb->constructSet(
'page', $db ),
781 $this->
addOption(
'GROUP BY', [
'page_namespace',
'page_title' ] );
782 $timestampRes = $this->
select( __METHOD__ );
784 $age = $config->get( MainConfigNames::WatchersMaxAge );
786 foreach ( $timestampRes as $row ) {
787 $revTimestamp =
wfTimestamp( TS_UNIX, (
int)$row->rev_timestamp );
788 $timestamps[$row->page_namespace][$row->page_title] = (int)$revTimestamp - $age;
790 $titlesWithThresholds = array_map(
791 static function (
LinkTarget $target ) use ( $timestamps ) {
800 if ( $this->missing ) {
801 $titlesWithThresholds = array_merge(
802 $titlesWithThresholds,
805 return [ $target, null ];
811 $this->visitingwatchers = $this->watchedItemStore->countVisitingWatchersMultiple(
812 $titlesWithThresholds,
813 !$canUnwatchedpages ? $unwatchedPageThreshold : null
829 if ( array_diff( (array)$params[
'prop'], $publicProps ) ) {
834 if ( $params[
'testactions'] ) {
844 ParamValidator::PARAM_ISMULTI =>
true,
845 ParamValidator::PARAM_TYPE => [
849 'watchers', #
private
850 'visitingwatchers', #
private
851 'notificationtimestamp', #
private
855 'readable', #
private
859 'linkclasses', #
private: stub length (and possibly hook colors)
864 EnumDef::PARAM_DEPRECATED_VALUES => [
869 ParamValidator::PARAM_TYPE =>
'title',
870 ParamValidator::PARAM_DEFAULT => $this->titleFactory->newMainPage()->getPrefixedText(),
871 TitleDef::PARAM_RETURN_OBJECT =>
true,
874 ParamValidator::PARAM_TYPE =>
'string',
875 ParamValidator::PARAM_ISMULTI =>
true,
877 'testactionsdetail' => [
878 ParamValidator::PARAM_TYPE => [
'boolean',
'full',
'quick' ],
879 ParamValidator::PARAM_DEFAULT =>
'boolean',
889 $title = Title::newMainPage()->getPrefixedText();
890 $mp = rawurlencode(
$title );
893 "action=query&prop=info&titles={$mp}"
894 =>
'apihelp-query+info-example-simple',
895 "action=query&prop=info&inprop=protection&titles={$mp}"
896 =>
'apihelp-query+info-example-protection',
901 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Info';