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