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