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