MediaWiki REL1_35
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 public 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 {
171 $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
172
173 $dbr = BotPassword::getDB( DB_REPLICA );
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,
213 'size' => BotPassword::APPID_MAXLENGTH,
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 ( $bp === null ) {
326 // Messages: botpasswords-insert-failed, botpasswords-update-failed
327 return Status::newFatal( "botpasswords-{$this->operation}-failed", $this->par );
328 }
329
330 if ( $this->operation === 'insert' || !empty( $data['resetPassword'] ) ) {
331 $this->password = BotPassword::generatePassword( $this->getConfig() );
332 $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
333 $password = $passwordFactory->newFromPlaintext( $this->password );
334 } else {
335 $password = null;
336 }
337
338 $res = $bp->save( $this->operation, $password );
339
340 $success = $res->isGood();
341
342 $this->logger->info(
343 'Bot password {op} for {user}@{app_id} ' . ( $success ? 'succeeded' : 'failed' ),
344 [
345 'op' => $this->operation,
346 'user' => $this->getUser()->getName(),
347 'app_id' => $this->par,
348 'centralId' => $this->userId,
349 'restrictions' => $data['restrictions'],
350 'grants' => $bp->getGrants(),
351 'client_ip' => $this->getRequest()->getIP(),
352 'success' => $success,
353 ]
354 );
355
356 return $res;
357 }
358
359 public function onSuccess() {
360 $out = $this->getOutput();
361
362 $username = $this->getUser()->getName();
363 switch ( $this->operation ) {
364 case 'insert':
365 $out->setPageTitle( $this->msg( 'botpasswords-created-title' )->text() );
366 $out->addWikiMsg( 'botpasswords-created-body', $this->par, $username );
367 break;
368
369 case 'update':
370 $out->setPageTitle( $this->msg( 'botpasswords-updated-title' )->text() );
371 $out->addWikiMsg( 'botpasswords-updated-body', $this->par, $username );
372 break;
373
374 case 'delete':
375 $out->setPageTitle( $this->msg( 'botpasswords-deleted-title' )->text() );
376 $out->addWikiMsg( 'botpasswords-deleted-body', $this->par, $username );
377 $this->password = null;
378 break;
379 }
380
381 if ( $this->password !== null ) {
382 $sep = BotPassword::getSeparator();
383 $out->addWikiMsg(
384 'botpasswords-newpassword',
385 htmlspecialchars( $username . $sep . $this->par ),
386 htmlspecialchars( $this->password ),
387 htmlspecialchars( $username ),
388 htmlspecialchars( $this->par . $sep . $this->password )
389 );
390 $this->password = null;
391 }
392
393 $out->addReturnTo( $this->getPageTitle() );
394 }
395
396 protected function getGroupName() {
397 return 'users';
398 }
399
400 protected function getDisplayFormat() {
401 return 'ooui';
402 }
403}
Utility class for bot passwords.
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:135
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:794
Represents an invalid password hash.
static getHiddenGrants()
Get the list of grants that are hidden and should always be granted.
Definition MWGrants.php:158
static getRightsByGrant()
Map all grants to corresponding user rights.
Definition MWGrants.php:41
static getValidGrants()
List all known grants.
Definition MWGrants.php:31
PSR-3 logger instance factory.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Show an error when any operation involving passwords fails to run.
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.
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...
int $userId
Central user ID.
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.
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.
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.
MediaWiki Linker LinkRenderer null $linkRenderer
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:60
const DB_REPLICA
Definition defines.php:25
if(!isset( $args[0])) $lang