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 = null;
48 
49  public function __construct() {
50  parent::__construct( 'BotPasswords', 'editmyprivateinfo' );
51  $this->logger = LoggerFactory::getInstance( 'authentication' );
52  }
53 
57  public function isListed() {
58  return $this->getConfig()->get( 'EnableBotPasswords' );
59  }
60 
61  protected function getLoginSecurityLevel() {
62  return $this->getName();
63  }
64 
69  function execute( $par ) {
70  $this->getOutput()->disallowUserJs();
71  $this->requireLogin();
72  $this->addHelpLink( 'Manual:Bot_passwords' );
73 
74  $par = trim( $par );
75  if ( strlen( $par ) === 0 ) {
76  $par = null;
77  } elseif ( strlen( $par ) > BotPassword::APPID_MAXLENGTH ) {
78  throw new ErrorPageError( 'botpasswords', 'botpasswords-bad-appid',
79  [ htmlspecialchars( $par ) ] );
80  }
81 
82  parent::execute( $par );
83  }
84 
85  protected function checkExecutePermissions( User $user ) {
86  parent::checkExecutePermissions( $user );
87 
88  if ( !$this->getConfig()->get( 'EnableBotPasswords' ) ) {
89  throw new ErrorPageError( 'botpasswords', 'botpasswords-disabled' );
90  }
91 
92  $this->userId = CentralIdLookup::factory()->centralIdFromLocalUser( $this->getUser() );
93  if ( !$this->userId ) {
94  throw new ErrorPageError( 'botpasswords', 'botpasswords-no-central-id' );
95  }
96  }
97 
98  protected function getFormFields() {
99  $fields = [];
100 
101  if ( $this->par !== null ) {
102  $this->botPassword = BotPassword::newFromCentralId( $this->userId, $this->par );
103  if ( !$this->botPassword ) {
104  $this->botPassword = BotPassword::newUnsaved( [
105  'centralId' => $this->userId,
106  'appId' => $this->par,
107  ] );
108  }
109 
110  $sep = BotPassword::getSeparator();
111  $fields[] = [
112  'type' => 'info',
113  'label-message' => 'username',
114  'default' => $this->getUser()->getName() . $sep . $this->par
115  ];
116 
117  if ( $this->botPassword->isSaved() ) {
118  $fields['resetPassword'] = [
119  'type' => 'check',
120  'label-message' => 'botpasswords-label-resetpassword',
121  ];
122  if ( $this->botPassword->isInvalid() ) {
123  $fields['resetPassword']['default'] = true;
124  }
125  }
126 
127  $lang = $this->getLanguage();
128  $showGrants = MWGrants::getValidGrants();
129  $fields['grants'] = [
130  'type' => 'checkmatrix',
131  'label-message' => 'botpasswords-label-grants',
132  'help-message' => 'botpasswords-help-grants',
133  'columns' => [
134  $this->msg( 'botpasswords-label-grants-column' )->escaped() => 'grant'
135  ],
136  'rows' => array_combine(
137  array_map( 'MWGrants::getGrantsLink', $showGrants ),
138  $showGrants
139  ),
140  'default' => array_map(
141  function ( $g ) {
142  return "grant-$g";
143  },
144  $this->botPassword->getGrants()
145  ),
146  'tooltips' => array_combine(
147  array_map( 'MWGrants::getGrantsLink', $showGrants ),
148  array_map(
149  function ( $rights ) use ( $lang ) {
150  return $lang->semicolonList( array_map( 'User::getRightDescription', $rights ) );
151  },
152  array_intersect_key( MWGrants::getRightsByGrant(), array_flip( $showGrants ) )
153  )
154  ),
155  'force-options-on' => array_map(
156  function ( $g ) {
157  return "grant-$g";
158  },
160  ),
161  ];
162 
163  $fields['restrictions'] = [
164  'class' => HTMLRestrictionsField::class,
165  'required' => true,
166  'default' => $this->botPassword->getRestrictions(),
167  ];
168 
169  } else {
170  $linkRenderer = $this->getLinkRenderer();
171  $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
172 
174  $res = $dbr->select(
175  'bot_passwords',
176  [ 'bp_app_id', 'bp_password' ],
177  [ 'bp_user' => $this->userId ],
178  __METHOD__
179  );
180  foreach ( $res as $row ) {
181  try {
182  $password = $passwordFactory->newFromCiphertext( $row->bp_password );
183  $passwordInvalid = $password instanceof InvalidPassword;
184  unset( $password );
185  } catch ( PasswordError $ex ) {
186  $passwordInvalid = true;
187  }
188 
189  $text = $linkRenderer->makeKnownLink(
190  $this->getPageTitle( $row->bp_app_id ),
191  $row->bp_app_id
192  );
193  if ( $passwordInvalid ) {
194  $text .= $this->msg( 'word-separator' )->escaped()
195  . $this->msg( 'botpasswords-label-needsreset' )->parse();
196  }
197 
198  $fields[] = [
199  'section' => 'existing',
200  'type' => 'info',
201  'raw' => true,
202  'default' => $text,
203  ];
204  }
205 
206  $fields['appId'] = [
207  'section' => 'createnew',
208  'type' => 'textwithbutton',
209  'label-message' => 'botpasswords-label-appid',
210  'buttondefault' => $this->msg( 'botpasswords-label-create' )->text(),
211  'buttonflags' => [ 'progressive', 'primary' ],
212  'required' => true,
214  'maxlength' => BotPassword::APPID_MAXLENGTH,
215  'validation-callback' => function ( $v ) {
216  $v = trim( $v );
217  return $v !== '' && strlen( $v ) <= BotPassword::APPID_MAXLENGTH;
218  },
219  ];
220 
221  $fields[] = [
222  'type' => 'hidden',
223  'default' => 'new',
224  'name' => 'op',
225  ];
226  }
227 
228  return $fields;
229  }
230 
231  protected function alterForm( HTMLForm $form ) {
232  $form->setId( 'mw-botpasswords-form' );
233  $form->setTableId( 'mw-botpasswords-table' );
234  $form->addPreText( $this->msg( 'botpasswords-summary' )->parseAsBlock() );
235  $form->suppressDefaultSubmit();
236 
237  if ( $this->par !== null ) {
238  if ( $this->botPassword->isSaved() ) {
239  $form->setWrapperLegendMsg( 'botpasswords-editexisting' );
240  $form->addButton( [
241  'name' => 'op',
242  'value' => 'update',
243  'label-message' => 'botpasswords-label-update',
244  'flags' => [ 'primary', 'progressive' ],
245  ] );
246  $form->addButton( [
247  'name' => 'op',
248  'value' => 'delete',
249  'label-message' => 'botpasswords-label-delete',
250  'flags' => [ 'destructive' ],
251  ] );
252  } else {
253  $form->setWrapperLegendMsg( 'botpasswords-createnew' );
254  $form->addButton( [
255  'name' => 'op',
256  'value' => 'create',
257  'label-message' => 'botpasswords-label-create',
258  'flags' => [ 'primary', 'progressive' ],
259  ] );
260  }
261 
262  $form->addButton( [
263  'name' => 'op',
264  'value' => 'cancel',
265  'label-message' => 'botpasswords-label-cancel'
266  ] );
267  }
268  }
269 
270  public function onSubmit( array $data ) {
271  $op = $this->getRequest()->getVal( 'op', '' );
272 
273  switch ( $op ) {
274  case 'new':
275  $this->getOutput()->redirect( $this->getPageTitle( $data['appId'] )->getFullURL() );
276  return false;
277 
278  case 'create':
279  $this->operation = 'insert';
280  return $this->save( $data );
281 
282  case 'update':
283  $this->operation = 'update';
284  return $this->save( $data );
285 
286  case 'delete':
287  $this->operation = 'delete';
288  $bp = BotPassword::newFromCentralId( $this->userId, $this->par );
289  if ( $bp ) {
290  $bp->delete();
291  $this->logger->info(
292  "Bot password {op} for {user}@{app_id}",
293  [
294  'app_id' => $this->par,
295  'user' => $this->getUser()->getName(),
296  'centralId' => $this->userId,
297  'op' => 'delete',
298  'client_ip' => $this->getRequest()->getIP()
299  ]
300  );
301  }
302  return Status::newGood();
303 
304  case 'cancel':
305  $this->getOutput()->redirect( $this->getPageTitle()->getFullURL() );
306  return false;
307  }
308 
309  return false;
310  }
311 
312  private function save( array $data ) {
313  $bp = BotPassword::newUnsaved( [
314  'centralId' => $this->userId,
315  'appId' => $this->par,
316  'restrictions' => $data['restrictions'],
317  'grants' => array_merge(
319  // @phan-suppress-next-next-line PhanTypeMismatchArgumentInternal See phan issue #3163,
320  // it's probably failing to infer the type of $data['grants']
321  preg_replace( '/^grant-/', '', $data['grants'] )
322  )
323  ] );
324 
325  if ( $this->operation === 'insert' || !empty( $data['resetPassword'] ) ) {
326  $this->password = BotPassword::generatePassword( $this->getConfig() );
327  $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
328  $password = $passwordFactory->newFromPlaintext( $this->password );
329  } else {
330  $password = null;
331  }
332 
333  if ( $bp->save( $this->operation, $password ) ) {
334  $this->logger->info(
335  "Bot password {op} for {user}@{app_id}",
336  [
337  'op' => $this->operation,
338  'user' => $this->getUser()->getName(),
339  'app_id' => $this->par,
340  'centralId' => $this->userId,
341  'restrictions' => $data['restrictions'],
342  'grants' => $bp->getGrants(),
343  'client_ip' => $this->getRequest()->getIP()
344  ]
345  );
346  return Status::newGood();
347  } else {
348  // Messages: botpasswords-insert-failed, botpasswords-update-failed
349  return Status::newFatal( "botpasswords-{$this->operation}-failed", $this->par );
350  }
351  }
352 
353  public function onSuccess() {
354  $out = $this->getOutput();
355 
356  $username = $this->getUser()->getName();
357  switch ( $this->operation ) {
358  case 'insert':
359  $out->setPageTitle( $this->msg( 'botpasswords-created-title' )->text() );
360  $out->addWikiMsg( 'botpasswords-created-body', $this->par, $username );
361  break;
362 
363  case 'update':
364  $out->setPageTitle( $this->msg( 'botpasswords-updated-title' )->text() );
365  $out->addWikiMsg( 'botpasswords-updated-body', $this->par, $username );
366  break;
367 
368  case 'delete':
369  $out->setPageTitle( $this->msg( 'botpasswords-deleted-title' )->text() );
370  $out->addWikiMsg( 'botpasswords-deleted-body', $this->par, $username );
371  $this->password = null;
372  break;
373  }
374 
375  if ( $this->password !== null ) {
376  $sep = BotPassword::getSeparator();
377  $out->addWikiMsg(
378  'botpasswords-newpassword',
379  htmlspecialchars( $username . $sep . $this->par ),
380  htmlspecialchars( $this->password ),
381  htmlspecialchars( $username ),
382  htmlspecialchars( $this->par . $sep . $this->password )
383  );
384  $this->password = null;
385  }
386 
387  $out->addReturnTo( $this->getPageTitle() );
388  }
389 
390  protected function getGroupName() {
391  return 'users';
392  }
393 
394  protected function getDisplayFormat() {
395  return 'ooui';
396  }
397 }
static getValidGrants()
List all known grants.
Definition: MWGrants.php:31
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:69
static getSeparator()
Get the separator for combined user name + app ID.
static generatePassword( $config)
Returns a (raw, unhashed) random password string.
if(!isset( $args[0])) $lang
const APPID_MAXLENGTH
Definition: BotPassword.php:32
addButton( $data)
Add a button to the form.
Definition: HTMLForm.php:995
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Represents an invalid password hash.
Psr Log LoggerInterface $logger
getOutput()
Get the OutputPage being used for this instance.
Special page which uses an HTMLForm to handle processing.
static newUnsaved(array $data, $flags=self::READ_NORMAL)
Create an unsaved BotPassword.
setId( $id)
Definition: HTMLForm.php:1524
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:51
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
static getDB( $db)
Get a database connection for the bot passwords database.
Definition: BotPassword.php:76
static getHiddenGrants()
Get the list of grants that are hidden and should always be granted.
Definition: MWGrants.php:157
An error page which can definitely be safely rendered using the OutputPage.
setTableId( $id)
Set the id of the <table> or outermost <div> element.
Definition: HTMLForm.php:1513
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
addPreText( $msg)
Add HTML to introductory message.
Definition: HTMLForm.php:781
string $operation
Operation being performed: create, update, delete.
BotPassword null $botPassword
Bot password being edited, if any.
string null $par
The sub-page of the special page.
static factory( $providerId=null)
Fetch a CentralIdLookup.
getName()
Get the name of this Special Page.
checkExecutePermissions(User $user)
execute( $par)
Main execution point.
Show an error when any operation involving passwords fails to run.
static getRightsByGrant()
Map all grants to corresponding user rights.
Definition: MWGrants.php:41
requireLogin( $reasonMsg='exception-nologin-text', $titleMsg='exception-nologin')
If the user is not logged in, throws UserNotLoggedIn error.
getUser()
Shortcut to get the User executing this instance.
getConfig()
Shortcut to get main config object.
getLanguage()
Shortcut to get user&#39;s language.
static newFromCentralId( $centralId, $appId, $flags=self::READ_NORMAL)
Load a BotPassword from the database.
const DB_REPLICA
Definition: defines.php:25
setWrapperLegendMsg( $msg)
Prompt the whole form to be wrapped in a "<fieldset>", with this message as its "<legend>" element...
Definition: HTMLForm.php:1579
getRequest()
Get the WebRequest being used for this instance.
suppressDefaultSubmit( $suppressSubmit=true)
Stop a default submit button being shown for this form.
Definition: HTMLForm.php:1476
getPageTitle( $subpage=false)
Get a self-referential title object.
string $password
New password set, for communication between onSubmit() and onSuccess()
Let users manage bot passwords.
int $userId
Central user ID.
MediaWiki Linker LinkRenderer null $linkRenderer
Definition: SpecialPage.php:67