Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
51.50% covered (warning)
51.50%
327 / 635
20.78% covered (danger)
20.78%
16 / 77
CRAP
n/a
0 / 0
wfLoadExtension
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
wfLoadExtensions
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
wfLoadSkin
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
wfLoadSkins
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
wfArrayDiff2
81.82% covered (warning)
81.82%
18 / 22
0.00% covered (danger)
0.00%
0 / 1
9.49
wfMergeErrorArrays
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
wfArrayInsertAfter
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
wfObjectToArray
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
wfRandom
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
wfRandomString
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
wfUrlencode
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
5
wfArrayToCgi
95.24% covered (success)
95.24%
20 / 21
0.00% covered (danger)
0.00%
0 / 1
12
wfCgiToArray
96.00% covered (success)
96.00%
24 / 25
0.00% covered (danger)
0.00%
0 / 1
10
wfAppendQuery
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
6
wfGetUrlUtils
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
wfExpandUrl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
wfGetServerUrl
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wfAssembleUrl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
wfRemoveDotSegments
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
wfUrlProtocols
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
wfUrlProtocolsWithoutProtRel
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wfParseUrl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
wfExpandIRI
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
wfMatchesDomainList
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
wfDebug
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
5.39
wfIsDebugRawPage
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
wfDebugLog
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
wfLogDBError
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
wfDeprecated
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
wfDeprecatedMsg
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
wfWarn
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wfLogWarning
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wfGetLangObj
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
56
wfMessage
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
wfMessageFallback
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wfMsgReplaceArgs
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
wfHostname
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
wfDebugBacktrace
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
wfBacktrace
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
wfGetCaller
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
wfGetAllCallers
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
wfFormatStackFrame
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
wfClientAcceptsGzip
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
8.02
wfEscapeWikiText
90.00% covered (success)
90.00%
45 / 50
0.00% covered (danger)
0.00%
0 / 1
10.10
wfSetVar
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
wfSetBit
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
wfVarDump
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
wfHttpError
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
6
wfResetOutputBuffers
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
90
wfTimestamp
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
wfTimestampOrNull
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
wfTimestampNow
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wfTempDir
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
wfMkdirParents
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
7.77
wfRecursiveRemoveDir
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
wfPercent
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
wfIniGetBool
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wfStringToBool
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
wfEscapeShellArg
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
wfShellExec
71.43% covered (warning)
71.43%
15 / 21
0.00% covered (danger)
0.00%
0 / 1
5.58
wfShellExecWithStderr
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
wfShellWikiCmd
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
wfMerge
90.91% covered (success)
90.91%
40 / 44
0.00% covered (danger)
0.00%
0 / 1
8.05
wfBaseName
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
3.21
wfRelativePath
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
wfGetDB
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
wfScript
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
wfBoolToStr
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
wfGetNull
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
wfStripIllegalFilenameChars
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
wfMemoryLimit
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
wfTransactionalTimeLimit
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
wfShorthandToInteger
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
8
wfUnpack
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
wfIsInfinity
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
wfThumbIsStandard
95.24% covered (success)
95.24%
40 / 42
0.00% covered (danger)
0.00%
0 / 1
13
wfArrayPlus2d
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * Global functions used everywhere.
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
23use MediaWiki\HookContainer\HookRunner;
24use MediaWiki\Libs\UnpackFailedException;
25use MediaWiki\Logger\LoggerFactory;
26use MediaWiki\MediaWikiServices;
27use MediaWiki\ProcOpenError;
28use MediaWiki\Request\WebRequest;
29use MediaWiki\Shell\Shell;
30use MediaWiki\StubObject\StubUserLang;
31use MediaWiki\Title\Title;
32use MediaWiki\Utils\MWTimestamp;
33use MediaWiki\Utils\UrlUtils;
34use Wikimedia\AtEase\AtEase;
35use Wikimedia\ParamValidator\TypeDef\ExpiryDef;
36use Wikimedia\RequestTimeout\RequestTimeout;
37
38/**
39 * Load an extension
40 *
41 * This queues an extension to be loaded through
42 * the ExtensionRegistry system.
43 *
44 * @param string $ext Name of the extension to load
45 * @param string|null $path Absolute path of where to find the extension.json file
46 * @since 1.25
47 */
48function wfLoadExtension( $ext, $path = null ) {
49    if ( !$path ) {
50        global $wgExtensionDirectory;
51        $path = "$wgExtensionDirectory/$ext/extension.json";
52    }
53    ExtensionRegistry::getInstance()->queue( $path );
54}
55
56/**
57 * Load multiple extensions at once
58 *
59 * Same as wfLoadExtension, but more efficient if you
60 * are loading multiple extensions.
61 *
62 * If you want to specify custom paths, you should interact with
63 * ExtensionRegistry directly.
64 *
65 * @see wfLoadExtension
66 * @param string[] $exts Array of extension names to load
67 * @since 1.25
68 */
69function wfLoadExtensions( array $exts ) {
70    global $wgExtensionDirectory;
71    $registry = ExtensionRegistry::getInstance();
72    foreach ( $exts as $ext ) {
73        $registry->queue( "$wgExtensionDirectory/$ext/extension.json" );
74    }
75}
76
77/**
78 * Load a skin
79 *
80 * @see wfLoadExtension
81 * @param string $skin Name of the extension to load
82 * @param string|null $path Absolute path of where to find the skin.json file
83 * @since 1.25
84 */
85function wfLoadSkin( $skin, $path = null ) {
86    if ( !$path ) {
87        global $wgStyleDirectory;
88        $path = "$wgStyleDirectory/$skin/skin.json";
89    }
90    ExtensionRegistry::getInstance()->queue( $path );
91}
92
93/**
94 * Load multiple skins at once
95 *
96 * @see wfLoadExtensions
97 * @param string[] $skins Array of extension names to load
98 * @since 1.25
99 */
100function wfLoadSkins( array $skins ) {
101    global $wgStyleDirectory;
102    $registry = ExtensionRegistry::getInstance();
103    foreach ( $skins as $skin ) {
104        $registry->queue( "$wgStyleDirectory/$skin/skin.json" );
105    }
106}
107
108/**
109 * Like array_diff( $arr1, $arr2 ) except that it works with two-dimensional arrays.
110 * @param string[]|array[] $arr1
111 * @param string[]|array[] $arr2
112 * @return array
113 */
114function wfArrayDiff2( $arr1, $arr2 ) {
115    /**
116     * @param string|array $a
117     * @param string|array $b
118     */
119    $comparator = static function ( $a, $b ): int {
120        if ( is_string( $a ) && is_string( $b ) ) {
121            return strcmp( $a, $b );
122        }
123        if ( !is_array( $a ) && !is_array( $b ) ) {
124            throw new InvalidArgumentException(
125                'This function assumes that array elements are all strings or all arrays'
126            );
127        }
128        if ( count( $a ) !== count( $b ) ) {
129            return count( $a ) <=> count( $b );
130        } else {
131            reset( $a );
132            reset( $b );
133            while ( key( $a ) !== null && key( $b ) !== null ) {
134                $valueA = current( $a );
135                $valueB = current( $b );
136                $cmp = strcmp( $valueA, $valueB );
137                if ( $cmp !== 0 ) {
138                    return $cmp;
139                }
140                next( $a );
141                next( $b );
142            }
143            return 0;
144        }
145    };
146    return array_udiff( $arr1, $arr2, $comparator );
147}
148
149/**
150 * Merge arrays in the style of PermissionManager::getPermissionErrors, with duplicate removal
151 * e.g.
152 *     wfMergeErrorArrays(
153 *       [ [ 'x' ] ],
154 *       [ [ 'x', '2' ] ],
155 *       [ [ 'x' ] ],
156 *       [ [ 'y' ] ]
157 *     );
158 * returns:
159 *     [
160 *       [ 'x', '2' ],
161 *       [ 'x' ],
162 *       [ 'y' ]
163 *     ]
164 *
165 * @param array[] ...$args
166 * @return array
167 */
168function wfMergeErrorArrays( ...$args ) {
169    $out = [];
170    foreach ( $args as $errors ) {
171        foreach ( $errors as $params ) {
172            $originalParams = $params;
173            if ( $params[0] instanceof MessageSpecifier ) {
174                $params = [ $params[0]->getKey(), ...$params[0]->getParams() ];
175            }
176            # @todo FIXME: Sometimes get nested arrays for $params,
177            # which leads to E_NOTICEs
178            $spec = implode( "\t", $params );
179            $out[$spec] = $originalParams;
180        }
181    }
182    return array_values( $out );
183}
184
185/**
186 * Insert an array into another array after the specified key. If the key is
187 * not present in the input array, it is returned without modification.
188 *
189 * @param array $array
190 * @param array $insert The array to insert.
191 * @param mixed $after The key to insert after.
192 * @return array
193 */
194function wfArrayInsertAfter( array $array, array $insert, $after ) {
195    // Find the offset of the element to insert after.
196    $keys = array_keys( $array );
197    $offsetByKey = array_flip( $keys );
198
199    if ( !\array_key_exists( $after, $offsetByKey ) ) {
200        return $array;
201    }
202    $offset = $offsetByKey[$after];
203
204    // Insert at the specified offset
205    $before = array_slice( $array, 0, $offset + 1, true );
206    $after = array_slice( $array, $offset + 1, count( $array ) - $offset, true );
207
208    $output = $before + $insert + $after;
209
210    return $output;
211}
212
213/**
214 * Recursively converts the parameter (an object) to an array with the same data
215 *
216 * @phpcs:ignore MediaWiki.Commenting.FunctionComment.ObjectTypeHintParam
217 * @param object|array $objOrArray
218 * @param bool $recursive
219 * @return array
220 */
221function wfObjectToArray( $objOrArray, $recursive = true ) {
222    $array = [];
223    if ( is_object( $objOrArray ) ) {
224        $objOrArray = get_object_vars( $objOrArray );
225    }
226    foreach ( $objOrArray as $key => $value ) {
227        if ( $recursive && ( is_object( $value ) || is_array( $value ) ) ) {
228            $value = wfObjectToArray( $value );
229        }
230
231        $array[$key] = $value;
232    }
233
234    return $array;
235}
236
237/**
238 * Get a random decimal value in the domain of [0, 1), in a way
239 * not likely to give duplicate values for any realistic
240 * number of articles.
241 *
242 * @note This is designed for use in relation to Special:RandomPage
243 *       and the page_random database field.
244 *
245 * @return string
246 */
247function wfRandom() {
248    // The maximum random value is "only" 2^31-1, so get two random
249    // values to reduce the chance of dupes
250    $max = mt_getrandmax() + 1;
251    $rand = number_format( ( mt_rand() * $max + mt_rand() ) / $max / $max, 12, '.', '' );
252    return $rand;
253}
254
255/**
256 * Get a random string containing a number of pseudo-random hex characters.
257 *
258 * @note This is not secure, if you are trying to generate some sort
259 *       of token please use MWCryptRand instead.
260 *
261 * @param int $length The length of the string to generate
262 * @return string
263 * @since 1.20
264 */
265function wfRandomString( $length = 32 ) {
266    $str = '';
267    for ( $n = 0; $n < $length; $n += 7 ) {
268        $str .= sprintf( '%07x', mt_rand() & 0xfffffff );
269    }
270    return substr( $str, 0, $length );
271}
272
273/**
274 * We want some things to be included as literal characters in our title URLs
275 * for prettiness, which urlencode encodes by default.  According to RFC 1738,
276 * all of the following should be safe:
277 *
278 * ;:@&=$-_.+!*'(),
279 *
280 * RFC 1738 says ~ is unsafe, however RFC 3986 considers it an unreserved
281 * character which should not be encoded. More importantly, google chrome
282 * always converts %7E back to ~, and converting it in this function can
283 * cause a redirect loop (T105265).
284 *
285 * But + is not safe because it's used to indicate a space; &= are only safe in
286 * paths and not in queries (and we don't distinguish here); ' seems kind of
287 * scary; and urlencode() doesn't touch -_. to begin with.  Plus, although /
288 * is reserved, we don't care.  So the list we unescape is:
289 *
290 * ;:@$!*(),/~
291 *
292 * However, IIS7 redirects fail when the url contains a colon (see T24709),
293 * so no fancy : for IIS7.
294 *
295 * %2F in the page titles seems to fatally break for some reason.
296 *
297 * @param string $s
298 * @return string
299 */
300function wfUrlencode( $s ) {
301    static $needle;
302
303    if ( $s === null ) {
304        // Reset $needle for testing.
305        $needle = null;
306        return '';
307    }
308
309    if ( $needle === null ) {
310        $needle = [ '%3B', '%40', '%24', '%21', '%2A', '%28', '%29', '%2C', '%2F', '%7E' ];
311        if ( !isset( $_SERVER['SERVER_SOFTWARE'] ) ||
312            !str_contains( $_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS/7' )
313        ) {
314            $needle[] = '%3A';
315        }
316    }
317
318    $s = urlencode( $s );
319    $s = str_ireplace(
320        $needle,
321        [ ';', '@', '$', '!', '*', '(', ')', ',', '/', '~', ':' ],
322        $s
323    );
324
325    return $s;
326}
327
328/**
329 * This function takes one or two arrays as input, and returns a CGI-style string, e.g.
330 * "days=7&limit=100". Options in the first array override options in the second.
331 * Options set to null or false will not be output.
332 *
333 * @param array $array1 ( String|Array )
334 * @param array|null $array2 ( String|Array )
335 * @param string $prefix
336 * @return string
337 */
338function wfArrayToCgi( $array1, $array2 = null, $prefix = '' ) {
339    if ( $array2 !== null ) {
340        $array1 += $array2;
341    }
342
343    $cgi = '';
344    foreach ( $array1 as $key => $value ) {
345        if ( $value !== null && $value !== false ) {
346            if ( $cgi != '' ) {
347                $cgi .= '&';
348            }
349            if ( $prefix !== '' ) {
350                $key = $prefix . "[$key]";
351            }
352            if ( is_array( $value ) ) {
353                $firstTime = true;
354                foreach ( $value as $k => $v ) {
355                    $cgi .= $firstTime ? '' : '&';
356                    if ( is_array( $v ) ) {
357                        $cgi .= wfArrayToCgi( $v, null, $key . "[$k]" );
358                    } else {
359                        $cgi .= urlencode( $key . "[$k]" ) . '=' . urlencode( $v );
360                    }
361                    $firstTime = false;
362                }
363            } else {
364                if ( is_object( $value ) ) {
365                    $value = $value->__toString();
366                }
367                $cgi .= urlencode( $key ) . '=' . urlencode( $value );
368            }
369        }
370    }
371    return $cgi;
372}
373
374/**
375 * This is the logical opposite of wfArrayToCgi(): it accepts a query string as
376 * its argument and returns the same string in array form.  This allows compatibility
377 * with legacy functions that accept raw query strings instead of nice
378 * arrays.  Of course, keys and values are urldecode()d.
379 *
380 * @param string $query Query string
381 * @return string[] Array version of input
382 */
383function wfCgiToArray( $query ) {
384    if ( isset( $query[0] ) && $query[0] == '?' ) {
385        $query = substr( $query, 1 );
386    }
387    $bits = explode( '&', $query );
388    $ret = [];
389    foreach ( $bits as $bit ) {
390        if ( $bit === '' ) {
391            continue;
392        }
393        if ( strpos( $bit, '=' ) === false ) {
394            // Pieces like &qwerty become 'qwerty' => '' (at least this is what php does)
395            $key = $bit;
396            $value = '';
397        } else {
398            [ $key, $value ] = explode( '=', $bit );
399        }
400        $key = urldecode( $key );
401        $value = urldecode( $value );
402        if ( strpos( $key, '[' ) !== false ) {
403            $keys = array_reverse( explode( '[', $key ) );
404            $key = array_pop( $keys );
405            $temp = $value;
406            foreach ( $keys as $k ) {
407                $k = substr( $k, 0, -1 );
408                $temp = [ $k => $temp ];
409            }
410            if ( isset( $ret[$key] ) && is_array( $ret[$key] ) ) {
411                $ret[$key] = array_merge( $ret[$key], $temp );
412            } else {
413                $ret[$key] = $temp;
414            }
415        } else {
416            $ret[$key] = $value;
417        }
418    }
419    return $ret;
420}
421
422/**
423 * Append a query string to an existing URL, which may or may not already
424 * have query string parameters already. If so, they will be combined.
425 *
426 * @param string $url
427 * @param string|array $query String or associative array
428 * @return string
429 */
430function wfAppendQuery( $url, $query ) {
431    if ( is_array( $query ) ) {
432        $query = wfArrayToCgi( $query );
433    }
434    if ( $query != '' ) {
435        // Remove the fragment, if there is one
436        $fragment = false;
437        $hashPos = strpos( $url, '#' );
438        if ( $hashPos !== false ) {
439            $fragment = substr( $url, $hashPos );
440            $url = substr( $url, 0, $hashPos );
441        }
442
443        // Add parameter
444        if ( strpos( $url, '?' ) === false ) {
445            $url .= '?';
446        } else {
447            $url .= '&';
448        }
449        $url .= $query;
450
451        // Put the fragment back
452        if ( $fragment !== false ) {
453            $url .= $fragment;
454        }
455    }
456    return $url;
457}
458
459/**
460 * @deprecated Get a UrlUtils from services, or construct your own
461 * @internal
462 * @return UrlUtils from services if initialized, otherwise make one from globals
463 */
464function wfGetUrlUtils(): UrlUtils {
465    global $wgServer, $wgCanonicalServer, $wgInternalServer, $wgRequest, $wgHttpsPort,
466        $wgUrlProtocols;
467
468    if ( MediaWikiServices::hasInstance() ) {
469        $services = MediaWikiServices::getInstance();
470        if ( $services->hasService( 'UrlUtils' ) ) {
471            return $services->getUrlUtils();
472        }
473    }
474
475    return new UrlUtils( [
476        // UrlUtils throws if the relevant $wg(|Canonical|Internal) variable is null, but the old
477        // implementations implicitly converted it to an empty string (presumably by mistake).
478        // Preserve the old behavior for compatibility.
479        UrlUtils::SERVER => $wgServer ?? '',
480        UrlUtils::CANONICAL_SERVER => $wgCanonicalServer ?? '',
481        UrlUtils::INTERNAL_SERVER => $wgInternalServer ?? '',
482        UrlUtils::FALLBACK_PROTOCOL => $wgRequest ? $wgRequest->getProtocol()
483            : WebRequest::detectProtocol(),
484        UrlUtils::HTTPS_PORT => $wgHttpsPort,
485        UrlUtils::VALID_PROTOCOLS => $wgUrlProtocols,
486    ] );
487}
488
489/**
490 * Expand a potentially local URL to a fully-qualified URL using $wgServer
491 * (or one of its alternatives).
492 *
493 * The meaning of the PROTO_* constants is as follows:
494 * PROTO_HTTP: Output a URL starting with http://
495 * PROTO_HTTPS: Output a URL starting with https://
496 * PROTO_RELATIVE: Output a URL starting with // (protocol-relative URL)
497 * PROTO_CURRENT: Output a URL starting with either http:// or https:// , depending
498 *    on which protocol was used for the current incoming request
499 * PROTO_CANONICAL: For URLs without a domain, like /w/index.php , use $wgCanonicalServer.
500 *    For protocol-relative URLs, use the protocol of $wgCanonicalServer
501 * PROTO_INTERNAL: Like PROTO_CANONICAL, but uses $wgInternalServer instead of $wgCanonicalServer
502 *
503 * If $url specifies a protocol, or $url is domain-relative and $wgServer
504 * specifies a protocol, PROTO_HTTP, PROTO_HTTPS, PROTO_RELATIVE and
505 * PROTO_CURRENT do not change that.
506 *
507 * Parent references (/../) in the path are resolved (as in wfRemoveDotSegments).
508 *
509 * @deprecated since 1.39, use UrlUtils::expand()
510 * @param string $url An URL; can be absolute (e.g. http://example.com/foo/bar),
511 *    protocol-relative (//example.com/foo/bar) or domain-relative (/foo/bar).
512 * @param string|int|null $defaultProto One of the PROTO_* constants, as described above.
513 * @return string|false Fully-qualified URL, current-path-relative URL or false if
514 *    no valid URL can be constructed
515 */
516function wfExpandUrl( $url, $defaultProto = PROTO_CURRENT ) {
517    return wfGetUrlUtils()->expand( (string)$url, $defaultProto ) ?? false;
518}
519
520/**
521 * Get the wiki's "server", i.e. the protocol and host part of the URL, with a
522 * protocol specified using a PROTO_* constant as in wfExpandUrl()
523 *
524 * @deprecated since 1.39, use UrlUtils::getServer()
525 * @since 1.32
526 * @param string|int|null $proto One of the PROTO_* constants.
527 * @return string The URL
528 */
529function wfGetServerUrl( $proto ) {
530    return wfGetUrlUtils()->getServer( $proto ) ?? '';
531}
532
533/**
534 * This function will reassemble a URL parsed with wfParseURL.  This is useful
535 * if you need to edit part of a URL and put it back together.
536 *
537 * This is the basic structure used (brackets contain keys for $urlParts):
538 * [scheme][delimiter][user]:[pass]@[host]:[port][path]?[query]#[fragment]
539 *
540 * @deprecated since 1.39, use UrlUtils::assemble()
541 * @since 1.19
542 * @param array $urlParts URL parts, as output from wfParseUrl
543 * @return string URL assembled from its component parts
544 */
545function wfAssembleUrl( $urlParts ) {
546    return UrlUtils::assemble( (array)$urlParts );
547}
548
549/**
550 * Remove all dot-segments in the provided URL path.  For example,
551 * '/a/./b/../c/' becomes '/a/c/'.  For details on the algorithm, please see
552 * RFC3986 section 5.2.4.
553 *
554 * @deprecated since 1.39, use UrlUtils::removeDotSegments()
555 * @since 1.19
556 * @param string $urlPath URL path, potentially containing dot-segments
557 * @return string URL path with all dot-segments removed
558 */
559function wfRemoveDotSegments( $urlPath ) {
560    return UrlUtils::removeDotSegments( (string)$urlPath );
561}
562
563/**
564 * Returns a partial regular expression of recognized URL protocols, e.g. "http:\/\/|https:\/\/"
565 *
566 * @deprecated since 1.39, use UrlUtils::validProtocols()
567 * @param bool $includeProtocolRelative If false, remove '//' from the returned protocol list.
568 *        DO NOT USE this directly, use wfUrlProtocolsWithoutProtRel() instead
569 * @return string
570 */
571function wfUrlProtocols( $includeProtocolRelative = true ) {
572    return $includeProtocolRelative ? wfGetUrlUtils()->validProtocols() :
573        wfGetUrlUtils()->validAbsoluteProtocols();
574}
575
576/**
577 * Like wfUrlProtocols(), but excludes '//' from the protocol list. Use this if
578 * you need a regex that matches all URL protocols but does not match protocol-
579 * relative URLs
580 * @deprecated since 1.39, use UrlUtils::validAbsoluteProtocols()
581 * @return string
582 */
583function wfUrlProtocolsWithoutProtRel() {
584    return wfGetUrlUtils()->validAbsoluteProtocols();
585}
586
587/**
588 * parse_url() work-alike, but non-broken.  Differences:
589 *
590 * 1) Handles protocols that don't use :// (e.g., mailto: and news:, as well as
591 *    protocol-relative URLs) correctly.
592 * 2) Adds a "delimiter" element to the array (see (2)).
593 * 3) Verifies that the protocol is on the $wgUrlProtocols allowed list.
594 * 4) Rejects some invalid URLs that parse_url doesn't, e.g. the empty string or URLs starting with
595 *    a line feed character.
596 *
597 * @deprecated since 1.39, use UrlUtils::parse()
598 * @param string $url A URL to parse
599 * @return string[]|false Bits of the URL in an associative array, or false on failure.
600 *   Possible fields:
601 *   - scheme: URI scheme (protocol), e.g. 'http', 'mailto'. Lowercase, always present, but can
602 *       be an empty string for protocol-relative URLs.
603 *   - delimiter: either '://', ':' or '//'. Always present.
604 *   - host: domain name / IP. Always present, but could be an empty string, e.g. for file: URLs.
605 *   - port: port number. Will be missing when port is not explicitly specified.
606 *   - user: user name, e.g. for HTTP Basic auth URLs such as http://user:pass@example.com/
607 *       Missing when there is no username.
608 *   - pass: password, same as above.
609 *   - path: path including the leading /. Will be missing when empty (e.g. 'http://example.com')
610 *   - query: query string (as a string; see wfCgiToArray() for parsing it), can be missing.
611 *   - fragment: the part after #, can be missing.
612 */
613function wfParseUrl( $url ) {
614    return wfGetUrlUtils()->parse( (string)$url ) ?? false;
615}
616
617/**
618 * Take a URL, make sure it's expanded to fully qualified, and replace any
619 * encoded non-ASCII Unicode characters with their UTF-8 original forms
620 * for more compact display and legibility for local audiences.
621 *
622 * @deprecated since 1.39, use UrlUtils::expandIRI()
623 * @param string $url
624 * @return string
625 */
626function wfExpandIRI( $url ) {
627    return wfGetUrlUtils()->expandIRI( (string)$url ) ?? '';
628}
629
630/**
631 * Check whether a given URL has a domain that occurs in a given set of domains
632 *
633 * @deprecated since 1.39, use UrlUtils::expandIRI()
634 * @param string $url
635 * @param array $domains Array of domains (strings)
636 * @return bool True if the host part of $url ends in one of the strings in $domains
637 */
638function wfMatchesDomainList( $url, $domains ) {
639    return wfGetUrlUtils()->matchesDomainList( (string)$url, (array)$domains );
640}
641
642/**
643 * Sends a line to the debug log if enabled or, optionally, to a comment in output.
644 * In normal operation this is a NOP.
645 *
646 * Controlling globals:
647 * $wgDebugLogFile - points to the log file
648 * $wgDebugRawPage - if false, 'action=raw' hits will not result in debug output.
649 * $wgDebugComments - if on, some debug items may appear in comments in the HTML output.
650 *
651 * @since 1.25 support for additional context data
652 *
653 * @param string $text
654 * @param string|bool $dest Destination of the message:
655 *     - 'all': both to the log and HTML (debug toolbar or HTML comments)
656 *     - 'private': excluded from HTML output
657 *   For backward compatibility, it can also take a boolean:
658 *     - true: same as 'all'
659 *     - false: same as 'private'
660 * @param array $context Additional logging context data
661 */
662function wfDebug( $text, $dest = 'all', array $context = [] ) {
663    global $wgDebugRawPage, $wgDebugLogPrefix;
664
665    if ( !$wgDebugRawPage && wfIsDebugRawPage() ) {
666        return;
667    }
668
669    $text = trim( $text );
670
671    if ( $wgDebugLogPrefix !== '' ) {
672        $context['prefix'] = $wgDebugLogPrefix;
673    }
674    $context['private'] = ( $dest === false || $dest === 'private' );
675
676    $logger = LoggerFactory::getInstance( 'wfDebug' );
677    $logger->debug( $text, $context );
678}
679
680/**
681 * Returns true if debug logging should be suppressed if $wgDebugRawPage = false
682 * @return bool
683 */
684function wfIsDebugRawPage() {
685    static $cache;
686    if ( $cache !== null ) {
687        return $cache;
688    }
689    // Check for raw action using $_GET not $wgRequest, since the latter might not be initialised yet
690    // phpcs:ignore MediaWiki.Usage.SuperGlobalsUsage.SuperGlobals
691    if ( ( isset( $_GET['action'] ) && $_GET['action'] == 'raw' )
692        || MW_ENTRY_POINT === 'load'
693    ) {
694        $cache = true;
695    } else {
696        $cache = false;
697    }
698    return $cache;
699}
700
701/**
702 * Send a line to a supplementary debug log file, if configured, or main debug
703 * log if not.
704 *
705 * To configure a supplementary log file, set $wgDebugLogGroups[$logGroup] to
706 * a string filename or an associative array mapping 'destination' to the
707 * desired filename. The associative array may also contain a 'sample' key
708 * with an integer value, specifying a sampling factor. Sampled log events
709 * will be emitted with a 1 in N random chance.
710 *
711 * @since 1.23 support for sampling log messages via $wgDebugLogGroups.
712 * @since 1.25 support for additional context data
713 * @since 1.25 sample behavior dependent on configured $wgMWLoggerDefaultSpi
714 *
715 * @param string $logGroup
716 * @param string $text
717 * @param string|bool $dest Destination of the message:
718 *     - 'all': both to the log and HTML (debug toolbar or HTML comments)
719 *     - 'private': only to the specific log if set in $wgDebugLogGroups and
720 *       discarded otherwise
721 *   For backward compatibility, it can also take a boolean:
722 *     - true: same as 'all'
723 *     - false: same as 'private'
724 * @param array $context Additional logging context data
725 */
726function wfDebugLog(
727    $logGroup, $text, $dest = 'all', array $context = []
728) {
729    $text = trim( $text );
730
731    $logger = LoggerFactory::getInstance( $logGroup );
732    $context['private'] = ( $dest === false || $dest === 'private' );
733    $logger->info( $text, $context );
734}
735
736/**
737 * Log for database errors
738 *
739 * @since 1.25 support for additional context data
740 *
741 * @param string $text Database error message.
742 * @param array $context Additional logging context data
743 */
744function wfLogDBError( $text, array $context = [] ) {
745    $logger = LoggerFactory::getInstance( 'wfLogDBError' );
746    $logger->error( trim( $text ), $context );
747}
748
749/**
750 * Logs a warning that a deprecated feature was used.
751 *
752 * To write a custom deprecation message, use wfDeprecatedMsg() instead.
753 *
754 * @param string $function Feature that is deprecated.
755 * @param string|false $version Version of MediaWiki that the feature
756 *  was deprecated in (Added in 1.19).
757 * @param string|bool $component Component to which the feature belongs.
758 *  If false, it is assumed the function is in MediaWiki core (Added in 1.19).
759 * @param int $callerOffset How far up the call stack is the original
760 *  caller. 2 = function that called the function that called
761 *  wfDeprecated (Added in 1.20).
762 * @throws InvalidArgumentException If the MediaWiki version
763 *  number specified by $version is neither a string nor false.
764 */
765function wfDeprecated( $function, $version = false, $component = false, $callerOffset = 2 ) {
766    if ( !is_string( $version ) && $version !== false ) {
767        throw new InvalidArgumentException(
768            "MediaWiki version must either be a string or false. " .
769            "Example valid version: '1.33'"
770        );
771    }
772
773    MWDebug::deprecated( $function, $version, $component, $callerOffset + 1 );
774}
775
776/**
777 * Log a deprecation warning with arbitrary message text. A caller
778 * description will be appended. If the message has already been sent for
779 * this caller, it won't be sent again.
780 *
781 * Although there are component and version parameters, they are not
782 * automatically appended to the message. The message text should include
783 * information about when the thing was deprecated. The component and version
784 * are just used to implement $wgDeprecationReleaseLimit.
785 *
786 * @since 1.35
787 * @param string $msg The message
788 * @param string|false $version Version of MediaWiki that the function
789 *  was deprecated in.
790 * @param string|bool $component Component to which the function belongs.
791 *  If false, it is assumed the function is in MediaWiki core.
792 * @param int|false $callerOffset How far up the call stack is the original
793 *  caller. 2 = function that called the function that called us. If false,
794 *  the caller description will not be appended.
795 */
796function wfDeprecatedMsg( $msg, $version = false, $component = false, $callerOffset = 2 ) {
797    MWDebug::deprecatedMsg( $msg, $version, $component,
798        $callerOffset === false ? false : $callerOffset + 1 );
799}
800
801/**
802 * Send a warning either to the debug log or in a PHP error depending on
803 * $wgDevelopmentWarnings. To log warnings in production, use wfLogWarning() instead.
804 *
805 * @param string $msg Message to send
806 * @param int $callerOffset Number of items to go back in the backtrace to
807 *        find the correct caller (1 = function calling wfWarn, ...)
808 * @param int $level PHP error level; defaults to E_USER_NOTICE;
809 *        only used when $wgDevelopmentWarnings is true
810 */
811function wfWarn( $msg, $callerOffset = 1, $level = E_USER_NOTICE ) {
812    MWDebug::warning( $msg, $callerOffset + 1, $level, 'auto' );
813}
814
815/**
816 * Send a warning as a PHP error and the debug log. This is intended for logging
817 * warnings in production. For logging development warnings, use WfWarn instead.
818 *
819 * @param string $msg Message to send
820 * @param int $callerOffset Number of items to go back in the backtrace to
821 *        find the correct caller (1 = function calling wfLogWarning, ...)
822 * @param int $level PHP error level; defaults to E_USER_WARNING
823 */
824function wfLogWarning( $msg, $callerOffset = 1, $level = E_USER_WARNING ) {
825    MWDebug::warning( $msg, $callerOffset + 1, $level, 'production' );
826}
827
828/**
829 * Return a Language object from $langcode
830 *
831 * @deprecated since 1.41, use MediaWiki\Languages\LanguageFactory::getLanguage instead.
832 * @param Language|string|bool $langcode Either:
833 *                  - a Language object
834 *                  - code of the language to get the message for, if it is
835 *                    a valid code create a language for that language, if
836 *                    it is a string but not a valid code then make a basic
837 *                    language object
838 *                  - a boolean: if it's false then use the global object for
839 *                    the current user's language (as a fallback for the old parameter
840 *                    functionality), or if it is true then use global object
841 *                    for the wiki's content language.
842 * @return Language|StubUserLang
843 */
844function wfGetLangObj( $langcode = false ) {
845    wfDeprecated( __FUNCTION__, '1.41' );
846    # Identify which language to get or create a language object for.
847    # Using is_object here due to Stub objects.
848    if ( is_object( $langcode ) ) {
849        # Great, we already have the object (hopefully)!
850        return $langcode;
851    }
852
853    global $wgLanguageCode;
854    $services = MediaWikiServices::getInstance();
855    if ( $langcode === true || $langcode === $wgLanguageCode ) {
856        # $langcode is the language code of the wikis content language object.
857        # or it is a boolean and value is true
858        return $services->getContentLanguage();
859    }
860
861    global $wgLang;
862    if ( $langcode === false || $langcode === $wgLang->getCode() ) {
863        # $langcode is the language code of user language object.
864        # or it was a boolean and value is false
865        return $wgLang;
866    }
867
868    $languageNames = $services->getLanguageNameUtils()->getLanguageNames();
869    // FIXME: Can we use isSupportedLanguage here?
870    if ( isset( $languageNames[$langcode] ) ) {
871        # $langcode corresponds to a valid language.
872        return $services->getLanguageFactory()->getLanguage( $langcode );
873    }
874
875    # $langcode is a string, but not a valid language code; use content language.
876    wfDebug( "Invalid language code passed to wfGetLangObj, falling back to content language." );
877    return $services->getContentLanguage();
878}
879
880/**
881 * This is the function for getting translated interface messages.
882 *
883 * @see Message class for documentation how to use them.
884 * @see https://www.mediawiki.org/wiki/Manual:Messages_API
885 *
886 * This function replaces all old wfMsg* functions.
887 *
888 * When the MessageSpecifier object is an instance of Message, a clone of the object is returned.
889 * This is unlike the `new Message( â€¦ )` constructor, which returns a new object constructed from
890 * scratch with the same key. This difference is mostly relevant when the passed object is an
891 * instance of a subclass like RawMessage or ApiMessage.
892 *
893 * @param string|string[]|MessageSpecifier $key Message key, or array of keys, or a MessageSpecifier
894 * @param mixed ...$params Normal message parameters
895 * @return Message
896 *
897 * @since 1.17
898 *
899 * @see Message::__construct
900 */
901function wfMessage( $key, ...$params ) {
902    if ( is_array( $key ) ) {
903        // Fallback keys are not allowed in message specifiers
904        $message = wfMessageFallback( ...$key );
905    } else {
906        $message = Message::newFromSpecifier( $key );
907    }
908
909    // We call Message::params() to reduce code duplication
910    if ( $params ) {
911        $message->params( ...$params );
912    }
913
914    return $message;
915}
916
917/**
918 * This function accepts multiple message keys and returns a message instance
919 * for the first message which is non-empty. If all messages are empty then an
920 * instance of the last message key is returned.
921 *
922 * @param string ...$keys Message keys
923 * @return Message
924 *
925 * @since 1.18
926 *
927 * @see Message::newFallbackSequence
928 */
929function wfMessageFallback( ...$keys ) {
930    return Message::newFallbackSequence( ...$keys );
931}
932
933/**
934 * Replace message parameter keys on the given formatted output.
935 *
936 * @param string $message
937 * @param array $args
938 * @return string
939 * @internal
940 */
941function wfMsgReplaceArgs( $message, $args ) {
942    # Fix windows line-endings
943    # Some messages are split with explode("\n", $msg)
944    $message = str_replace( "\r", '', $message );
945
946    // Replace arguments
947    if ( is_array( $args ) && $args ) {
948        if ( is_array( $args[0] ) ) {
949            $args = array_values( $args[0] );
950        }
951        $replacementKeys = [];
952        foreach ( $args as $n => $param ) {
953            $replacementKeys['$' . ( $n + 1 )] = $param;
954        }
955        $message = strtr( $message, $replacementKeys );
956    }
957
958    return $message;
959}
960
961/**
962 * Get host name of the current machine, for use in error reporting.
963 *
964 * This helps to know which machine in a data center generated the
965 * current page.
966 *
967 * @return string
968 */
969function wfHostname() {
970    // Hostname overriding
971    global $wgOverrideHostname;
972    if ( $wgOverrideHostname !== false ) {
973        return $wgOverrideHostname;
974    }
975
976    return php_uname( 'n' ) ?: 'unknown';
977}
978
979/**
980 * Safety wrapper for debug_backtrace().
981 *
982 * Will return an empty array if debug_backtrace is disabled, otherwise
983 * the output from debug_backtrace() (trimmed).
984 *
985 * @param int $limit This parameter can be used to limit the number of stack frames returned
986 *
987 * @return array Array of backtrace information
988 */
989function wfDebugBacktrace( $limit = 0 ) {
990    static $disabled = null;
991
992    if ( $disabled === null ) {
993        $disabled = !function_exists( 'debug_backtrace' );
994        if ( $disabled ) {
995            wfDebug( "debug_backtrace() is disabled" );
996        }
997    }
998    if ( $disabled ) {
999        return [];
1000    }
1001
1002    if ( $limit ) {
1003        return array_slice( debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT, $limit + 1 ), 1 );
1004    } else {
1005        return array_slice( debug_backtrace(), 1 );
1006    }
1007}
1008
1009/**
1010 * Get a debug backtrace as a string
1011 *
1012 * @param bool|null $raw If true, the return value is plain text. If false, HTML.
1013 *   Defaults to true if MW_ENTRY_POINT is 'cli', otherwise false.
1014 * @return string
1015 * @since 1.25 Supports $raw parameter.
1016 */
1017function wfBacktrace( $raw = null ) {
1018    $raw ??= MW_ENTRY_POINT === 'cli';
1019    if ( $raw ) {
1020        $frameFormat = "%s line %s calls %s()\n";
1021        $traceFormat = "%s";
1022    } else {
1023        $frameFormat = "<li>%s line %s calls %s()</li>\n";
1024        $traceFormat = "<ul>\n%s</ul>\n";
1025    }
1026
1027    $frames = array_map( static function ( $frame ) use ( $frameFormat ) {
1028        $file = !empty( $frame['file'] ) ? basename( $frame['file'] ) : '-';
1029        $line = $frame['line'] ?? '-';
1030        $call = $frame['function'];
1031        if ( !empty( $frame['class'] ) ) {
1032            $call = $frame['class'] . $frame['type'] . $call;
1033        }
1034        return sprintf( $frameFormat, $file, $line, $call );
1035    }, wfDebugBacktrace() );
1036
1037    return sprintf( $traceFormat, implode( '', $frames ) );
1038}
1039
1040/**
1041 * Get the name of the function which called this function
1042 * wfGetCaller( 1 ) is the function with the wfGetCaller() call (ie. __FUNCTION__)
1043 * wfGetCaller( 2 ) [default] is the caller of the function running wfGetCaller()
1044 * wfGetCaller( 3 ) is the parent of that.
1045 *
1046 * @param int $level
1047 * @return string
1048 */
1049function wfGetCaller( $level = 2 ) {
1050    $backtrace = wfDebugBacktrace( $level + 1 );
1051    if ( isset( $backtrace[$level] ) ) {
1052        return wfFormatStackFrame( $backtrace[$level] );
1053    } else {
1054        return 'unknown';
1055    }
1056}
1057
1058/**
1059 * Return a string consisting of callers in the stack. Useful sometimes
1060 * for profiling specific points.
1061 *
1062 * @param int|false $limit The maximum depth of the stack frame to return, or false for the entire stack.
1063 * @return string
1064 */
1065function wfGetAllCallers( $limit = 3 ) {
1066    $trace = array_reverse( wfDebugBacktrace() );
1067    if ( !$limit || $limit > count( $trace ) - 1 ) {
1068        $limit = count( $trace ) - 1;
1069    }
1070    $trace = array_slice( $trace, -$limit - 1, $limit );
1071    return implode( '/', array_map( 'wfFormatStackFrame', $trace ) );
1072}
1073
1074/**
1075 * Return a string representation of frame
1076 *
1077 * @param array $frame
1078 * @return string
1079 */
1080function wfFormatStackFrame( $frame ) {
1081    if ( !isset( $frame['function'] ) ) {
1082        return 'NO_FUNCTION_GIVEN';
1083    }
1084    return isset( $frame['class'] ) && isset( $frame['type'] ) ?
1085        $frame['class'] . $frame['type'] . $frame['function'] :
1086        $frame['function'];
1087}
1088
1089/**
1090 * Whether the client accept gzip encoding
1091 *
1092 * Uses the Accept-Encoding header to check if the client supports gzip encoding.
1093 * Use this when considering to send a gzip-encoded response to the client.
1094 *
1095 * @param bool $force Forces another check even if we already have a cached result.
1096 * @return bool
1097 */
1098function wfClientAcceptsGzip( $force = false ) {
1099    static $result = null;
1100    if ( $result === null || $force ) {
1101        $result = false;
1102        if ( isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) ) {
1103            # @todo FIXME: We may want to disallow some broken browsers
1104            $m = [];
1105            if ( preg_match(
1106                    '/\bgzip(?:;(q)=([0-9]+(?:\.[0-9]+)))?\b/',
1107                    $_SERVER['HTTP_ACCEPT_ENCODING'],
1108                    $m
1109                )
1110            ) {
1111                if ( isset( $m[2] ) && ( $m[1] == 'q' ) && ( $m[2] == 0 ) ) {
1112                    return $result;
1113                }
1114                wfDebug( "wfClientAcceptsGzip: client accepts gzip." );
1115                $result = true;
1116            }
1117        }
1118    }
1119    return $result;
1120}
1121
1122/**
1123 * Escapes the given text so that it may be output using addWikiText()
1124 * without any linking, formatting, etc. making its way through. This
1125 * is achieved by substituting certain characters with HTML entities.
1126 * As required by the callers, "<nowiki>" is not used.
1127 *
1128 * @param string|null|false $input Text to be escaped
1129 * @param-taint $input escapes_html
1130 * @return string
1131 */
1132function wfEscapeWikiText( $input ): string {
1133    global $wgEnableMagicLinks;
1134    static $repl = null, $repl2 = null, $repl3 = null, $repl4 = null;
1135    if ( $repl === null || defined( 'MW_PARSER_TEST' ) || defined( 'MW_PHPUNIT_TEST' ) ) {
1136        // Tests depend upon being able to change $wgEnableMagicLinks, so don't cache
1137        // in those situations
1138        $repl = [
1139            '"' => '&#34;', '&' => '&#38;', "'" => '&#39;', '<' => '&#60;',
1140            '=' => '&#61;', '>' => '&#62;', '[' => '&#91;', ']' => '&#93;',
1141            '{' => '&#123;', '|' => '&#124;', '}' => '&#125;',
1142            ';' => '&#59;', // a token inside language converter brackets
1143            '!!' => '&#33;!', // a token inside table context
1144            "\n!" => "\n&#33;", "\r!" => "\r&#33;", // a token inside table context
1145            "\n#" => "\n&#35;", "\r#" => "\r&#35;",
1146            "\n*" => "\n&#42;", "\r*" => "\r&#42;",
1147            "\n:" => "\n&#58;", "\r:" => "\r&#58;",
1148            "\n " => "\n&#32;", "\r " => "\r&#32;",
1149            "\n\n" => "\n&#10;", "\r\n" => "&#13;\n",
1150            "\n\r" => "\n&#13;", "\r\r" => "\r&#13;",
1151            "\n\t" => "\n&#9;", "\r\t" => "\r&#9;", // "\n\t\n" is treated like "\n\n"
1152            "\n----" => "\n&#45;---", "\r----" => "\r&#45;---",
1153            '__' => '_&#95;', '://' => '&#58;//',
1154            '~~~' => '~~&#126;', // protect from PST, just to be safe(r)
1155        ];
1156
1157        $magicLinks = array_keys( array_filter( $wgEnableMagicLinks ) );
1158        // We have to catch everything "\s" matches in PCRE
1159        foreach ( $magicLinks as $magic ) {
1160            $repl["$magic "] = "$magic&#32;";
1161            $repl["$magic\t"] = "$magic&#9;";
1162            $repl["$magic\r"] = "$magic&#13;";
1163            $repl["$magic\n"] = "$magic&#10;";
1164            $repl["$magic\f"] = "$magic&#12;";
1165        }
1166        // Additionally escape the following characters at the beginning of the
1167        // string, in case they merge to form tokens when spliced into a
1168        // string.  Tokens like -{ {{ [[ {| etc are already escaped because
1169        // the second character is escaped above, but the following tokens
1170        // are handled here: |+ |- __FOO__ ~~~
1171        $repl3 = [
1172            '+' => '&#43;', '-' => '&#45;', '_' => '&#95;', '~' => '&#126;',
1173        ];
1174        // Similarly, protect the following characters at the end of the
1175        // string, which could turn form the start of `__FOO__` or `~~~~`
1176        // A trailing newline could also form the unintended start of a
1177        // paragraph break if it is glued to a newline in the following
1178        // context.
1179        $repl4 = [
1180            '_' => '&#95;', '~' => '&#126;',
1181            "\n" => "&#10;", "\r" => "&#13;",
1182            "\t" => "&#9;", // "\n\t\n" is treated like "\n\n"
1183        ];
1184
1185        // And handle protocols that don't use "://"
1186        global $wgUrlProtocols;
1187        $repl2 = [];
1188        foreach ( $wgUrlProtocols as $prot ) {
1189            if ( substr( $prot, -1 ) === ':' ) {
1190                $repl2[] = preg_quote( substr( $prot, 0, -1 ), '/' );
1191            }
1192        }
1193        $repl2 = $repl2 ? '/\b(' . implode( '|', $repl2 ) . '):/i' : '/^(?!)/';
1194    }
1195    // Tell phan that $repl2, $repl3 and $repl4 will also be non-null here
1196    '@phan-var string $repl2';
1197    '@phan-var string $repl3';
1198    '@phan-var string $repl4';
1199    // This will also stringify input in case it's not a string
1200    $text = substr( strtr( "\n$input", $repl ), 1 );
1201    if ( $text === '' ) {
1202        return $text;
1203    }
1204    $first = strtr( $text[0], $repl3 ); // protect first character
1205    if ( strlen( $text ) > 1 ) {
1206        $text = $first . substr( $text, 1, -1 ) .
1207        strtr( substr( $text, -1 ), $repl4 ); // protect last character
1208    } else {
1209        // special case for single-character strings
1210        $text = strtr( $first, $repl4 ); // protect last character
1211    }
1212    $text = preg_replace( $repl2, '$1&#58;', $text );
1213    return $text;
1214}
1215
1216/**
1217 * Sets dest to source and returns the original value of dest
1218 * If source is NULL, it just returns the value, it doesn't set the variable
1219 * If force is true, it will set the value even if source is NULL
1220 *
1221 * @param mixed &$dest
1222 * @param mixed $source
1223 * @param bool $force
1224 * @return mixed
1225 */
1226function wfSetVar( &$dest, $source, $force = false ) {
1227    $temp = $dest;
1228    if ( $source !== null || $force ) {
1229        $dest = $source;
1230    }
1231    return $temp;
1232}
1233
1234/**
1235 * As for wfSetVar except setting a bit
1236 *
1237 * @param int &$dest
1238 * @param int $bit
1239 * @param bool $state
1240 *
1241 * @return bool
1242 */
1243function wfSetBit( &$dest, $bit, $state = true ) {
1244    $temp = (bool)( $dest & $bit );
1245    if ( $state !== null ) {
1246        if ( $state ) {
1247            $dest |= $bit;
1248        } else {
1249            $dest &= ~$bit;
1250        }
1251    }
1252    return $temp;
1253}
1254
1255/**
1256 * A wrapper around the PHP function var_export().
1257 * Either print it or add it to the regular output ($wgOut).
1258 *
1259 * @param mixed $var A PHP variable to dump.
1260 */
1261function wfVarDump( $var ) {
1262    global $wgOut;
1263    $s = str_replace( "\n", "<br />\n", var_export( $var, true ) . "\n" );
1264    if ( headers_sent() || !isset( $wgOut ) || !is_object( $wgOut ) ) {
1265        print $s;
1266    } else {
1267        $wgOut->addHTML( $s );
1268    }
1269}
1270
1271/**
1272 * Provide a simple HTTP error.
1273 *
1274 * @param int|string $code
1275 * @param string $label
1276 * @param string $desc
1277 */
1278function wfHttpError( $code, $label, $desc ) {
1279    global $wgOut;
1280    HttpStatus::header( $code );
1281    if ( $wgOut ) {
1282        $wgOut->disable();
1283        $wgOut->sendCacheControl();
1284    }
1285
1286    \MediaWiki\Request\HeaderCallback::warnIfHeadersSent();
1287    header( 'Content-type: text/html; charset=utf-8' );
1288    ob_start();
1289    print '<!DOCTYPE html>' .
1290        '<html><head><title>' .
1291        htmlspecialchars( $label ) .
1292        '</title></head><body><h1>' .
1293        htmlspecialchars( $label ) .
1294        '</h1><p>' .
1295        nl2br( htmlspecialchars( $desc ) ) .
1296        "</p></body></html>\n";
1297    header( 'Content-Length: ' . ob_get_length() );
1298    ob_end_flush();
1299}
1300
1301/**
1302 * Clear away any user-level output buffers, discarding contents.
1303 *
1304 * Suitable for 'starting afresh', for instance when streaming
1305 * relatively large amounts of data without buffering, or wanting to
1306 * output image files without ob_gzhandler's compression.
1307 *
1308 * The optional $resetGzipEncoding parameter controls suppression of
1309 * the Content-Encoding header sent by ob_gzhandler; by default it
1310 * is left. This should be used for HTTP 304 responses, where you need to
1311 * preserve the Content-Encoding header of the real result, but
1312 * also need to suppress the output of ob_gzhandler to keep to spec
1313 * and avoid breaking Firefox in rare cases where the headers and
1314 * body are broken over two packets.
1315 *
1316 * Note that some PHP configuration options may add output buffer
1317 * layers which cannot be removed; these are left in place.
1318 *
1319 * @param bool $resetGzipEncoding
1320 */
1321function wfResetOutputBuffers( $resetGzipEncoding = true ) {
1322    while ( $status = ob_get_status() ) {
1323        if ( isset( $status['flags'] ) ) {
1324            $flags = PHP_OUTPUT_HANDLER_CLEANABLE | PHP_OUTPUT_HANDLER_REMOVABLE;
1325            $deleteable = ( $status['flags'] & $flags ) === $flags;
1326        } elseif ( isset( $status['del'] ) ) {
1327            $deleteable = $status['del'];
1328        } else {
1329            // Guess that any PHP-internal setting can't be removed.
1330            $deleteable = $status['type'] !== 0; /* PHP_OUTPUT_HANDLER_INTERNAL */
1331        }
1332        if ( !$deleteable ) {
1333            // Give up, and hope the result doesn't break
1334            // output behavior.
1335            break;
1336        }
1337        if ( $status['name'] === 'MediaWikiIntegrationTestCase::wfResetOutputBuffersBarrier' ) {
1338            // Unit testing barrier to prevent this function from breaking PHPUnit.
1339            break;
1340        }
1341        if ( !ob_end_clean() ) {
1342            // Could not remove output buffer handler; abort now
1343            // to avoid getting in some kind of infinite loop.
1344            break;
1345        }
1346        if ( $resetGzipEncoding && $status['name'] == 'ob_gzhandler' ) {
1347            // Reset the 'Content-Encoding' field set by this handler
1348            // so we can start fresh.
1349            header_remove( 'Content-Encoding' );
1350            break;
1351        }
1352    }
1353}
1354
1355/**
1356 * Get a timestamp string in one of various formats
1357 *
1358 * @param mixed $outputtype Output format, one of the TS_* constants. Defaults to
1359 *   Unix timestamp.
1360 * @param mixed $ts A timestamp in any supported format. The
1361 *   function will autodetect which format is supplied and act accordingly. Use 0 or
1362 *   omit to use current time
1363 * @return string|false The date in the specified format, or false on error.
1364 */
1365function wfTimestamp( $outputtype = TS_UNIX, $ts = 0 ) {
1366    $ret = MWTimestamp::convert( $outputtype, $ts );
1367    if ( $ret === false ) {
1368        wfDebug( "wfTimestamp() fed bogus time value: TYPE=$outputtype; VALUE=$ts" );
1369    }
1370    return $ret;
1371}
1372
1373/**
1374 * Return a formatted timestamp, or null if input is null.
1375 * For dealing with nullable timestamp columns in the database.
1376 *
1377 * @param mixed $outputtype
1378 * @param mixed|null $ts
1379 * @return string|false|null Null if called with null, otherwise the result of wfTimestamp()
1380 */
1381function wfTimestampOrNull( $outputtype = TS_UNIX, $ts = null ) {
1382    if ( $ts === null ) {
1383        return null;
1384    } else {
1385        return wfTimestamp( $outputtype, $ts );
1386    }
1387}
1388
1389/**
1390 * Convenience function; returns MediaWiki timestamp for the present time.
1391 *
1392 * @return string TS_MW timestamp
1393 */
1394function wfTimestampNow() {
1395    return MWTimestamp::now( TS_MW );
1396}
1397
1398/**
1399 * Tries to get the system directory for temporary files. First
1400 * $wgTmpDirectory is checked, and then the TMPDIR, TMP, and TEMP
1401 * environment variables are then checked in sequence, then
1402 * sys_get_temp_dir(), then upload_tmp_dir from php.ini.
1403 *
1404 * NOTE: When possible, use instead the tmpfile() function to create
1405 * temporary files to avoid race conditions on file creation, etc.
1406 *
1407 * @return string
1408 */
1409function wfTempDir() {
1410    global $wgTmpDirectory;
1411
1412    if ( $wgTmpDirectory !== false ) {
1413        return $wgTmpDirectory;
1414    }
1415
1416    return TempFSFile::getUsableTempDirectory();
1417}
1418
1419/**
1420 * Make directory, and make all parent directories if they don't exist
1421 *
1422 * @param string $dir Full path to directory to create. Callers should make sure this is not a storage path.
1423 * @param int|null $mode Chmod value to use, default is $wgDirectoryMode
1424 * @param string|null $caller Optional caller param for debugging.
1425 * @return bool
1426 */
1427function wfMkdirParents( $dir, $mode = null, $caller = null ) {
1428    global $wgDirectoryMode;
1429
1430    if ( FileBackend::isStoragePath( $dir ) ) {
1431        throw new LogicException( __FUNCTION__ . " given storage path '$dir'." );
1432    }
1433    if ( $caller !== null ) {
1434        wfDebug( "$caller: called wfMkdirParents($dir)" );
1435    }
1436    if ( strval( $dir ) === '' ) {
1437        return true;
1438    }
1439
1440    $dir = str_replace( [ '\\', '/' ], DIRECTORY_SEPARATOR, $dir );
1441    $mode ??= $wgDirectoryMode;
1442
1443    // Turn off the normal warning, we're doing our own below
1444    // PHP doesn't include the path in its warning message, so we add our own to aid in diagnosis.
1445    //
1446    // Repeat existence check if creation failed so that we silently recover in case of
1447    // a race condition where another request created it since the first check.
1448    //
1449    // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1450    $ok = is_dir( $dir ) || @mkdir( $dir, $mode, true ) || is_dir( $dir );
1451    if ( !$ok ) {
1452        trigger_error( sprintf( "failed to mkdir \"%s\" mode 0%o", $dir, $mode ), E_USER_WARNING );
1453    }
1454
1455    return $ok;
1456}
1457
1458/**
1459 * Remove a directory and all its content.
1460 * Does not hide error.
1461 * @param string $dir
1462 */
1463function wfRecursiveRemoveDir( $dir ) {
1464    // taken from https://www.php.net/manual/en/function.rmdir.php#98622
1465    if ( is_dir( $dir ) ) {
1466        $objects = scandir( $dir );
1467        foreach ( $objects as $object ) {
1468            if ( $object != "." && $object != ".." ) {
1469                if ( filetype( $dir . '/' . $object ) == "dir" ) {
1470                    wfRecursiveRemoveDir( $dir . '/' . $object );
1471                } else {
1472                    unlink( $dir . '/' . $object );
1473                }
1474            }
1475        }
1476        rmdir( $dir );
1477    }
1478}
1479
1480/**
1481 * @param float|int $nr The number to format
1482 * @param int $acc The number of digits after the decimal point, default 2
1483 * @param bool $round Whether or not to round the value, default true
1484 * @return string
1485 */
1486function wfPercent( $nr, int $acc = 2, bool $round = true ) {
1487    $accForFormat = $acc >= 0 ? $acc : 0;
1488    $ret = sprintf( "%.{$accForFormat}f", $nr );
1489    return $round ? round( (float)$ret, $acc ) . '%' : "$ret%";
1490}
1491
1492/**
1493 * Safety wrapper around ini_get() for boolean settings.
1494 * The values returned from ini_get() are pre-normalized for settings
1495 * set via php.ini or php_flag/php_admin_flag... but *not*
1496 * for those set via php_value/php_admin_value.
1497 *
1498 * It's fairly common for people to use php_value instead of php_flag,
1499 * which can leave you with an 'off' setting giving a false positive
1500 * for code that just takes the ini_get() return value as a boolean.
1501 *
1502 * To make things extra interesting, setting via php_value accepts
1503 * "true" and "yes" as true, but php.ini and php_flag consider them false. :)
1504 * Unrecognized values go false... again opposite PHP's own coercion
1505 * from string to bool.
1506 *
1507 * Luckily, 'properly' set settings will always come back as '0' or '1',
1508 * so we only have to worry about them and the 'improper' settings.
1509 *
1510 * I frickin' hate PHP... :P
1511 *
1512 * @param string $setting
1513 * @return bool
1514 */
1515function wfIniGetBool( $setting ) {
1516    return wfStringToBool( ini_get( $setting ) );
1517}
1518
1519/**
1520 * Convert string value to boolean, when the following are interpreted as true:
1521 * - on
1522 * - true
1523 * - yes
1524 * - Any number, except 0
1525 * All other strings are interpreted as false.
1526 *
1527 * @param string $val
1528 * @return bool
1529 * @since 1.31
1530 */
1531function wfStringToBool( $val ) {
1532    $val = strtolower( $val );
1533    // 'on' and 'true' can't have whitespace around them, but '1' can.
1534    return $val == 'on'
1535        || $val == 'true'
1536        || $val == 'yes'
1537        || preg_match( "/^\s*[+-]?0*[1-9]/", $val ); // approx C atoi() function
1538}
1539
1540/**
1541 * Locale-independent version of escapeshellarg()
1542 *
1543 * Originally, this fixed the incorrect use of single quotes on Windows
1544 * (https://bugs.php.net/bug.php?id=26285) and the locale problems on Linux in
1545 * PHP 5.2.6+ (https://bugs.php.net/bug.php?id=54391). The second bug is still
1546 * open as of 2021.
1547 *
1548 * @param string|string[] ...$args strings to escape and glue together,
1549 *  or a single array of strings parameter
1550 * @return string
1551 * @deprecated since 1.30 use MediaWiki\Shell\Shell::escape()
1552 */
1553function wfEscapeShellArg( ...$args ) {
1554    return Shell::escape( ...$args );
1555}
1556
1557/**
1558 * Execute a shell command, with time and memory limits mirrored from the PHP
1559 * configuration if supported.
1560 *
1561 * @param string|string[] $cmd If string, a properly shell-escaped command line,
1562 *   or an array of unescaped arguments, in which case each value will be escaped
1563 *   Example:   [ 'convert', '-font', 'font name' ] would produce "'convert' '-font' 'font name'"
1564 * @param null|mixed &$retval Optional, will receive the program's exit code.
1565 *   (non-zero is usually failure). If there is an error from
1566 *   read, select, or proc_open(), this will be set to -1.
1567 * @param array $environ Optional environment variables which should be
1568 *   added to the executed command environment.
1569 * @param array $limits Optional array with limits(filesize, memory, time, walltime)
1570 *   this overwrites the global wgMaxShell* limits.
1571 * @param array $options Array of options:
1572 *   - duplicateStderr: Set this to true to duplicate stderr to stdout,
1573 *     including errors from limit.sh
1574 *   - profileMethod: By default this function will profile based on the calling
1575 *     method. Set this to a string for an alternative method to profile from
1576 * @phan-param array{duplicateStderr?:bool,profileMethod?:string} $options
1577 *
1578 * @return string Collected stdout as a string
1579 * @deprecated since 1.30 use class MediaWiki\Shell\Shell
1580 */
1581function wfShellExec( $cmd, &$retval = null, $environ = [],
1582    $limits = [], $options = []
1583) {
1584    if ( Shell::isDisabled() ) {
1585        $retval = 1;
1586        // Backwards compatibility be upon us...
1587        return 'Unable to run external programs, proc_open() is disabled.';
1588    }
1589
1590    if ( is_array( $cmd ) ) {
1591        $cmd = Shell::escape( $cmd );
1592    }
1593
1594    $includeStderr = isset( $options['duplicateStderr'] ) && $options['duplicateStderr'];
1595    $profileMethod = $options['profileMethod'] ?? wfGetCaller();
1596
1597    try {
1598        $result = Shell::command( [] )
1599            ->unsafeParams( (array)$cmd )
1600            ->environment( $environ )
1601            ->limits( $limits )
1602            ->includeStderr( $includeStderr )
1603            ->profileMethod( $profileMethod )
1604            // For b/c
1605            ->restrict( Shell::RESTRICT_NONE )
1606            ->execute();
1607    } catch ( ProcOpenError $ex ) {
1608        $retval = -1;
1609        return '';
1610    }
1611
1612    $retval = $result->getExitCode();
1613
1614    return $result->getStdout();
1615}
1616
1617/**
1618 * Execute a shell command, returning both stdout and stderr. Convenience
1619 * function, as all the arguments to wfShellExec can become unwieldy.
1620 *
1621 * @note This also includes errors from limit.sh, e.g. if $wgMaxShellFileSize is exceeded.
1622 * @param string|string[] $cmd If string, a properly shell-escaped command line,
1623 *   or an array of unescaped arguments, in which case each value will be escaped
1624 *   Example:   [ 'convert', '-font', 'font name' ] would produce "'convert' '-font' 'font name'"
1625 * @param null|mixed &$retval Optional, will receive the program's exit code.
1626 *   (non-zero is usually failure)
1627 * @param array $environ Optional environment variables which should be
1628 *   added to the executed command environment.
1629 * @param array $limits Optional array with limits(filesize, memory, time, walltime)
1630 *   this overwrites the global wgMaxShell* limits.
1631 * @return string Collected stdout and stderr as a string
1632 * @deprecated since 1.30 use class MediaWiki\Shell\Shell
1633 */
1634function wfShellExecWithStderr( $cmd, &$retval = null, $environ = [], $limits = [] ) {
1635    return wfShellExec( $cmd, $retval, $environ, $limits,
1636        [ 'duplicateStderr' => true, 'profileMethod' => wfGetCaller() ] );
1637}
1638
1639/**
1640 * Generate a shell-escaped command line string to run a MediaWiki cli script.
1641 * Note that $parameters should be a flat array and an option with an argument
1642 * should consist of two consecutive items in the array (do not use "--option value").
1643 *
1644 * @deprecated since 1.31, use Shell::makeScriptCommand()
1645 *
1646 * @param string $script MediaWiki cli script path
1647 * @param array $parameters Arguments and options to the script
1648 * @param array $options Associative array of options:
1649 *     'php': The path to the php executable
1650 *     'wrapper': Path to a PHP wrapper to handle the maintenance script
1651 * @phan-param array{php?:string,wrapper?:string} $options
1652 * @return string
1653 */
1654function wfShellWikiCmd( $script, array $parameters = [], array $options = [] ) {
1655    global $wgPhpCli;
1656    // Give site config file a chance to run the script in a wrapper.
1657    // The caller may likely want to call wfBasename() on $script.
1658    ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
1659        ->onWfShellWikiCmd( $script, $parameters, $options );
1660    $cmd = [ $options['php'] ?? $wgPhpCli ];
1661    if ( isset( $options['wrapper'] ) ) {
1662        $cmd[] = $options['wrapper'];
1663    }
1664    $cmd[] = $script;
1665    // Escape each parameter for shell
1666    return Shell::escape( array_merge( $cmd, $parameters ) );
1667}
1668
1669/**
1670 * wfMerge attempts to merge differences between three texts.
1671 *
1672 * @param string $old Common base revision
1673 * @param string $mine The edit we wish to store but which potentially conflicts with another edit
1674 *     which happened since we started editing.
1675 * @param string $yours The most recent stored revision of the article. Note that "mine" and "yours"
1676 *     might have another meaning depending on the specific use case.
1677 * @param string|null &$simplisticMergeAttempt Automatically merged text, with overlapping edits
1678 *     falling back to "my" text.
1679 * @param string|null &$mergeLeftovers Optional out parameter containing an "ed" script with the
1680 *     remaining bits of "your" text that could not be merged into $simplisticMergeAttempt.
1681 *     The "ed" file format is documented here:
1682 *     https://www.gnu.org/software/diffutils/manual/html_node/Detailed-ed.html
1683 * @return bool true for a clean merge and false for failure or a conflict.
1684 */
1685function wfMerge(
1686    string $old,
1687    string $mine,
1688    string $yours,
1689    ?string &$simplisticMergeAttempt,
1690    string &$mergeLeftovers = null
1691): bool {
1692    global $wgDiff3;
1693
1694    # This check may also protect against code injection in
1695    # case of broken installations.
1696    AtEase::suppressWarnings();
1697    $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 );
1698    AtEase::restoreWarnings();
1699
1700    if ( !$haveDiff3 ) {
1701        wfDebug( "diff3 not found" );
1702        return false;
1703    }
1704
1705    # Make temporary files
1706    $td = wfTempDir();
1707    $oldtextFile = fopen( $oldtextName = tempnam( $td, 'merge-old-' ), 'w' );
1708    $mytextFile = fopen( $mytextName = tempnam( $td, 'merge-mine-' ), 'w' );
1709    $yourtextFile = fopen( $yourtextName = tempnam( $td, 'merge-your-' ), 'w' );
1710
1711    # NOTE: diff3 issues a warning to stderr if any of the files does not end with
1712    #       a newline character. To avoid this, we normalize the trailing whitespace before
1713    #       creating the diff.
1714
1715    fwrite( $oldtextFile, rtrim( $old ) . "\n" );
1716    fclose( $oldtextFile );
1717    fwrite( $mytextFile, rtrim( $mine ) . "\n" );
1718    fclose( $mytextFile );
1719    fwrite( $yourtextFile, rtrim( $yours ) . "\n" );
1720    fclose( $yourtextFile );
1721
1722    # Check for a conflict
1723    $cmd = Shell::escape( $wgDiff3, '--text', '--overlap-only', $mytextName,
1724        $oldtextName, $yourtextName );
1725    $handle = popen( $cmd, 'r' );
1726
1727    $mergeLeftovers = '';
1728    do {
1729        $data = fread( $handle, 8192 );
1730        if ( strlen( $data ) == 0 ) {
1731            break;
1732        }
1733        $mergeLeftovers .= $data;
1734    } while ( true );
1735    pclose( $handle );
1736
1737    $conflict = $mergeLeftovers !== '';
1738
1739    # Merge differences automatically where possible, preferring "my" text for conflicts.
1740    $cmd = Shell::escape( $wgDiff3, '--text', '--ed', '--merge', $mytextName,
1741        $oldtextName, $yourtextName );
1742    $handle = popen( $cmd, 'r' );
1743    $simplisticMergeAttempt = '';
1744    do {
1745        $data = fread( $handle, 8192 );
1746        if ( strlen( $data ) == 0 ) {
1747            break;
1748        }
1749        $simplisticMergeAttempt .= $data;
1750    } while ( true );
1751    pclose( $handle );
1752    unlink( $mytextName );
1753    unlink( $oldtextName );
1754    unlink( $yourtextName );
1755
1756    if ( $simplisticMergeAttempt === '' && $old !== '' && !$conflict ) {
1757        wfDebug( "Unexpected null result from diff3. Command: $cmd" );
1758        $conflict = true;
1759    }
1760    return !$conflict;
1761}
1762
1763/**
1764 * Return the final portion of a pathname.
1765 * Reimplemented because PHP5's "basename()" is buggy with multibyte text.
1766 * https://bugs.php.net/bug.php?id=33898
1767 *
1768 * PHP's basename() only considers '\' a pathchar on Windows and Netware.
1769 * We'll consider it so always, as we don't want '\s' in our Unix paths either.
1770 *
1771 * @param string $path
1772 * @param string $suffix String to remove if present
1773 * @return string
1774 */
1775function wfBaseName( $path, $suffix = '' ) {
1776    if ( $suffix == '' ) {
1777        $encSuffix = '';
1778    } else {
1779        $encSuffix = '(?:' . preg_quote( $suffix, '#' ) . ')?';
1780    }
1781
1782    $matches = [];
1783    if ( preg_match( "#([^/\\\\]*?){$encSuffix}[/\\\\]*$#", $path, $matches ) ) {
1784        return $matches[1];
1785    } else {
1786        return '';
1787    }
1788}
1789
1790/**
1791 * Generate a relative path name to the given file.
1792 * May explode on non-matching case-insensitive paths,
1793 * funky symlinks, etc.
1794 *
1795 * @param string $path Absolute destination path including target filename
1796 * @param string $from Absolute source path, directory only
1797 * @return string
1798 */
1799function wfRelativePath( $path, $from ) {
1800    // Normalize mixed input on Windows...
1801    $path = str_replace( '/', DIRECTORY_SEPARATOR, $path );
1802    $from = str_replace( '/', DIRECTORY_SEPARATOR, $from );
1803
1804    // Trim trailing slashes -- fix for drive root
1805    $path = rtrim( $path, DIRECTORY_SEPARATOR );
1806    $from = rtrim( $from, DIRECTORY_SEPARATOR );
1807
1808    $pieces = explode( DIRECTORY_SEPARATOR, dirname( $path ) );
1809    $against = explode( DIRECTORY_SEPARATOR, $from );
1810
1811    if ( $pieces[0] !== $against[0] ) {
1812        // Non-matching Windows drive letters?
1813        // Return a full path.
1814        return $path;
1815    }
1816
1817    // Trim off common prefix
1818    while ( count( $pieces ) && count( $against )
1819        && $pieces[0] == $against[0] ) {
1820        array_shift( $pieces );
1821        array_shift( $against );
1822    }
1823
1824    // relative dots to bump us to the parent
1825    while ( count( $against ) ) {
1826        array_unshift( $pieces, '..' );
1827        array_shift( $against );
1828    }
1829
1830    $pieces[] = wfBaseName( $path );
1831
1832    return implode( DIRECTORY_SEPARATOR, $pieces );
1833}
1834
1835/**
1836 * Get a Database object.
1837 *
1838 * @param int $db Index of the connection to get. May be DB_PRIMARY for the
1839 *            primary (for write queries), DB_REPLICA for potentially lagged read
1840 *            queries, or an integer >= 0 for a particular server.
1841 *
1842 * @param string|string[] $groups Query groups. An array of group names that this query
1843 *                belongs to. May contain a single string if the query is only
1844 *                in one group.
1845 *
1846 * @param string|false $wiki The wiki ID, or false for the current wiki
1847 *
1848 * Note: multiple calls to wfGetDB(DB_REPLICA) during the course of one request
1849 * will always return the same object, unless the underlying connection or load
1850 * balancer is manually destroyed.
1851 *
1852 * Note 2: use $this->getDB() in maintenance scripts that may be invoked by
1853 * updater to ensure that a proper database is being updated.
1854 *
1855 * Note 3: When replacing calls to this with calls to methods on an injected
1856 * LoadBalancer, LoadBalancer::getConnection is more commonly needed than
1857 * LoadBalancer::getMaintenanceConnectionRef, which is needed for more advanced
1858 * administrative tasks. See the IMaintainableDatabase and IDatabase interfaces
1859 * for details.
1860 *
1861 * @deprecated since 1.39, emitting warnings since 1.42; instead, you can use:
1862 *   $services = MediaWikiServices::getInstance();
1863 *   $dbr = $services->getConnectionProvider()->getReplicaDatabase();
1864 *   $dbw = $services->getConnectionProvider()->getPrimaryDatabase();
1865 *
1866 *      â€¦ or, in rare circumstances, you may need to use:
1867 *
1868 *   $services->getDBLoadBalancer()->getConnection() / getMaintenanceConnectionRef()
1869 *
1870 * @return \Wikimedia\Rdbms\DBConnRef
1871 */
1872function wfGetDB( $db, $groups = [], $wiki = false ) {
1873    wfDeprecated( __FUNCTION__, '1.39' );
1874
1875    if ( $wiki === false ) {
1876        return MediaWikiServices::getInstance()
1877            ->getDBLoadBalancer()
1878            ->getMaintenanceConnectionRef( $db, $groups, $wiki );
1879    } else {
1880        return MediaWikiServices::getInstance()
1881            ->getDBLoadBalancerFactory()
1882            ->getMainLB( $wiki )
1883            ->getMaintenanceConnectionRef( $db, $groups, $wiki );
1884    }
1885}
1886
1887/**
1888 * Get the URL path to a MediaWiki entry point.
1889 *
1890 * This is a wrapper to respect $wgScript and $wgLoadScript overrides.
1891 *
1892 * @see MW_ENTRY_POINT
1893 * @param string $script Name of entrypoint, without `.php` extension.
1894 * @return string
1895 */
1896function wfScript( $script = 'index' ) {
1897    global $wgScriptPath, $wgScript, $wgLoadScript;
1898    if ( $script === 'index' ) {
1899        return $wgScript;
1900    } elseif ( $script === 'load' ) {
1901        return $wgLoadScript;
1902    } else {
1903        return "{$wgScriptPath}/{$script}.php";
1904    }
1905}
1906
1907/**
1908 * Convenience function converts boolean values into "true"
1909 * or "false" (string) values
1910 *
1911 * @param bool $value
1912 * @return string
1913 */
1914function wfBoolToStr( $value ) {
1915    return $value ? 'true' : 'false';
1916}
1917
1918/**
1919 * Get a platform-independent path to the null file, e.g. /dev/null
1920 *
1921 * @return string
1922 */
1923function wfGetNull() {
1924    return wfIsWindows() ? 'NUL' : '/dev/null';
1925}
1926
1927/**
1928 * Replace all invalid characters with '-'.
1929 * Additional characters can be defined in $wgIllegalFileChars (see T22489).
1930 * By default, $wgIllegalFileChars includes ':', '/', '\'.
1931 *
1932 * @param string $name Filename to process
1933 * @return string
1934 */
1935function wfStripIllegalFilenameChars( $name ) {
1936    global $wgIllegalFileChars;
1937    $illegalFileChars = $wgIllegalFileChars ? "|[" . $wgIllegalFileChars . "]" : '';
1938    $name = preg_replace(
1939        "/[^" . Title::legalChars() . "]" . $illegalFileChars . "/",
1940        '-',
1941        $name
1942    );
1943    // $wgIllegalFileChars may not include '/' and '\', so we still need to do this
1944    $name = wfBaseName( $name );
1945    return $name;
1946}
1947
1948/**
1949 * Raise PHP's memory limit (if needed).
1950 *
1951 * @internal For use by Setup.php
1952 * @param int $newLimit
1953 */
1954function wfMemoryLimit( $newLimit ) {
1955    $oldLimit = wfShorthandToInteger( ini_get( 'memory_limit' ) );
1956    // If the INI config is already unlimited, there is nothing larger
1957    if ( $oldLimit != -1 ) {
1958        $newLimit = wfShorthandToInteger( (string)$newLimit );
1959        if ( $newLimit == -1 ) {
1960            wfDebug( "Removing PHP's memory limit" );
1961            AtEase::suppressWarnings();
1962            ini_set( 'memory_limit', $newLimit );
1963            AtEase::restoreWarnings();
1964        } elseif ( $newLimit > $oldLimit ) {
1965            wfDebug( "Raising PHP's memory limit to $newLimit bytes" );
1966            AtEase::suppressWarnings();
1967            ini_set( 'memory_limit', $newLimit );
1968            AtEase::restoreWarnings();
1969        }
1970    }
1971}
1972
1973/**
1974 * Raise the request time limit to $wgTransactionalTimeLimit
1975 *
1976 * @return int Prior time limit
1977 * @since 1.26
1978 */
1979function wfTransactionalTimeLimit() {
1980    global $wgTransactionalTimeLimit;
1981
1982    $timeout = RequestTimeout::singleton();
1983    $timeLimit = $timeout->getWallTimeLimit();
1984    if ( $timeLimit !== INF ) {
1985        // RequestTimeout library is active
1986        if ( $wgTransactionalTimeLimit > $timeLimit ) {
1987            $timeout->setWallTimeLimit( $wgTransactionalTimeLimit );
1988        }
1989    } else {
1990        // Fallback case, likely $wgRequestTimeLimit === null
1991        $timeLimit = (int)ini_get( 'max_execution_time' );
1992        // Note that CLI scripts use 0
1993        if ( $timeLimit > 0 && $wgTransactionalTimeLimit > $timeLimit ) {
1994            $timeout->setWallTimeLimit( $wgTransactionalTimeLimit );
1995        }
1996    }
1997    ignore_user_abort( true ); // ignore client disconnects
1998
1999    return $timeLimit;
2000}
2001
2002/**
2003 * Converts shorthand byte notation to integer form
2004 *
2005 * @param null|string $string
2006 * @param int $default Returned if $string is empty
2007 * @return int
2008 */
2009function wfShorthandToInteger( ?string $string = '', int $default = -1 ): int {
2010    $string = trim( $string ?? '' );
2011    if ( $string === '' ) {
2012        return $default;
2013    }
2014    $last = $string[strlen( $string ) - 1];
2015    $val = intval( $string );
2016    switch ( $last ) {
2017        case 'g':
2018        case 'G':
2019            $val *= 1024;
2020            // break intentionally missing
2021        case 'm':
2022        case 'M':
2023            $val *= 1024;
2024            // break intentionally missing
2025        case 'k':
2026        case 'K':
2027            $val *= 1024;
2028    }
2029
2030    return $val;
2031}
2032
2033/**
2034 * Wrapper around php's unpack.
2035 *
2036 * @param string $format The format string (See php's docs)
2037 * @param string $data A binary string of binary data
2038 * @param int|bool $length The minimum length of $data or false. This is to
2039 *     prevent reading beyond the end of $data. false to disable the check.
2040 *
2041 * Also be careful when using this function to read unsigned 32 bit integer
2042 * because php might make it negative.
2043 *
2044 * @throws MWException If $data not long enough, or if unpack fails
2045 * @return array Associative array of the extracted data
2046 * @deprecated since 1.42 Use StringUtils::unpack instead
2047 */
2048function wfUnpack( $format, $data, $length = false ) {
2049    wfDeprecated( __FUNCTION__, '1.42' );
2050    try {
2051        return StringUtils::unpack( (string)$format, (string)$data, $length );
2052    } catch ( UnpackFailedException $e ) {
2053        throw new MWException( $e->getMessage(), 0, $e );
2054    }
2055}
2056
2057/**
2058 * Determine input string is represents as infinity
2059 *
2060 * @param string $str The string to determine
2061 * @return bool
2062 * @since 1.25
2063 */
2064function wfIsInfinity( $str ) {
2065    // The INFINITY_VALS are hardcoded elsewhere in MediaWiki (e.g. mediawiki.special.block.js).
2066    return in_array( $str, ExpiryDef::INFINITY_VALS );
2067}
2068
2069/**
2070 * Returns true if these thumbnail parameters match one that MediaWiki
2071 * requests from file description pages and/or parser output.
2072 *
2073 * $params is considered non-standard if they involve a non-standard
2074 * width or any non-default parameters aside from width and page number.
2075 * The number of possible files with standard parameters is far less than
2076 * that of all combinations; rate-limiting for them can thus be more generous.
2077 *
2078 * @param File $file
2079 * @param array $params
2080 * @return bool
2081 * @since 1.24 Moved from thumb.php to GlobalFunctions in 1.25
2082 */
2083function wfThumbIsStandard( File $file, array $params ) {
2084    global $wgThumbLimits, $wgImageLimits, $wgResponsiveImages;
2085
2086    $multipliers = [ 1 ];
2087    if ( $wgResponsiveImages ) {
2088        // These available sizes are hardcoded currently elsewhere in MediaWiki.
2089        // @see Linker::processResponsiveImages
2090        $multipliers[] = 1.5;
2091        $multipliers[] = 2;
2092    }
2093
2094    $handler = $file->getHandler();
2095    if ( !$handler || !isset( $params['width'] ) ) {
2096        return false;
2097    }
2098
2099    $basicParams = [];
2100    if ( isset( $params['page'] ) ) {
2101        $basicParams['page'] = $params['page'];
2102    }
2103
2104    $thumbLimits = [];
2105    $imageLimits = [];
2106    // Expand limits to account for multipliers
2107    foreach ( $multipliers as $multiplier ) {
2108        $thumbLimits = array_merge( $thumbLimits, array_map(
2109            static function ( $width ) use ( $multiplier ) {
2110                return round( $width * $multiplier );
2111            }, $wgThumbLimits )
2112        );
2113        $imageLimits = array_merge( $imageLimits, array_map(
2114            static function ( $pair ) use ( $multiplier ) {
2115                return [
2116                    round( $pair[0] * $multiplier ),
2117                    round( $pair[1] * $multiplier ),
2118                ];
2119            }, $wgImageLimits )
2120        );
2121    }
2122
2123    // Check if the width matches one of $wgThumbLimits
2124    if ( in_array( $params['width'], $thumbLimits ) ) {
2125        $normalParams = $basicParams + [ 'width' => $params['width'] ];
2126        // Append any default values to the map (e.g. "lossy", "lossless", ...)
2127        $handler->normaliseParams( $file, $normalParams );
2128    } else {
2129        // If not, then check if the width matches one of $wgImageLimits
2130        $match = false;
2131        foreach ( $imageLimits as $pair ) {
2132            $normalParams = $basicParams + [ 'width' => $pair[0], 'height' => $pair[1] ];
2133            // Decide whether the thumbnail should be scaled on width or height.
2134            // Also append any default values to the map (e.g. "lossy", "lossless", ...)
2135            $handler->normaliseParams( $file, $normalParams );
2136            // Check if this standard thumbnail size maps to the given width
2137            if ( $normalParams['width'] == $params['width'] ) {
2138                $match = true;
2139                break;
2140            }
2141        }
2142        if ( !$match ) {
2143            return false; // not standard for description pages
2144        }
2145    }
2146
2147    // Check that the given values for non-page, non-width, params are just defaults
2148    foreach ( $params as $key => $value ) {
2149        if ( !isset( $normalParams[$key] ) || $normalParams[$key] != $value ) {
2150            return false;
2151        }
2152    }
2153
2154    return true;
2155}
2156
2157/**
2158 * Merges two (possibly) 2 dimensional arrays into the target array ($baseArray).
2159 *
2160 * Values that exist in both values will be combined with += (all values of the array
2161 * of $newValues will be added to the values of the array of $baseArray, while values,
2162 * that exists in both, the value of $baseArray will be used).
2163 *
2164 * @param array $baseArray The array where you want to add the values of $newValues to
2165 * @param array $newValues An array with new values
2166 * @return array The combined array
2167 * @since 1.26
2168 */
2169function wfArrayPlus2d( array $baseArray, array $newValues ) {
2170    // First merge items that are in both arrays
2171    foreach ( $baseArray as $name => &$groupVal ) {
2172        if ( isset( $newValues[$name] ) ) {
2173            $groupVal += $newValues[$name];
2174        }
2175    }
2176    // Now add items that didn't exist yet
2177    $baseArray += $newValues;
2178
2179    return $baseArray;
2180}