Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.44% covered (success)
97.44%
114 / 117
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
RequestClient
97.44% covered (success)
97.44%
114 / 117
83.33% covered (warning)
83.33%
5 / 6
18
0.00% covered (danger)
0.00%
0 / 1
 getFixedParams
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 getBodyParamSettings
100.00% covered (success)
100.00%
74 / 74
100.00% covered (success)
100.00%
1 / 1
1
 getUnifiedParams
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 adjustScopes
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 findAndRemoveScope
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 validate
72.73% covered (warning)
72.73%
8 / 11
0.00% covered (danger)
0.00%
0 / 1
7.99
1<?php
2
3namespace MediaWiki\Extension\OAuth\Rest\Handler;
4
5use MediaWiki\Extension\OAuth\Entity\ClientEntity;
6use MediaWiki\Extension\OAuth\Repository\ScopeRepository;
7use MediaWiki\Json\FormatJson;
8use MediaWiki\Rest\LocalizedHttpException;
9use MediaWiki\Rest\Validator\Validator;
10use MediaWiki\Utils\MWRestrictions;
11use Wikimedia\Message\MessageValue;
12use Wikimedia\ParamValidator\ParamValidator;
13
14/**
15 * Handles the oauth2/client endpoint, which creates
16 * a new consumer for the user
17 */
18class RequestClient extends AbstractClientHandler {
19
20    /**
21     * @inheritDoc
22     */
23    protected function getFixedParams(): array {
24        return [
25            'oauthVersion' => '2.0',
26            'agreement' => true,
27            'action' => 'propose',
28            'granttype' => 'normal',
29            'rsaKey' => '',
30            'restrictions' => MWRestrictions::newDefault(),
31        ];
32    }
33
34    /**
35     * @inheritDoc
36     */
37    public function getBodyParamSettings(): array {
38        $scopeRepo = new ScopeRepository();
39        return [
40            'name' => [
41                self::PARAM_SOURCE => 'body',
42                ParamValidator::PARAM_TYPE => 'string',
43                ParamValidator::PARAM_REQUIRED => true,
44                self::PARAM_DESCRIPTION => new MessageValue( 'mwoauth-rest-param-desc-name' ),
45            ],
46            'version' => [
47                self::PARAM_SOURCE => 'body',
48                ParamValidator::PARAM_TYPE => [ '1.0', '2.0' ],
49                ParamValidator::PARAM_REQUIRED => false,
50                ParamValidator::PARAM_DEFAULT => '1.0',
51                self::PARAM_DESCRIPTION => new MessageValue( 'mwoauth-rest-param-desc-version' ),
52            ],
53            'description' => [
54                self::PARAM_SOURCE => 'body',
55                ParamValidator::PARAM_TYPE => 'string',
56                ParamValidator::PARAM_REQUIRED => true,
57                self::PARAM_DESCRIPTION => new MessageValue( 'mwoauth-rest-param-desc-description' ),
58            ],
59            'wiki' => [
60                self::PARAM_SOURCE => 'body',
61                ParamValidator::PARAM_TYPE => 'string',
62                ParamValidator::PARAM_REQUIRED => false,
63                ParamValidator::PARAM_DEFAULT => '*',
64                self::PARAM_DESCRIPTION => new MessageValue( 'mwoauth-rest-param-desc-wiki' ),
65            ],
66            'owner_only' => [
67                self::PARAM_SOURCE => 'body',
68                ParamValidator::PARAM_TYPE => 'boolean',
69                self::PARAM_DESCRIPTION => new MessageValue( 'mwoauth-rest-param-desc-owner_only' ),
70            ],
71            'callback_url' => [
72                self::PARAM_SOURCE => 'body',
73                ParamValidator::PARAM_TYPE => 'string',
74                ParamValidator::PARAM_REQUIRED => false,
75                ParamValidator::PARAM_DEFAULT => '',
76                self::PARAM_DESCRIPTION => new MessageValue( 'mwoauth-rest-param-desc-callback_url' ),
77            ],
78            'callback_is_prefix' => [
79                self::PARAM_SOURCE => 'body',
80                ParamValidator::PARAM_TYPE => 'boolean',
81                ParamValidator::PARAM_REQUIRED => false,
82                ParamValidator::PARAM_DEFAULT => false,
83                self::PARAM_DESCRIPTION => new MessageValue( 'mwoauth-rest-param-desc-callback_is_prefix' ),
84            ],
85            // Ignored; retained as an optional parameter for backwards compatibility.
86            // The consumer's contact address is always taken from the proposer's account.
87            'email' => [
88                self::PARAM_SOURCE => 'body',
89                ParamValidator::PARAM_TYPE => 'string',
90                ParamValidator::PARAM_REQUIRED => false,
91                self::PARAM_DESCRIPTION => new MessageValue( 'mwoauth-rest-param-desc-email' ),
92            ],
93            'is_confidential' => [
94                self::PARAM_SOURCE => 'body',
95                ParamValidator::PARAM_TYPE => 'boolean',
96                ParamValidator::PARAM_REQUIRED => true,
97                self::PARAM_DESCRIPTION => new MessageValue( 'mwoauth-rest-param-desc-is_confidential' ),
98            ],
99            'grant_types'  => [
100                self::PARAM_SOURCE => 'body',
101                ParamValidator::PARAM_TYPE => 'string',
102                ParamValidator::PARAM_REQUIRED => true,
103                ParamValidator::PARAM_ISMULTI => true,
104                self::PARAM_DESCRIPTION => new MessageValue( 'mwoauth-rest-param-desc-grant_types' ),
105            ],
106            'scopes' => [
107                self::PARAM_SOURCE => 'body',
108                ParamValidator::PARAM_TYPE => $scopeRepo->getAllowedScopes(),
109                ParamValidator::PARAM_REQUIRED => true,
110                ParamValidator::PARAM_ISMULTI => true,
111                self::PARAM_DESCRIPTION => new MessageValue( 'mwoauth-rest-param-desc-scopes' ),
112            ]
113        ];
114    }
115
116    /**
117     * @inheritDoc
118     */
119    protected function getUnifiedParams(): array {
120        $params = parent::getUnifiedParams();
121        if (
122            count( $params['oauth2GrantTypes'] ) === 1
123            && in_array( ClientEntity::GRANT_TYPE_CLIENT_CREDENTIALS, $params['oauth2GrantTypes'], true )
124        ) {
125            $params['callbackUrl'] = '';
126            $params['oauth2IsConfidential'] = true;
127        }
128        return $this->adjustScopes( $params );
129    }
130
131    /**
132     * This is needed to adjust OAuth2 scope array to old grant/granttype params
133     *
134     * @param array $finalParams
135     * @return array
136     */
137    private function adjustScopes( array $finalParams ): array {
138        $scopeRepo = new ScopeRepository();
139        $allowedScopes = $scopeRepo->getAllowedScopes();
140
141        $scopes = array_filter( $finalParams['grants'], static function ( $scope ) use ( $allowedScopes ) {
142            return in_array( $scope, $allowedScopes );
143        } );
144
145        if ( $this->findAndRemoveScope( 'mwoauth-authonly', $scopes ) ) {
146            $finalParams['granttype'] = 'authonly';
147        }
148        if ( $this->findAndRemoveScope( 'mwoauth-authonlyprivate', $scopes ) ) {
149            $finalParams['granttype'] = 'authonlyprivate';
150        }
151
152        if ( !in_array( 'basic', $scopes ) ) {
153            $scopes[] = 'basic';
154        }
155        $finalParams['grants'] = FormatJson::encode( $scopes );
156
157        return $finalParams;
158    }
159
160    /**
161     * @param string $searchKey
162     * @param array &$values
163     * @return bool
164     */
165    private function findAndRemoveScope( $searchKey, array &$values ): bool {
166        $index = array_search( $searchKey, $values );
167        if ( $index === false ) {
168            return false;
169        }
170        array_splice( $values, $index, 1 );
171
172        return true;
173    }
174
175    /**
176     * @inheritDoc
177     */
178    public function validate( Validator $restValidator ) {
179        parent::validate( $restValidator );
180
181        $params = $this->getValidatedBody();
182        $grantTypes = $params['grant_types'] ?? [];
183        $isClientCredentialsOnly = count( $grantTypes ) === 1
184            && in_array( ClientEntity::GRANT_TYPE_CLIENT_CREDENTIALS, $grantTypes, true );
185
186        if (
187            ( isset( $params['owner_only'] ) && !$params['owner_only'] ) &&
188            ( isset( $params['callback_url'] ) && !$params['callback_url'] ) &&
189            !$isClientCredentialsOnly
190        ) {
191            throw new LocalizedHttpException(
192                new MessageValue( 'mwoauth-error-missing-callback-url-non-owner', [] ), 400
193            );
194        }
195    }
196}