Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 501
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialMWOAuthConsumerRegistration
0.00% covered (danger)
0.00%
0 / 501
0.00% covered (danger)
0.00%
0 / 10
3660
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 doesWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 userCanExecute
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 displayRestrictionError
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 173
0.00% covered (danger)
0.00%
0 / 1
870
 addSubtitleLinks
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
90
 formatRow
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 1
12
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 proposeOAuth
0.00% covered (danger)
0.00%
0 / 210
0.00% covered (danger)
0.00%
0 / 1
132
 fillDefaultFields
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace 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
24use ErrorPageError;
25use FormatJson;
26use HTMLForm;
27use HTMLHiddenField;
28use HTMLRestrictionsField;
29use IContextSource;
30use InvalidArgumentException;
31use LogEventsList;
32use LogPage;
33use MediaWiki\Extension\OAuth\Backend\Consumer;
34use MediaWiki\Extension\OAuth\Backend\Utils;
35use MediaWiki\Extension\OAuth\Control\ConsumerAccessControl;
36use MediaWiki\Extension\OAuth\Control\ConsumerSubmitControl;
37use MediaWiki\Extension\OAuth\Frontend\Pagers\ListMyConsumersPager;
38use MediaWiki\Extension\OAuth\Frontend\UIUtils;
39use MediaWiki\Html\Html;
40use MediaWiki\Permissions\GrantsInfo;
41use MediaWiki\Permissions\GrantsLocalization;
42use MediaWiki\Permissions\PermissionManager;
43use MediaWiki\SpecialPage\SpecialPage;
44use MediaWiki\Status\Status;
45use MediaWiki\User\User;
46use MediaWiki\WikiMap\WikiMap;
47use Message;
48use MWRestrictions;
49use PermissionsError;
50use stdClass;
51use UserBlockedError;
52use Wikimedia\Rdbms\IDatabase;
53use Xml;
54
55/**
56 * Page that has registration request form and consumer update form
57 */
58class 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}