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
25 * Let users manage bot passwords
27 * @ingroup SpecialPage
29 class SpecialBotPasswords
extends FormSpecialPage
{
31 /** @var int Central user ID */
34 /** @var BotPassword|null Bot password being edited, if any */
35 private $botPassword = null;
37 /** @var string Operation being performed: create, update, delete */
38 private $operation = null;
40 /** @var string New password set, for communication between onSubmit() and onSuccess() */
41 private $password = null;
43 public function __construct() {
44 parent
::__construct( 'BotPasswords', 'editmyprivateinfo' );
50 public function isListed() {
51 return $this->getConfig()->get( 'EnableBotPasswords' );
55 * Main execution point
56 * @param string|null $par
58 function execute( $par ) {
59 $this->getOutput()->disallowUserJs();
60 $this->requireLogin();
63 if ( strlen( $par ) === 0 ) {
65 } elseif ( strlen( $par ) > BotPassword
::APPID_MAXLENGTH
) {
66 throw new ErrorPageError( 'botpasswords', 'botpasswords-bad-appid',
67 [ htmlspecialchars( $par ) ] );
70 parent
::execute( $par );
73 protected function checkExecutePermissions( User
$user ) {
74 parent
::checkExecutePermissions( $user );
76 if ( !$this->getConfig()->get( 'EnableBotPasswords' ) ) {
77 throw new ErrorPageError( 'botpasswords', 'botpasswords-disabled' );
80 $this->userId
= CentralIdLookup
::factory()->centralIdFromLocalUser( $this->getUser() );
81 if ( !$this->userId
) {
82 throw new ErrorPageError( 'botpasswords', 'botpasswords-no-central-id' );
86 protected function getFormFields() {
87 $user = $this->getUser();
88 $request = $this->getRequest();
92 if ( $this->par
!== null ) {
93 $this->botPassword
= BotPassword
::newFromCentralId( $this->userId
, $this->par
);
94 if ( !$this->botPassword
) {
95 $this->botPassword
= BotPassword
::newUnsaved( [
96 'centralId' => $this->userId
,
97 'appId' => $this->par
,
101 $sep = BotPassword
::getSeparator();
104 'label-message' => 'username',
105 'default' => $this->getUser()->getName() . $sep . $this->par
108 if ( $this->botPassword
->isSaved() ) {
109 $fields['resetPassword'] = [
111 'label-message' => 'botpasswords-label-resetpassword',
115 $lang = $this->getLanguage();
116 $showGrants = MWGrants
::getValidGrants();
117 $fields['grants'] = [
118 'type' => 'checkmatrix',
119 'label-message' => 'botpasswords-label-grants',
120 'help-message' => 'botpasswords-help-grants',
122 $this->msg( 'botpasswords-label-grants-column' )->escaped() => 'grant'
124 'rows' => array_combine(
125 array_map( 'MWGrants::getGrantsLink', $showGrants ),
128 'default' => array_map(
132 $this->botPassword
->getGrants()
134 'tooltips' => array_combine(
135 array_map( 'MWGrants::getGrantsLink', $showGrants ),
137 function( $rights ) use ( $lang ) {
138 return $lang->semicolonList( array_map( 'User::getRightDescription', $rights ) );
140 array_intersect_key( MWGrants
::getRightsByGrant(), array_flip( $showGrants ) )
143 'force-options-on' => array_map(
147 MWGrants
::getHiddenGrants()
151 $fields['restrictions'] = [
152 'type' => 'textarea',
153 'label-message' => 'botpasswords-label-restrictions',
155 'default' => $this->botPassword
->getRestrictions()->toJson( true ),
157 'validation-callback' => function ( $v ) {
159 MWRestrictions
::newFromJson( $v );
161 } catch ( InvalidArgumentException
$ex ) {
162 return $ex->getMessage();
168 $dbr = BotPassword
::getDB( DB_SLAVE
);
172 [ 'bp_user' => $this->userId
],
175 foreach ( $res as $row ) {
177 'section' => 'existing',
180 'default' => Linker
::link(
181 $this->getPageTitle( $row->bp_app_id
),
182 htmlspecialchars( $row->bp_app_id
),
191 'section' => 'createnew',
192 'type' => 'textwithbutton',
193 'label-message' => 'botpasswords-label-appid',
194 'buttondefault' => $this->msg( 'botpasswords-label-create' )->text(),
196 'size' => BotPassword
::APPID_MAXLENGTH
,
197 'maxlength' => BotPassword
::APPID_MAXLENGTH
,
198 'validation-callback' => function ( $v ) {
200 return $v !== '' && strlen( $v ) <= BotPassword
::APPID_MAXLENGTH
;
214 protected function alterForm( HTMLForm
$form ) {
215 $form->setId( 'mw-botpasswords-form' );
216 $form->setTableId( 'mw-botpasswords-table' );
217 $form->addPreText( $this->msg( 'botpasswords-summary' )->parseAsBlock() );
218 $form->suppressDefaultSubmit();
220 if ( $this->par
!== null ) {
221 if ( $this->botPassword
->isSaved() ) {
222 $form->setWrapperLegendMsg( 'botpasswords-editexisting' );
226 'label-message' => 'botpasswords-label-update',
227 'flags' => [ 'primary', 'progressive' ],
232 'label-message' => 'botpasswords-label-delete',
233 'flags' => [ 'destructive' ],
236 $form->setWrapperLegendMsg( 'botpasswords-createnew' );
240 'label-message' => 'botpasswords-label-create',
241 'flags' => [ 'primary', 'constructive' ],
248 'label-message' => 'botpasswords-label-cancel'
253 public function onSubmit( array $data ) {
254 $op = $this->getRequest()->getVal( 'op', '' );
258 $this->getOutput()->redirect( $this->getPageTitle( $data['appId'] )->getFullURL() );
262 $this->operation
= 'insert';
263 return $this->save( $data );
266 $this->operation
= 'update';
267 return $this->save( $data );
270 $this->operation
= 'delete';
271 $bp = BotPassword
::newFromCentralId( $this->userId
, $this->par
);
275 return Status
::newGood();
278 $this->getOutput()->redirect( $this->getPageTitle()->getFullURL() );
285 private function save( array $data ) {
286 $bp = BotPassword
::newUnsaved( [
287 'centralId' => $this->userId
,
288 'appId' => $this->par
,
289 'restrictions' => MWRestrictions
::newFromJson( $data['restrictions'] ),
290 'grants' => array_merge(
291 MWGrants
::getHiddenGrants(),
292 preg_replace( '/^grant-/', '', $data['grants'] )
296 if ( $this->operation
=== 'insert' ||
!empty( $data['resetPassword'] ) ) {
297 $this->password
= PasswordFactory
::generateRandomPasswordString(
298 max( 32, $this->getConfig()->get( 'MinimalPasswordLength' ) )
300 $passwordFactory = new PasswordFactory();
301 $passwordFactory->init( RequestContext
::getMain()->getConfig() );
302 $password = $passwordFactory->newFromPlaintext( $this->password
);
307 if ( $bp->save( $this->operation
, $password ) ) {
308 return Status
::newGood();
310 // Messages: botpasswords-insert-failed, botpasswords-update-failed
311 return Status
::newFatal( "botpasswords-{$this->operation}-failed", $this->par
);
315 public function onSuccess() {
316 $out = $this->getOutput();
318 switch ( $this->operation
) {
320 $out->setPageTitle( $this->msg( 'botpasswords-created-title' )->text() );
321 $out->addWikiMsg( 'botpasswords-created-body', $this->par
);
325 $out->setPageTitle( $this->msg( 'botpasswords-updated-title' )->text() );
326 $out->addWikiMsg( 'botpasswords-updated-body', $this->par
);
330 $out->setPageTitle( $this->msg( 'botpasswords-deleted-title' )->text() );
331 $out->addWikiMsg( 'botpasswords-deleted-body', $this->par
);
332 $this->password
= null;
336 if ( $this->password
!== null ) {
337 $sep = BotPassword
::getSeparator();
339 'botpasswords-newpassword',
340 htmlspecialchars( $this->getUser()->getName() . $sep . $this->par
),
341 htmlspecialchars( $this->password
)
343 $this->password
= null;
346 $out->addReturnTo( $this->getPageTitle() );
350 * Return an array of subpages beginning with $search that this special page will accept.
352 * @param string $search Prefix to search for
353 * @param int $limit Maximum number of results to return (usually 10)
354 * @param int $offset Number of results to skip (usually 0)
355 * @return string[] Matching subpages
357 public function prefixSearchSubpages( $search, $limit, $offset ) {
358 $user = User
::newFromName( $search );
360 // No prefix suggestion for invalid user
363 // Autocomplete subpage as user list - public to allow caching
364 return UserNamePrefixSearch
::search( 'public', $search, $limit, $offset );
367 protected function getGroupName() {
371 protected function getDisplayFormat() {