MediaWiki master
SpecialBotPasswords.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Specials;
25
27use HTMLRestrictionsField;
43use Psr\Log\LoggerInterface;
44
51
53 private $userId = 0;
54
56 private $botPassword = null;
57
59 private $operation = null;
60
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
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
96 public function isListed() {
97 return $this->getConfig()->get( MainConfigNames::EnableBotPasswords );
98 }
99
100 protected function getLoginSecurityLevel() {
101 return $this->getName();
102 }
103
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
453class_alias( SpecialBotPasswords::class, 'SpecialBotPasswords' );
An error page which can definitely be safely rendered using the OutputPage.
Represents an invalid password hash.
This serves as the entry point to the authentication system.
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:206
setWrapperLegendMsg( $msg)
Prompt the whole form to be wrapped in a "<fieldset>", with this message as its "<legend>" element.
suppressDefaultSubmit( $suppressSubmit=true)
Stop a default submit button being shown for this form.
addButton( $data)
Add a button to the form.
setTableId( $id)
Set the id of the <table> or outermost <div> element.
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
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 canonical, unlocalized name of this special page without namespace.
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:93
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...