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