MediaWiki REL1_37
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 {
192
193 $dbr = BotPassword::getDB( DB_REPLICA );
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
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,
233 'size' => BotPassword::APPID_MAXLENGTH,
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}
Utility class for bot passwords.
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:143
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:805
Represents an invalid password hash.
static getHiddenGrants()
Get the list of grants that are hidden and should always be granted.
Definition MWGrants.php:154
static getRightsByGrant()
Map all grants to corresponding user rights.
Definition MWGrants.php:41
static getValidGrants()
List all known grants.
Definition MWGrants.php:31
This serves as the entry point to the authentication system.
makeKnownLink( $target, $text=null, array $extraAttribs=[], array $query=[])
PSR-3 logger instance factory.
Show an error when any operation involving passwords fails to run.
Factory class for creating and checking Password objects.
Let users manage bot passwords.
string $password
New password set, for communication between onSubmit() and onSuccess()
BotPassword null $botPassword
Bot password being edited, if any.
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)
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.
CentralIdLookup $centralIdLookup
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...
int $userId
Central user ID.
PasswordFactory $passwordFactory
string $operation
Operation being performed: create, update, delete.
Psr Log LoggerInterface $logger
getName()
Get the name of this Special Page.
getOutput()
Get the OutputPage being used for this instance.
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.
AuthManager null $authManager
LinkRenderer null $linkRenderer
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.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:69
const DB_REPLICA
Definition defines.php:25
if(!isset( $args[0])) $lang