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