Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 501 |
|
0.00% |
0 / 10 |
CRAP | |
0.00% |
0 / 1 |
SpecialMWOAuthConsumerRegistration | |
0.00% |
0 / 501 |
|
0.00% |
0 / 10 |
3660 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
userCanExecute | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
displayRestrictionError | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 173 |
|
0.00% |
0 / 1 |
870 | |||
addSubtitleLinks | |
0.00% |
0 / 43 |
|
0.00% |
0 / 1 |
90 | |||
formatRow | |
0.00% |
0 / 56 |
|
0.00% |
0 / 1 |
12 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
proposeOAuth | |
0.00% |
0 / 210 |
|
0.00% |
0 / 1 |
132 | |||
fillDefaultFields | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\OAuth\Frontend\SpecialPages; |
4 | |
5 | /** |
6 | * (c) Aaron Schulz 2013, GPL |
7 | * |
8 | * This program is free software; you can redistribute it and/or modify |
9 | * it under the terms of the GNU General Public License as published by |
10 | * the Free Software Foundation; either version 2 of the License, or |
11 | * (at your option) any later version. |
12 | * |
13 | * This program is distributed in the hope that it will be useful, |
14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
16 | * GNU General Public License for more details. |
17 | * |
18 | * You should have received a copy of the GNU General Public License along |
19 | * with this program; if not, write to the Free Software Foundation, Inc., |
20 | * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. |
21 | * http://www.gnu.org/copyleft/gpl.html |
22 | */ |
23 | |
24 | use ErrorPageError; |
25 | use InvalidArgumentException; |
26 | use LogEventsList; |
27 | use LogPage; |
28 | use MediaWiki\Context\IContextSource; |
29 | use MediaWiki\Extension\OAuth\Backend\Consumer; |
30 | use MediaWiki\Extension\OAuth\Backend\Utils; |
31 | use MediaWiki\Extension\OAuth\Control\ConsumerAccessControl; |
32 | use MediaWiki\Extension\OAuth\Control\ConsumerSubmitControl; |
33 | use MediaWiki\Extension\OAuth\Frontend\Pagers\ListMyConsumersPager; |
34 | use MediaWiki\Extension\OAuth\Frontend\UIUtils; |
35 | use MediaWiki\Html\Html; |
36 | use MediaWiki\HTMLForm\Field\HTMLHiddenField; |
37 | use MediaWiki\HTMLForm\Field\HTMLRestrictionsField; |
38 | use MediaWiki\HTMLForm\HTMLForm; |
39 | use MediaWiki\Json\FormatJson; |
40 | use MediaWiki\Message\Message; |
41 | use MediaWiki\Permissions\GrantsInfo; |
42 | use MediaWiki\Permissions\GrantsLocalization; |
43 | use MediaWiki\Permissions\PermissionManager; |
44 | use MediaWiki\SpecialPage\SpecialPage; |
45 | use MediaWiki\Status\Status; |
46 | use MediaWiki\User\User; |
47 | use MediaWiki\WikiMap\WikiMap; |
48 | use MediaWiki\Xml\Xml; |
49 | use MWRestrictions; |
50 | use PermissionsError; |
51 | use stdClass; |
52 | use UserBlockedError; |
53 | use Wikimedia\Rdbms\IDatabase; |
54 | |
55 | /** |
56 | * Page that has registration request form and consumer update form |
57 | */ |
58 | class SpecialMWOAuthConsumerRegistration extends SpecialPage { |
59 | private PermissionManager $permissionManager; |
60 | private GrantsInfo $grantsInfo; |
61 | private GrantsLocalization $grantsLocalization; |
62 | |
63 | public function __construct( |
64 | PermissionManager $permissionManager, |
65 | GrantsInfo $grantsInfo, |
66 | GrantsLocalization $grantsLocalization |
67 | ) { |
68 | parent::__construct( 'OAuthConsumerRegistration' ); |
69 | $this->permissionManager = $permissionManager; |
70 | $this->grantsInfo = $grantsInfo; |
71 | $this->grantsLocalization = $grantsLocalization; |
72 | } |
73 | |
74 | public function doesWrites() { |
75 | return true; |
76 | } |
77 | |
78 | public function userCanExecute( User $user ) { |
79 | return $user->isEmailConfirmed(); |
80 | } |
81 | |
82 | public function displayRestrictionError() { |
83 | throw new PermissionsError( null, [ 'mwoauthconsumerregistration-need-emailconfirmed' ] ); |
84 | } |
85 | |
86 | public function execute( $par ) { |
87 | $this->requireNamedUser( 'mwoauth-named-account-required-reason' ); |
88 | $this->checkPermissions(); |
89 | |
90 | $request = $this->getRequest(); |
91 | $user = $this->getUser(); |
92 | $lang = $this->getLanguage(); |
93 | $centralUserId = Utils::getCentralIdFromLocalUser( $user ); |
94 | |
95 | // Redirect to HTTPs if attempting to access this page via HTTP. |
96 | // Proposals and updates to consumers can involve sending new secrets. |
97 | if ( $this->getConfig()->get( 'MWOAuthSecureTokenTransfer' ) |
98 | && $request->detectProtocol() == 'http' |
99 | && substr( wfExpandUrl( '/', PROTO_HTTPS ), 0, 8 ) === 'https://' |
100 | ) { |
101 | $redirUrl = str_replace( 'http://', 'https://', $request->getFullRequestURL() ); |
102 | $this->getOutput()->redirect( $redirUrl ); |
103 | $this->getOutput()->addVaryHeader( 'X-Forwarded-Proto' ); |
104 | return; |
105 | } |
106 | |
107 | $this->setHeaders(); |
108 | $this->getOutput()->disallowUserJs(); |
109 | $this->getOutput()->addModules( 'mediawiki.special' ); |
110 | $this->addHelpLink( 'Help:OAuth' ); |
111 | |
112 | $block = $user->getBlock(); |
113 | if ( $block ) { |
114 | throw new UserBlockedError( $block ); |
115 | } |
116 | $this->checkReadOnly(); |
117 | |
118 | // Format is Special:OAuthConsumerRegistration[/propose/<oauth1a|oauth2>|/list|/update/<consumer key>] |
119 | $navigation = $par !== null ? explode( '/', $par ) : []; |
120 | $action = $navigation[0] ?? ''; |
121 | $subPage = $navigation[1] ?? ''; |
122 | |
123 | if ( $this->getConfig()->get( 'MWOAuthReadOnly' ) && $action !== 'list' ) { |
124 | throw new ErrorPageError( 'mwoauth-error', 'mwoauth-db-readonly' ); |
125 | } |
126 | |
127 | switch ( $action ) { |
128 | case 'propose': |
129 | if ( !$this->permissionManager->userHasRight( $user, 'mwoauthproposeconsumer' ) ) { |
130 | throw new PermissionsError( 'mwoauthproposeconsumer' ); |
131 | } |
132 | |
133 | if ( $subPage === '' ) { |
134 | $this->getOutput()->addWikiMsg( 'mwoauthconsumerregistration-propose-text' ); |
135 | break; |
136 | } |
137 | |
138 | $allWikis = Utils::getAllWikiNames(); |
139 | $showGrants = $this->grantsInfo->getValidGrants(); |
140 | if ( $subPage === 'oauth2' ) { |
141 | $this->proposeOAuth( Consumer::OAUTH_VERSION_2, $user, $allWikis, $lang, $showGrants ); |
142 | break; |
143 | } elseif ( $subPage === 'oauth1a' ) { |
144 | $this->proposeOAuth( Consumer::OAUTH_VERSION_1, $user, $allWikis, $lang, $showGrants ); |
145 | break; |
146 | } else { |
147 | $this->getOutput()->redirect( 'Special:OAuthConsumerRegistration/propose' ); |
148 | } |
149 | break; |
150 | case 'update': |
151 | if ( !$this->permissionManager->userHasRight( $user, 'mwoauthupdateownconsumer' ) ) { |
152 | throw new PermissionsError( 'mwoauthupdateownconsumer' ); |
153 | } |
154 | |
155 | $dbr = Utils::getCentralDB( DB_REPLICA ); |
156 | $cmrAc = ConsumerAccessControl::wrap( |
157 | Consumer::newFromKey( $dbr, $subPage ), $this->getContext() ); |
158 | if ( !$cmrAc ) { |
159 | $this->getOutput()->addWikiMsg( 'mwoauth-invalid-consumer-key' ); |
160 | break; |
161 | } elseif ( $cmrAc->getDAO()->getDeleted() |
162 | && !$this->permissionManager->userHasRight( $user, 'mwoauthviewsuppressed' ) |
163 | ) { |
164 | throw new PermissionsError( 'mwoauthviewsuppressed' ); |
165 | } elseif ( $cmrAc->getDAO()->getUserId() !== $centralUserId ) { |
166 | // Do not show private information to other users |
167 | $this->getOutput()->addWikiMsg( 'mwoauth-invalid-consumer-key' ); |
168 | break; |
169 | } |
170 | $oldSecretKey = $cmrAc->getDAO()->getSecretKey(); |
171 | |
172 | $dbw = Utils::getCentralDB( DB_PRIMARY ); |
173 | $control = new ConsumerSubmitControl( $this->getContext(), [], $dbw ); |
174 | $form = HTMLForm::factory( 'ooui', |
175 | $control->registerValidators( [ |
176 | 'info' => [ |
177 | 'type' => 'info', |
178 | 'raw' => true, |
179 | 'default' => UIUtils::generateInfoTable( [ |
180 | 'mwoauth-consumer-name' => $cmrAc->getName(), |
181 | 'mwoauth-consumer-version' => $cmrAc->getVersion(), |
182 | 'mwoauth-oauth-version' => $cmrAc->getOAuthVersion() === Consumer::OAUTH_VERSION_2 ? |
183 | $this->msg( 'mwoauth-oauth-version-2' )->text() : |
184 | $this->msg( 'mwoauth-oauth-version-1' )->text(), |
185 | 'mwoauth-consumer-key' => $cmrAc->getConsumerKey(), |
186 | ], $this->getContext() ), |
187 | ], |
188 | 'restrictions' => [ |
189 | 'class' => HTMLRestrictionsField::class, |
190 | 'required' => true, |
191 | 'default' => $cmrAc->getDAO()->getRestrictions(), |
192 | ], |
193 | 'resetSecret' => [ |
194 | 'type' => 'check', |
195 | 'label-message' => 'mwoauthconsumerregistration-resetsecretkey', |
196 | 'default' => false, |
197 | ], |
198 | 'rsaKey' => [ |
199 | 'type' => 'textarea', |
200 | 'label-message' => 'mwoauth-consumer-rsakey', |
201 | 'required' => false, |
202 | 'default' => $cmrAc->getDAO()->getRsaKey(), |
203 | 'rows' => 5, |
204 | ], |
205 | 'reason' => [ |
206 | 'type' => 'text', |
207 | 'label-message' => 'mwoauth-consumer-reason', |
208 | 'required' => !$cmrAc->getOwnerOnly(), |
209 | ], |
210 | 'consumerKey' => [ |
211 | 'type' => 'hidden', |
212 | 'default' => $cmrAc->getConsumerKey(), |
213 | ], |
214 | 'changeToken' => [ |
215 | 'type' => 'hidden', |
216 | 'default' => $cmrAc->getDAO()->getChangeToken( $this->getContext() ), |
217 | ], |
218 | 'action' => [ |
219 | 'type' => 'hidden', |
220 | 'default' => 'update' |
221 | ] |
222 | ] ), |
223 | $this->getContext() |
224 | ); |
225 | $form->setSubmitCallback( |
226 | static function ( array $data, IContextSource $context ) use ( $control ) { |
227 | $control->setInputParameters( $data ); |
228 | return $control->submit(); |
229 | } |
230 | ); |
231 | $form->setWrapperLegendMsg( 'mwoauthconsumerregistration-update-legend' ); |
232 | $form->setSubmitTextMsg( 'mwoauthconsumerregistration-update-submit' ); |
233 | $form->addPreHtml( |
234 | $this->msg( 'mwoauthconsumerregistration-update-text' )->parseAsBlock() ); |
235 | |
236 | $status = $form->show(); |
237 | if ( $status instanceof Status && $status->isOK() ) { |
238 | /** @var Consumer $cmr */ |
239 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable |
240 | $cmr = $status->value['result']['consumer']; |
241 | $this->getOutput()->addWikiMsg( 'mwoauthconsumerregistration-updated' ); |
242 | $curSecretKey = $cmr->getSecretKey(); |
243 | // token reset? |
244 | if ( $oldSecretKey !== $curSecretKey ) { |
245 | if ( $cmr->getOwnerOnly() ) { |
246 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable |
247 | $accessToken = $status->value['result']['accessToken']; |
248 | if ( $cmr->getOAuthVersion() === Consumer::OAUTH_VERSION_2 ) { |
249 | // If we just add raw AT to the page, it would go 3000px wide |
250 | $accessToken = Html::element( 'span', [ |
251 | 'style' => 'overflow-wrap: break-word' |
252 | ], (string)$accessToken ); |
253 | |
254 | $this->getOutput()->addWikiMsg( |
255 | 'mwoauthconsumerregistration-secretreset-owner-only-oauth2', |
256 | $cmr->getConsumerKey(), |
257 | Utils::hmacDBSecret( $cmr->getSecretKey() ), |
258 | Message::rawParam( $accessToken ) |
259 | ); |
260 | } else { |
261 | $this->getOutput()->addWikiMsg( |
262 | 'mwoauthconsumerregistration-secretreset-owner-only-oauth1', |
263 | $cmr->getConsumerKey(), |
264 | Utils::hmacDBSecret( $curSecretKey ), |
265 | $accessToken->key, |
266 | Utils::hmacDBSecret( $accessToken->secret ) |
267 | ); |
268 | } |
269 | } else { |
270 | $this->getOutput()->addWikiMsg( 'mwoauthconsumerregistration-secretreset', |
271 | Utils::hmacDBSecret( $curSecretKey ) ); |
272 | } |
273 | } |
274 | $this->getOutput()->returnToMain(); |
275 | } else { |
276 | $out = $this->getOutput(); |
277 | // Show all of the status updates |
278 | $logPage = new LogPage( 'mwoauthconsumer' ); |
279 | $out->addHTML( Xml::element( 'h2', null, $logPage->getName()->text() ) ); |
280 | LogEventsList::showLogExtract( $out, 'mwoauthconsumer', '', '', [ |
281 | 'conds' => [ |
282 | 'ls_field' => 'OAuthConsumer', |
283 | 'ls_value' => $cmrAc->getConsumerKey(), |
284 | ], |
285 | 'flags' => LogEventsList::NO_EXTRA_USER_LINKS, |
286 | ] ); |
287 | } |
288 | break; |
289 | case 'list': |
290 | $pager = new ListMyConsumersPager( $this, [], $centralUserId ); |
291 | if ( $pager->getNumRows() ) { |
292 | $this->getOutput()->addHTML( $pager->getNavigationBar() ); |
293 | $this->getOutput()->addHTML( $pager->getBody() ); |
294 | $this->getOutput()->addHTML( $pager->getNavigationBar() ); |
295 | } else { |
296 | $this->getOutput()->addWikiMsg( "mwoauthconsumerregistration-none" ); |
297 | } |
298 | # Every 30th view, prune old deleted items |
299 | if ( mt_rand( 0, 29 ) == 0 ) { |
300 | Utils::runAutoMaintenance( Utils::getCentralDB( DB_PRIMARY ) ); |
301 | } |
302 | break; |
303 | default: |
304 | $this->getOutput()->addWikiMsg( 'mwoauthconsumerregistration-maintext' ); |
305 | } |
306 | |
307 | $this->addSubtitleLinks( $action, $subPage ); |
308 | |
309 | $this->getOutput()->addModuleStyles( 'ext.MWOAuth.styles' ); |
310 | } |
311 | |
312 | /** |
313 | * Show navigation links |
314 | * |
315 | * @param string $action |
316 | * @param string $subPage |
317 | * @return void |
318 | */ |
319 | protected function addSubtitleLinks( $action, $subPage ) { |
320 | $listLinks = []; |
321 | if ( $action === 'propose' && $subPage ) { |
322 | if ( $subPage === 'oauth1a' ) { |
323 | $listLinks[] = $this->msg( 'mwoauthconsumerregistration-propose-oauth1' )->escaped(); |
324 | $listLinks[] = $this->getLinkRenderer()->makeKnownLink( |
325 | $this->getPageTitle( 'propose/oauth2' ), |
326 | $this->msg( 'mwoauthconsumerregistration-propose-oauth2' )->text() |
327 | ); |
328 | } elseif ( $subPage === 'oauth2' ) { |
329 | $listLinks[] = $this->getLinkRenderer()->makeKnownLink( |
330 | $this->getPageTitle( 'propose/oauth1a' ), |
331 | $this->msg( 'mwoauthconsumerregistration-propose-oauth1' )->text() |
332 | ); |
333 | $listLinks[] = $this->msg( 'mwoauthconsumerregistration-propose-oauth2' )->escaped(); |
334 | } |
335 | } else { |
336 | $listLinks[] = $this->getLinkRenderer()->makeKnownLink( |
337 | $this->getPageTitle( 'propose/oauth1a' ), |
338 | $this->msg( 'mwoauthconsumerregistration-propose-oauth1' )->text() |
339 | ); |
340 | $listLinks[] = $this->getLinkRenderer()->makeKnownLink( |
341 | $this->getPageTitle( 'propose/oauth2' ), |
342 | $this->msg( 'mwoauthconsumerregistration-propose-oauth2' )->text() |
343 | ); |
344 | } |
345 | if ( $subPage || $action !== 'list' ) { |
346 | $listLinks[] = $this->getLinkRenderer()->makeKnownLink( |
347 | $this->getPageTitle( 'list' ), |
348 | $this->msg( 'mwoauthconsumerregistration-list' )->text() |
349 | ); |
350 | } else { |
351 | $listLinks[] = $this->msg( 'mwoauthconsumerregistration-list' )->escaped(); |
352 | } |
353 | if ( $subPage && $action == 'update' ) { |
354 | $listLinks[] = $this->getLinkRenderer()->makeKnownLink( |
355 | SpecialPage::getTitleFor( 'OAuthListConsumers', "view/$subPage" ), |
356 | $this->msg( 'mwoauthconsumer-consumer-view' )->text() |
357 | ); |
358 | } |
359 | |
360 | $linkHtml = $this->getLanguage()->pipeList( $listLinks ); |
361 | |
362 | $viewall = $this->msg( 'parentheses' )->rawParams( |
363 | $this->getLinkRenderer()->makeKnownLink( |
364 | $this->getPageTitle(), |
365 | $this->msg( 'mwoauthconsumerregistration-main' )->text() |
366 | ) |
367 | )->escaped(); |
368 | |
369 | $this->getOutput()->setSubtitle( |
370 | "<strong>" . $this->msg( 'mwoauthconsumerregistration-navigation' )->escaped() . |
371 | "</strong> [{$linkHtml}] <strong>{$viewall}</strong>" ); |
372 | } |
373 | |
374 | /** |
375 | * @param IDatabase $db |
376 | * @param stdClass $row |
377 | * @return string |
378 | */ |
379 | public function formatRow( IDatabase $db, $row ) { |
380 | $cmrAc = ConsumerAccessControl::wrap( |
381 | Consumer::newFromRow( $db, $row ), $this->getContext() ); |
382 | $cmrKey = $cmrAc->getConsumerKey(); |
383 | |
384 | $links = []; |
385 | $links[] = $this->getLinkRenderer()->makeKnownLink( |
386 | SpecialPage::getTitleFor( 'OAuthListConsumers', "view/$cmrKey" ), |
387 | $this->msg( 'mwoauthlistconsumers-view' )->text() |
388 | ); |
389 | |
390 | $links[] = $this->getLinkRenderer()->makeKnownLink( |
391 | $this->getPageTitle( 'update/' . $cmrKey ), |
392 | $this->msg( 'mwoauthconsumerregistration-manage' )->text() |
393 | ); |
394 | |
395 | $links = $this->getLanguage()->pipeList( $links ); |
396 | |
397 | $time = htmlspecialchars( $this->getLanguage()->timeanddate( |
398 | wfTimestamp( TS_MW, $cmrAc->getRegistration() ), true ) ); |
399 | |
400 | $stageKey = Consumer::$stageNames[$cmrAc->getStage()]; |
401 | $encStageKey = htmlspecialchars( $stageKey ); |
402 | // Show last log entry (@TODO: title namespace?) |
403 | // @TODO: inject DB |
404 | $logHtml = ''; |
405 | LogEventsList::showLogExtract( $logHtml, 'mwoauthconsumer', '', '', [ |
406 | 'conds' => [ |
407 | 'ls_field' => 'OAuthConsumer', |
408 | 'ls_value' => $cmrAc->getConsumerKey(), |
409 | ], |
410 | 'lim' => 1, |
411 | 'flags' => LogEventsList::NO_EXTRA_USER_LINKS, |
412 | ] ); |
413 | |
414 | $lang = $this->getLanguage(); |
415 | $oauthVersionMessage = $cmrAc->getOAuthVersion() === Consumer::OAUTH_VERSION_2 ? |
416 | $this->msg( 'mwoauth-oauth-version-2' )->text() : |
417 | $this->msg( 'mwoauth-oauth-version-1' )->text(); |
418 | $data = [ |
419 | 'mwoauthconsumerregistration-name' => $cmrAc->escapeForHtml( $cmrAc->getNameAndVersion() ), |
420 | 'mwoauth-oauth-version' => $cmrAc->escapeForHtml( $oauthVersionMessage ), |
421 | // Messages: mwoauth-consumer-stage-proposed, mwoauth-consumer-stage-rejected, |
422 | // mwoauth-consumer-stage-expired, mwoauth-consumer-stage-approved, |
423 | // mwoauth-consumer-stage-disabled |
424 | 'mwoauthconsumerregistration-stage' => |
425 | $this->msg( "mwoauth-consumer-stage-$stageKey" )->escaped(), |
426 | 'mwoauthconsumerregistration-description' => $cmrAc->escapeForHtml( |
427 | $cmrAc->get( 'description', static function ( $s ) use ( $lang ) { |
428 | return $lang->truncateForVisual( $s, 10024 ); |
429 | } ) |
430 | ), |
431 | 'mwoauthconsumerregistration-email' => $cmrAc->escapeForHtml( $cmrAc->getEmail() ), |
432 | 'mwoauthconsumerregistration-consumerkey' => $cmrAc->escapeForHtml( $cmrAc->getConsumerKey() ), |
433 | 'mwoauthconsumerregistration-lastchange' => $logHtml, |
434 | ]; |
435 | |
436 | $r = "<li class='mw-mwoauthconsumerregistration-{$encStageKey}'>"; |
437 | $r .= "<span>$time (<strong>{$links}</strong>)</span>"; |
438 | $r .= "<table class='mw-mwoauthconsumerregistration-body' " . |
439 | "cellspacing='1' cellpadding='3' border='1' width='100%'>"; |
440 | foreach ( $data as $msg => $encValue ) { |
441 | $r .= '<tr>' . |
442 | '<td><strong>' . $this->msg( $msg )->escaped() . '</strong></td>' . |
443 | '<td width=\'90%\'>' . $encValue . '</td>' . |
444 | '</tr>'; |
445 | } |
446 | $r .= '</table>'; |
447 | |
448 | $r .= '</li>'; |
449 | |
450 | return $r; |
451 | } |
452 | |
453 | protected function getGroupName() { |
454 | return 'users'; |
455 | } |
456 | |
457 | private function proposeOAuth( int $oauthVersion, User $user, $allWikis, $lang, $showGrants ) { |
458 | if ( !in_array( $oauthVersion, [ Consumer::OAUTH_VERSION_1, Consumer::OAUTH_VERSION_2 ] ) ) { |
459 | throw new InvalidArgumentException( 'Invalid OAuth version' ); |
460 | } |
461 | $dbw = Utils::getCentralDB( DB_PRIMARY ); |
462 | $control = new ConsumerSubmitControl( $this->getContext(), [], $dbw ); |
463 | |
464 | $grantNames = $this->grantsLocalization->getGrantDescriptionsWithClasses( |
465 | $showGrants, $this->getLanguage() ); |
466 | $formDescriptor = [ |
467 | 'oauthVersion' => [ |
468 | 'class' => HTMLHiddenField::class, |
469 | 'default' => $oauthVersion, |
470 | ], |
471 | 'name' => [ |
472 | 'type' => 'text', |
473 | 'label-message' => 'mwoauth-consumer-name', |
474 | 'size' => '45', |
475 | 'required' => true |
476 | ], |
477 | 'version' => [ |
478 | 'type' => 'text', |
479 | 'label-message' => 'mwoauth-consumer-version', |
480 | 'required' => true, |
481 | 'default' => "1.0" |
482 | ], |
483 | 'description' => [ |
484 | 'type' => 'textarea', |
485 | 'label-message' => 'mwoauth-consumer-description', |
486 | 'required' => true, |
487 | 'rows' => 5 |
488 | ], |
489 | 'ownerOnly' => [ |
490 | 'type' => 'check', |
491 | 'label-message' => [ 'mwoauth-consumer-owner-only', $user->getName() ], |
492 | 'help-message' => [ 'mwoauth-consumer-owner-only-help', $user->getName() ], |
493 | ], |
494 | 'callbackUrl' => [ |
495 | 'type' => 'text', |
496 | 'label-message' => 'mwoauth-consumer-callbackurl', |
497 | 'help-message' => ( $oauthVersion === Consumer::OAUTH_VERSION_2 ) |
498 | ? 'mwoauth-consumer-callbackurl-help' : null, |
499 | 'required' => true, |
500 | 'hide-if' => [ '!==', 'ownerOnly', '' ], |
501 | ], |
502 | 'callbackIsPrefix' => [ |
503 | 'oauthVersion' => Consumer::OAUTH_VERSION_1, |
504 | 'type' => 'check', |
505 | 'label-message' => 'mwoauth-consumer-callbackisprefix', |
506 | 'hide-if' => [ '!==', 'ownerOnly', '' ], |
507 | ], |
508 | 'email' => [ |
509 | 'type' => 'text', |
510 | 'label-message' => 'mwoauth-consumer-email', |
511 | 'required' => true, |
512 | 'readonly' => true, |
513 | 'default' => $user->getEmail(), |
514 | 'help-message' => 'mwoauth-consumer-email-help', |
515 | ], |
516 | 'wiki' => [ |
517 | 'type' => $allWikis ? 'combobox' : 'select', |
518 | 'options' => [ |
519 | $this->msg( 'mwoauth-consumer-allwikis' )->escaped() => '*', |
520 | $this->msg( 'mwoauth-consumer-wiki-thiswiki', WikiMap::getCurrentWikiId() ) |
521 | ->escaped() => WikiMap::getCurrentWikiId() |
522 | ] + array_flip( $allWikis ), |
523 | 'label-message' => 'mwoauth-consumer-wiki', |
524 | 'required' => true, |
525 | 'default' => '*' |
526 | ], |
527 | 'oauth2IsConfidential' => [ |
528 | 'oauthVersion' => Consumer::OAUTH_VERSION_2, |
529 | 'type' => 'check', |
530 | 'label-message' => 'mwoauth-oauth2-is-confidential', |
531 | 'help-message' => 'mwoauth-oauth2-is-confidential-help', |
532 | 'default' => 1 |
533 | ], |
534 | 'oauth2GrantTypes' => [ |
535 | 'oauthVersion' => Consumer::OAUTH_VERSION_2, |
536 | 'type' => 'multiselect', |
537 | 'label-message' => 'mwoauth-oauth2-granttypes', |
538 | 'hide-if' => [ '!==', 'ownerOnly', '' ], |
539 | 'options' => array_filter( [ |
540 | $this->msg( 'mwoauth-oauth2-granttype-auth-code' )->escaped() => 'authorization_code', |
541 | $this->msg( 'mwoauth-oauth2-granttype-refresh-token' )->escaped() => 'refresh_token', |
542 | $this->msg( 'mwoauth-oauth2-granttype-client-credentials' )->escaped() => 'client_credentials', |
543 | ], fn ( $grantType ) => in_array( $grantType, $this->getConfig()->get( 'OAuth2EnabledGrantTypes' ) ) ), |
544 | 'required' => true, |
545 | 'default' => [ 'authorization_code', 'refresh_token' ] |
546 | ], |
547 | 'granttype' => [ |
548 | 'type' => 'radio', |
549 | 'options-messages' => [ |
550 | 'grant-mwoauth-authonly' => 'authonly', |
551 | 'grant-mwoauth-authonlyprivate' => 'authonlyprivate', |
552 | 'mwoauth-granttype-normal' => 'normal', |
553 | ], |
554 | 'label-message' => 'mwoauth-consumer-granttypes', |
555 | 'default' => 'normal', |
556 | ], |
557 | // HACK separate field from grants because HTMLFormField cannot position help text on top |
558 | 'grantsHelp' => [ |
559 | 'type' => 'info', |
560 | 'default' => '', |
561 | 'help-message' => 'mwoauth-consumer-grantshelp', |
562 | ], |
563 | 'grants' => [ |
564 | 'type' => 'checkmatrix', |
565 | 'label-message' => 'mwoauth-consumer-grantsneeded', |
566 | 'hide-if' => [ '!==', 'granttype', 'normal' ], |
567 | 'columns' => [ |
568 | $this->msg( 'mwoauth-consumer-required-grant' )->escaped() => 'grant' |
569 | ], |
570 | 'rows' => array_combine( |
571 | $grantNames, |
572 | $showGrants |
573 | ), |
574 | 'tooltips-html' => array_combine( |
575 | $grantNames, |
576 | array_map( |
577 | fn ( $rights ) => Html::rawElement( 'ul', [], implode( '', array_map( |
578 | fn ( $right ) => Html::rawElement( 'li', [], $this->msg( "right-$right" )->parse() ), |
579 | $rights |
580 | ) ) ), |
581 | array_intersect_key( $this->grantsInfo->getRightsByGrant(), |
582 | array_fill_keys( $showGrants, true ) ) |
583 | ) |
584 | ), |
585 | 'force-options-on' => array_map( |
586 | static function ( $g ) { |
587 | return "grant-$g"; |
588 | }, |
589 | $this->grantsInfo->getHiddenGrants() |
590 | ), |
591 | // different format |
592 | 'validation-callback' => null, |
593 | ], |
594 | 'restrictions' => [ |
595 | 'class' => HTMLRestrictionsField::class, |
596 | 'required' => true, |
597 | 'default' => MWRestrictions::newDefault(), |
598 | ], |
599 | 'rsaKey' => [ |
600 | 'oauthVersion' => Consumer::OAUTH_VERSION_1, |
601 | 'type' => 'textarea', |
602 | 'label-message' => 'mwoauth-consumer-rsakey', |
603 | 'help-message' => 'mwoauth-consumer-rsakey-help', |
604 | 'required' => false, |
605 | 'default' => '', |
606 | 'rows' => 5 |
607 | ], |
608 | 'agreement' => [ |
609 | 'type' => 'check', |
610 | 'label-message' => 'mwoauth-consumer-developer-agreement', |
611 | 'required' => true, |
612 | ], |
613 | 'action' => [ |
614 | 'type' => 'hidden', |
615 | 'default' => 'propose' |
616 | ] |
617 | ]; |
618 | $formDescriptor = array_filter( $formDescriptor, |
619 | fn ( $field ) => !isset( $field['oauthVersion'] ) || $field['oauthVersion'] === $oauthVersion |
620 | ); |
621 | |
622 | $form = HTMLForm::factory( 'ooui', |
623 | $control->registerValidators( $formDescriptor ), |
624 | $this->getContext() |
625 | ); |
626 | $form->setSubmitCallback( |
627 | function ( array $data, IContextSource $context ) use ( $control ) { |
628 | // adapt form to controller |
629 | $data = $this->fillDefaultFields( $data ); |
630 | |
631 | $data['grants'] = FormatJson::encode( |
632 | preg_replace( '/^grant-/', '', $data['grants'] ) |
633 | ); |
634 | |
635 | // Force all ownerOnly clients to use client_credentials |
636 | if ( $data['ownerOnly'] ) { |
637 | $data['oauth2GrantTypes'] = [ 'client_credentials' ]; |
638 | } |
639 | |
640 | $control->setInputParameters( $data ); |
641 | return $control->submit(); |
642 | } |
643 | ); |
644 | $form->setWrapperLegendMsg( 'mwoauthconsumerregistration-propose-legend' ); |
645 | $form->setSubmitTextMsg( 'mwoauthconsumerregistration-propose-submit' ); |
646 | $form->addPreHtml( |
647 | // mwoauthconsumerregistration-propose-text-oauth1 |
648 | // mwoauthconsumerregistration-propose-text-oauth2 |
649 | $this->msg( "mwoauthconsumerregistration-propose-text-oauth$oauthVersion" )->parseAsBlock() ); |
650 | |
651 | $status = $form->show(); |
652 | if ( $status instanceof Status && $status->isOK() ) { |
653 | /** @var Consumer $cmr */ |
654 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable |
655 | $cmr = $status->value['result']['consumer']; |
656 | if ( $cmr->getOwnerOnly() ) { |
657 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable |
658 | $accessToken = $status->value['result']['accessToken']; |
659 | if ( $oauthVersion === Consumer::OAUTH_VERSION_1 ) { |
660 | $this->getOutput()->addWikiMsg( |
661 | "mwoauthconsumerregistration-created-owner-only-oauth1", |
662 | $cmr->getConsumerKey(), |
663 | Utils::hmacDBSecret( $cmr->getSecretKey() ), |
664 | $accessToken->key, |
665 | Utils::hmacDBSecret( $accessToken->secret ) |
666 | ); |
667 | } else { |
668 | // OAuth 2 access tokens are very long |
669 | $accessToken = Html::element( 'span', [ |
670 | 'style' => 'overflow-wrap: break-word' |
671 | ], (string)$accessToken ); |
672 | $this->getOutput()->addWikiMsg( |
673 | 'mwoauthconsumerregistration-created-owner-only-oauth2', |
674 | $cmr->getConsumerKey(), |
675 | Utils::hmacDBSecret( $cmr->getSecretKey() ), |
676 | Message::rawParam( $accessToken ) |
677 | ); |
678 | } |
679 | } elseif ( $cmr->getStage() === Consumer::STAGE_APPROVED ) { |
680 | // mwoauthconsumerregistration-autoapproved-oauth1 |
681 | // mwoauthconsumerregistration-autoapproved-oauth2 |
682 | $this->getOutput()->addWikiMsg( "mwoauthconsumerregistration-autoapproved-oauth$oauthVersion", |
683 | $cmr->getConsumerKey(), |
684 | Utils::hmacDBSecret( $cmr->getSecretKey() ) |
685 | ); |
686 | } else { |
687 | // mwoauthconsumerregistration-proposed-oauth1 |
688 | // mwoauthconsumerregistration-proposed-oauth2 |
689 | $this->getOutput()->addWikiMsg( "mwoauthconsumerregistration-proposed-oauth$oauthVersion", |
690 | $cmr->getConsumerKey(), |
691 | Utils::hmacDBSecret( $cmr->getSecretKey() ) ); |
692 | } |
693 | $this->getOutput()->returnToMain(); |
694 | } |
695 | } |
696 | |
697 | /** |
698 | * Used to adapt both OAuth forms to the same structure so SubmitControl::validateFields() doesn't fail |
699 | * |
700 | * @param array $form |
701 | * @return array |
702 | */ |
703 | private function fillDefaultFields( array $form ): array { |
704 | // These defaults are taken from the legacy form and are present regardless of OAuth version |
705 | $defaults = [ |
706 | 'callbackIsPrefix' => false, |
707 | 'oauth2IsConfidential' => true, |
708 | 'oauth2GrantTypes' => [ 'authorization_code', 'refresh_token' ], |
709 | 'granttype' => 'normal', |
710 | 'rsaKey' => '', |
711 | ]; |
712 | |
713 | $form = array_merge( $defaults, $form ); |
714 | |
715 | // 'callbackUrl' must be present, |
716 | // otherwise SubmitControl::validateFields() fails. |
717 | if ( $form['ownerOnly'] && !isset( $form['callbackUrl'] ) ) { |
718 | $form['callbackUrl'] = ''; |
719 | } |
720 | |
721 | return $form; |
722 | } |
723 | } |