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