MediaWiki master
SpecialBotPasswords.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Specials;
22
40use Psr\Log\LoggerInterface;
41
48
50 private $userId = 0;
51
53 private $botPassword = null;
54
56 private $operation = null;
57
59 private $password = null;
60
61 private LoggerInterface $logger;
62 private PasswordFactory $passwordFactory;
63 private CentralIdLookup $centralIdLookup;
64 private GrantsInfo $grantsInfo;
65 private GrantsLocalization $grantsLocalization;
66
74 public function __construct(
75 PasswordFactory $passwordFactory,
76 AuthManager $authManager,
77 CentralIdLookup $centralIdLookup,
78 GrantsInfo $grantsInfo,
79 GrantsLocalization $grantsLocalization
80 ) {
81 parent::__construct( 'BotPasswords', 'editmyprivateinfo' );
82 $this->logger = LoggerFactory::getInstance( 'authentication' );
83 $this->passwordFactory = $passwordFactory;
84 $this->centralIdLookup = $centralIdLookup;
85 $this->setAuthManager( $authManager );
86 $this->grantsInfo = $grantsInfo;
87 $this->grantsLocalization = $grantsLocalization;
88 }
89
93 public function isListed() {
94 return $this->getConfig()->get( MainConfigNames::EnableBotPasswords );
95 }
96
97 protected function getLoginSecurityLevel() {
98 return $this->getName();
99 }
100
105 public function execute( $par ) {
106 $this->requireNamedUser();
107 $this->getOutput()->disallowUserJs();
108 $this->getOutput()->addModuleStyles( 'mediawiki.special' );
109 $this->addHelpLink( 'Manual:Bot_passwords' );
110
111 if ( $par !== null ) {
112 $par = trim( $par );
113 if ( $par === '' ) {
114 $par = null;
115 } elseif ( strlen( $par ) > BotPassword::APPID_MAXLENGTH ) {
116 throw new ErrorPageError(
117 'botpasswords', 'botpasswords-bad-appid', [ htmlspecialchars( $par ) ]
118 );
119 }
120 }
121
122 parent::execute( $par );
123 }
124
125 protected function checkExecutePermissions( User $user ) {
126 parent::checkExecutePermissions( $user );
127
128 if ( !$this->getConfig()->get( MainConfigNames::EnableBotPasswords ) ) {
129 throw new ErrorPageError( 'botpasswords', 'botpasswords-disabled' );
130 }
131
132 $this->userId = $this->centralIdLookup->centralIdFromLocalUser( $this->getUser() );
133 if ( !$this->userId ) {
134 throw new ErrorPageError( 'botpasswords', 'botpasswords-no-central-id' );
135 }
136 }
137
138 protected function getFormFields() {
139 $fields = [];
140
141 if ( $this->par !== null ) {
142 $this->botPassword = BotPassword::newFromCentralId( $this->userId, $this->par );
143 if ( !$this->botPassword ) {
144 $this->botPassword = BotPassword::newUnsaved( [
145 'centralId' => $this->userId,
146 'appId' => $this->par,
147 ] );
148 }
149
150 $sep = BotPassword::getSeparator();
151 $fields[] = [
152 'type' => 'info',
153 'label-message' => 'username',
154 'default' => $this->getUser()->getName() . $sep . $this->par
155 ];
156
157 if ( $this->botPassword->isSaved() ) {
158 $fields['resetPassword'] = [
159 'type' => 'check',
160 'label-message' => 'botpasswords-label-resetpassword',
161 ];
162 if ( $this->botPassword->isInvalid() ) {
163 $fields['resetPassword']['default'] = true;
164 }
165 }
166
167 $showGrants = $this->grantsInfo->getValidGrants();
168 $grantNames = $this->grantsLocalization->getGrantDescriptionsWithClasses(
169 $showGrants, $this->getLanguage() );
170
171 $fields[] = [
172 'type' => 'info',
173 'default' => '',
174 'help-message' => 'botpasswords-help-grants',
175 ];
176 $fields['grants'] = [
177 'type' => 'checkmatrix',
178 'label-message' => 'botpasswords-label-grants',
179 'columns' => [
180 $this->msg( 'botpasswords-label-grants-column' )->escaped() => 'grant'
181 ],
182 'rows' => array_combine(
183 $grantNames,
184 $showGrants
185 ),
186 'default' => array_map(
187 static function ( $g ) {
188 return "grant-$g";
189 },
190 $this->botPassword->getGrants()
191 ),
192 'tooltips-html' => array_combine(
193 $grantNames,
194 array_map(
195 fn ( $rights ) => Html::rawElement( 'ul', [], implode( '', array_map(
196 fn ( $right ) => Html::rawElement( 'li', [], $this->msg( "right-$right" )->parse() ),
197 $rights
198 ) ) ),
199 array_intersect_key( $this->grantsInfo->getRightsByGrant(),
200 array_fill_keys( $showGrants, true ) )
201 )
202 ),
203 'force-options-on' => array_map(
204 static function ( $g ) {
205 return "grant-$g";
206 },
207 $this->grantsInfo->getHiddenGrants()
208 ),
209 ];
210
211 $fields['restrictions'] = [
212 'class' => HTMLRestrictionsField::class,
213 'required' => true,
214 'default' => $this->botPassword->getRestrictions(),
215 ];
216
217 } else {
218 $linkRenderer = $this->getLinkRenderer();
219
220 $dbr = BotPassword::getReplicaDatabase();
221 $res = $dbr->newSelectQueryBuilder()
222 ->select( [ 'bp_app_id', 'bp_password' ] )
223 ->from( 'bot_passwords' )
224 ->where( [ 'bp_user' => $this->userId ] )
225 ->caller( __METHOD__ )->fetchResultSet();
226 foreach ( $res as $row ) {
227 try {
228 $password = $this->passwordFactory->newFromCiphertext( $row->bp_password );
229 $passwordInvalid = $password instanceof InvalidPassword;
230 unset( $password );
231 } catch ( PasswordError $ex ) {
232 $passwordInvalid = true;
233 }
234
235 $text = $linkRenderer->makeKnownLink(
236 $this->getPageTitle( $row->bp_app_id ),
237 $row->bp_app_id
238 );
239 if ( $passwordInvalid ) {
240 $text .= $this->msg( 'word-separator' )->escaped()
241 . $this->msg( 'botpasswords-label-needsreset' )->parse();
242 }
243
244 $fields[] = [
245 'section' => 'existing',
246 'type' => 'info',
247 'raw' => true,
248 'default' => $text,
249 ];
250 }
251
252 $fields['appId'] = [
253 'section' => 'createnew',
254 'type' => 'textwithbutton',
255 'label-message' => 'botpasswords-label-appid',
256 'buttondefault' => $this->msg( 'botpasswords-label-create' )->text(),
257 'buttonflags' => [ 'progressive', 'primary' ],
258 'required' => true,
259 'size' => BotPassword::APPID_MAXLENGTH,
260 'maxlength' => BotPassword::APPID_MAXLENGTH,
261 'validation-callback' => static function ( $v ) {
262 $v = trim( $v );
263 return $v !== '' && strlen( $v ) <= BotPassword::APPID_MAXLENGTH;
264 },
265 ];
266
267 $fields[] = [
268 'type' => 'hidden',
269 'default' => 'new',
270 'name' => 'op',
271 ];
272 }
273
274 return $fields;
275 }
276
277 protected function alterForm( HTMLForm $form ) {
278 $form->setId( 'mw-botpasswords-form' );
279 $form->setTableId( 'mw-botpasswords-table' );
280 $form->suppressDefaultSubmit();
281
282 if ( $this->par !== null ) {
283 if ( $this->botPassword->isSaved() ) {
284 $form->setWrapperLegendMsg( 'botpasswords-editexisting' );
285 $form->addButton( [
286 'name' => 'op',
287 'value' => 'update',
288 'label-message' => 'botpasswords-label-update',
289 'flags' => [ 'primary', 'progressive' ],
290 ] );
291 $form->addButton( [
292 'name' => 'op',
293 'value' => 'delete',
294 'label-message' => 'botpasswords-label-delete',
295 'flags' => [ 'destructive' ],
296 ] );
297 } else {
298 $form->setWrapperLegendMsg( 'botpasswords-createnew' );
299 $form->addButton( [
300 'name' => 'op',
301 'value' => 'create',
302 'label-message' => 'botpasswords-label-create',
303 'flags' => [ 'primary', 'progressive' ],
304 ] );
305 }
306
307 $form->addButton( [
308 'name' => 'op',
309 'value' => 'cancel',
310 'label-message' => 'botpasswords-label-cancel'
311 ] );
312 }
313 }
314
315 public function onSubmit( array $data ) {
316 $op = $this->getRequest()->getVal( 'op', '' );
317
318 switch ( $op ) {
319 case 'new':
320 $this->getOutput()->redirect( $this->getPageTitle( $data['appId'] )->getFullURL() );
321 return false;
322
323 case 'create':
324 $this->operation = 'insert';
325 return $this->save( $data );
326
327 case 'update':
328 $this->operation = 'update';
329 return $this->save( $data );
330
331 case 'delete':
332 $this->operation = 'delete';
333 $bp = BotPassword::newFromCentralId( $this->userId, $this->par );
334 if ( $bp ) {
335 $bp->delete();
336 $this->logger->info(
337 "Bot password {op} for {user}@{app_id}",
338 [
339 'app_id' => $this->par,
340 'user' => $this->getUser()->getName(),
341 'centralId' => $this->userId,
342 'op' => 'delete',
343 'client_ip' => $this->getRequest()->getIP()
344 ]
345 );
346 }
347 return Status::newGood();
348
349 case 'cancel':
350 $this->getOutput()->redirect( $this->getPageTitle()->getFullURL() );
351 return false;
352 }
353
354 return false;
355 }
356
357 private function save( array $data ) {
358 $bp = BotPassword::newUnsaved( [
359 'centralId' => $this->userId,
360 'appId' => $this->par,
361 'restrictions' => $data['restrictions'],
362 'grants' => array_merge(
363 $this->grantsInfo->getHiddenGrants(),
364 // @phan-suppress-next-next-line PhanTypeMismatchArgumentInternal See phan issue #3163,
365 // it's probably failing to infer the type of $data['grants']
366 preg_replace( '/^grant-/', '', $data['grants'] )
367 )
368 ] );
369
370 if ( $bp === null ) {
371 // Messages: botpasswords-insert-failed, botpasswords-update-failed
372 return Status::newFatal( "botpasswords-{$this->operation}-failed", $this->par );
373 }
374
375 if ( $this->operation === 'insert' || !empty( $data['resetPassword'] ) ) {
376 $this->password = BotPassword::generatePassword( $this->getConfig() );
377 $password = $this->passwordFactory->newFromPlaintext( $this->password );
378 } else {
379 $password = null;
380 }
381
382 $res = $bp->save( $this->operation, $password );
383
384 $success = $res->isGood();
385
386 $this->logger->info(
387 'Bot password {op} for {user}@{app_id} ' . ( $success ? 'succeeded' : 'failed' ),
388 [
389 'op' => $this->operation,
390 'user' => $this->getUser()->getName(),
391 'app_id' => $this->par,
392 'centralId' => $this->userId,
393 'restrictions' => $data['restrictions'],
394 'grants' => $bp->getGrants(),
395 'client_ip' => $this->getRequest()->getIP(),
396 'success' => $success,
397 ]
398 );
399
400 return $res;
401 }
402
403 public function onSuccess() {
404 $out = $this->getOutput();
405
406 $username = $this->getUser()->getName();
407 switch ( $this->operation ) {
408 case 'insert':
409 $out->setPageTitleMsg( $this->msg( 'botpasswords-created-title' ) );
410 $out->addWikiMsg( 'botpasswords-created-body', $this->par, $username );
411 break;
412
413 case 'update':
414 $out->setPageTitleMsg( $this->msg( 'botpasswords-updated-title' ) );
415 $out->addWikiMsg( 'botpasswords-updated-body', $this->par, $username );
416 break;
417
418 case 'delete':
419 $out->setPageTitleMsg( $this->msg( 'botpasswords-deleted-title' ) );
420 $out->addWikiMsg( 'botpasswords-deleted-body', $this->par, $username );
421 $this->password = null;
422 break;
423 }
424
425 if ( $this->password !== null ) {
426 $sep = BotPassword::getSeparator();
427 $out->addWikiMsg(
428 'botpasswords-newpassword',
429 htmlspecialchars( $username . $sep . $this->par ),
430 htmlspecialchars( $this->password ),
431 htmlspecialchars( $username ),
432 htmlspecialchars( $this->par . $sep . $this->password )
433 );
434 $this->password = null;
435 }
436
437 $out->addReturnTo( $this->getPageTitle() );
438 }
439
440 protected function getGroupName() {
441 return 'login';
442 }
443
444 protected function getDisplayFormat() {
445 return 'ooui';
446 }
447}
448
450class_alias( SpecialBotPasswords::class, 'SpecialBotPasswords' );
An error page which can definitely be safely rendered using the OutputPage.
This serves as the entry point to the authentication system.
Class for updating an MWRestrictions value (which is, currently, basically just an IP address list).
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:209
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()
Represents an invalid password hash.
Show an error when any operation involving passwords fails to run.
Factory class for creating and checking Password objects.
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 subpage of the special page.
getUser()
Shortcut to get the User executing this instance.
requireNamedUser( $reasonMsg='exception-nologin-text', $titleMsg='exception-nologin', bool $alwaysRedirectToLoginPage=false)
If the user is not logged in or is a temporary user, throws UserNotLoggedIn.
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.
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