MediaWiki  master
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 
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,
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 }
$success
const APPID_MAXLENGTH
Definition: BotPassword.php:36
static generatePassword( $config)
Returns a (raw, unhashed) random password string.
static getDB( $db)
Get a database connection for the bot passwords database.
Definition: BotPassword.php:99
static newFromCentralId( $centralId, $appId, $flags=self::READ_NORMAL)
Load a BotPassword from the database.
static newUnsaved(array $data, $flags=self::READ_NORMAL)
Create an unsaved BotPassword.
static getSeparator()
Get the separator for combined user name + app ID.
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.
Definition: HTMLForm.php:1752
setWrapperLegendMsg( $msg)
Prompt the whole form to be wrapped in a "<fieldset>", with this message as its "<legend>" element.
Definition: HTMLForm.php:1818
setId( $id)
Definition: HTMLForm.php:1763
addButton( $data)
Add a button to the form.
Definition: HTMLForm.php:1183
suppressDefaultSubmit( $suppressSubmit=true)
Stop a default submit button being shown for this form.
Definition: HTMLForm.php:1697
addPreText( $msg)
Add HTML to introductory message.
Definition: HTMLForm.php:857
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.
Definition: GrantsInfo.php:33
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.
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:73
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:85
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:70
const DB_REPLICA
Definition: defines.php:26
if(!isset( $args[0])) $lang