Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 132
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
JCUtils
0.00% covered (danger)
0.00%
0 / 132
0.00% covered (danger)
0.00%
0 / 13
4422
0.00% covered (danger)
0.00%
0 / 1
 warn
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
90
 initApiRequestObj
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
72
 callApi
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
20
 isList
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 isDictionary
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 allValuesAreStrings
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 isValidLineString
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 fieldPathToString
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 sanitize
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
42
 sanitizeRecursive
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
110
 isListOfLangs
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 isLocalizedArray
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 pickLocalizedString
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
56
1<?php
2
3namespace JsonConfig;
4
5use FormatJson;
6use InvalidArgumentException;
7use Language;
8use MediaWiki\MediaWikiServices;
9use MediaWiki\Status\Status;
10use MediaWiki\StubObject\StubUserLang;
11use MWHttpRequest;
12use stdClass;
13
14/**
15 * Various useful utility functions (all static)
16 */
17class 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}