MediaWiki master
ApiQueryContributors.php
Go to the documentation of this file.
1<?php
12namespace MediaWiki\Api;
13
23
35 private const MAX_PAGES = 100;
36
37 private RevisionStore $revisionStore;
38 private ActorMigration $actorMigration;
39 private UserGroupManager $userGroupManager;
40 private GroupPermissionsLookup $groupPermissionsLookup;
41 private TempUserConfig $tempUserConfig;
42
43 public function __construct(
44 ApiQuery $query,
45 string $moduleName,
46 RevisionStore $revisionStore,
47 ActorMigration $actorMigration,
48 UserGroupManager $userGroupManager,
49 GroupPermissionsLookup $groupPermissionsLookup,
50 TempUserConfig $tempUserConfig
51 ) {
52 // "pc" is short for "page contributors", "co" was already taken by the
53 // GeoData extension's prop=coordinates.
54 parent::__construct( $query, $moduleName, 'pc' );
55 $this->revisionStore = $revisionStore;
56 $this->actorMigration = $actorMigration;
57 $this->userGroupManager = $userGroupManager;
58 $this->groupPermissionsLookup = $groupPermissionsLookup;
59 $this->tempUserConfig = $tempUserConfig;
60 }
61
62 public function execute() {
63 $db = $this->getDB();
64 $params = $this->extractRequestParams();
65 $this->requireMaxOneParameter( $params, 'group', 'excludegroup', 'rights', 'excluderights' );
66
67 // Only operate on existing pages
68 $pages = array_keys( $this->getPageSet()->getGoodPages() );
69
70 // Filter out already-processed pages
71 if ( $params['continue'] !== null ) {
72 $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'int', 'int' ] );
73 $cont_page = (int)$cont[0];
74 $pages = array_filter( $pages, static function ( $v ) use ( $cont_page ) {
75 return $v >= $cont_page;
76 } );
77 }
78 if ( $pages === [] ) {
79 // Nothing to do
80 return;
81 }
82
83 // Apply MAX_PAGES, leaving any over the limit for a continue.
84 sort( $pages );
85 $continuePages = null;
86 if ( count( $pages ) > self::MAX_PAGES ) {
87 $continuePages = $pages[self::MAX_PAGES] . '|0';
88 $pages = array_slice( $pages, 0, self::MAX_PAGES );
89 }
90
91 $result = $this->getResult();
92 $revQuery = $this->revisionStore->getQueryInfo();
93 $pageField = 'rev_page';
94 $idField = 'rev_actor';
95 $countField = 'rev_actor';
96
97 // First, count anons
98 $this->addTables( $revQuery['tables'] );
99 $this->addJoinConds( $revQuery['joins'] );
100 $this->addFields( [
101 'page' => $pageField,
102 'anons' => "COUNT(DISTINCT $countField)",
103 ] );
104 $this->addWhereFld( $pageField, $pages );
105 $this->addWhere( $this->actorMigration->isAnon( $revQuery['fields']['rev_user'] ) );
106 $this->addWhere( [ $db->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER ) => 0 ] );
107 $this->addOption( 'GROUP BY', $pageField );
108 $res = $this->select( __METHOD__ );
109 foreach ( $res as $row ) {
110 $fit = $result->addValue( [ 'query', 'pages', $row->page ],
111 'anoncontributors', (int)$row->anons
112 );
113 if ( !$fit ) {
114 // This not fitting isn't reasonable, so it probably means that
115 // some other module used up all the space. Just set a dummy
116 // continue and hope it works next time.
117 $this->setContinueEnumParameter( 'continue',
118 $params['continue'] ?? '0|0'
119 );
120
121 return;
122 }
123 }
124
125 // Next, add logged-in users
126 $this->resetQueryParams();
127 $this->addTables( $revQuery['tables'] );
128 $this->addJoinConds( $revQuery['joins'] );
129 $this->addFields( [
130 'page' => $pageField,
131 'id' => $idField,
132 // Non-MySQL databases don't like partial group-by
133 'userid' => 'MAX(' . $revQuery['fields']['rev_user'] . ')',
134 'username' => 'MAX(' . $revQuery['fields']['rev_user_text'] . ')',
135 ] );
136 $this->addWhereFld( $pageField, $pages );
137 $this->addWhere( $this->actorMigration->isNotAnon( $revQuery['fields']['rev_user'] ) );
138 $this->addWhere( [ $db->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER ) => 0 ] );
139 $this->addOption( 'GROUP BY', [ $pageField, $idField ] );
140 $this->addOption( 'LIMIT', $params['limit'] + 1 );
141
142 // Force a sort order to ensure that properties are grouped by page
143 // But only if rev_page is not constant in the WHERE clause.
144 if ( count( $pages ) > 1 ) {
145 $this->addOption( 'ORDER BY', [ 'page', 'id' ] );
146 } else {
147 $this->addOption( 'ORDER BY', 'id' );
148 }
149
150 $limitGroups = [];
151 if ( $params['group'] ) {
152 $excludeGroups = false;
153 $limitGroups = $params['group'];
154 } elseif ( $params['excludegroup'] ) {
155 $excludeGroups = true;
156 $limitGroups = $params['excludegroup'];
157 } elseif ( $params['rights'] ) {
158 $excludeGroups = false;
159 foreach ( $params['rights'] as $r ) {
160 $limitGroups = array_merge( $limitGroups,
161 $this->groupPermissionsLookup->getGroupsWithPermission( $r ) );
162 }
163
164 // If no group has the rights requested, no need to query
165 if ( !$limitGroups ) {
166 if ( $continuePages !== null ) {
167 // But we still need to continue for the next page's worth
168 // of anoncontributors
169 $this->setContinueEnumParameter( 'continue', $continuePages );
170 }
171
172 return;
173 }
174 } elseif ( $params['excluderights'] ) {
175 $excludeGroups = true;
176 foreach ( $params['excluderights'] as $r ) {
177 $limitGroups = array_merge( $limitGroups,
178 $this->groupPermissionsLookup->getGroupsWithPermission( $r ) );
179 }
180 }
181
182 if ( $limitGroups ) {
183 $limitGroups = array_unique( $limitGroups );
184 $this->addTables( 'user_groups' );
185 $this->addJoinConds( [ 'user_groups' => [
186 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable excludeGroups declared when limitGroups set
187 $excludeGroups ? 'LEFT JOIN' : 'JOIN',
188 [
189 'ug_user=' . $revQuery['fields']['rev_user'],
190 'ug_group' => $limitGroups,
191 $db->expr( 'ug_expiry', '=', null )->or( 'ug_expiry', '>=', $db->timestamp() )
192 ]
193 ] ] );
194 // @phan-suppress-next-next-line PhanTypeMismatchArgumentNullable,PhanPossiblyUndeclaredVariable
195 // excludeGroups declared when limitGroups set
196 $this->addWhereIf( [ 'ug_user' => null ], $excludeGroups );
197 }
198
199 if ( $params['continue'] !== null ) {
200 $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'int', 'int' ] );
201 $this->addWhere( $db->buildComparison( '>=', [
202 $pageField => $cont[0],
203 $idField => $cont[1],
204 ] ) );
205 }
206
207 $res = $this->select( __METHOD__ );
208 $count = 0;
209 foreach ( $res as $row ) {
210 if ( ++$count > $params['limit'] ) {
211 // We've reached the one extra which shows that
212 // there are additional pages to be had. Stop here...
213 $this->setContinueEnumParameter( 'continue', $row->page . '|' . $row->id );
214 return;
215 }
216
217 $fit = $this->addPageSubItem( $row->page,
218 [ 'userid' => (int)$row->userid, 'name' => $row->username ],
219 'user'
220 );
221 if ( !$fit ) {
222 $this->setContinueEnumParameter( 'continue', $row->page . '|' . $row->id );
223 return;
224 }
225 }
226
227 if ( $continuePages !== null ) {
228 $this->setContinueEnumParameter( 'continue', $continuePages );
229 }
230 }
231
233 public function getCacheMode( $params ) {
234 return 'public';
235 }
236
238 public function getAllowedParams( $flags = 0 ) {
239 $userGroups = $this->userGroupManager->listAllGroups();
240 $userRights = $this->getPermissionManager()->getAllPermissions();
241
242 if ( $flags & ApiBase::GET_VALUES_FOR_HELP ) {
243 sort( $userGroups );
244 }
245
246 return [
247 'group' => [
248 ParamValidator::PARAM_TYPE => $userGroups,
249 ParamValidator::PARAM_ISMULTI => true,
250 ],
251 'excludegroup' => [
252 ParamValidator::PARAM_TYPE => $userGroups,
253 ParamValidator::PARAM_ISMULTI => true,
254 ],
255 'rights' => [
256 ParamValidator::PARAM_TYPE => $userRights,
257 ParamValidator::PARAM_ISMULTI => true,
258 ],
259 'excluderights' => [
260 ParamValidator::PARAM_TYPE => $userRights,
261 ParamValidator::PARAM_ISMULTI => true,
262 ],
263 'limit' => [
264 ParamValidator::PARAM_DEFAULT => 10,
265 ParamValidator::PARAM_TYPE => 'limit',
266 IntegerDef::PARAM_MIN => 1,
267 IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
268 IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
269 ],
270 'continue' => [
271 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
272 ],
273 ];
274 }
275
277 protected function getExamplesMessages() {
278 $title = Title::newMainPage()->getPrefixedText();
279 $mp = rawurlencode( $title );
280
281 return [
282 "action=query&prop=contributors&titles={$mp}"
283 => 'apihelp-query+contributors-example-simple',
284 ];
285 }
286
288 public function getHelpUrls() {
289 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Contributors';
290 }
291
293 protected function getSummaryMessage() {
294 if ( $this->tempUserConfig->isKnown() ) {
295 return 'apihelp-query+contributors-summary-tempusers-enabled';
296 }
297 return parent::getSummaryMessage();
298 }
299}
300
302class_alias( ApiQueryContributors::class, 'ApiQueryContributors' );
parseContinueParamOrDie(string $continue, array $types)
Parse the 'continue' parameter in the usual format and validate the types of each part,...
Definition ApiBase.php:1696
getResult()
Get the result object.
Definition ApiBase.php:682
requireMaxOneParameter( $params,... $required)
Dies if more than one parameter from a certain set of parameters are set and not false.
Definition ApiBase.php:998
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition ApiBase.php:167
const LIMIT_BIG2
Fast query, apihighlimits limit.
Definition ApiBase.php:234
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:823
getPermissionManager()
Obtain a PermissionManager instance that subclasses may use in their authorization checks.
Definition ApiBase.php:742
const GET_VALUES_FOR_HELP
getAllowedParams() flag: When this is set, the result could take longer to generate,...
Definition ApiBase.php:245
const LIMIT_BIG1
Fast query, standard limit.
Definition ApiBase.php:232
This is a base class for all Query modules.
addOption( $name, $value=null)
Add an option such as LIMIT or USE INDEX.
addWhereIf( $value, $condition)
Same as addWhere(), but add the WHERE clauses only if a condition is met.
addTables( $tables, $alias=null)
Add a set of tables to the internal array.
addPageSubItem( $pageId, $item, $elemname=null)
Same as addPageSubItems(), but one element of $data at a time.
addJoinConds( $join_conds)
Add a set of JOIN conditions to the internal array.
getDB()
Get the Query database connection (read-only).
select( $method, $extraQuery=[], ?array &$hookData=null)
Execute a SELECT query based on the values in the internal arrays.
addWhere( $value)
Add a set of WHERE clauses to the internal array.
getPageSet()
Get the PageSet object to work on.
setContinueEnumParameter( $paramName, $paramValue)
Set a query-continue value.
resetQueryParams()
Blank the internal arrays with query parameters.
addWhereFld( $field, $value)
Equivalent to addWhere( [ $field => $value ] )
addFields( $value)
Add a set of fields to select to the internal array.
A query module to show contributors to a page.
getHelpUrls()
Return links to more detailed help pages about the module.1.25, returning boolean false is deprecated...
getCacheMode( $params)
Get the cache mode for the data generated by this module.Override this in the module subclass....
getExamplesMessages()
Returns usage examples for this module.Return value has query strings as keys, with values being eith...
getSummaryMessage()
Return the summary message.This is a one-line description of the module, suitable for display in a li...
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
__construct(ApiQuery $query, string $moduleName, RevisionStore $revisionStore, ActorMigration $actorMigration, UserGroupManager $userGroupManager, GroupPermissionsLookup $groupPermissionsLookup, TempUserConfig $tempUserConfig)
This is the main query class.
Definition ApiQuery.php:36
Page revision base class.
Service for looking up page revisions.
Represents a title within MediaWiki.
Definition Title.php:70
This is not intended to be a long-term part of MediaWiki; it will be deprecated and removed once acto...
Manage user group memberships.
Service for formatting and validating API parameters.
Type definition for integer types.
Interface for temporary user creation config and name matching.
Language name search API.