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