Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 162 |
|
0.00% |
0 / 7 |
CRAP | |
0.00% |
0 / 1 |
ApiQueryContributors | |
0.00% |
0 / 162 |
|
0.00% |
0 / 7 |
930 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 113 |
|
0.00% |
0 / 1 |
506 | |||
getCacheMode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getAllowedParams | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
6 | |||
getExamplesMessages | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
getHelpUrls | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSummaryMessage | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | /** |
3 | * Query the list of contributors to a page |
4 | * |
5 | * Copyright © 2013 Wikimedia Foundation and contributors |
6 | * |
7 | * This program is free software; you can redistribute it and/or modify |
8 | * it under the terms of the GNU General Public License as published by |
9 | * the Free Software Foundation; either version 2 of the License, or |
10 | * (at your option) any later version. |
11 | * |
12 | * This program is distributed in the hope that it will be useful, |
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
15 | * GNU General Public License for more details. |
16 | * |
17 | * You should have received a copy of the GNU General Public License along |
18 | * with this program; if not, write to the Free Software Foundation, Inc., |
19 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
20 | * http://www.gnu.org/copyleft/gpl.html |
21 | * |
22 | * @file |
23 | * @since 1.23 |
24 | */ |
25 | |
26 | use MediaWiki\Permissions\GroupPermissionsLookup; |
27 | use MediaWiki\Revision\RevisionRecord; |
28 | use MediaWiki\Revision\RevisionStore; |
29 | use MediaWiki\Title\Title; |
30 | use MediaWiki\User\ActorMigration; |
31 | use MediaWiki\User\TempUser\TempUserConfig; |
32 | use MediaWiki\User\UserGroupManager; |
33 | use Wikimedia\ParamValidator\ParamValidator; |
34 | use Wikimedia\ParamValidator\TypeDef\IntegerDef; |
35 | |
36 | /** |
37 | * A query module to show contributors to a page |
38 | * |
39 | * @ingroup API |
40 | * @since 1.23 |
41 | */ |
42 | class ApiQueryContributors extends ApiQueryBase { |
43 | /** We don't want to process too many pages at once (it hits cold |
44 | * database pages too heavily), so only do the first MAX_PAGES input pages |
45 | * in each API call (leaving the rest for continuation). |
46 | */ |
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 | |
55 | /** |
56 | * @param ApiQuery $query |
57 | * @param string $moduleName |
58 | * @param RevisionStore $revisionStore |
59 | * @param ActorMigration $actorMigration |
60 | * @param UserGroupManager $userGroupManager |
61 | * @param GroupPermissionsLookup $groupPermissionsLookup |
62 | * @param TempUserConfig $tempUserConfig |
63 | */ |
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 | } |