MediaWiki 1.41.2
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['grants'] = [
173 'type' => 'checkmatrix',
174 'label-message' => 'botpasswords-label-grants',
175 'help-message' => 'botpasswords-help-grants',
176 'columns' => [
177 $this->msg( 'botpasswords-label-grants-column' )->escaped() => 'grant'
178 ],
179 'rows' => array_combine(
180 $grantLinks,
181 $showGrants
182 ),
183 'default' => array_map(
184 static function ( $g ) {
185 return "grant-$g";
186 },
187 $this->botPassword->getGrants()
188 ),
189 'tooltips' => array_combine(
190 $grantLinks,
191 array_map(
192 static function ( $rights ) use ( $lang ) {
193 return $lang->semicolonList( array_map( [ User::class, 'getRightDescription' ], $rights ) );
194 },
195 array_intersect_key( $this->grantsInfo->getRightsByGrant(),
196 array_fill_keys( $showGrants, true ) )
197 )
198 ),
199 'force-options-on' => array_map(
200 static function ( $g ) {
201 return "grant-$g";
202 },
203 $this->grantsInfo->getHiddenGrants()
204 ),
205 ];
206
207 $fields['restrictions'] = [
208 'class' => HTMLRestrictionsField::class,
209 'required' => true,
210 'default' => $this->botPassword->getRestrictions(),
211 ];
212
213 } else {
214 $linkRenderer = $this->getLinkRenderer();
215
216 $dbr = BotPassword::getDB( DB_REPLICA );
217 $res = $dbr->newSelectQueryBuilder()
218 ->select( [ 'bp_app_id', 'bp_password' ] )
219 ->from( 'bot_passwords' )
220 ->where( [ 'bp_user' => $this->userId ] )
221 ->caller( __METHOD__ )->fetchResultSet();
222 foreach ( $res as $row ) {
223 try {
224 $password = $this->passwordFactory->newFromCiphertext( $row->bp_password );
225 $passwordInvalid = $password instanceof InvalidPassword;
226 unset( $password );
227 } catch ( PasswordError $ex ) {
228 $passwordInvalid = true;
229 }
230
231 $text = $linkRenderer->makeKnownLink(
232 $this->getPageTitle( $row->bp_app_id ),
233 $row->bp_app_id
234 );
235 if ( $passwordInvalid ) {
236 $text .= $this->msg( 'word-separator' )->escaped()
237 . $this->msg( 'botpasswords-label-needsreset' )->parse();
238 }
239
240 $fields[] = [
241 'section' => 'existing',
242 'type' => 'info',
243 'raw' => true,
244 'default' => $text,
245 ];
246 }
247
248 $fields['appId'] = [
249 'section' => 'createnew',
250 'type' => 'textwithbutton',
251 'label-message' => 'botpasswords-label-appid',
252 'buttondefault' => $this->msg( 'botpasswords-label-create' )->text(),
253 'buttonflags' => [ 'progressive', 'primary' ],
254 'required' => true,
255 'size' => BotPassword::APPID_MAXLENGTH,
256 'maxlength' => BotPassword::APPID_MAXLENGTH,
257 'validation-callback' => static function ( $v ) {
258 $v = trim( $v );
259 return $v !== '' && strlen( $v ) <= BotPassword::APPID_MAXLENGTH;
260 },
261 ];
262
263 $fields[] = [
264 'type' => 'hidden',
265 'default' => 'new',
266 'name' => 'op',
267 ];
268 }
269
270 return $fields;
271 }
272
273 protected function alterForm( HTMLForm $form ) {
274 $form->setId( 'mw-botpasswords-form' );
275 $form->setTableId( 'mw-botpasswords-table' );
276 $form->suppressDefaultSubmit();
277
278 if ( $this->par !== null ) {
279 if ( $this->botPassword->isSaved() ) {
280 $form->setWrapperLegendMsg( 'botpasswords-editexisting' );
281 $form->addButton( [
282 'name' => 'op',
283 'value' => 'update',
284 'label-message' => 'botpasswords-label-update',
285 'flags' => [ 'primary', 'progressive' ],
286 ] );
287 $form->addButton( [
288 'name' => 'op',
289 'value' => 'delete',
290 'label-message' => 'botpasswords-label-delete',
291 'flags' => [ 'destructive' ],
292 ] );
293 } else {
294 $form->setWrapperLegendMsg( 'botpasswords-createnew' );
295 $form->addButton( [
296 'name' => 'op',
297 'value' => 'create',
298 'label-message' => 'botpasswords-label-create',
299 'flags' => [ 'primary', 'progressive' ],
300 ] );
301 }
302
303 $form->addButton( [
304 'name' => 'op',
305 'value' => 'cancel',
306 'label-message' => 'botpasswords-label-cancel'
307 ] );
308 }
309 }
310
311 public function onSubmit( array $data ) {
312 $op = $this->getRequest()->getVal( 'op', '' );
313
314 switch ( $op ) {
315 case 'new':
316 $this->getOutput()->redirect( $this->getPageTitle( $data['appId'] )->getFullURL() );
317 return false;
318
319 case 'create':
320 $this->operation = 'insert';
321 return $this->save( $data );
322
323 case 'update':
324 $this->operation = 'update';
325 return $this->save( $data );
326
327 case 'delete':
328 $this->operation = 'delete';
329 $bp = BotPassword::newFromCentralId( $this->userId, $this->par );
330 if ( $bp ) {
331 $bp->delete();
332 $this->logger->info(
333 "Bot password {op} for {user}@{app_id}",
334 [
335 'app_id' => $this->par,
336 'user' => $this->getUser()->getName(),
337 'centralId' => $this->userId,
338 'op' => 'delete',
339 'client_ip' => $this->getRequest()->getIP()
340 ]
341 );
342 }
343 return Status::newGood();
344
345 case 'cancel':
346 $this->getOutput()->redirect( $this->getPageTitle()->getFullURL() );
347 return false;
348 }
349
350 return false;
351 }
352
353 private function save( array $data ) {
354 $bp = BotPassword::newUnsaved( [
355 'centralId' => $this->userId,
356 'appId' => $this->par,
357 'restrictions' => $data['restrictions'],
358 'grants' => array_merge(
359 $this->grantsInfo->getHiddenGrants(),
360 // @phan-suppress-next-next-line PhanTypeMismatchArgumentInternal See phan issue #3163,
361 // it's probably failing to infer the type of $data['grants']
362 preg_replace( '/^grant-/', '', $data['grants'] )
363 )
364 ] );
365
366 if ( $bp === null ) {
367 // Messages: botpasswords-insert-failed, botpasswords-update-failed
368 return Status::newFatal( "botpasswords-{$this->operation}-failed", $this->par );
369 }
370
371 if ( $this->operation === 'insert' || !empty( $data['resetPassword'] ) ) {
372 $this->password = BotPassword::generatePassword( $this->getConfig() );
373 $password = $this->passwordFactory->newFromPlaintext( $this->password );
374 } else {
375 $password = null;
376 }
377
378 $res = $bp->save( $this->operation, $password );
379
380 $success = $res->isGood();
381
382 $this->logger->info(
383 'Bot password {op} for {user}@{app_id} ' . ( $success ? 'succeeded' : 'failed' ),
384 [
385 'op' => $this->operation,
386 'user' => $this->getUser()->getName(),
387 'app_id' => $this->par,
388 'centralId' => $this->userId,
389 'restrictions' => $data['restrictions'],
390 'grants' => $bp->getGrants(),
391 'client_ip' => $this->getRequest()->getIP(),
392 'success' => $success,
393 ]
394 );
395
396 return $res;
397 }
398
399 public function onSuccess() {
400 $out = $this->getOutput();
401
402 $username = $this->getUser()->getName();
403 switch ( $this->operation ) {
404 case 'insert':
405 $out->setPageTitleMsg( $this->msg( 'botpasswords-created-title' ) );
406 $out->addWikiMsg( 'botpasswords-created-body', $this->par, $username );
407 break;
408
409 case 'update':
410 $out->setPageTitleMsg( $this->msg( 'botpasswords-updated-title' ) );
411 $out->addWikiMsg( 'botpasswords-updated-body', $this->par, $username );
412 break;
413
414 case 'delete':
415 $out->setPageTitleMsg( $this->msg( 'botpasswords-deleted-title' ) );
416 $out->addWikiMsg( 'botpasswords-deleted-body', $this->par, $username );
417 $this->password = null;
418 break;
419 }
420
421 if ( $this->password !== null ) {
422 $sep = BotPassword::getSeparator();
423 $out->addWikiMsg(
424 'botpasswords-newpassword',
425 htmlspecialchars( $username . $sep . $this->par ),
426 htmlspecialchars( $this->password ),
427 htmlspecialchars( $username ),
428 htmlspecialchars( $this->par . $sep . $this->password )
429 );
430 $this->password = null;
431 }
432
433 $out->addReturnTo( $this->getPageTitle() );
434 }
435
436 protected function getGroupName() {
437 return 'login';
438 }
439
440 protected function getDisplayFormat() {
441 return 'ooui';
442 }
443}
444
448class_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:58
Utility class for bot passwords.
The CentralIdLookup service allows for connecting local users with cluster-wide IDs.
internal since 1.36
Definition User.php:98
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