3 * Implements Special:BotPasswords
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
21 * @ingroup SpecialPage
24 use MediaWiki\Logger\LoggerFactory
;
27 * Let users manage bot passwords
29 * @ingroup SpecialPage
31 class SpecialBotPasswords
extends FormSpecialPage
{
33 /** @var int Central user ID */
36 /** @var BotPassword|null Bot password being edited, if any */
37 private $botPassword = null;
39 /** @var string Operation being performed: create, update, delete */
40 private $operation = null;
42 /** @var string New password set, for communication between onSubmit() and onSuccess() */
43 private $password = null;
45 /** @var Psr\Log\LoggerInterface */
46 private $logger = null;
48 public function __construct() {
49 parent
::__construct( 'BotPasswords', 'editmyprivateinfo' );
50 $this->logger
= LoggerFactory
::getInstance( 'authentication' );
56 public function isListed() {
57 return $this->getConfig()->get( 'EnableBotPasswords' );
61 * Main execution point
62 * @param string|null $par
64 function execute( $par ) {
65 $this->getOutput()->disallowUserJs();
66 $this->requireLogin();
69 if ( strlen( $par ) === 0 ) {
71 } elseif ( strlen( $par ) > BotPassword
::APPID_MAXLENGTH
) {
72 throw new ErrorPageError( 'botpasswords', 'botpasswords-bad-appid',
73 [ htmlspecialchars( $par ) ] );
76 parent
::execute( $par );
79 protected function checkExecutePermissions( User
$user ) {
80 parent
::checkExecutePermissions( $user );
82 if ( !$this->getConfig()->get( 'EnableBotPasswords' ) ) {
83 throw new ErrorPageError( 'botpasswords', 'botpasswords-disabled' );
86 $this->userId
= CentralIdLookup
::factory()->centralIdFromLocalUser( $this->getUser() );
87 if ( !$this->userId
) {
88 throw new ErrorPageError( 'botpasswords', 'botpasswords-no-central-id' );
92 protected function getFormFields() {
95 if ( $this->par
!== null ) {
96 $this->botPassword
= BotPassword
::newFromCentralId( $this->userId
, $this->par
);
97 if ( !$this->botPassword
) {
98 $this->botPassword
= BotPassword
::newUnsaved( [
99 'centralId' => $this->userId
,
100 'appId' => $this->par
,
104 $sep = BotPassword
::getSeparator();
107 'label-message' => 'username',
108 'default' => $this->getUser()->getName() . $sep . $this->par
111 if ( $this->botPassword
->isSaved() ) {
112 $fields['resetPassword'] = [
114 'label-message' => 'botpasswords-label-resetpassword',
118 $lang = $this->getLanguage();
119 $showGrants = MWGrants
::getValidGrants();
120 $fields['grants'] = [
121 'type' => 'checkmatrix',
122 'label-message' => 'botpasswords-label-grants',
123 'help-message' => 'botpasswords-help-grants',
125 $this->msg( 'botpasswords-label-grants-column' )->escaped() => 'grant'
127 'rows' => array_combine(
128 array_map( 'MWGrants::getGrantsLink', $showGrants ),
131 'default' => array_map(
135 $this->botPassword
->getGrants()
137 'tooltips' => array_combine(
138 array_map( 'MWGrants::getGrantsLink', $showGrants ),
140 function ( $rights ) use ( $lang ) {
141 return $lang->semicolonList( array_map( 'User::getRightDescription', $rights ) );
143 array_intersect_key( MWGrants
::getRightsByGrant(), array_flip( $showGrants ) )
146 'force-options-on' => array_map(
150 MWGrants
::getHiddenGrants()
154 $fields['restrictions'] = [
155 'class' => HTMLRestrictionsField
::class,
157 'default' => $this->botPassword
->getRestrictions(),
161 $linkRenderer = $this->getLinkRenderer();
162 $dbr = BotPassword
::getDB( DB_REPLICA
);
166 [ 'bp_user' => $this->userId
],
169 foreach ( $res as $row ) {
171 'section' => 'existing',
174 'default' => $linkRenderer->makeKnownLink(
175 $this->getPageTitle( $row->bp_app_id
),
182 'section' => 'createnew',
183 'type' => 'textwithbutton',
184 'label-message' => 'botpasswords-label-appid',
185 'buttondefault' => $this->msg( 'botpasswords-label-create' )->text(),
186 'buttonflags' => [ 'progressive', 'primary' ],
188 'size' => BotPassword
::APPID_MAXLENGTH
,
189 'maxlength' => BotPassword
::APPID_MAXLENGTH
,
190 'validation-callback' => function ( $v ) {
192 return $v !== '' && strlen( $v ) <= BotPassword
::APPID_MAXLENGTH
;
206 protected function alterForm( HTMLForm
$form ) {
207 $form->setId( 'mw-botpasswords-form' );
208 $form->setTableId( 'mw-botpasswords-table' );
209 $form->addPreText( $this->msg( 'botpasswords-summary' )->parseAsBlock() );
210 $form->suppressDefaultSubmit();
212 if ( $this->par
!== null ) {
213 if ( $this->botPassword
->isSaved() ) {
214 $form->setWrapperLegendMsg( 'botpasswords-editexisting' );
218 'label-message' => 'botpasswords-label-update',
219 'flags' => [ 'primary', 'progressive' ],
224 'label-message' => 'botpasswords-label-delete',
225 'flags' => [ 'destructive' ],
228 $form->setWrapperLegendMsg( 'botpasswords-createnew' );
232 'label-message' => 'botpasswords-label-create',
233 'flags' => [ 'primary', 'progressive' ],
240 'label-message' => 'botpasswords-label-cancel'
245 public function onSubmit( array $data ) {
246 $op = $this->getRequest()->getVal( 'op', '' );
250 $this->getOutput()->redirect( $this->getPageTitle( $data['appId'] )->getFullURL() );
254 $this->operation
= 'insert';
255 return $this->save( $data );
258 $this->operation
= 'update';
259 return $this->save( $data );
262 $this->operation
= 'delete';
263 $bp = BotPassword
::newFromCentralId( $this->userId
, $this->par
);
267 "Bot password {op} for {user}@{app_id}",
269 'app_id' => $this->par
,
270 'user' => $this->getUser()->getName(),
271 'centralId' => $this->userId
,
273 'client_ip' => $this->getRequest()->getIP()
277 return Status
::newGood();
280 $this->getOutput()->redirect( $this->getPageTitle()->getFullURL() );
287 private function save( array $data ) {
288 $bp = BotPassword
::newUnsaved( [
289 'centralId' => $this->userId
,
290 'appId' => $this->par
,
291 'restrictions' => $data['restrictions'],
292 'grants' => array_merge(
293 MWGrants
::getHiddenGrants(),
294 preg_replace( '/^grant-/', '', $data['grants'] )
298 if ( $this->operation
=== 'insert' ||
!empty( $data['resetPassword'] ) ) {
299 $this->password
= BotPassword
::generatePassword( $this->getConfig() );
300 $passwordFactory = new PasswordFactory();
301 $passwordFactory->init( RequestContext
::getMain()->getConfig() );
302 $password = $passwordFactory->newFromPlaintext( $this->password
);
307 if ( $bp->save( $this->operation
, $password ) ) {
309 "Bot password {op} for {user}@{app_id}",
311 'op' => $this->operation
,
312 'user' => $this->getUser()->getName(),
313 'app_id' => $this->par
,
314 'centralId' => $this->userId
,
315 'restrictions' => $data['restrictions'],
316 'grants' => $bp->getGrants(),
317 'client_ip' => $this->getRequest()->getIP()
320 return Status
::newGood();
322 // Messages: botpasswords-insert-failed, botpasswords-update-failed
323 return Status
::newFatal( "botpasswords-{$this->operation}-failed", $this->par
);
327 public function onSuccess() {
328 $out = $this->getOutput();
330 $username = $this->getUser()->getName();
331 switch ( $this->operation
) {
333 $out->setPageTitle( $this->msg( 'botpasswords-created-title' )->text() );
334 $out->addWikiMsg( 'botpasswords-created-body', $this->par
, $username );
338 $out->setPageTitle( $this->msg( 'botpasswords-updated-title' )->text() );
339 $out->addWikiMsg( 'botpasswords-updated-body', $this->par
, $username );
343 $out->setPageTitle( $this->msg( 'botpasswords-deleted-title' )->text() );
344 $out->addWikiMsg( 'botpasswords-deleted-body', $this->par
, $username );
345 $this->password
= null;
349 if ( $this->password
!== null ) {
350 $sep = BotPassword
::getSeparator();
352 'botpasswords-newpassword',
353 htmlspecialchars( $username . $sep . $this->par
),
354 htmlspecialchars( $this->password
),
355 htmlspecialchars( $username ),
356 htmlspecialchars( $this->par
. $sep . $this->password
)
358 $this->password
= null;
361 $out->addReturnTo( $this->getPageTitle() );
364 protected function getGroupName() {
368 protected function getDisplayFormat() {