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