3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
21 namespace MediaWiki\Specials
;
24 use MediaWiki\Auth\AuthManager
;
25 use MediaWiki\Html\Html
;
26 use MediaWiki\HTMLForm\Field\HTMLRestrictionsField
;
27 use MediaWiki\HTMLForm\HTMLForm
;
28 use MediaWiki\Logger\LoggerFactory
;
29 use MediaWiki\MainConfigNames
;
30 use MediaWiki\Password\InvalidPassword
;
31 use MediaWiki\Password\PasswordError
;
32 use MediaWiki\Password\PasswordFactory
;
33 use MediaWiki\Permissions\GrantsInfo
;
34 use MediaWiki\Permissions\GrantsLocalization
;
35 use MediaWiki\SpecialPage\FormSpecialPage
;
36 use MediaWiki\Status\Status
;
37 use MediaWiki\User\BotPassword
;
38 use MediaWiki\User\CentralId\CentralIdLookup
;
39 use MediaWiki\User\User
;
40 use Psr\Log\LoggerInterface
;
43 * Let users manage bot passwords
45 * @ingroup SpecialPage
47 class SpecialBotPasswords
extends FormSpecialPage
{
49 /** @var int Central user ID */
52 /** @var BotPassword|null Bot password being edited, if any */
53 private $botPassword = null;
55 /** @var string|null Operation being performed: create, update, delete */
56 private $operation = null;
58 /** @var string|null New password set, for communication between onSubmit() and onSuccess() */
59 private $password = null;
61 private LoggerInterface
$logger;
62 private PasswordFactory
$passwordFactory;
63 private CentralIdLookup
$centralIdLookup;
64 private GrantsInfo
$grantsInfo;
65 private GrantsLocalization
$grantsLocalization;
68 * @param PasswordFactory $passwordFactory
69 * @param AuthManager $authManager
70 * @param CentralIdLookup $centralIdLookup
71 * @param GrantsInfo $grantsInfo
72 * @param GrantsLocalization $grantsLocalization
74 public function __construct(
75 PasswordFactory
$passwordFactory,
76 AuthManager
$authManager,
77 CentralIdLookup
$centralIdLookup,
78 GrantsInfo
$grantsInfo,
79 GrantsLocalization
$grantsLocalization
81 parent
::__construct( 'BotPasswords', 'editmyprivateinfo' );
82 $this->logger
= LoggerFactory
::getInstance( 'authentication' );
83 $this->passwordFactory
= $passwordFactory;
84 $this->centralIdLookup
= $centralIdLookup;
85 $this->setAuthManager( $authManager );
86 $this->grantsInfo
= $grantsInfo;
87 $this->grantsLocalization
= $grantsLocalization;
93 public function isListed() {
94 return $this->getConfig()->get( MainConfigNames
::EnableBotPasswords
);
97 protected function getLoginSecurityLevel() {
98 return $this->getName();
102 * Main execution point
103 * @param string|null $par
105 public function execute( $par ) {
106 $this->requireNamedUser();
107 $this->getOutput()->disallowUserJs();
108 $this->getOutput()->addModuleStyles( 'mediawiki.special' );
109 $this->addHelpLink( 'Manual:Bot_passwords' );
111 if ( $par !== null ) {
115 } elseif ( strlen( $par ) > BotPassword
::APPID_MAXLENGTH
) {
116 throw new ErrorPageError(
117 'botpasswords', 'botpasswords-bad-appid', [ htmlspecialchars( $par ) ]
122 parent
::execute( $par );
125 protected function checkExecutePermissions( User
$user ) {
126 parent
::checkExecutePermissions( $user );
128 if ( !$this->getConfig()->get( MainConfigNames
::EnableBotPasswords
) ) {
129 throw new ErrorPageError( 'botpasswords', 'botpasswords-disabled' );
132 $this->userId
= $this->centralIdLookup
->centralIdFromLocalUser( $this->getUser() );
133 if ( !$this->userId
) {
134 throw new ErrorPageError( 'botpasswords', 'botpasswords-no-central-id' );
138 protected function getFormFields() {
141 if ( $this->par
!== null ) {
142 $this->botPassword
= BotPassword
::newFromCentralId( $this->userId
, $this->par
);
143 if ( !$this->botPassword
) {
144 $this->botPassword
= BotPassword
::newUnsaved( [
145 'centralId' => $this->userId
,
146 'appId' => $this->par
,
150 $sep = BotPassword
::getSeparator();
153 'label-message' => 'username',
154 'default' => $this->getUser()->getName() . $sep . $this->par
157 if ( $this->botPassword
->isSaved() ) {
158 $fields['resetPassword'] = [
160 'label-message' => 'botpasswords-label-resetpassword',
162 if ( $this->botPassword
->isInvalid() ) {
163 $fields['resetPassword']['default'] = true;
167 $showGrants = $this->grantsInfo
->getValidGrants();
168 $grantNames = $this->grantsLocalization
->getGrantDescriptionsWithClasses(
169 $showGrants, $this->getLanguage() );
174 'help-message' => 'botpasswords-help-grants',
176 $fields['grants'] = [
177 'type' => 'checkmatrix',
178 'label-message' => 'botpasswords-label-grants',
180 $this->msg( 'botpasswords-label-grants-column' )->escaped() => 'grant'
182 'rows' => array_combine(
186 'default' => array_map(
187 static function ( $g ) {
190 $this->botPassword
->getGrants()
192 'tooltips-html' => array_combine(
195 fn ( $rights ) => Html
::rawElement( 'ul', [], implode( '', array_map(
196 fn ( $right ) => Html
::rawElement( 'li', [], $this->msg( "right-$right" )->parse() ),
199 array_intersect_key( $this->grantsInfo
->getRightsByGrant(),
200 array_fill_keys( $showGrants, true ) )
203 'force-options-on' => array_map(
204 static function ( $g ) {
207 $this->grantsInfo
->getHiddenGrants()
211 $fields['restrictions'] = [
212 'class' => HTMLRestrictionsField
::class,
214 'default' => $this->botPassword
->getRestrictions(),
218 $linkRenderer = $this->getLinkRenderer();
220 $dbr = BotPassword
::getReplicaDatabase();
221 $res = $dbr->newSelectQueryBuilder()
222 ->select( [ 'bp_app_id', 'bp_password' ] )
223 ->from( 'bot_passwords' )
224 ->where( [ 'bp_user' => $this->userId
] )
225 ->caller( __METHOD__
)->fetchResultSet();
226 foreach ( $res as $row ) {
228 $password = $this->passwordFactory
->newFromCiphertext( $row->bp_password
);
229 $passwordInvalid = $password instanceof InvalidPassword
;
231 } catch ( PasswordError
$ex ) {
232 $passwordInvalid = true;
235 $text = $linkRenderer->makeKnownLink(
236 $this->getPageTitle( $row->bp_app_id
),
239 if ( $passwordInvalid ) {
240 $text .= $this->msg( 'word-separator' )->escaped()
241 . $this->msg( 'botpasswords-label-needsreset' )->parse();
245 'section' => 'existing',
253 'section' => 'createnew',
254 'type' => 'textwithbutton',
255 'label-message' => 'botpasswords-label-appid',
256 'buttondefault' => $this->msg( 'botpasswords-label-create' )->text(),
257 'buttonflags' => [ 'progressive', 'primary' ],
259 'size' => BotPassword
::APPID_MAXLENGTH
,
260 'maxlength' => BotPassword
::APPID_MAXLENGTH
,
261 'validation-callback' => static function ( $v ) {
263 return $v !== '' && strlen( $v ) <= BotPassword
::APPID_MAXLENGTH
;
277 protected function alterForm( HTMLForm
$form ) {
278 $form->setId( 'mw-botpasswords-form' );
279 $form->setTableId( 'mw-botpasswords-table' );
280 $form->suppressDefaultSubmit();
282 if ( $this->par
!== null ) {
283 if ( $this->botPassword
->isSaved() ) {
284 $form->setWrapperLegendMsg( 'botpasswords-editexisting' );
288 'label-message' => 'botpasswords-label-update',
289 'flags' => [ 'primary', 'progressive' ],
294 'label-message' => 'botpasswords-label-delete',
295 'flags' => [ 'destructive' ],
298 $form->setWrapperLegendMsg( 'botpasswords-createnew' );
302 'label-message' => 'botpasswords-label-create',
303 'flags' => [ 'primary', 'progressive' ],
310 'label-message' => 'botpasswords-label-cancel'
315 public function onSubmit( array $data ) {
316 $op = $this->getRequest()->getVal( 'op', '' );
320 $this->getOutput()->redirect( $this->getPageTitle( $data['appId'] )->getFullURL() );
324 $this->operation
= 'insert';
325 return $this->save( $data );
328 $this->operation
= 'update';
329 return $this->save( $data );
332 $this->operation
= 'delete';
333 $bp = BotPassword
::newFromCentralId( $this->userId
, $this->par
);
337 "Bot password {op} for {user}@{app_id}",
339 'app_id' => $this->par
,
340 'user' => $this->getUser()->getName(),
341 'centralId' => $this->userId
,
343 'client_ip' => $this->getRequest()->getIP()
347 return Status
::newGood();
350 $this->getOutput()->redirect( $this->getPageTitle()->getFullURL() );
357 private function save( array $data ) {
358 $bp = BotPassword
::newUnsaved( [
359 'centralId' => $this->userId
,
360 'appId' => $this->par
,
361 'restrictions' => $data['restrictions'],
362 'grants' => array_merge(
363 $this->grantsInfo
->getHiddenGrants(),
364 // @phan-suppress-next-next-line PhanTypeMismatchArgumentInternal See phan issue #3163,
365 // it's probably failing to infer the type of $data['grants']
366 preg_replace( '/^grant-/', '', $data['grants'] )
370 if ( $bp === null ) {
371 // Messages: botpasswords-insert-failed, botpasswords-update-failed
372 return Status
::newFatal( "botpasswords-{$this->operation}-failed", $this->par
);
375 if ( $this->operation
=== 'insert' ||
!empty( $data['resetPassword'] ) ) {
376 $this->password
= BotPassword
::generatePassword( $this->getConfig() );
377 $password = $this->passwordFactory
->newFromPlaintext( $this->password
);
382 $res = $bp->save( $this->operation
, $password );
384 $success = $res->isGood();
387 'Bot password {op} for {user}@{app_id} ' . ( $success ?
'succeeded' : 'failed' ),
389 'op' => $this->operation
,
390 'user' => $this->getUser()->getName(),
391 'app_id' => $this->par
,
392 'centralId' => $this->userId
,
393 'restrictions' => $data['restrictions'],
394 'grants' => $bp->getGrants(),
395 'client_ip' => $this->getRequest()->getIP(),
396 'success' => $success,
403 public function onSuccess() {
404 $out = $this->getOutput();
406 $username = $this->getUser()->getName();
407 switch ( $this->operation
) {
409 $out->setPageTitleMsg( $this->msg( 'botpasswords-created-title' ) );
410 $out->addWikiMsg( 'botpasswords-created-body', $this->par
, $username );
414 $out->setPageTitleMsg( $this->msg( 'botpasswords-updated-title' ) );
415 $out->addWikiMsg( 'botpasswords-updated-body', $this->par
, $username );
419 $out->setPageTitleMsg( $this->msg( 'botpasswords-deleted-title' ) );
420 $out->addWikiMsg( 'botpasswords-deleted-body', $this->par
, $username );
421 $this->password
= null;
425 if ( $this->password
!== null ) {
426 $sep = BotPassword
::getSeparator();
428 'botpasswords-newpassword',
429 htmlspecialchars( $username . $sep . $this->par
),
430 htmlspecialchars( $this->password
),
431 htmlspecialchars( $username ),
432 htmlspecialchars( $this->par
. $sep . $this->password
)
434 $this->password
= null;
437 $out->addReturnTo( $this->getPageTitle() );
440 protected function getGroupName() {
444 protected function getDisplayFormat() {
449 /** @deprecated class alias since 1.41 */
450 class_alias( SpecialBotPasswords
::class, 'SpecialBotPasswords' );