Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
88.89% |
112 / 126 |
|
61.54% |
8 / 13 |
CRAP | |
0.00% |
0 / 1 |
ApiOptions | |
88.89% |
112 / 126 |
|
61.54% |
8 / 13 |
44.42 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
97.56% |
80 / 82 |
|
0.00% |
0 / 1 |
29 | |||
getUserForUpdates | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getPreferences | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
resetPreferences | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setPreference | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
commitChanges | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
mustBePosted | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isWriteMode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getAllowedParams | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
1 | |||
needsToken | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getHelpUrls | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getExamplesMessages | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Copyright © 2012 Szymon Świerkosz beau@adres.pl |
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 | */ |
22 | |
23 | use MediaWiki\Logger\LoggerFactory; |
24 | use MediaWiki\MediaWikiServices; |
25 | use MediaWiki\Preferences\DefaultPreferencesFactory; |
26 | use MediaWiki\Preferences\PreferencesFactory; |
27 | use MediaWiki\User\Options\UserOptionsManager; |
28 | use MediaWiki\User\User; |
29 | use Wikimedia\ParamValidator\ParamValidator; |
30 | |
31 | /** |
32 | * API module that facilitates the changing of user's preferences. |
33 | * Requires API write mode to be enabled. |
34 | * |
35 | * @ingroup API |
36 | */ |
37 | class ApiOptions extends ApiBase { |
38 | /** @var User User account to modify */ |
39 | private $userForUpdates; |
40 | |
41 | private UserOptionsManager $userOptionsManager; |
42 | private PreferencesFactory $preferencesFactory; |
43 | |
44 | /** |
45 | * @param ApiMain $main |
46 | * @param string $action |
47 | * @param UserOptionsManager|null $userOptionsManager |
48 | * @param PreferencesFactory|null $preferencesFactory |
49 | */ |
50 | public function __construct( |
51 | ApiMain $main, |
52 | $action, |
53 | UserOptionsManager $userOptionsManager = null, |
54 | PreferencesFactory $preferencesFactory = null |
55 | ) { |
56 | parent::__construct( $main, $action ); |
57 | /** |
58 | * This class is extended by GlobalPreferences extension. |
59 | * So it falls back to the global state. |
60 | */ |
61 | $services = MediaWikiServices::getInstance(); |
62 | $this->userOptionsManager = $userOptionsManager ?? $services->getUserOptionsManager(); |
63 | $this->preferencesFactory = $preferencesFactory ?? $services->getPreferencesFactory(); |
64 | } |
65 | |
66 | /** |
67 | * Changes preferences of the current user. |
68 | */ |
69 | public function execute() { |
70 | $user = $this->getUserForUpdates(); |
71 | if ( !$user || !$user->isNamed() ) { |
72 | $this->dieWithError( |
73 | [ 'apierror-mustbeloggedin', $this->msg( 'action-editmyoptions' ) ], 'notloggedin' |
74 | ); |
75 | } |
76 | |
77 | $this->checkUserRightsAny( 'editmyoptions' ); |
78 | |
79 | $params = $this->extractRequestParams(); |
80 | $changed = false; |
81 | |
82 | if ( isset( $params['optionvalue'] ) && !isset( $params['optionname'] ) ) { |
83 | $this->dieWithError( [ 'apierror-missingparam', 'optionname' ] ); |
84 | } |
85 | |
86 | $resetKinds = $params['resetkinds']; |
87 | if ( !$params['reset'] ) { |
88 | $resetKinds = []; |
89 | } |
90 | |
91 | $changes = []; |
92 | if ( $params['change'] ) { |
93 | foreach ( $params['change'] as $entry ) { |
94 | $array = explode( '=', $entry, 2 ); |
95 | $changes[$array[0]] = $array[1] ?? null; |
96 | } |
97 | } |
98 | if ( isset( $params['optionname'] ) ) { |
99 | $newValue = $params['optionvalue'] ?? null; |
100 | $changes[$params['optionname']] = $newValue; |
101 | } |
102 | |
103 | $this->getHookRunner()->onApiOptions( $this, $user, $changes, $resetKinds ); |
104 | |
105 | if ( $resetKinds ) { |
106 | $this->resetPreferences( $resetKinds ); |
107 | $changed = true; |
108 | } |
109 | |
110 | if ( !$changed && !count( $changes ) ) { |
111 | $this->dieWithError( 'apierror-nochanges' ); |
112 | } |
113 | |
114 | $prefs = $this->getPreferences(); |
115 | $prefsKinds = $this->userOptionsManager->getOptionKinds( $user, $this->getContext(), $changes ); |
116 | |
117 | $htmlForm = new HTMLForm( DefaultPreferencesFactory::simplifyFormDescriptor( $prefs ), $this ); |
118 | foreach ( $changes as $key => $value ) { |
119 | switch ( $prefsKinds[$key] ) { |
120 | case 'registered': |
121 | // Regular option. |
122 | if ( $value === null ) { |
123 | // Reset it |
124 | $validation = true; |
125 | } else { |
126 | // Validate |
127 | $field = $htmlForm->getField( $key ); |
128 | $validation = $field->validate( $value, $this->userOptionsManager->getOptions( $user ) ); |
129 | } |
130 | break; |
131 | case 'registered-multiselect': |
132 | case 'registered-checkmatrix': |
133 | // A key for a multiselect or checkmatrix option. |
134 | // TODO: Apply validation properly. |
135 | $validation = true; |
136 | $value = $value !== null ? (bool)$value : null; |
137 | break; |
138 | case 'userjs': |
139 | // Allow non-default preferences prefixed with 'userjs-', to be set by user scripts |
140 | if ( strlen( $key ) > 255 ) { |
141 | $validation = $this->msg( 'apiwarn-validationfailed-keytoolong', Message::numParam( 255 ) ); |
142 | } elseif ( preg_match( '/[^a-zA-Z0-9_-]/', $key ) !== 0 ) { |
143 | $validation = $this->msg( 'apiwarn-validationfailed-badchars' ); |
144 | } else { |
145 | $validation = true; |
146 | } |
147 | |
148 | LoggerFactory::getInstance( 'api-warning' )->info( |
149 | 'ApiOptions: Setting userjs option', |
150 | [ |
151 | 'phab' => 'T259073', |
152 | 'OptionName' => substr( $key, 0, 255 ), |
153 | 'OptionValue' => substr( $value ?? '', 0, 255 ), |
154 | 'OptionSize' => strlen( $value ?? '' ), |
155 | 'OptionValidation' => $validation, |
156 | 'UserId' => $user->getId(), |
157 | 'RequestIP' => $this->getRequest()->getIP(), |
158 | 'RequestUA' => $this->getRequest()->getHeader( 'User-Agent' ) |
159 | ] |
160 | ); |
161 | break; |
162 | case 'special': |
163 | $validation = $this->msg( 'apiwarn-validationfailed-cannotset' ); |
164 | break; |
165 | case 'unused': |
166 | default: |
167 | $validation = $this->msg( 'apiwarn-validationfailed-badpref' ); |
168 | break; |
169 | } |
170 | if ( $validation === true && is_string( $value ) && |
171 | strlen( $value ) > UserOptionsManager::MAX_BYTES_OPTION_VALUE |
172 | ) { |
173 | $validation = $this->msg( |
174 | 'apiwarn-validationfailed-valuetoolong', |
175 | Message::numParam( UserOptionsManager::MAX_BYTES_OPTION_VALUE ) |
176 | ); |
177 | } |
178 | if ( $validation === true ) { |
179 | $this->setPreference( $key, $value ); |
180 | $changed = true; |
181 | } else { |
182 | $this->addWarning( [ 'apiwarn-validationfailed', wfEscapeWikiText( $key ), $validation ] ); |
183 | } |
184 | } |
185 | |
186 | if ( $changed ) { |
187 | $this->commitChanges(); |
188 | } |
189 | |
190 | $this->getResult()->addValue( null, $this->getModuleName(), 'success' ); |
191 | } |
192 | |
193 | /** |
194 | * Load the user from the primary to reduce CAS errors on double post (T95839) |
195 | * |
196 | * @return User|null |
197 | */ |
198 | protected function getUserForUpdates() { |
199 | if ( !$this->userForUpdates ) { |
200 | $this->userForUpdates = $this->getUser()->getInstanceForUpdate(); |
201 | } |
202 | |
203 | return $this->userForUpdates; |
204 | } |
205 | |
206 | /** |
207 | * Returns preferences form descriptor |
208 | * @return mixed[][] |
209 | */ |
210 | protected function getPreferences() { |
211 | return $this->preferencesFactory->getFormDescriptor( $this->getUserForUpdates(), |
212 | $this->getContext() ); |
213 | } |
214 | |
215 | /** |
216 | * @param string[] $kinds One or more types returned by UserOptionsManager::listOptionKinds() or 'all' |
217 | */ |
218 | protected function resetPreferences( array $kinds ) { |
219 | $this->userOptionsManager->resetOptions( $this->getUserForUpdates(), $this->getContext(), $kinds ); |
220 | } |
221 | |
222 | /** |
223 | * Sets one user preference to be applied by commitChanges() |
224 | * |
225 | * @param string $preference |
226 | * @param mixed $value |
227 | */ |
228 | protected function setPreference( $preference, $value ) { |
229 | $this->userOptionsManager->setOption( $this->getUserForUpdates(), $preference, $value ); |
230 | } |
231 | |
232 | /** |
233 | * Applies changes to user preferences |
234 | */ |
235 | protected function commitChanges() { |
236 | $this->getUserForUpdates()->saveSettings(); |
237 | } |
238 | |
239 | public function mustBePosted() { |
240 | return true; |
241 | } |
242 | |
243 | public function isWriteMode() { |
244 | return true; |
245 | } |
246 | |
247 | public function getAllowedParams() { |
248 | $optionKinds = $this->userOptionsManager->listOptionKinds(); |
249 | $optionKinds[] = 'all'; |
250 | |
251 | return [ |
252 | 'reset' => false, |
253 | 'resetkinds' => [ |
254 | ParamValidator::PARAM_TYPE => $optionKinds, |
255 | ParamValidator::PARAM_DEFAULT => 'all', |
256 | ParamValidator::PARAM_ISMULTI => true |
257 | ], |
258 | 'change' => [ |
259 | ParamValidator::PARAM_ISMULTI => true, |
260 | ], |
261 | 'optionname' => [ |
262 | ParamValidator::PARAM_TYPE => 'string', |
263 | ], |
264 | 'optionvalue' => [ |
265 | ParamValidator::PARAM_TYPE => 'string', |
266 | ], |
267 | ]; |
268 | } |
269 | |
270 | public function needsToken() { |
271 | return 'csrf'; |
272 | } |
273 | |
274 | public function getHelpUrls() { |
275 | return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Options'; |
276 | } |
277 | |
278 | protected function getExamplesMessages() { |
279 | return [ |
280 | 'action=options&reset=&token=123ABC' |
281 | => 'apihelp-options-example-reset', |
282 | 'action=options&change=skin=vector|hideminor=1&token=123ABC' |
283 | => 'apihelp-options-example-change', |
284 | 'action=options&reset=&change=skin=monobook&optionname=nickname&' . |
285 | 'optionvalue=[[User:Beau|Beau]]%20([[User_talk:Beau|talk]])&token=123ABC' |
286 | => 'apihelp-options-example-complex', |
287 | ]; |
288 | } |
289 | } |