Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 263 |
|
0.00% |
0 / 12 |
CRAP | |
0.00% |
0 / 1 |
SpecialBotPasswords | |
0.00% |
0 / 262 |
|
0.00% |
0 / 12 |
1722 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
isListed | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getLoginSecurityLevel | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
checkExecutePermissions | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getFormFields | |
0.00% |
0 / 114 |
|
0.00% |
0 / 1 |
90 | |||
alterForm | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
12 | |||
onSubmit | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
56 | |||
save | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
30 | |||
onSuccess | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
30 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDisplayFormat | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Implements Special:BotPasswords |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | * @ingroup SpecialPage |
22 | */ |
23 | |
24 | namespace MediaWiki\Specials; |
25 | |
26 | use ErrorPageError; |
27 | use HTMLRestrictionsField; |
28 | use InvalidPassword; |
29 | use MediaWiki\Auth\AuthManager; |
30 | use MediaWiki\Html\Html; |
31 | use MediaWiki\HTMLForm\HTMLForm; |
32 | use MediaWiki\Logger\LoggerFactory; |
33 | use MediaWiki\MainConfigNames; |
34 | use MediaWiki\Permissions\GrantsInfo; |
35 | use MediaWiki\Permissions\GrantsLocalization; |
36 | use MediaWiki\SpecialPage\FormSpecialPage; |
37 | use MediaWiki\Status\Status; |
38 | use MediaWiki\User\BotPassword; |
39 | use MediaWiki\User\CentralId\CentralIdLookup; |
40 | use MediaWiki\User\User; |
41 | use PasswordError; |
42 | use PasswordFactory; |
43 | use Psr\Log\LoggerInterface; |
44 | |
45 | /** |
46 | * Let users manage bot passwords |
47 | * |
48 | * @ingroup SpecialPage |
49 | */ |
50 | class SpecialBotPasswords extends FormSpecialPage { |
51 | |
52 | /** @var int Central user ID */ |
53 | private $userId = 0; |
54 | |
55 | /** @var BotPassword|null Bot password being edited, if any */ |
56 | private $botPassword = null; |
57 | |
58 | /** @var string|null Operation being performed: create, update, delete */ |
59 | private $operation = null; |
60 | |
61 | /** @var string|null New password set, for communication between onSubmit() and onSuccess() */ |
62 | private $password = null; |
63 | |
64 | private LoggerInterface $logger; |
65 | private PasswordFactory $passwordFactory; |
66 | private CentralIdLookup $centralIdLookup; |
67 | private GrantsInfo $grantsInfo; |
68 | private GrantsLocalization $grantsLocalization; |
69 | |
70 | /** |
71 | * @param PasswordFactory $passwordFactory |
72 | * @param AuthManager $authManager |
73 | * @param CentralIdLookup $centralIdLookup |
74 | * @param GrantsInfo $grantsInfo |
75 | * @param GrantsLocalization $grantsLocalization |
76 | */ |
77 | public function __construct( |
78 | PasswordFactory $passwordFactory, |
79 | AuthManager $authManager, |
80 | CentralIdLookup $centralIdLookup, |
81 | GrantsInfo $grantsInfo, |
82 | GrantsLocalization $grantsLocalization |
83 | ) { |
84 | parent::__construct( 'BotPasswords', 'editmyprivateinfo' ); |
85 | $this->logger = LoggerFactory::getInstance( 'authentication' ); |
86 | $this->passwordFactory = $passwordFactory; |
87 | $this->centralIdLookup = $centralIdLookup; |
88 | $this->setAuthManager( $authManager ); |
89 | $this->grantsInfo = $grantsInfo; |
90 | $this->grantsLocalization = $grantsLocalization; |
91 | } |
92 | |
93 | /** |
94 | * @return bool |
95 | */ |
96 | public function isListed() { |
97 | return $this->getConfig()->get( MainConfigNames::EnableBotPasswords ); |
98 | } |
99 | |
100 | protected function getLoginSecurityLevel() { |
101 | return $this->getName(); |
102 | } |
103 | |
104 | /** |
105 | * Main execution point |
106 | * @param string|null $par |
107 | */ |
108 | public function execute( $par ) { |
109 | $this->requireNamedUser(); |
110 | $this->getOutput()->disallowUserJs(); |
111 | $this->getOutput()->addModuleStyles( 'mediawiki.special' ); |
112 | $this->addHelpLink( 'Manual:Bot_passwords' ); |
113 | |
114 | if ( $par !== null ) { |
115 | $par = trim( $par ); |
116 | if ( $par === '' ) { |
117 | $par = null; |
118 | } elseif ( strlen( $par ) > BotPassword::APPID_MAXLENGTH ) { |
119 | throw new ErrorPageError( |
120 | 'botpasswords', 'botpasswords-bad-appid', [ htmlspecialchars( $par ) ] |
121 | ); |
122 | } |
123 | } |
124 | |
125 | parent::execute( $par ); |
126 | } |
127 | |
128 | protected function checkExecutePermissions( User $user ) { |
129 | parent::checkExecutePermissions( $user ); |
130 | |
131 | if ( !$this->getConfig()->get( MainConfigNames::EnableBotPasswords ) ) { |
132 | throw new ErrorPageError( 'botpasswords', 'botpasswords-disabled' ); |
133 | } |
134 | |
135 | $this->userId = $this->centralIdLookup->centralIdFromLocalUser( $this->getUser() ); |
136 | if ( !$this->userId ) { |
137 | throw new ErrorPageError( 'botpasswords', 'botpasswords-no-central-id' ); |
138 | } |
139 | } |
140 | |
141 | protected function getFormFields() { |
142 | $fields = []; |
143 | |
144 | if ( $this->par !== null ) { |
145 | $this->botPassword = BotPassword::newFromCentralId( $this->userId, $this->par ); |
146 | if ( !$this->botPassword ) { |
147 | $this->botPassword = BotPassword::newUnsaved( [ |
148 | 'centralId' => $this->userId, |
149 | 'appId' => $this->par, |
150 | ] ); |
151 | } |
152 | |
153 | $sep = BotPassword::getSeparator(); |
154 | $fields[] = [ |
155 | 'type' => 'info', |
156 | 'label-message' => 'username', |
157 | 'default' => $this->getUser()->getName() . $sep . $this->par |
158 | ]; |
159 | |
160 | if ( $this->botPassword->isSaved() ) { |
161 | $fields['resetPassword'] = [ |
162 | 'type' => 'check', |
163 | 'label-message' => 'botpasswords-label-resetpassword', |
164 | ]; |
165 | if ( $this->botPassword->isInvalid() ) { |
166 | $fields['resetPassword']['default'] = true; |
167 | } |
168 | } |
169 | |
170 | $showGrants = $this->grantsInfo->getValidGrants(); |
171 | $grantNames = $this->grantsLocalization->getGrantDescriptionsWithClasses( |
172 | $showGrants, $this->getLanguage() ); |
173 | |
174 | $fields[] = [ |
175 | 'type' => 'info', |
176 | 'default' => '', |
177 | 'help-message' => 'botpasswords-help-grants', |
178 | ]; |
179 | $fields['grants'] = [ |
180 | 'type' => 'checkmatrix', |
181 | 'label-message' => 'botpasswords-label-grants', |
182 | 'columns' => [ |
183 | $this->msg( 'botpasswords-label-grants-column' )->escaped() => 'grant' |
184 | ], |
185 | 'rows' => array_combine( |
186 | $grantNames, |
187 | $showGrants |
188 | ), |
189 | 'default' => array_map( |
190 | static function ( $g ) { |
191 | return "grant-$g"; |
192 | }, |
193 | $this->botPassword->getGrants() |
194 | ), |
195 | 'tooltips-html' => array_combine( |
196 | $grantNames, |
197 | array_map( |
198 | fn ( $rights ) => Html::rawElement( 'ul', [], implode( '', array_map( |
199 | fn ( $right ) => Html::rawElement( 'li', [], $this->msg( "right-$right" )->parse() ), |
200 | $rights |
201 | ) ) ), |
202 | array_intersect_key( $this->grantsInfo->getRightsByGrant(), |
203 | array_fill_keys( $showGrants, true ) ) |
204 | ) |
205 | ), |
206 | 'force-options-on' => array_map( |
207 | static function ( $g ) { |
208 | return "grant-$g"; |
209 | }, |
210 | $this->grantsInfo->getHiddenGrants() |
211 | ), |
212 | ]; |
213 | |
214 | $fields['restrictions'] = [ |
215 | 'class' => HTMLRestrictionsField::class, |
216 | 'required' => true, |
217 | 'default' => $this->botPassword->getRestrictions(), |
218 | ]; |
219 | |
220 | } else { |
221 | $linkRenderer = $this->getLinkRenderer(); |
222 | |
223 | $dbr = BotPassword::getReplicaDatabase(); |
224 | $res = $dbr->newSelectQueryBuilder() |
225 | ->select( [ 'bp_app_id', 'bp_password' ] ) |
226 | ->from( 'bot_passwords' ) |
227 | ->where( [ 'bp_user' => $this->userId ] ) |
228 | ->caller( __METHOD__ )->fetchResultSet(); |
229 | foreach ( $res as $row ) { |
230 | try { |
231 | $password = $this->passwordFactory->newFromCiphertext( $row->bp_password ); |
232 | $passwordInvalid = $password instanceof InvalidPassword; |
233 | unset( $password ); |
234 | } catch ( PasswordError $ex ) { |
235 | $passwordInvalid = true; |
236 | } |
237 | |
238 | $text = $linkRenderer->makeKnownLink( |
239 | $this->getPageTitle( $row->bp_app_id ), |
240 | $row->bp_app_id |
241 | ); |
242 | if ( $passwordInvalid ) { |
243 | $text .= $this->msg( 'word-separator' )->escaped() |
244 | . $this->msg( 'botpasswords-label-needsreset' )->parse(); |
245 | } |
246 | |
247 | $fields[] = [ |
248 | 'section' => 'existing', |
249 | 'type' => 'info', |
250 | 'raw' => true, |
251 | 'default' => $text, |
252 | ]; |
253 | } |
254 | |
255 | $fields['appId'] = [ |
256 | 'section' => 'createnew', |
257 | 'type' => 'textwithbutton', |
258 | 'label-message' => 'botpasswords-label-appid', |
259 | 'buttondefault' => $this->msg( 'botpasswords-label-create' )->text(), |
260 | 'buttonflags' => [ 'progressive', 'primary' ], |
261 | 'required' => true, |
262 | 'size' => BotPassword::APPID_MAXLENGTH, |
263 | 'maxlength' => BotPassword::APPID_MAXLENGTH, |
264 | 'validation-callback' => static function ( $v ) { |
265 | $v = trim( $v ); |
266 | return $v !== '' && strlen( $v ) <= BotPassword::APPID_MAXLENGTH; |
267 | }, |
268 | ]; |
269 | |
270 | $fields[] = [ |
271 | 'type' => 'hidden', |
272 | 'default' => 'new', |
273 | 'name' => 'op', |
274 | ]; |
275 | } |
276 | |
277 | return $fields; |
278 | } |
279 | |
280 | protected function alterForm( HTMLForm $form ) { |
281 | $form->setId( 'mw-botpasswords-form' ); |
282 | $form->setTableId( 'mw-botpasswords-table' ); |
283 | $form->suppressDefaultSubmit(); |
284 | |
285 | if ( $this->par !== null ) { |
286 | if ( $this->botPassword->isSaved() ) { |
287 | $form->setWrapperLegendMsg( 'botpasswords-editexisting' ); |
288 | $form->addButton( [ |
289 | 'name' => 'op', |
290 | 'value' => 'update', |
291 | 'label-message' => 'botpasswords-label-update', |
292 | 'flags' => [ 'primary', 'progressive' ], |
293 | ] ); |
294 | $form->addButton( [ |
295 | 'name' => 'op', |
296 | 'value' => 'delete', |
297 | 'label-message' => 'botpasswords-label-delete', |
298 | 'flags' => [ 'destructive' ], |
299 | ] ); |
300 | } else { |
301 | $form->setWrapperLegendMsg( 'botpasswords-createnew' ); |
302 | $form->addButton( [ |
303 | 'name' => 'op', |
304 | 'value' => 'create', |
305 | 'label-message' => 'botpasswords-label-create', |
306 | 'flags' => [ 'primary', 'progressive' ], |
307 | ] ); |
308 | } |
309 | |
310 | $form->addButton( [ |
311 | 'name' => 'op', |
312 | 'value' => 'cancel', |
313 | 'label-message' => 'botpasswords-label-cancel' |
314 | ] ); |
315 | } |
316 | } |
317 | |
318 | public function onSubmit( array $data ) { |
319 | $op = $this->getRequest()->getVal( 'op', '' ); |
320 | |
321 | switch ( $op ) { |
322 | case 'new': |
323 | $this->getOutput()->redirect( $this->getPageTitle( $data['appId'] )->getFullURL() ); |
324 | return false; |
325 | |
326 | case 'create': |
327 | $this->operation = 'insert'; |
328 | return $this->save( $data ); |
329 | |
330 | case 'update': |
331 | $this->operation = 'update'; |
332 | return $this->save( $data ); |
333 | |
334 | case 'delete': |
335 | $this->operation = 'delete'; |
336 | $bp = BotPassword::newFromCentralId( $this->userId, $this->par ); |
337 | if ( $bp ) { |
338 | $bp->delete(); |
339 | $this->logger->info( |
340 | "Bot password {op} for {user}@{app_id}", |
341 | [ |
342 | 'app_id' => $this->par, |
343 | 'user' => $this->getUser()->getName(), |
344 | 'centralId' => $this->userId, |
345 | 'op' => 'delete', |
346 | 'client_ip' => $this->getRequest()->getIP() |
347 | ] |
348 | ); |
349 | } |
350 | return Status::newGood(); |
351 | |
352 | case 'cancel': |
353 | $this->getOutput()->redirect( $this->getPageTitle()->getFullURL() ); |
354 | return false; |
355 | } |
356 | |
357 | return false; |
358 | } |
359 | |
360 | private function save( array $data ) { |
361 | $bp = BotPassword::newUnsaved( [ |
362 | 'centralId' => $this->userId, |
363 | 'appId' => $this->par, |
364 | 'restrictions' => $data['restrictions'], |
365 | 'grants' => array_merge( |
366 | $this->grantsInfo->getHiddenGrants(), |
367 | // @phan-suppress-next-next-line PhanTypeMismatchArgumentInternal See phan issue #3163, |
368 | // it's probably failing to infer the type of $data['grants'] |
369 | preg_replace( '/^grant-/', '', $data['grants'] ) |
370 | ) |
371 | ] ); |
372 | |
373 | if ( $bp === null ) { |
374 | // Messages: botpasswords-insert-failed, botpasswords-update-failed |
375 | return Status::newFatal( "botpasswords-{$this->operation}-failed", $this->par ); |
376 | } |
377 | |
378 | if ( $this->operation === 'insert' || !empty( $data['resetPassword'] ) ) { |
379 | $this->password = BotPassword::generatePassword( $this->getConfig() ); |
380 | $password = $this->passwordFactory->newFromPlaintext( $this->password ); |
381 | } else { |
382 | $password = null; |
383 | } |
384 | |
385 | $res = $bp->save( $this->operation, $password ); |
386 | |
387 | $success = $res->isGood(); |
388 | |
389 | $this->logger->info( |
390 | 'Bot password {op} for {user}@{app_id} ' . ( $success ? 'succeeded' : 'failed' ), |
391 | [ |
392 | 'op' => $this->operation, |
393 | 'user' => $this->getUser()->getName(), |
394 | 'app_id' => $this->par, |
395 | 'centralId' => $this->userId, |
396 | 'restrictions' => $data['restrictions'], |
397 | 'grants' => $bp->getGrants(), |
398 | 'client_ip' => $this->getRequest()->getIP(), |
399 | 'success' => $success, |
400 | ] |
401 | ); |
402 | |
403 | return $res; |
404 | } |
405 | |
406 | public function onSuccess() { |
407 | $out = $this->getOutput(); |
408 | |
409 | $username = $this->getUser()->getName(); |
410 | switch ( $this->operation ) { |
411 | case 'insert': |
412 | $out->setPageTitleMsg( $this->msg( 'botpasswords-created-title' ) ); |
413 | $out->addWikiMsg( 'botpasswords-created-body', $this->par, $username ); |
414 | break; |
415 | |
416 | case 'update': |
417 | $out->setPageTitleMsg( $this->msg( 'botpasswords-updated-title' ) ); |
418 | $out->addWikiMsg( 'botpasswords-updated-body', $this->par, $username ); |
419 | break; |
420 | |
421 | case 'delete': |
422 | $out->setPageTitleMsg( $this->msg( 'botpasswords-deleted-title' ) ); |
423 | $out->addWikiMsg( 'botpasswords-deleted-body', $this->par, $username ); |
424 | $this->password = null; |
425 | break; |
426 | } |
427 | |
428 | if ( $this->password !== null ) { |
429 | $sep = BotPassword::getSeparator(); |
430 | $out->addWikiMsg( |
431 | 'botpasswords-newpassword', |
432 | htmlspecialchars( $username . $sep . $this->par ), |
433 | htmlspecialchars( $this->password ), |
434 | htmlspecialchars( $username ), |
435 | htmlspecialchars( $this->par . $sep . $this->password ) |
436 | ); |
437 | $this->password = null; |
438 | } |
439 | |
440 | $out->addReturnTo( $this->getPageTitle() ); |
441 | } |
442 | |
443 | protected function getGroupName() { |
444 | return 'login'; |
445 | } |
446 | |
447 | protected function getDisplayFormat() { |
448 | return 'ooui'; |
449 | } |
450 | } |
451 | |
452 | /** @deprecated class alias since 1.41 */ |
453 | class_alias( SpecialBotPasswords::class, 'SpecialBotPasswords' ); |