MediaWiki REL1_39
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->addPreText( $this->msg( 'botpasswords-summary' )->parseAsBlock() );
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 ) {
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->setPageTitle( $this->msg( 'botpasswords-created-title' )->text() );
403 $out->addWikiMsg( 'botpasswords-created-body', $this->par, $username );
404 break;
405
406 case 'update':
407 $out->setPageTitle( $this->msg( 'botpasswords-updated-title' )->text() );
408 $out->addWikiMsg( 'botpasswords-updated-body', $this->par, $username );
409 break;
410
411 case 'delete':
412 $out->setPageTitle( $this->msg( 'botpasswords-deleted-title' )->text() );
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 'users';
435 }
436
437 protected function getDisplayFormat() {
438 return 'ooui';
439 }
440}
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:150
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.
addPreText( $msg)
Add HTML to introductory message.
Definition HTMLForm.php:856
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 POST 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:70
const DB_REPLICA
Definition defines.php:26
if(!isset( $args[0])) $lang