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