Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 97 |
|
0.00% |
0 / 10 |
CRAP | |
0.00% |
0 / 1 |
SubmitControl | |
0.00% |
0 / 97 |
|
0.00% |
0 / 10 |
2162 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
setInputParameters | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
submit | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
30 | |||
registerValidators | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
validateFieldInternal | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
56 | |||
getDefaultValidationError | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
42 | |||
getValidationResult | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
132 | |||
getRequiredFields | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
checkBasePermissions | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
validateFields | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
72 | |||
processAction | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
failure | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
success | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\OAuth\Control; |
4 | |
5 | use ApiMessage; |
6 | use ContextSource; |
7 | use HTMLForm; |
8 | use IContextSource; |
9 | use LogicException; |
10 | use MediaWiki\Status\Status; |
11 | use Message; |
12 | use MessageSpecifier; |
13 | use MWException; |
14 | use StatusValue; |
15 | |
16 | /** |
17 | * (c) Aaron Schulz 2013, GPL |
18 | * |
19 | * This program is free software; you can redistribute it and/or modify |
20 | * it under the terms of the GNU General Public License as published by |
21 | * the Free Software Foundation; either version 2 of the License, or |
22 | * (at your option) any later version. |
23 | * |
24 | * This program is distributed in the hope that it will be useful, |
25 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
26 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
27 | * GNU General Public License for more details. |
28 | * |
29 | * You should have received a copy of the GNU General Public License along |
30 | * with this program; if not, write to the Free Software Foundation, Inc., |
31 | * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. |
32 | * http://www.gnu.org/copyleft/gpl.html |
33 | */ |
34 | |
35 | /** |
36 | * Handle the logic of submitting a client request |
37 | */ |
38 | abstract class SubmitControl extends ContextSource { |
39 | /** @var array (field name => value) */ |
40 | protected $vals; |
41 | |
42 | /** |
43 | * @param IContextSource $context |
44 | * @param array $params |
45 | */ |
46 | public function __construct( IContextSource $context, array $params ) { |
47 | $this->setContext( $context ); |
48 | $this->vals = $params; |
49 | } |
50 | |
51 | /** |
52 | * @param array $params |
53 | */ |
54 | public function setInputParameters( array $params ) { |
55 | $this->vals = $params; |
56 | } |
57 | |
58 | /** |
59 | * Attempt to validate and submit this data |
60 | * |
61 | * This will check basic permissions, validate the action and parameters |
62 | * and route the submission handling to the internal subclass function. |
63 | * |
64 | * @throws MWException |
65 | * @return Status |
66 | */ |
67 | public function submit() { |
68 | $status = $this->checkBasePermissions(); |
69 | if ( !$status->isOK() ) { |
70 | return $status; |
71 | } |
72 | |
73 | $action = $this->vals['action']; |
74 | $required = $this->getRequiredFields(); |
75 | if ( !isset( $required[$action] ) ) { |
76 | // @TODO: check for field-specific message first |
77 | return $this->failure( 'invalid_field_action', 'mwoauth-invalid-field', 'action' ); |
78 | } |
79 | |
80 | $status = $this->validateFields( $required[$action] ); |
81 | if ( !$status->isOK() ) { |
82 | return $status; |
83 | } |
84 | |
85 | $status = $this->processAction( $action ); |
86 | if ( $status instanceof Status ) { |
87 | return $status; |
88 | } else { |
89 | throw new MWException( "Submission action '$action' not handled." ); |
90 | } |
91 | } |
92 | |
93 | /** |
94 | * Add the validators from getRequiredFields() to the given HTMLForm descriptor. |
95 | * Existing validators are not overridden. |
96 | * |
97 | * It also adds a checkbox to override warnings when necessary. |
98 | * |
99 | * @param array[] $descriptors |
100 | * @return array[] |
101 | */ |
102 | public function registerValidators( array $descriptors ) { |
103 | foreach ( $descriptors as $field => &$description ) { |
104 | if ( array_key_exists( 'validation-callback', $description ) ) { |
105 | // already set to something |
106 | continue; |
107 | } |
108 | $description['validation-callback'] = |
109 | function ( $value, $allValues, $form ) use ( $field ) { |
110 | return $this->validateFieldInternal( $field, $value, $allValues, $form ); |
111 | }; |
112 | } |
113 | $descriptors['ignorewarnings'] = [ |
114 | 'type' => 'check', |
115 | 'label-message' => 'mwoauth-ignorewarnings', |
116 | 'cssclass' => 'mw-oauth-form-ignorewarnings-hidden', |
117 | ]; |
118 | return $descriptors; |
119 | } |
120 | |
121 | /** |
122 | * Do some basic checks and call the validator provided by getRequiredFields(). |
123 | * This method should not be called outside SubmitControl. |
124 | * |
125 | * @param string $field |
126 | * @param mixed $value |
127 | * @param array $allValues |
128 | * @param HTMLForm $form |
129 | * @throws MWException |
130 | * @return true|string |
131 | */ |
132 | public function validateFieldInternal( string $field, $value, array $allValues, HTMLForm $form ) { |
133 | if ( !isset( $allValues['action'] ) && isset( $this->vals['action'] ) ) { |
134 | // The action may be derived, especially for multi-button forms. |
135 | // Such an HTMLForm will not have an action key set in $allValues. |
136 | $allValues['action'] = $this->vals['action']; |
137 | } |
138 | if ( !isset( $allValues['action'] ) ) { |
139 | throw new MWException( "No form action defined; cannot validate fields." ); |
140 | } |
141 | $validators = $this->getRequiredFields(); |
142 | if ( !isset( $validators[$allValues['action']][$field] ) ) { |
143 | // nothing to check |
144 | return true; |
145 | } |
146 | $validator = $validators[$allValues['action']][$field]; |
147 | $validationResult = $this->getValidationResult( $validator, $value, $allValues, $form ); |
148 | if ( $validationResult === false ) { |
149 | return $this->getDefaultValidationError( $field, $value, $form )->text(); |
150 | } elseif ( $validationResult instanceof ApiMessage ) { |
151 | return $validationResult->parse(); |
152 | } |
153 | return true; |
154 | } |
155 | |
156 | /** |
157 | * Generate an error message for a field. Used when the validator returns false. |
158 | * |
159 | * @param string $field |
160 | * @param mixed $value |
161 | * @param HTMLForm|null $form |
162 | * @return Message Error message (to be rendered via text()). |
163 | */ |
164 | private function getDefaultValidationError( string $field, $value, HTMLForm $form = null ): Message { |
165 | $errorMessage = $this->msg( 'mwoauth-invalid-field-' . $field ); |
166 | if ( !$errorMessage->isDisabled() ) { |
167 | return $errorMessage; |
168 | } |
169 | |
170 | $generic = ''; |
171 | if ( $form && $form->getField( $field )->canDisplayErrors() ) { |
172 | // error can be shown right next to the field so no need to mention the field name |
173 | $generic = '-generic'; |
174 | } |
175 | |
176 | $problem = 'invalid'; |
177 | if ( $value === '' && !$generic ) { |
178 | $problem = 'missing'; |
179 | } |
180 | |
181 | // messages: mwoauth-missing-field, mwoauth-invalid-field, mwoauth-invalid-field-generic |
182 | return $this->msg( "mwoauth-$problem-field$generic", $field ); |
183 | } |
184 | |
185 | /** |
186 | * @param mixed $validator One of the callbacks registered via registerValidator. |
187 | * @param mixed $value The value of the field being validated. |
188 | * @param array $allValues All field values, keyed by field name. |
189 | * @param HTMLForm|null $form |
190 | * @return bool|ApiMessage |
191 | * @phan-param string|callable(mixed,array):(bool|StatusValue) $validator |
192 | */ |
193 | private function getValidationResult( $validator, $value, array $allValues, HTMLForm $form = null ) { |
194 | if ( is_string( $validator ) ) { |
195 | return preg_match( $validator, $value ?? '' ); |
196 | } |
197 | $result = $validator( $value, $allValues ); |
198 | if ( $result instanceof StatusValue ) { |
199 | if ( $result->isGood() ) { |
200 | return true; |
201 | } elseif ( count( $result->getErrors() ) !== 1 ) { |
202 | throw new LogicException( 'Validator return status has too many errors: ' |
203 | . $result ); |
204 | } |
205 | [ $errors, $warnings ] = $result->splitByErrorType(); |
206 | if ( $errors->isOK() ) { |
207 | // $result is a warning - if the user checked "ignore warnings", ignore; |
208 | // otherwise show the checkbox |
209 | if ( $form ) { |
210 | // This is a horrible hack. There doesn't seem to be a way to modify a form's |
211 | // CSS classes or other display properties between validation and rendering. |
212 | $form->setId( 'oauth-form-with-warnings' ); |
213 | } |
214 | |
215 | if ( $allValues['ignorewarnings'] ?? false ) { |
216 | return true; |
217 | } |
218 | } |
219 | $result = $result->getErrors()[0]['message']; |
220 | } |
221 | if ( is_bool( $result ) || $result instanceof ApiMessage ) { |
222 | return $result; |
223 | } |
224 | |
225 | $type = gettype( $result ); |
226 | if ( $type === 'object' ) { |
227 | $type = get_class( $result ); |
228 | } |
229 | throw new LogicException( 'Invalid validator return type: ' . $type ); |
230 | } |
231 | |
232 | /** |
233 | * Get the field names and their validation methods. Fields can be omitted. Fields that are |
234 | * not |
235 | * |
236 | * A validation method is either a regex string or a callable. |
237 | * Callables take (field value, field/value map) as params and must return a boolean or a |
238 | * StatusValue with a single ApiMessage in it. If that is a warning, the user will be allowed |
239 | * to override it. A StatusValue with an error of boolean false will prevent submission. |
240 | * |
241 | * When false is returned, the error message mwoauth-invalid-field-<fieldname> will be shown |
242 | * if it exists. Otherwise, a generic message will be used (see getDefaultValidationError()). |
243 | * |
244 | * @return array (action => (field name => validation regex or function)) |
245 | * @phan-return array<string,array<string,string|callable(mixed):(bool|StatusValue)|callable(mixed,array):(bool|StatusValue)>> |
246 | */ |
247 | abstract protected function getRequiredFields(); |
248 | |
249 | /** |
250 | * Check action-independent permissions against the user for this submission |
251 | * |
252 | * @return Status |
253 | */ |
254 | abstract protected function checkBasePermissions(); |
255 | |
256 | /** |
257 | * Check that the action is valid and that the required fields are valid |
258 | * |
259 | * @param array $required (field => regex or callback) |
260 | * @phan-param array<string,string|callable(mixed,array):bool|StatusValue> $required |
261 | * @return Status |
262 | */ |
263 | protected function validateFields( array $required ) { |
264 | foreach ( $required as $field => $validator ) { |
265 | if ( !isset( $this->vals[$field] ) ) { |
266 | return $this->failure( "missing_field_$field", 'mwoauth-missing-field', $field ); |
267 | } elseif ( !is_scalar( $this->vals[$field] ) |
268 | && !in_array( $field, [ 'restrictions', 'oauth2GrantTypes' ], true ) |
269 | ) { |
270 | return $this->failure( "invalid_field_$field", 'mwoauth-invalid-field', $field ); |
271 | } |
272 | if ( is_string( $this->vals[$field] ) ) { |
273 | $this->vals[$field] = trim( $this->vals[$field] ); |
274 | } |
275 | $validationResult = $this->getValidationResult( $validator, $this->vals[$field], $this->vals ); |
276 | if ( $validationResult === false ) { |
277 | $message = $this->getDefaultValidationError( $field, $this->vals[$field] ); |
278 | return $this->failure( "invalid_field_$field", $message ); |
279 | } elseif ( $validationResult instanceof ApiMessage ) { |
280 | return $this->failure( $validationResult->getApiCode(), $validationResult, $field ); |
281 | } |
282 | } |
283 | return $this->success(); |
284 | } |
285 | |
286 | /** |
287 | * Attempt to validate and submit this data for the given action |
288 | * |
289 | * @param string $action |
290 | * @return Status |
291 | */ |
292 | abstract protected function processAction( $action ); |
293 | |
294 | /** |
295 | * @param string $error API error key |
296 | * @param string|MessageSpecifier $msg Message |
297 | * @param mixed ...$params Additional arguments used as message parameters |
298 | * @return Status |
299 | */ |
300 | protected function failure( $error, $msg, ...$params ) { |
301 | // Use the same logic as wfMessage |
302 | if ( isset( $params[0] ) && is_array( $params[0] ) ) { |
303 | $params = $params[0]; |
304 | } |
305 | $status = Status::newFatal( $this->msg( $msg, $params ) ); |
306 | $status->value = [ 'error' => $error, 'result' => null ]; |
307 | return $status; |
308 | } |
309 | |
310 | /** |
311 | * @param mixed|null $value |
312 | * @return Status |
313 | */ |
314 | protected function success( $value = null ) { |
315 | return Status::newGood( [ 'error' => null, 'result' => $value ] ); |
316 | } |
317 | } |