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