Add PasswordFactory to MediaWikiServices
[lhc/web/wiklou.git] / includes / specials / SpecialBotPasswords.php
1 <?php
2 /**
3 * Implements Special:BotPasswords
4 *
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.
9 *
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.
14 *
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
19 *
20 * @file
21 * @ingroup SpecialPage
22 */
23
24 use MediaWiki\Logger\LoggerFactory;
25 use MediaWiki\MediaWikiServices;
26
27 /**
28 * Let users manage bot passwords
29 *
30 * @ingroup SpecialPage
31 */
32 class SpecialBotPasswords extends FormSpecialPage {
33
34 /** @var int Central user ID */
35 private $userId = 0;
36
37 /** @var BotPassword|null Bot password being edited, if any */
38 private $botPassword = null;
39
40 /** @var string Operation being performed: create, update, delete */
41 private $operation = null;
42
43 /** @var string New password set, for communication between onSubmit() and onSuccess() */
44 private $password = null;
45
46 /** @var Psr\Log\LoggerInterface */
47 private $logger = null;
48
49 public function __construct() {
50 parent::__construct( 'BotPasswords', 'editmyprivateinfo' );
51 $this->logger = LoggerFactory::getInstance( 'authentication' );
52 }
53
54 /**
55 * @return bool
56 */
57 public function isListed() {
58 return $this->getConfig()->get( 'EnableBotPasswords' );
59 }
60
61 protected function getLoginSecurityLevel() {
62 return $this->getName();
63 }
64
65 /**
66 * Main execution point
67 * @param string|null $par
68 */
69 function execute( $par ) {
70 $this->getOutput()->disallowUserJs();
71 $this->requireLogin();
72
73 $par = trim( $par );
74 if ( strlen( $par ) === 0 ) {
75 $par = null;
76 } elseif ( strlen( $par ) > BotPassword::APPID_MAXLENGTH ) {
77 throw new ErrorPageError( 'botpasswords', 'botpasswords-bad-appid',
78 [ htmlspecialchars( $par ) ] );
79 }
80
81 parent::execute( $par );
82 }
83
84 protected function checkExecutePermissions( User $user ) {
85 parent::checkExecutePermissions( $user );
86
87 if ( !$this->getConfig()->get( 'EnableBotPasswords' ) ) {
88 throw new ErrorPageError( 'botpasswords', 'botpasswords-disabled' );
89 }
90
91 $this->userId = CentralIdLookup::factory()->centralIdFromLocalUser( $this->getUser() );
92 if ( !$this->userId ) {
93 throw new ErrorPageError( 'botpasswords', 'botpasswords-no-central-id' );
94 }
95 }
96
97 protected function getFormFields() {
98 $fields = [];
99
100 if ( $this->par !== null ) {
101 $this->botPassword = BotPassword::newFromCentralId( $this->userId, $this->par );
102 if ( !$this->botPassword ) {
103 $this->botPassword = BotPassword::newUnsaved( [
104 'centralId' => $this->userId,
105 'appId' => $this->par,
106 ] );
107 }
108
109 $sep = BotPassword::getSeparator();
110 $fields[] = [
111 'type' => 'info',
112 'label-message' => 'username',
113 'default' => $this->getUser()->getName() . $sep . $this->par
114 ];
115
116 if ( $this->botPassword->isSaved() ) {
117 $fields['resetPassword'] = [
118 'type' => 'check',
119 'label-message' => 'botpasswords-label-resetpassword',
120 ];
121 if ( $this->botPassword->isInvalid() ) {
122 $fields['resetPassword']['default'] = true;
123 }
124 }
125
126 $lang = $this->getLanguage();
127 $showGrants = MWGrants::getValidGrants();
128 $fields['grants'] = [
129 'type' => 'checkmatrix',
130 'label-message' => 'botpasswords-label-grants',
131 'help-message' => 'botpasswords-help-grants',
132 'columns' => [
133 $this->msg( 'botpasswords-label-grants-column' )->escaped() => 'grant'
134 ],
135 'rows' => array_combine(
136 array_map( 'MWGrants::getGrantsLink', $showGrants ),
137 $showGrants
138 ),
139 'default' => array_map(
140 function ( $g ) {
141 return "grant-$g";
142 },
143 $this->botPassword->getGrants()
144 ),
145 'tooltips' => array_combine(
146 array_map( 'MWGrants::getGrantsLink', $showGrants ),
147 array_map(
148 function ( $rights ) use ( $lang ) {
149 return $lang->semicolonList( array_map( 'User::getRightDescription', $rights ) );
150 },
151 array_intersect_key( MWGrants::getRightsByGrant(), array_flip( $showGrants ) )
152 )
153 ),
154 'force-options-on' => array_map(
155 function ( $g ) {
156 return "grant-$g";
157 },
158 MWGrants::getHiddenGrants()
159 ),
160 ];
161
162 $fields['restrictions'] = [
163 'class' => HTMLRestrictionsField::class,
164 'required' => true,
165 'default' => $this->botPassword->getRestrictions(),
166 ];
167
168 } else {
169 $linkRenderer = $this->getLinkRenderer();
170 $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
171
172 $dbr = BotPassword::getDB( DB_REPLICA );
173 $res = $dbr->select(
174 'bot_passwords',
175 [ 'bp_app_id', 'bp_password' ],
176 [ 'bp_user' => $this->userId ],
177 __METHOD__
178 );
179 foreach ( $res as $row ) {
180 try {
181 $password = $passwordFactory->newFromCiphertext( $row->bp_password );
182 $passwordInvalid = $password instanceof InvalidPassword;
183 unset( $password );
184 } catch ( PasswordError $ex ) {
185 $passwordInvalid = true;
186 }
187
188 $text = $linkRenderer->makeKnownLink(
189 $this->getPageTitle( $row->bp_app_id ),
190 $row->bp_app_id
191 );
192 if ( $passwordInvalid ) {
193 $text .= $this->msg( 'word-separator' )->escaped()
194 . $this->msg( 'botpasswords-label-needsreset' )->parse();
195 }
196
197 $fields[] = [
198 'section' => 'existing',
199 'type' => 'info',
200 'raw' => true,
201 'default' => $text,
202 ];
203 }
204
205 $fields['appId'] = [
206 'section' => 'createnew',
207 'type' => 'textwithbutton',
208 'label-message' => 'botpasswords-label-appid',
209 'buttondefault' => $this->msg( 'botpasswords-label-create' )->text(),
210 'buttonflags' => [ 'progressive', 'primary' ],
211 'required' => true,
212 'size' => BotPassword::APPID_MAXLENGTH,
213 'maxlength' => BotPassword::APPID_MAXLENGTH,
214 'validation-callback' => function ( $v ) {
215 $v = trim( $v );
216 return $v !== '' && strlen( $v ) <= BotPassword::APPID_MAXLENGTH;
217 },
218 ];
219
220 $fields[] = [
221 'type' => 'hidden',
222 'default' => 'new',
223 'name' => 'op',
224 ];
225 }
226
227 return $fields;
228 }
229
230 protected function alterForm( HTMLForm $form ) {
231 $form->setId( 'mw-botpasswords-form' );
232 $form->setTableId( 'mw-botpasswords-table' );
233 $form->addPreText( $this->msg( 'botpasswords-summary' )->parseAsBlock() );
234 $form->suppressDefaultSubmit();
235
236 if ( $this->par !== null ) {
237 if ( $this->botPassword->isSaved() ) {
238 $form->setWrapperLegendMsg( 'botpasswords-editexisting' );
239 $form->addButton( [
240 'name' => 'op',
241 'value' => 'update',
242 'label-message' => 'botpasswords-label-update',
243 'flags' => [ 'primary', 'progressive' ],
244 ] );
245 $form->addButton( [
246 'name' => 'op',
247 'value' => 'delete',
248 'label-message' => 'botpasswords-label-delete',
249 'flags' => [ 'destructive' ],
250 ] );
251 } else {
252 $form->setWrapperLegendMsg( 'botpasswords-createnew' );
253 $form->addButton( [
254 'name' => 'op',
255 'value' => 'create',
256 'label-message' => 'botpasswords-label-create',
257 'flags' => [ 'primary', 'progressive' ],
258 ] );
259 }
260
261 $form->addButton( [
262 'name' => 'op',
263 'value' => 'cancel',
264 'label-message' => 'botpasswords-label-cancel'
265 ] );
266 }
267 }
268
269 public function onSubmit( array $data ) {
270 $op = $this->getRequest()->getVal( 'op', '' );
271
272 switch ( $op ) {
273 case 'new':
274 $this->getOutput()->redirect( $this->getPageTitle( $data['appId'] )->getFullURL() );
275 return false;
276
277 case 'create':
278 $this->operation = 'insert';
279 return $this->save( $data );
280
281 case 'update':
282 $this->operation = 'update';
283 return $this->save( $data );
284
285 case 'delete':
286 $this->operation = 'delete';
287 $bp = BotPassword::newFromCentralId( $this->userId, $this->par );
288 if ( $bp ) {
289 $bp->delete();
290 $this->logger->info(
291 "Bot password {op} for {user}@{app_id}",
292 [
293 'app_id' => $this->par,
294 'user' => $this->getUser()->getName(),
295 'centralId' => $this->userId,
296 'op' => 'delete',
297 'client_ip' => $this->getRequest()->getIP()
298 ]
299 );
300 }
301 return Status::newGood();
302
303 case 'cancel':
304 $this->getOutput()->redirect( $this->getPageTitle()->getFullURL() );
305 return false;
306 }
307
308 return false;
309 }
310
311 private function save( array $data ) {
312 $bp = BotPassword::newUnsaved( [
313 'centralId' => $this->userId,
314 'appId' => $this->par,
315 'restrictions' => $data['restrictions'],
316 'grants' => array_merge(
317 MWGrants::getHiddenGrants(),
318 preg_replace( '/^grant-/', '', $data['grants'] )
319 )
320 ] );
321
322 if ( $this->operation === 'insert' || !empty( $data['resetPassword'] ) ) {
323 $this->password = BotPassword::generatePassword( $this->getConfig() );
324 $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
325 $password = $passwordFactory->newFromPlaintext( $this->password );
326 } else {
327 $password = null;
328 }
329
330 if ( $bp->save( $this->operation, $password ) ) {
331 $this->logger->info(
332 "Bot password {op} for {user}@{app_id}",
333 [
334 'op' => $this->operation,
335 'user' => $this->getUser()->getName(),
336 'app_id' => $this->par,
337 'centralId' => $this->userId,
338 'restrictions' => $data['restrictions'],
339 'grants' => $bp->getGrants(),
340 'client_ip' => $this->getRequest()->getIP()
341 ]
342 );
343 return Status::newGood();
344 } else {
345 // Messages: botpasswords-insert-failed, botpasswords-update-failed
346 return Status::newFatal( "botpasswords-{$this->operation}-failed", $this->par );
347 }
348 }
349
350 public function onSuccess() {
351 $out = $this->getOutput();
352
353 $username = $this->getUser()->getName();
354 switch ( $this->operation ) {
355 case 'insert':
356 $out->setPageTitle( $this->msg( 'botpasswords-created-title' )->text() );
357 $out->addWikiMsg( 'botpasswords-created-body', $this->par, $username );
358 break;
359
360 case 'update':
361 $out->setPageTitle( $this->msg( 'botpasswords-updated-title' )->text() );
362 $out->addWikiMsg( 'botpasswords-updated-body', $this->par, $username );
363 break;
364
365 case 'delete':
366 $out->setPageTitle( $this->msg( 'botpasswords-deleted-title' )->text() );
367 $out->addWikiMsg( 'botpasswords-deleted-body', $this->par, $username );
368 $this->password = null;
369 break;
370 }
371
372 if ( $this->password !== null ) {
373 $sep = BotPassword::getSeparator();
374 $out->addWikiMsg(
375 'botpasswords-newpassword',
376 htmlspecialchars( $username . $sep . $this->par ),
377 htmlspecialchars( $this->password ),
378 htmlspecialchars( $username ),
379 htmlspecialchars( $this->par . $sep . $this->password )
380 );
381 $this->password = null;
382 }
383
384 $out->addReturnTo( $this->getPageTitle() );
385 }
386
387 protected function getGroupName() {
388 return 'users';
389 }
390
391 protected function getDisplayFormat() {
392 return 'ooui';
393 }
394 }