Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 132 |
|
0.00% |
0 / 13 |
CRAP | |
0.00% |
0 / 1 |
JCUtils | |
0.00% |
0 / 132 |
|
0.00% |
0 / 13 |
4422 | |
0.00% |
0 / 1 |
warn | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
90 | |||
initApiRequestObj | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
72 | |||
callApi | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
20 | |||
isList | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
isDictionary | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
allValuesAreStrings | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
isValidLineString | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
12 | |||
fieldPathToString | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
42 | |||
sanitize | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
42 | |||
sanitizeRecursive | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
110 | |||
isListOfLangs | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
isLocalizedArray | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
30 | |||
pickLocalizedString | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
56 |
1 | <?php |
2 | |
3 | namespace JsonConfig; |
4 | |
5 | use FormatJson; |
6 | use InvalidArgumentException; |
7 | use Language; |
8 | use MediaWiki\MediaWikiServices; |
9 | use MediaWiki\Status\Status; |
10 | use MediaWiki\StubObject\StubUserLang; |
11 | use MWHttpRequest; |
12 | use stdClass; |
13 | |
14 | /** |
15 | * Various useful utility functions (all static) |
16 | */ |
17 | class JCUtils { |
18 | |
19 | /** |
20 | * Uses wfLogWarning() to report an error. |
21 | * All complex arguments are escaped with FormatJson::encode() |
22 | * @param string $msg |
23 | * @param mixed|array $vals |
24 | * @param array $query |
25 | */ |
26 | public static function warn( $msg, $vals, $query = [] ) { |
27 | if ( !is_array( $vals ) ) { |
28 | $vals = [ $vals ]; |
29 | } |
30 | if ( $query ) { |
31 | foreach ( $query as $k => &$v ) { |
32 | if ( stripos( $k, 'password' ) !== false ) { |
33 | $v = '***'; |
34 | } |
35 | } |
36 | $vals['query'] = $query; |
37 | } |
38 | $isFirst = true; |
39 | foreach ( $vals as $k => $v ) { |
40 | $msg .= $isFirst ? ': ' : ', '; |
41 | $isFirst = false; |
42 | if ( is_string( $k ) ) { |
43 | $msg .= $k . '='; |
44 | } |
45 | $msg .= is_scalar( $v ) ? $v : FormatJson::encode( $v ); |
46 | } |
47 | wfLogWarning( $msg ); |
48 | } |
49 | |
50 | /** Init HTTP request object to make requests to the API, and login |
51 | * @param string $url |
52 | * @param string $username |
53 | * @param string $password |
54 | * @return MWHttpRequest|false |
55 | */ |
56 | public static function initApiRequestObj( $url, $username, $password ) { |
57 | $apiUri = wfAppendQuery( $url, [ 'format' => 'json' ] ); |
58 | $options = [ |
59 | 'timeout' => 3, |
60 | 'connectTimeout' => 'default', |
61 | 'method' => 'POST', |
62 | ]; |
63 | $req = MediaWikiServices::getInstance()->getHttpRequestFactory() |
64 | ->create( $apiUri, $options, __METHOD__ ); |
65 | |
66 | if ( $username && $password ) { |
67 | $tokenQuery = [ |
68 | 'action' => 'query', |
69 | 'meta' => 'tokens', |
70 | 'type' => 'login', |
71 | ]; |
72 | $query = [ |
73 | 'action' => 'login', |
74 | 'lgname' => $username, |
75 | 'lgpassword' => $password, |
76 | ]; |
77 | $res = self::callApi( $req, $tokenQuery, 'get login token' ); |
78 | if ( $res !== false ) { |
79 | if ( isset( $res['query']['tokens']['logintoken'] ) ) { |
80 | $query['lgtoken'] = $res['query']['tokens']['logintoken']; |
81 | $res = self::callApi( $req, $query, 'login with token' ); |
82 | } |
83 | } |
84 | if ( $res === false ) { |
85 | $req = false; |
86 | } elseif ( !isset( $res['login']['result'] ) || |
87 | $res['login']['result'] !== 'Success' |
88 | ) { |
89 | self::warn( 'Failed to login', [ |
90 | 'url' => $url, |
91 | 'user' => $username, |
92 | 'result' => $res['login']['result'] ?? '???' |
93 | ] ); |
94 | $req = false; |
95 | } |
96 | } |
97 | return $req; |
98 | } |
99 | |
100 | /** |
101 | * Make an API call on a given request object and warn in case of failures |
102 | * @param MWHttpRequest $req logged-in session |
103 | * @param array $query api call parameters |
104 | * @param string $debugMsg extra message for debug logs in case of failure |
105 | * @return array|false api result or false on error |
106 | */ |
107 | public static function callApi( $req, $query, $debugMsg ) { |
108 | $req->setData( $query ); |
109 | $status = $req->execute(); |
110 | if ( !$status->isGood() ) { |
111 | self::warn( |
112 | 'API call failed to ' . $debugMsg, |
113 | [ 'status' => Status::wrap( $status )->getWikiText() ], |
114 | $query |
115 | ); |
116 | return false; |
117 | } |
118 | $res = FormatJson::decode( $req->getContent(), true ); |
119 | if ( isset( $res['warnings'] ) ) { |
120 | self::warn( 'API call had warnings trying to ' . $debugMsg, |
121 | [ 'warnings' => $res['warnings'] ], $query ); |
122 | } |
123 | if ( isset( $res['error'] ) ) { |
124 | self::warn( |
125 | 'API call failed trying to ' . $debugMsg, [ 'error' => $res['error'] ], $query |
126 | ); |
127 | return false; |
128 | } |
129 | return $res; |
130 | } |
131 | |
132 | /** |
133 | * Helper function to check if the given value is an array, |
134 | * and all keys are integers (non-associative array) |
135 | * @param array $value array to check |
136 | * @return bool |
137 | */ |
138 | public static function isList( $value ) { |
139 | return is_array( $value ) && |
140 | count( array_filter( array_keys( $value ), 'is_int' ) ) === count( $value ); |
141 | } |
142 | |
143 | /** |
144 | * Helper function to check if the given value is an array, |
145 | * and all keys are strings (associative array) |
146 | * @param array $value array to check |
147 | * @return bool |
148 | */ |
149 | public static function isDictionary( $value ) { |
150 | return is_array( $value ) && |
151 | count( array_filter( array_keys( $value ), 'is_string' ) ) === count( $value ); |
152 | } |
153 | |
154 | /** |
155 | * Helper function to check if the given value is an array and if each value in it is a string |
156 | * @param array $array array to check |
157 | * @return bool |
158 | */ |
159 | public static function allValuesAreStrings( $array ) { |
160 | return is_array( $array ) |
161 | && count( array_filter( $array, 'is_string' ) ) === count( $array ); |
162 | } |
163 | |
164 | /** Helper function to check if the given value is a valid string no longer than maxlength, |
165 | * that it has no tabs or new line chars, and that it does not begin or end with spaces |
166 | * @param string $str |
167 | * @param int $maxlength |
168 | * @return bool |
169 | */ |
170 | public static function isValidLineString( $str, $maxlength ) { |
171 | return is_string( $str ) && mb_strlen( $str ) <= $maxlength && |
172 | !preg_match( '/^\s|[\r\n\t]|\s$/', $str ); |
173 | } |
174 | |
175 | /** |
176 | * Converts an array representing path to a field into a string in 'a/b/c[0]/d' format |
177 | * @param array $fieldPath |
178 | * @return string |
179 | */ |
180 | public static function fieldPathToString( array $fieldPath ) { |
181 | $res = ''; |
182 | foreach ( $fieldPath as $fld ) { |
183 | if ( is_int( $fld ) ) { |
184 | $res .= '[' . $fld . ']'; |
185 | } elseif ( is_string( $fld ) ) { |
186 | $res .= $res !== '' ? ( '/' . $fld ) : $fld; |
187 | } else { |
188 | throw new InvalidArgumentException( |
189 | 'Unexpected field type, only strings and integers are allowed' |
190 | ); |
191 | } |
192 | } |
193 | return $res === '' ? '/' : $res; |
194 | } |
195 | |
196 | /** |
197 | * Recursively copies values from the data, converting JCValues into the actual values |
198 | * @param mixed|JCValue $data |
199 | * @param bool $skipDefaults if true, will clone all items except those marked as default |
200 | * @return mixed |
201 | */ |
202 | public static function sanitize( $data, $skipDefaults = false ) { |
203 | if ( $data instanceof JCValue ) { |
204 | $value = $data->getValue(); |
205 | if ( $skipDefaults && $data->defaultUsed() ) { |
206 | return is_array( $value ) ? [] : ( is_object( $value ) ? (object)[] : null ); |
207 | } |
208 | } else { |
209 | $value = $data; |
210 | } |
211 | return self::sanitizeRecursive( $value, $skipDefaults ); |
212 | } |
213 | |
214 | /** |
215 | * @param mixed $data |
216 | * @param bool $skipDefaults |
217 | * @return mixed |
218 | */ |
219 | private static function sanitizeRecursive( $data, $skipDefaults ) { |
220 | if ( !is_array( $data ) && !is_object( $data ) ) { |
221 | return $data; |
222 | } |
223 | if ( is_array( $data ) ) { |
224 | // do not filter lists - only subelements if they were checked |
225 | foreach ( $data as &$valRef ) { |
226 | if ( $valRef instanceof JCValue ) { |
227 | /** @var JCValue $valRef */ |
228 | $valRef = self::sanitizeRecursive( $valRef->getValue(), $skipDefaults ); |
229 | } |
230 | } |
231 | return $data; |
232 | } |
233 | $result = (object)[]; |
234 | foreach ( $data as $fld => $val ) { |
235 | if ( $val instanceof JCValue ) { |
236 | /** @var JCValue $val */ |
237 | if ( $skipDefaults === true && $val->defaultUsed() ) { |
238 | continue; |
239 | } |
240 | $result->$fld = self::sanitizeRecursive( $val->getValue(), $skipDefaults ); |
241 | } else { |
242 | $result->$fld = $val; |
243 | } |
244 | } |
245 | return $result; |
246 | } |
247 | |
248 | /** |
249 | * Returns true if each of the array's values is a valid language code |
250 | * @param array $arr |
251 | * @return bool |
252 | */ |
253 | public static function isListOfLangs( $arr ) { |
254 | $languageNameUtils = MediaWikiServices::getInstance()->getLanguageNameUtils(); |
255 | return count( $arr ) === count( array_filter( $arr, static function ( $v ) use ( $languageNameUtils ) { |
256 | return is_string( $v ) && $languageNameUtils->isValidBuiltInCode( $v ); |
257 | } ) ); |
258 | } |
259 | |
260 | /** |
261 | * Returns true if the array is a valid key->value localized nonempty array |
262 | * @param array $arr |
263 | * @param int $maxlength |
264 | * @return bool |
265 | */ |
266 | public static function isLocalizedArray( $arr, $maxlength ) { |
267 | if ( is_array( $arr ) && |
268 | $arr && |
269 | self::isListOfLangs( array_keys( $arr ) ) |
270 | ) { |
271 | $validStrCount = count( array_filter( $arr, function ( $str ) use ( $maxlength ) { |
272 | return self::isValidLineString( $str, $maxlength ); |
273 | } ) ); |
274 | if ( $validStrCount === count( $arr ) ) { |
275 | return true; |
276 | } |
277 | } |
278 | return false; |
279 | } |
280 | |
281 | /** |
282 | * Find a message in a dictionary for the given language, |
283 | * or use language fallbacks if message is not defined. |
284 | * @param stdClass $map Dictionary of languageCode => string |
285 | * @param Language|StubUserLang $lang |
286 | * @param bool|string $defaultValue if non-false, use this value in case no fallback and no 'en' |
287 | * @return string message from the dictionary or "" if nothing found |
288 | */ |
289 | public static function pickLocalizedString( stdClass $map, $lang, $defaultValue = false ) { |
290 | $langCode = $lang->getCode(); |
291 | if ( property_exists( $map, $langCode ) ) { |
292 | return $map->$langCode; |
293 | } |
294 | foreach ( $lang->getFallbackLanguages() as $l ) { |
295 | if ( property_exists( $map, $l ) ) { |
296 | return $map->$l; |
297 | } |
298 | } |
299 | // If fallbacks fail, check if english is defined |
300 | if ( property_exists( $map, 'en' ) ) { |
301 | return $map->en; |
302 | } |
303 | |
304 | // We have a custom default, return that |
305 | if ( $defaultValue !== false ) { |
306 | return $defaultValue; |
307 | } |
308 | |
309 | // Return first available value, or an empty string |
310 | // There might be a better way to get the first value from an object |
311 | $map = (array)$map; |
312 | return reset( $map ) ? : ''; |
313 | } |
314 | } |