MediaWiki master
SpecialBotPasswords.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Specials;
25
27use HTMLForm;
42use Psr\Log\LoggerInterface;
43
50
52 private $userId = 0;
53
55 private $botPassword = null;
56
58 private $operation = null;
59
61 private $password = null;
62
63 private LoggerInterface $logger;
64 private PasswordFactory $passwordFactory;
65 private CentralIdLookup $centralIdLookup;
66 private GrantsInfo $grantsInfo;
67 private GrantsLocalization $grantsLocalization;
68
76 public function __construct(
77 PasswordFactory $passwordFactory,
78 AuthManager $authManager,
79 CentralIdLookup $centralIdLookup,
80 GrantsInfo $grantsInfo,
81 GrantsLocalization $grantsLocalization
82 ) {
83 parent::__construct( 'BotPasswords', 'editmyprivateinfo' );
84 $this->logger = LoggerFactory::getInstance( 'authentication' );
85 $this->passwordFactory = $passwordFactory;
86 $this->centralIdLookup = $centralIdLookup;
87 $this->setAuthManager( $authManager );
88 $this->grantsInfo = $grantsInfo;
89 $this->grantsLocalization = $grantsLocalization;
90 }
91
95 public function isListed() {
96 return $this->getConfig()->get( MainConfigNames::EnableBotPasswords );
97 }
98
99 protected function getLoginSecurityLevel() {
100 return $this->getName();
101 }
102
107 public function execute( $par ) {
108 $this->getOutput()->disallowUserJs();
109 $this->requireNamedUser();
110 $this->addHelpLink( 'Manual:Bot_passwords' );
111
112 if ( $par !== null ) {
113 $par = trim( $par );
114 if ( $par === '' ) {
115 $par = null;
116 } elseif ( strlen( $par ) > BotPassword::APPID_MAXLENGTH ) {
117 throw new ErrorPageError(
118 'botpasswords', 'botpasswords-bad-appid', [ htmlspecialchars( $par ) ]
119 );
120 }
121 }
122
123 parent::execute( $par );
124 }
125
126 protected function checkExecutePermissions( User $user ) {
127 parent::checkExecutePermissions( $user );
128
129 if ( !$this->getConfig()->get( MainConfigNames::EnableBotPasswords ) ) {
130 throw new ErrorPageError( 'botpasswords', 'botpasswords-disabled' );
131 }
132
133 $this->userId = $this->centralIdLookup->centralIdFromLocalUser( $this->getUser() );
134 if ( !$this->userId ) {
135 throw new ErrorPageError( 'botpasswords', 'botpasswords-no-central-id' );
136 }
137 }
138
139 protected function getFormFields() {
140 $fields = [];
141
142 if ( $this->par !== null ) {
143 $this->botPassword = BotPassword::newFromCentralId( $this->userId, $this->par );
144 if ( !$this->botPassword ) {
145 $this->botPassword = BotPassword::newUnsaved( [
146 'centralId' => $this->userId,
147 'appId' => $this->par,
148 ] );
149 }
150
151 $sep = BotPassword::getSeparator();
152 $fields[] = [
153 'type' => 'info',
154 'label-message' => 'username',
155 'default' => $this->getUser()->getName() . $sep . $this->par
156 ];
157
158 if ( $this->botPassword->isSaved() ) {
159 $fields['resetPassword'] = [
160 'type' => 'check',
161 'label-message' => 'botpasswords-label-resetpassword',
162 ];
163 if ( $this->botPassword->isInvalid() ) {
164 $fields['resetPassword']['default'] = true;
165 }
166 }
167
168 $lang = $this->getLanguage();
169 $showGrants = $this->grantsInfo->getValidGrants();
170 $grantLinks = array_map( [ $this->grantsLocalization, 'getGrantsLink' ], $showGrants );
171
172 $fields[] = [
173 'type' => 'info',
174 'default' => '',
175 'help-message' => 'botpasswords-help-grants',
176 ];
177 $fields['grants'] = [
178 'type' => 'checkmatrix',
179 'label-message' => 'botpasswords-label-grants',
180 'columns' => [
181 $this->msg( 'botpasswords-label-grants-column' )->escaped() => 'grant'
182 ],
183 'rows' => array_combine(
184 $grantLinks,
185 $showGrants
186 ),
187 'default' => array_map(
188 static function ( $g ) {
189 return "grant-$g";
190 },
191 $this->botPassword->getGrants()
192 ),
193 'tooltips-html' => array_combine(
194 $grantLinks,
195 array_map(
196 function ( $rights ) use ( $lang ) {
197 return $lang->semicolonList(
198 array_map(
199 fn ( $right ) => $this->msg( "right-$right" )->parse(),
200 $rights
201 )
202 );
203 },
204 array_intersect_key( $this->grantsInfo->getRightsByGrant(),
205 array_fill_keys( $showGrants, true ) )
206 )
207 ),
208 'force-options-on' => array_map(
209 static function ( $g ) {
210 return "grant-$g";
211 },
212 $this->grantsInfo->getHiddenGrants()
213 ),
214 ];
215
216 $fields['restrictions'] = [
217 'class' => HTMLRestrictionsField::class,
218 'required' => true,
219 'default' => $this->botPassword->getRestrictions(),
220 ];
221
222 } else {
223 $linkRenderer = $this->getLinkRenderer();
224
225 $dbr = BotPassword::getDB( DB_REPLICA );
226 $res = $dbr->newSelectQueryBuilder()
227 ->select( [ 'bp_app_id', 'bp_password' ] )
228 ->from( 'bot_passwords' )
229 ->where( [ 'bp_user' => $this->userId ] )
230 ->caller( __METHOD__ )->fetchResultSet();
231 foreach ( $res as $row ) {
232 try {
233 $password = $this->passwordFactory->newFromCiphertext( $row->bp_password );
234 $passwordInvalid = $password instanceof InvalidPassword;
235 unset( $password );
236 } catch ( PasswordError $ex ) {
237 $passwordInvalid = true;
238 }
239
240 $text = $linkRenderer->makeKnownLink(
241 $this->getPageTitle( $row->bp_app_id ),
242 $row->bp_app_id
243 );
244 if ( $passwordInvalid ) {
245 $text .= $this->msg( 'word-separator' )->escaped()
246 . $this->msg( 'botpasswords-label-needsreset' )->parse();
247 }
248
249 $fields[] = [
250 'section' => 'existing',
251 'type' => 'info',
252 'raw' => true,
253 'default' => $text,
254 ];
255 }
256
257 $fields['appId'] = [
258 'section' => 'createnew',
259 'type' => 'textwithbutton',
260 'label-message' => 'botpasswords-label-appid',
261 'buttondefault' => $this->msg( 'botpasswords-label-create' )->text(),
262 'buttonflags' => [ 'progressive', 'primary' ],
263 'required' => true,
264 'size' => BotPassword::APPID_MAXLENGTH,
265 'maxlength' => BotPassword::APPID_MAXLENGTH,
266 'validation-callback' => static function ( $v ) {
267 $v = trim( $v );
268 return $v !== '' && strlen( $v ) <= BotPassword::APPID_MAXLENGTH;
269 },
270 ];
271
272 $fields[] = [
273 'type' => 'hidden',
274 'default' => 'new',
275 'name' => 'op',
276 ];
277 }
278
279 return $fields;
280 }
281
282 protected function alterForm( HTMLForm $form ) {
283 $form->setId( 'mw-botpasswords-form' );
284 $form->setTableId( 'mw-botpasswords-table' );
285 $form->suppressDefaultSubmit();
286
287 if ( $this->par !== null ) {
288 if ( $this->botPassword->isSaved() ) {
289 $form->setWrapperLegendMsg( 'botpasswords-editexisting' );
290 $form->addButton( [
291 'name' => 'op',
292 'value' => 'update',
293 'label-message' => 'botpasswords-label-update',
294 'flags' => [ 'primary', 'progressive' ],
295 ] );
296 $form->addButton( [
297 'name' => 'op',
298 'value' => 'delete',
299 'label-message' => 'botpasswords-label-delete',
300 'flags' => [ 'destructive' ],
301 ] );
302 } else {
303 $form->setWrapperLegendMsg( 'botpasswords-createnew' );
304 $form->addButton( [
305 'name' => 'op',
306 'value' => 'create',
307 'label-message' => 'botpasswords-label-create',
308 'flags' => [ 'primary', 'progressive' ],
309 ] );
310 }
311
312 $form->addButton( [
313 'name' => 'op',
314 'value' => 'cancel',
315 'label-message' => 'botpasswords-label-cancel'
316 ] );
317 }
318 }
319
320 public function onSubmit( array $data ) {
321 $op = $this->getRequest()->getVal( 'op', '' );
322
323 switch ( $op ) {
324 case 'new':
325 $this->getOutput()->redirect( $this->getPageTitle( $data['appId'] )->getFullURL() );
326 return false;
327
328 case 'create':
329 $this->operation = 'insert';
330 return $this->save( $data );
331
332 case 'update':
333 $this->operation = 'update';
334 return $this->save( $data );
335
336 case 'delete':
337 $this->operation = 'delete';
338 $bp = BotPassword::newFromCentralId( $this->userId, $this->par );
339 if ( $bp ) {
340 $bp->delete();
341 $this->logger->info(
342 "Bot password {op} for {user}@{app_id}",
343 [
344 'app_id' => $this->par,
345 'user' => $this->getUser()->getName(),
346 'centralId' => $this->userId,
347 'op' => 'delete',
348 'client_ip' => $this->getRequest()->getIP()
349 ]
350 );
351 }
352 return Status::newGood();
353
354 case 'cancel':
355 $this->getOutput()->redirect( $this->getPageTitle()->getFullURL() );
356 return false;
357 }
358
359 return false;
360 }
361
362 private function save( array $data ) {
363 $bp = BotPassword::newUnsaved( [
364 'centralId' => $this->userId,
365 'appId' => $this->par,
366 'restrictions' => $data['restrictions'],
367 'grants' => array_merge(
368 $this->grantsInfo->getHiddenGrants(),
369 // @phan-suppress-next-next-line PhanTypeMismatchArgumentInternal See phan issue #3163,
370 // it's probably failing to infer the type of $data['grants']
371 preg_replace( '/^grant-/', '', $data['grants'] )
372 )
373 ] );
374
375 if ( $bp === null ) {
376 // Messages: botpasswords-insert-failed, botpasswords-update-failed
377 return Status::newFatal( "botpasswords-{$this->operation}-failed", $this->par );
378 }
379
380 if ( $this->operation === 'insert' || !empty( $data['resetPassword'] ) ) {
381 $this->password = BotPassword::generatePassword( $this->getConfig() );
382 $password = $this->passwordFactory->newFromPlaintext( $this->password );
383 } else {
384 $password = null;
385 }
386
387 $res = $bp->save( $this->operation, $password );
388
389 $success = $res->isGood();
390
391 $this->logger->info(
392 'Bot password {op} for {user}@{app_id} ' . ( $success ? 'succeeded' : 'failed' ),
393 [
394 'op' => $this->operation,
395 'user' => $this->getUser()->getName(),
396 'app_id' => $this->par,
397 'centralId' => $this->userId,
398 'restrictions' => $data['restrictions'],
399 'grants' => $bp->getGrants(),
400 'client_ip' => $this->getRequest()->getIP(),
401 'success' => $success,
402 ]
403 );
404
405 return $res;
406 }
407
408 public function onSuccess() {
409 $out = $this->getOutput();
410
411 $username = $this->getUser()->getName();
412 switch ( $this->operation ) {
413 case 'insert':
414 $out->setPageTitleMsg( $this->msg( 'botpasswords-created-title' ) );
415 $out->addWikiMsg( 'botpasswords-created-body', $this->par, $username );
416 break;
417
418 case 'update':
419 $out->setPageTitleMsg( $this->msg( 'botpasswords-updated-title' ) );
420 $out->addWikiMsg( 'botpasswords-updated-body', $this->par, $username );
421 break;
422
423 case 'delete':
424 $out->setPageTitleMsg( $this->msg( 'botpasswords-deleted-title' ) );
425 $out->addWikiMsg( 'botpasswords-deleted-body', $this->par, $username );
426 $this->password = null;
427 break;
428 }
429
430 if ( $this->password !== null ) {
431 $sep = BotPassword::getSeparator();
432 $out->addWikiMsg(
433 'botpasswords-newpassword',
434 htmlspecialchars( $username . $sep . $this->par ),
435 htmlspecialchars( $this->password ),
436 htmlspecialchars( $username ),
437 htmlspecialchars( $this->par . $sep . $this->password )
438 );
439 $this->password = null;
440 }
441
442 $out->addReturnTo( $this->getPageTitle() );
443 }
444
445 protected function getGroupName() {
446 return 'login';
447 }
448
449 protected function getDisplayFormat() {
450 return 'ooui';
451 }
452}
453
457class_alias( SpecialBotPasswords::class, 'SpecialBotPasswords' );
An error page which can definitely be safely rendered using the OutputPage.
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:158
setTableId( $id)
Set the id of the <table> or outermost <div> element.
setWrapperLegendMsg( $msg)
Prompt the whole form to be wrapped in a "<fieldset>", with this message as its "<legend>" element.
setId( $id)
addButton( $data)
Add a button to the form.
suppressDefaultSubmit( $suppressSubmit=true)
Stop a default submit button being shown for this form.
Class for updating an MWRestrictions value (which is, currently, basically just an IP address list).
Represents an invalid password hash.
This serves as the entry point to the authentication system.
Create PSR-3 logger objects.
A class containing constants representing the names of configuration variables.
const EnableBotPasswords
Name constant for the EnableBotPasswords setting, for use with Config::get()
Users can authorize applications to use their account via OAuth.
This separate service is needed because the ::getGrantsLink method requires a LinkRenderer and if we ...
Special page which uses an HTMLForm to handle processing.
string null $par
The sub-page of the special page.
getUser()
Shortcut to get the User executing this instance.
setAuthManager(AuthManager $authManager)
Set the injected AuthManager from the special page constructor.
getPageTitle( $subpage=false)
Get a self-referential title object.
getConfig()
Shortcut to get main config object.
getRequest()
Get the WebRequest being used for this instance.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
requireNamedUser( $reasonMsg='exception-nologin-text', $titleMsg='exception-nologin')
If the user is not logged in or is a temporary user, throws UserNotLoggedIn.
getOutput()
Get the OutputPage being used for this instance.
getLanguage()
Shortcut to get user's language.
getName()
Get the name of this Special Page.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
checkExecutePermissions(User $user)
Called from execute() to check if the given user can perform this action.
getDisplayFormat()
Get display format for the form.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
__construct(PasswordFactory $passwordFactory, AuthManager $authManager, CentralIdLookup $centralIdLookup, GrantsInfo $grantsInfo, GrantsLocalization $grantsLocalization)
getLoginSecurityLevel()
Tells if the special page does something security-sensitive and needs extra defense against a stolen ...
onSuccess()
Do something exciting on successful processing of the form, most likely to show a confirmation messag...
alterForm(HTMLForm $form)
Play with the HTMLForm if you need to more substantially.
onSubmit(array $data)
Process the form on submission.
getFormFields()
Get an HTMLForm descriptor array.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
Utility class for bot passwords.
The CentralIdLookup service allows for connecting local users with cluster-wide IDs.
internal since 1.36
Definition User.php:96
Show an error when any operation involving passwords fails to run.
Factory class for creating and checking Password objects.
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Ge...
const DB_REPLICA
Definition defines.php:26