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 public function __construct(
38 ApiQuery $query,
39 string $moduleName,
40 private readonly RevisionStore $revisionStore,
41 private readonly ActorMigration $actorMigration,
42 private readonly UserGroupManager $userGroupManager,
43 private readonly GroupPermissionsLookup $groupPermissionsLookup,
44 private readonly TempUserConfig $tempUserConfig,
45 ) {
46 // "pc" is short for "page contributors", "co" was already taken by the
47 // GeoData extension's prop=coordinates.
48 parent::__construct( $query, $moduleName, 'pc' );
49 }
50
51 public function execute() {
52 $db = $this->getDB();
53 $params = $this->extractRequestParams();
54 $this->requireMaxOneParameter( $params, 'group', 'excludegroup', 'rights', 'excluderights' );
55
56 // Only operate on existing pages
57 $pages = array_keys( $this->getPageSet()->getGoodPages() );
58
59 // Filter out already-processed pages
60 if ( $params['continue'] !== null ) {
61 $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'int', 'int' ] );
62 $cont_page = (int)$cont[0];
63 $pages = array_filter( $pages, static function ( $v ) use ( $cont_page ) {
64 return $v >= $cont_page;
65 } );
66 }
67 if ( $pages === [] ) {
68 // Nothing to do
69 return;
70 }
71
72 // Apply MAX_PAGES, leaving any over the limit for a continue.
73 sort( $pages );
74 $continuePages = null;
75 if ( count( $pages ) > self::MAX_PAGES ) {
76 $continuePages = $pages[self::MAX_PAGES] . '|0';
77 $pages = array_slice( $pages, 0, self::MAX_PAGES );
78 }
79
80 $result = $this->getResult();
81 $revQuery = $this->revisionStore->getQueryInfo();
82 $pageField = 'rev_page';
83 $idField = 'rev_actor';
84 $countField = 'rev_actor';
85
86 // First, count anons
87 $this->addTables( $revQuery['tables'] );
88 $this->addJoinConds( $revQuery['joins'] );
89 $this->addFields( [
90 'page' => $pageField,
91 'anons' => "COUNT(DISTINCT $countField)",
92 ] );
93 $this->addWhereFld( $pageField, $pages );
94 $this->addWhere( $this->actorMigration->isAnon( $revQuery['fields']['rev_user'] ) );
95 $this->addWhere( [ $db->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER ) => 0 ] );
96 $this->addOption( 'GROUP BY', $pageField );
97 $res = $this->select( __METHOD__ );
98 foreach ( $res as $row ) {
99 $fit = $result->addValue( [ 'query', 'pages', $row->page ],
100 'anoncontributors', (int)$row->anons
101 );
102 if ( !$fit ) {
103 // This not fitting isn't reasonable, so it probably means that
104 // some other module used up all the space. Just set a dummy
105 // continue and hope it works next time.
106 $this->setContinueEnumParameter( 'continue',
107 $params['continue'] ?? '0|0'
108 );
109
110 return;
111 }
112 }
113
114 // Next, add logged-in users
115 $this->resetQueryParams();
116 $this->addTables( $revQuery['tables'] );
117 $this->addJoinConds( $revQuery['joins'] );
118 $this->addFields( [
119 'page' => $pageField,
120 'id' => $idField,
121 // Non-MySQL databases don't like partial group-by
122 'userid' => 'MAX(' . $revQuery['fields']['rev_user'] . ')',
123 'username' => 'MAX(' . $revQuery['fields']['rev_user_text'] . ')',
124 ] );
125 $this->addWhereFld( $pageField, $pages );
126 $this->addWhere( $this->actorMigration->isNotAnon( $revQuery['fields']['rev_user'] ) );
127 $this->addWhere( [ $db->bitAnd( 'rev_deleted', RevisionRecord::DELETED_USER ) => 0 ] );
128 $this->addOption( 'GROUP BY', [ $pageField, $idField ] );
129 $this->addOption( 'LIMIT', $params['limit'] + 1 );
130
131 // Force a sort order to ensure that properties are grouped by page
132 // But only if rev_page is not constant in the WHERE clause.
133 if ( count( $pages ) > 1 ) {
134 $this->addOption( 'ORDER BY', [ 'page', 'id' ] );
135 } else {
136 $this->addOption( 'ORDER BY', 'id' );
137 }
138
139 $limitGroups = [];
140 if ( $params['group'] ) {
141 $excludeGroups = false;
142 $limitGroups = $params['group'];
143 } elseif ( $params['excludegroup'] ) {
144 $excludeGroups = true;
145 $limitGroups = $params['excludegroup'];
146 } elseif ( $params['rights'] ) {
147 $excludeGroups = false;
148 foreach ( $params['rights'] as $r ) {
149 $limitGroups = array_merge( $limitGroups,
150 $this->groupPermissionsLookup->getGroupsWithPermission( $r ) );
151 }
152
153 // If no group has the rights requested, no need to query
154 if ( !$limitGroups ) {
155 if ( $continuePages !== null ) {
156 // But we still need to continue for the next page's worth
157 // of anoncontributors
158 $this->setContinueEnumParameter( 'continue', $continuePages );
159 }
160
161 return;
162 }
163 } elseif ( $params['excluderights'] ) {
164 $excludeGroups = true;
165 foreach ( $params['excluderights'] as $r ) {
166 $limitGroups = array_merge( $limitGroups,
167 $this->groupPermissionsLookup->getGroupsWithPermission( $r ) );
168 }
169 }
170
171 if ( $limitGroups ) {
172 $limitGroups = array_unique( $limitGroups );
173 $this->addTables( 'user_groups' );
174 $this->addJoinConds( [ 'user_groups' => [
175 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable excludeGroups declared when limitGroups set
176 $excludeGroups ? 'LEFT JOIN' : 'JOIN',
177 [
178 'ug_user=' . $revQuery['fields']['rev_user'],
179 'ug_group' => $limitGroups,
180 $db->expr( 'ug_expiry', '=', null )->or( 'ug_expiry', '>=', $db->timestamp() )
181 ]
182 ] ] );
183 // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable
184 // excludeGroups declared when limitGroups set
185 $this->addWhereIf( [ 'ug_user' => null ], $excludeGroups );
186 }
187
188 if ( $params['continue'] !== null ) {
189 $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'int', 'int' ] );
190 $this->addWhere( $db->buildComparison( '>=', [
191 $pageField => $cont[0],
192 $idField => $cont[1],
193 ] ) );
194 }
195
196 $res = $this->select( __METHOD__ );
197 $count = 0;
198 foreach ( $res as $row ) {
199 if ( ++$count > $params['limit'] ) {
200 // We've reached the one extra which shows that
201 // there are additional pages to be had. Stop here...
202 $this->setContinueEnumParameter( 'continue', $row->page . '|' . $row->id );
203 return;
204 }
205
206 $fit = $this->addPageSubItem( $row->page,
207 [ 'userid' => (int)$row->userid, 'name' => $row->username ],
208 'user'
209 );
210 if ( !$fit ) {
211 $this->setContinueEnumParameter( 'continue', $row->page . '|' . $row->id );
212 return;
213 }
214 }
215
216 if ( $continuePages !== null ) {
217 $this->setContinueEnumParameter( 'continue', $continuePages );
218 }
219 }
220
222 public function getCacheMode( $params ) {
223 return 'public';
224 }
225
227 public function getAllowedParams( $flags = 0 ) {
228 $userGroups = $this->userGroupManager->listAllGroups();
229 $userRights = $this->getPermissionManager()->getAllPermissions();
230
231 if ( $flags & ApiBase::GET_VALUES_FOR_HELP ) {
232 sort( $userGroups );
233 }
234
235 return [
236 'group' => [
237 ParamValidator::PARAM_TYPE => $userGroups,
238 ParamValidator::PARAM_ISMULTI => true,
239 ],
240 'excludegroup' => [
241 ParamValidator::PARAM_TYPE => $userGroups,
242 ParamValidator::PARAM_ISMULTI => true,
243 ],
244 'rights' => [
245 ParamValidator::PARAM_TYPE => $userRights,
246 ParamValidator::PARAM_ISMULTI => true,
247 ],
248 'excluderights' => [
249 ParamValidator::PARAM_TYPE => $userRights,
250 ParamValidator::PARAM_ISMULTI => true,
251 ],
252 'limit' => [
253 ParamValidator::PARAM_DEFAULT => 10,
254 ParamValidator::PARAM_TYPE => 'limit',
255 IntegerDef::PARAM_MIN => 1,
256 IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
257 IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
258 ],
259 'continue' => [
260 ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
261 ],
262 ];
263 }
264
266 protected function getExamplesMessages() {
267 $title = Title::newMainPage()->getPrefixedText();
268 $mp = rawurlencode( $title );
269
270 return [
271 "action=query&prop=contributors&titles={$mp}"
272 => 'apihelp-query+contributors-example-simple',
273 ];
274 }
275
277 public function getHelpUrls() {
278 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Contributors';
279 }
280
282 protected function getSummaryMessage() {
283 if ( $this->tempUserConfig->isKnown() ) {
284 return 'apihelp-query+contributors-summary-tempusers-enabled';
285 }
286 return parent::getSummaryMessage();
287 }
288}
289
291class_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:1707
getResult()
Get the result object.
Definition ApiBase.php:696
requireMaxOneParameter( $params,... $required)
Dies if more than one parameter from a certain set of parameters are set and not false.
Definition ApiBase.php:1012
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition ApiBase.php:166
const LIMIT_BIG2
Fast query, apihighlimits limit.
Definition ApiBase.php:233
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:837
getPermissionManager()
Obtain a PermissionManager instance that subclasses may use in their authorization checks.
Definition ApiBase.php:756
const GET_VALUES_FOR_HELP
getAllowedParams() flag: When this is set, the result could take longer to generate,...
Definition ApiBase.php:244
const LIMIT_BIG1
Fast query, standard limit.
Definition ApiBase.php:231
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...
__construct(ApiQuery $query, string $moduleName, private readonly RevisionStore $revisionStore, private readonly ActorMigration $actorMigration, private readonly UserGroupManager $userGroupManager, private readonly GroupPermissionsLookup $groupPermissionsLookup, private readonly TempUserConfig $tempUserConfig,)
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.
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:69
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.