Completed
Branch master (8ef871)
by
unknown
29:40
created

Language::alignStart()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 3
rs 10
cc 2
eloc 2
nc 2
nop 0
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 43 and the first side effect is on line 29.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
/**
3
 * Internationalisation code.
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
 * @ingroup Language
22
 */
23
24
/**
25
 * @defgroup Language Language
26
 */
27
28
if ( !defined( 'MEDIAWIKI' ) ) {
29
	echo "This file is part of MediaWiki, it is not a valid entry point.\n";
30
	exit( 1 );
31
}
32
33
if ( function_exists( 'mb_strtoupper' ) ) {
34
	mb_internal_encoding( 'UTF-8' );
35
}
36
37
use CLDRPluralRuleParser\Evaluator;
38
39
/**
40
 * Internationalisation code
41
 * @ingroup Language
42
 */
43
class Language {
44
	/**
45
	 * @var LanguageConverter
46
	 */
47
	public $mConverter;
48
49
	public $mVariants, $mCode, $mLoaded = false;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
50
	public $mMagicExtensions = [], $mMagicHookDone = false;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
51
	private $mHtmlCode = null, $mParentLanguage = false;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
52
53
	public $dateFormatStrings = [];
54
	public $mExtendedSpecialPageAliases;
55
56
	protected $namespaceNames, $mNamespaceIds, $namespaceAliases;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
57
58
	/**
59
	 * ReplacementArray object caches
60
	 */
61
	public $transformData = [];
62
63
	/**
64
	 * @var LocalisationCache
65
	 */
66
	static public $dataCache;
67
68
	static public $mLangObjCache = [];
69
70
	static public $mWeekdayMsgs = [
71
		'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
72
		'friday', 'saturday'
73
	];
74
75
	static public $mWeekdayAbbrevMsgs = [
76
		'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
77
	];
78
79
	static public $mMonthMsgs = [
80
		'january', 'february', 'march', 'april', 'may_long', 'june',
81
		'july', 'august', 'september', 'october', 'november',
82
		'december'
83
	];
84
	static public $mMonthGenMsgs = [
85
		'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
86
		'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
87
		'december-gen'
88
	];
89
	static public $mMonthAbbrevMsgs = [
90
		'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
91
		'sep', 'oct', 'nov', 'dec'
92
	];
93
94
	static public $mIranianCalendarMonthMsgs = [
95
		'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
96
		'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
97
		'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
98
		'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
99
	];
100
101
	static public $mHebrewCalendarMonthMsgs = [
102
		'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
103
		'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
104
		'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
105
		'hebrew-calendar-m10', 'hebrew-calendar-m11', 'hebrew-calendar-m12',
106
		'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
107
	];
108
109
	static public $mHebrewCalendarMonthGenMsgs = [
110
		'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
111
		'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
112
		'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
113
		'hebrew-calendar-m10-gen', 'hebrew-calendar-m11-gen', 'hebrew-calendar-m12-gen',
114
		'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
115
	];
116
117
	static public $mHijriCalendarMonthMsgs = [
118
		'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
119
		'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
120
		'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
121
		'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12'
122
	];
123
124
	/**
125
	 * @since 1.20
126
	 * @var array
127
	 */
128
	static public $durationIntervals = [
129
		'millennia' => 31556952000,
130
		'centuries' => 3155695200,
131
		'decades' => 315569520,
132
		'years' => 31556952, // 86400 * ( 365 + ( 24 * 3 + 25 ) / 400 )
133
		'weeks' => 604800,
134
		'days' => 86400,
135
		'hours' => 3600,
136
		'minutes' => 60,
137
		'seconds' => 1,
138
	];
139
140
	/**
141
	 * Cache for language fallbacks.
142
	 * @see Language::getFallbacksIncludingSiteLanguage
143
	 * @since 1.21
144
	 * @var array
145
	 */
146
	static private $fallbackLanguageCache = [];
147
148
	/**
149
	 * Cache for language names
150
	 * @var HashBagOStuff|null
151
	 */
152
	static private $languageNameCache;
153
154
	/**
155
	 * Unicode directional formatting characters, for embedBidi()
156
	 */
157
	static private $lre = "\xE2\x80\xAA"; // U+202A LEFT-TO-RIGHT EMBEDDING
158
	static private $rle = "\xE2\x80\xAB"; // U+202B RIGHT-TO-LEFT EMBEDDING
159
	static private $pdf = "\xE2\x80\xAC"; // U+202C POP DIRECTIONAL FORMATTING
160
161
	/**
162
	 * Directionality test regex for embedBidi(). Matches the first strong directionality codepoint:
163
	 * - in group 1 if it is LTR
164
	 * - in group 2 if it is RTL
165
	 * Does not match if there is no strong directionality codepoint.
166
	 *
167
	 * The form is '/(?:([strong ltr codepoint])|([strong rtl codepoint]))/u' .
168
	 *
169
	 * Generated by UnicodeJS (see tools/strongDir) from the UCD; see
170
	 * https://git.wikimedia.org/summary/unicodejs.git .
171
	 */
172
	// @codingStandardsIgnoreStart
173
	// @codeCoverageIgnoreStart
174
	static private $strongDirRegex = '/(?:([\x{41}-\x{5a}\x{61}-\x{7a}\x{aa}\x{b5}\x{ba}\x{c0}-\x{d6}\x{d8}-\x{f6}\x{f8}-\x{2b8}\x{2bb}-\x{2c1}\x{2d0}\x{2d1}\x{2e0}-\x{2e4}\x{2ee}\x{370}-\x{373}\x{376}\x{377}\x{37a}-\x{37d}\x{37f}\x{386}\x{388}-\x{38a}\x{38c}\x{38e}-\x{3a1}\x{3a3}-\x{3f5}\x{3f7}-\x{482}\x{48a}-\x{52f}\x{531}-\x{556}\x{559}-\x{55f}\x{561}-\x{587}\x{589}\x{903}-\x{939}\x{93b}\x{93d}-\x{940}\x{949}-\x{94c}\x{94e}-\x{950}\x{958}-\x{961}\x{964}-\x{980}\x{982}\x{983}\x{985}-\x{98c}\x{98f}\x{990}\x{993}-\x{9a8}\x{9aa}-\x{9b0}\x{9b2}\x{9b6}-\x{9b9}\x{9bd}-\x{9c0}\x{9c7}\x{9c8}\x{9cb}\x{9cc}\x{9ce}\x{9d7}\x{9dc}\x{9dd}\x{9df}-\x{9e1}\x{9e6}-\x{9f1}\x{9f4}-\x{9fa}\x{a03}\x{a05}-\x{a0a}\x{a0f}\x{a10}\x{a13}-\x{a28}\x{a2a}-\x{a30}\x{a32}\x{a33}\x{a35}\x{a36}\x{a38}\x{a39}\x{a3e}-\x{a40}\x{a59}-\x{a5c}\x{a5e}\x{a66}-\x{a6f}\x{a72}-\x{a74}\x{a83}\x{a85}-\x{a8d}\x{a8f}-\x{a91}\x{a93}-\x{aa8}\x{aaa}-\x{ab0}\x{ab2}\x{ab3}\x{ab5}-\x{ab9}\x{abd}-\x{ac0}\x{ac9}\x{acb}\x{acc}\x{ad0}\x{ae0}\x{ae1}\x{ae6}-\x{af0}\x{af9}\x{b02}\x{b03}\x{b05}-\x{b0c}\x{b0f}\x{b10}\x{b13}-\x{b28}\x{b2a}-\x{b30}\x{b32}\x{b33}\x{b35}-\x{b39}\x{b3d}\x{b3e}\x{b40}\x{b47}\x{b48}\x{b4b}\x{b4c}\x{b57}\x{b5c}\x{b5d}\x{b5f}-\x{b61}\x{b66}-\x{b77}\x{b83}\x{b85}-\x{b8a}\x{b8e}-\x{b90}\x{b92}-\x{b95}\x{b99}\x{b9a}\x{b9c}\x{b9e}\x{b9f}\x{ba3}\x{ba4}\x{ba8}-\x{baa}\x{bae}-\x{bb9}\x{bbe}\x{bbf}\x{bc1}\x{bc2}\x{bc6}-\x{bc8}\x{bca}-\x{bcc}\x{bd0}\x{bd7}\x{be6}-\x{bf2}\x{c01}-\x{c03}\x{c05}-\x{c0c}\x{c0e}-\x{c10}\x{c12}-\x{c28}\x{c2a}-\x{c39}\x{c3d}\x{c41}-\x{c44}\x{c58}-\x{c5a}\x{c60}\x{c61}\x{c66}-\x{c6f}\x{c7f}\x{c82}\x{c83}\x{c85}-\x{c8c}\x{c8e}-\x{c90}\x{c92}-\x{ca8}\x{caa}-\x{cb3}\x{cb5}-\x{cb9}\x{cbd}-\x{cc4}\x{cc6}-\x{cc8}\x{cca}\x{ccb}\x{cd5}\x{cd6}\x{cde}\x{ce0}\x{ce1}\x{ce6}-\x{cef}\x{cf1}\x{cf2}\x{d02}\x{d03}\x{d05}-\x{d0c}\x{d0e}-\x{d10}\x{d12}-\x{d3a}\x{d3d}-\x{d40}\x{d46}-\x{d48}\x{d4a}-\x{d4c}\x{d4e}\x{d57}\x{d5f}-\x{d61}\x{d66}-\x{d75}\x{d79}-\x{d7f}\x{d82}\x{d83}\x{d85}-\x{d96}\x{d9a}-\x{db1}\x{db3}-\x{dbb}\x{dbd}\x{dc0}-\x{dc6}\x{dcf}-\x{dd1}\x{dd8}-\x{ddf}\x{de6}-\x{def}\x{df2}-\x{df4}\x{e01}-\x{e30}\x{e32}\x{e33}\x{e40}-\x{e46}\x{e4f}-\x{e5b}\x{e81}\x{e82}\x{e84}\x{e87}\x{e88}\x{e8a}\x{e8d}\x{e94}-\x{e97}\x{e99}-\x{e9f}\x{ea1}-\x{ea3}\x{ea5}\x{ea7}\x{eaa}\x{eab}\x{ead}-\x{eb0}\x{eb2}\x{eb3}\x{ebd}\x{ec0}-\x{ec4}\x{ec6}\x{ed0}-\x{ed9}\x{edc}-\x{edf}\x{f00}-\x{f17}\x{f1a}-\x{f34}\x{f36}\x{f38}\x{f3e}-\x{f47}\x{f49}-\x{f6c}\x{f7f}\x{f85}\x{f88}-\x{f8c}\x{fbe}-\x{fc5}\x{fc7}-\x{fcc}\x{fce}-\x{fda}\x{1000}-\x{102c}\x{1031}\x{1038}\x{103b}\x{103c}\x{103f}-\x{1057}\x{105a}-\x{105d}\x{1061}-\x{1070}\x{1075}-\x{1081}\x{1083}\x{1084}\x{1087}-\x{108c}\x{108e}-\x{109c}\x{109e}-\x{10c5}\x{10c7}\x{10cd}\x{10d0}-\x{1248}\x{124a}-\x{124d}\x{1250}-\x{1256}\x{1258}\x{125a}-\x{125d}\x{1260}-\x{1288}\x{128a}-\x{128d}\x{1290}-\x{12b0}\x{12b2}-\x{12b5}\x{12b8}-\x{12be}\x{12c0}\x{12c2}-\x{12c5}\x{12c8}-\x{12d6}\x{12d8}-\x{1310}\x{1312}-\x{1315}\x{1318}-\x{135a}\x{1360}-\x{137c}\x{1380}-\x{138f}\x{13a0}-\x{13f5}\x{13f8}-\x{13fd}\x{1401}-\x{167f}\x{1681}-\x{169a}\x{16a0}-\x{16f8}\x{1700}-\x{170c}\x{170e}-\x{1711}\x{1720}-\x{1731}\x{1735}\x{1736}\x{1740}-\x{1751}\x{1760}-\x{176c}\x{176e}-\x{1770}\x{1780}-\x{17b3}\x{17b6}\x{17be}-\x{17c5}\x{17c7}\x{17c8}\x{17d4}-\x{17da}\x{17dc}\x{17e0}-\x{17e9}\x{1810}-\x{1819}\x{1820}-\x{1877}\x{1880}-\x{18a8}\x{18aa}\x{18b0}-\x{18f5}\x{1900}-\x{191e}\x{1923}-\x{1926}\x{1929}-\x{192b}\x{1930}\x{1931}\x{1933}-\x{1938}\x{1946}-\x{196d}\x{1970}-\x{1974}\x{1980}-\x{19ab}\x{19b0}-\x{19c9}\x{19d0}-\x{19da}\x{1a00}-\x{1a16}\x{1a19}\x{1a1a}\x{1a1e}-\x{1a55}\x{1a57}\x{1a61}\x{1a63}\x{1a64}\x{1a6d}-\x{1a72}\x{1a80}-\x{1a89}\x{1a90}-\x{1a99}\x{1aa0}-\x{1aad}\x{1b04}-\x{1b33}\x{1b35}\x{1b3b}\x{1b3d}-\x{1b41}\x{1b43}-\x{1b4b}\x{1b50}-\x{1b6a}\x{1b74}-\x{1b7c}\x{1b82}-\x{1ba1}\x{1ba6}\x{1ba7}\x{1baa}\x{1bae}-\x{1be5}\x{1be7}\x{1bea}-\x{1bec}\x{1bee}\x{1bf2}\x{1bf3}\x{1bfc}-\x{1c2b}\x{1c34}\x{1c35}\x{1c3b}-\x{1c49}\x{1c4d}-\x{1c7f}\x{1cc0}-\x{1cc7}\x{1cd3}\x{1ce1}\x{1ce9}-\x{1cec}\x{1cee}-\x{1cf3}\x{1cf5}\x{1cf6}\x{1d00}-\x{1dbf}\x{1e00}-\x{1f15}\x{1f18}-\x{1f1d}\x{1f20}-\x{1f45}\x{1f48}-\x{1f4d}\x{1f50}-\x{1f57}\x{1f59}\x{1f5b}\x{1f5d}\x{1f5f}-\x{1f7d}\x{1f80}-\x{1fb4}\x{1fb6}-\x{1fbc}\x{1fbe}\x{1fc2}-\x{1fc4}\x{1fc6}-\x{1fcc}\x{1fd0}-\x{1fd3}\x{1fd6}-\x{1fdb}\x{1fe0}-\x{1fec}\x{1ff2}-\x{1ff4}\x{1ff6}-\x{1ffc}\x{200e}\x{2071}\x{207f}\x{2090}-\x{209c}\x{2102}\x{2107}\x{210a}-\x{2113}\x{2115}\x{2119}-\x{211d}\x{2124}\x{2126}\x{2128}\x{212a}-\x{212d}\x{212f}-\x{2139}\x{213c}-\x{213f}\x{2145}-\x{2149}\x{214e}\x{214f}\x{2160}-\x{2188}\x{2336}-\x{237a}\x{2395}\x{249c}-\x{24e9}\x{26ac}\x{2800}-\x{28ff}\x{2c00}-\x{2c2e}\x{2c30}-\x{2c5e}\x{2c60}-\x{2ce4}\x{2ceb}-\x{2cee}\x{2cf2}\x{2cf3}\x{2d00}-\x{2d25}\x{2d27}\x{2d2d}\x{2d30}-\x{2d67}\x{2d6f}\x{2d70}\x{2d80}-\x{2d96}\x{2da0}-\x{2da6}\x{2da8}-\x{2dae}\x{2db0}-\x{2db6}\x{2db8}-\x{2dbe}\x{2dc0}-\x{2dc6}\x{2dc8}-\x{2dce}\x{2dd0}-\x{2dd6}\x{2dd8}-\x{2dde}\x{3005}-\x{3007}\x{3021}-\x{3029}\x{302e}\x{302f}\x{3031}-\x{3035}\x{3038}-\x{303c}\x{3041}-\x{3096}\x{309d}-\x{309f}\x{30a1}-\x{30fa}\x{30fc}-\x{30ff}\x{3105}-\x{312d}\x{3131}-\x{318e}\x{3190}-\x{31ba}\x{31f0}-\x{321c}\x{3220}-\x{324f}\x{3260}-\x{327b}\x{327f}-\x{32b0}\x{32c0}-\x{32cb}\x{32d0}-\x{32fe}\x{3300}-\x{3376}\x{337b}-\x{33dd}\x{33e0}-\x{33fe}\x{3400}-\x{4db5}\x{4e00}-\x{9fd5}\x{a000}-\x{a48c}\x{a4d0}-\x{a60c}\x{a610}-\x{a62b}\x{a640}-\x{a66e}\x{a680}-\x{a69d}\x{a6a0}-\x{a6ef}\x{a6f2}-\x{a6f7}\x{a722}-\x{a787}\x{a789}-\x{a7ad}\x{a7b0}-\x{a7b7}\x{a7f7}-\x{a801}\x{a803}-\x{a805}\x{a807}-\x{a80a}\x{a80c}-\x{a824}\x{a827}\x{a830}-\x{a837}\x{a840}-\x{a873}\x{a880}-\x{a8c3}\x{a8ce}-\x{a8d9}\x{a8f2}-\x{a8fd}\x{a900}-\x{a925}\x{a92e}-\x{a946}\x{a952}\x{a953}\x{a95f}-\x{a97c}\x{a983}-\x{a9b2}\x{a9b4}\x{a9b5}\x{a9ba}\x{a9bb}\x{a9bd}-\x{a9cd}\x{a9cf}-\x{a9d9}\x{a9de}-\x{a9e4}\x{a9e6}-\x{a9fe}\x{aa00}-\x{aa28}\x{aa2f}\x{aa30}\x{aa33}\x{aa34}\x{aa40}-\x{aa42}\x{aa44}-\x{aa4b}\x{aa4d}\x{aa50}-\x{aa59}\x{aa5c}-\x{aa7b}\x{aa7d}-\x{aaaf}\x{aab1}\x{aab5}\x{aab6}\x{aab9}-\x{aabd}\x{aac0}\x{aac2}\x{aadb}-\x{aaeb}\x{aaee}-\x{aaf5}\x{ab01}-\x{ab06}\x{ab09}-\x{ab0e}\x{ab11}-\x{ab16}\x{ab20}-\x{ab26}\x{ab28}-\x{ab2e}\x{ab30}-\x{ab65}\x{ab70}-\x{abe4}\x{abe6}\x{abe7}\x{abe9}-\x{abec}\x{abf0}-\x{abf9}\x{ac00}-\x{d7a3}\x{d7b0}-\x{d7c6}\x{d7cb}-\x{d7fb}\x{e000}-\x{fa6d}\x{fa70}-\x{fad9}\x{fb00}-\x{fb06}\x{fb13}-\x{fb17}\x{ff21}-\x{ff3a}\x{ff41}-\x{ff5a}\x{ff66}-\x{ffbe}\x{ffc2}-\x{ffc7}\x{ffca}-\x{ffcf}\x{ffd2}-\x{ffd7}\x{ffda}-\x{ffdc}\x{10000}-\x{1000b}\x{1000d}-\x{10026}\x{10028}-\x{1003a}\x{1003c}\x{1003d}\x{1003f}-\x{1004d}\x{10050}-\x{1005d}\x{10080}-\x{100fa}\x{10100}\x{10102}\x{10107}-\x{10133}\x{10137}-\x{1013f}\x{101d0}-\x{101fc}\x{10280}-\x{1029c}\x{102a0}-\x{102d0}\x{10300}-\x{10323}\x{10330}-\x{1034a}\x{10350}-\x{10375}\x{10380}-\x{1039d}\x{1039f}-\x{103c3}\x{103c8}-\x{103d5}\x{10400}-\x{1049d}\x{104a0}-\x{104a9}\x{10500}-\x{10527}\x{10530}-\x{10563}\x{1056f}\x{10600}-\x{10736}\x{10740}-\x{10755}\x{10760}-\x{10767}\x{11000}\x{11002}-\x{11037}\x{11047}-\x{1104d}\x{11066}-\x{1106f}\x{11082}-\x{110b2}\x{110b7}\x{110b8}\x{110bb}-\x{110c1}\x{110d0}-\x{110e8}\x{110f0}-\x{110f9}\x{11103}-\x{11126}\x{1112c}\x{11136}-\x{11143}\x{11150}-\x{11172}\x{11174}-\x{11176}\x{11182}-\x{111b5}\x{111bf}-\x{111c9}\x{111cd}\x{111d0}-\x{111df}\x{111e1}-\x{111f4}\x{11200}-\x{11211}\x{11213}-\x{1122e}\x{11232}\x{11233}\x{11235}\x{11238}-\x{1123d}\x{11280}-\x{11286}\x{11288}\x{1128a}-\x{1128d}\x{1128f}-\x{1129d}\x{1129f}-\x{112a9}\x{112b0}-\x{112de}\x{112e0}-\x{112e2}\x{112f0}-\x{112f9}\x{11302}\x{11303}\x{11305}-\x{1130c}\x{1130f}\x{11310}\x{11313}-\x{11328}\x{1132a}-\x{11330}\x{11332}\x{11333}\x{11335}-\x{11339}\x{1133d}-\x{1133f}\x{11341}-\x{11344}\x{11347}\x{11348}\x{1134b}-\x{1134d}\x{11350}\x{11357}\x{1135d}-\x{11363}\x{11480}-\x{114b2}\x{114b9}\x{114bb}-\x{114be}\x{114c1}\x{114c4}-\x{114c7}\x{114d0}-\x{114d9}\x{11580}-\x{115b1}\x{115b8}-\x{115bb}\x{115be}\x{115c1}-\x{115db}\x{11600}-\x{11632}\x{1163b}\x{1163c}\x{1163e}\x{11641}-\x{11644}\x{11650}-\x{11659}\x{11680}-\x{116aa}\x{116ac}\x{116ae}\x{116af}\x{116b6}\x{116c0}-\x{116c9}\x{11700}-\x{11719}\x{11720}\x{11721}\x{11726}\x{11730}-\x{1173f}\x{118a0}-\x{118f2}\x{118ff}\x{11ac0}-\x{11af8}\x{12000}-\x{12399}\x{12400}-\x{1246e}\x{12470}-\x{12474}\x{12480}-\x{12543}\x{13000}-\x{1342e}\x{14400}-\x{14646}\x{16800}-\x{16a38}\x{16a40}-\x{16a5e}\x{16a60}-\x{16a69}\x{16a6e}\x{16a6f}\x{16ad0}-\x{16aed}\x{16af5}\x{16b00}-\x{16b2f}\x{16b37}-\x{16b45}\x{16b50}-\x{16b59}\x{16b5b}-\x{16b61}\x{16b63}-\x{16b77}\x{16b7d}-\x{16b8f}\x{16f00}-\x{16f44}\x{16f50}-\x{16f7e}\x{16f93}-\x{16f9f}\x{1b000}\x{1b001}\x{1bc00}-\x{1bc6a}\x{1bc70}-\x{1bc7c}\x{1bc80}-\x{1bc88}\x{1bc90}-\x{1bc99}\x{1bc9c}\x{1bc9f}\x{1d000}-\x{1d0f5}\x{1d100}-\x{1d126}\x{1d129}-\x{1d166}\x{1d16a}-\x{1d172}\x{1d183}\x{1d184}\x{1d18c}-\x{1d1a9}\x{1d1ae}-\x{1d1e8}\x{1d360}-\x{1d371}\x{1d400}-\x{1d454}\x{1d456}-\x{1d49c}\x{1d49e}\x{1d49f}\x{1d4a2}\x{1d4a5}\x{1d4a6}\x{1d4a9}-\x{1d4ac}\x{1d4ae}-\x{1d4b9}\x{1d4bb}\x{1d4bd}-\x{1d4c3}\x{1d4c5}-\x{1d505}\x{1d507}-\x{1d50a}\x{1d50d}-\x{1d514}\x{1d516}-\x{1d51c}\x{1d51e}-\x{1d539}\x{1d53b}-\x{1d53e}\x{1d540}-\x{1d544}\x{1d546}\x{1d54a}-\x{1d550}\x{1d552}-\x{1d6a5}\x{1d6a8}-\x{1d6da}\x{1d6dc}-\x{1d714}\x{1d716}-\x{1d74e}\x{1d750}-\x{1d788}\x{1d78a}-\x{1d7c2}\x{1d7c4}-\x{1d7cb}\x{1d800}-\x{1d9ff}\x{1da37}-\x{1da3a}\x{1da6d}-\x{1da74}\x{1da76}-\x{1da83}\x{1da85}-\x{1da8b}\x{1f110}-\x{1f12e}\x{1f130}-\x{1f169}\x{1f170}-\x{1f19a}\x{1f1e6}-\x{1f202}\x{1f210}-\x{1f23a}\x{1f240}-\x{1f248}\x{1f250}\x{1f251}\x{20000}-\x{2a6d6}\x{2a700}-\x{2b734}\x{2b740}-\x{2b81d}\x{2b820}-\x{2cea1}\x{2f800}-\x{2fa1d}\x{f0000}-\x{ffffd}\x{100000}-\x{10fffd}])|([\x{590}\x{5be}\x{5c0}\x{5c3}\x{5c6}\x{5c8}-\x{5ff}\x{7c0}-\x{7ea}\x{7f4}\x{7f5}\x{7fa}-\x{815}\x{81a}\x{824}\x{828}\x{82e}-\x{858}\x{85c}-\x{89f}\x{200f}\x{fb1d}\x{fb1f}-\x{fb28}\x{fb2a}-\x{fb4f}\x{10800}-\x{1091e}\x{10920}-\x{10a00}\x{10a04}\x{10a07}-\x{10a0b}\x{10a10}-\x{10a37}\x{10a3b}-\x{10a3e}\x{10a40}-\x{10ae4}\x{10ae7}-\x{10b38}\x{10b40}-\x{10e5f}\x{10e7f}-\x{10fff}\x{1e800}-\x{1e8cf}\x{1e8d7}-\x{1edff}\x{1ef00}-\x{1efff}\x{608}\x{60b}\x{60d}\x{61b}-\x{64a}\x{66d}-\x{66f}\x{671}-\x{6d5}\x{6e5}\x{6e6}\x{6ee}\x{6ef}\x{6fa}-\x{710}\x{712}-\x{72f}\x{74b}-\x{7a5}\x{7b1}-\x{7bf}\x{8a0}-\x{8e2}\x{fb50}-\x{fd3d}\x{fd40}-\x{fdcf}\x{fdf0}-\x{fdfc}\x{fdfe}\x{fdff}\x{fe70}-\x{fefe}\x{1ee00}-\x{1eeef}\x{1eef2}-\x{1eeff}]))/u';
175
	// @codeCoverageIgnoreEnd
176
	// @codingStandardsIgnoreEnd
177
178
	/**
179
	 * Get a cached or new language object for a given language code
180
	 * @param string $code
181
	 * @return Language
182
	 */
183
	static function factory( $code ) {
184
		global $wgDummyLanguageCodes, $wgLangObjCacheSize;
185
186
		if ( isset( $wgDummyLanguageCodes[$code] ) ) {
187
			$code = $wgDummyLanguageCodes[$code];
188
		}
189
190
		// get the language object to process
191
		$langObj = isset( self::$mLangObjCache[$code] )
192
			? self::$mLangObjCache[$code]
193
			: self::newFromCode( $code );
194
195
		// merge the language object in to get it up front in the cache
196
		self::$mLangObjCache = array_merge( [ $code => $langObj ], self::$mLangObjCache );
197
		// get rid of the oldest ones in case we have an overflow
198
		self::$mLangObjCache = array_slice( self::$mLangObjCache, 0, $wgLangObjCacheSize, true );
199
200
		return $langObj;
201
	}
202
203
	/**
204
	 * Create a language object for a given language code
205
	 * @param string $code
206
	 * @throws MWException
207
	 * @return Language
208
	 */
209
	protected static function newFromCode( $code ) {
210
		if ( !Language::isValidCode( $code ) ) {
211
			throw new MWException( "Invalid language code \"$code\"" );
212
		}
213
214
		if ( !Language::isValidBuiltInCode( $code ) ) {
215
			// It's not possible to customise this code with class files, so
216
			// just return a Language object. This is to support uselang= hacks.
217
			$lang = new Language;
218
			$lang->setCode( $code );
219
			return $lang;
220
		}
221
222
		// Check if there is a language class for the code
223
		$class = self::classFromCode( $code );
224
		if ( class_exists( $class ) ) {
225
			$lang = new $class;
226
			return $lang;
227
		}
228
229
		// Keep trying the fallback list until we find an existing class
230
		$fallbacks = Language::getFallbacksFor( $code );
231
		foreach ( $fallbacks as $fallbackCode ) {
232
			if ( !Language::isValidBuiltInCode( $fallbackCode ) ) {
233
				throw new MWException( "Invalid fallback '$fallbackCode' in fallback sequence for '$code'" );
234
			}
235
236
			$class = self::classFromCode( $fallbackCode );
237
			if ( class_exists( $class ) ) {
238
				$lang = new $class;
239
				$lang->setCode( $code );
240
				return $lang;
241
			}
242
		}
243
244
		throw new MWException( "Invalid fallback sequence for language '$code'" );
245
	}
246
247
	/**
248
	 * Checks whether any localisation is available for that language tag
249
	 * in MediaWiki (MessagesXx.php exists).
250
	 *
251
	 * @param string $code Language tag (in lower case)
252
	 * @return bool Whether language is supported
253
	 * @since 1.21
254
	 */
255
	public static function isSupportedLanguage( $code ) {
256
		if ( !self::isValidBuiltInCode( $code ) ) {
257
			return false;
258
		}
259
260
		if ( $code === 'qqq' ) {
261
			return false;
262
		}
263
264
		return is_readable( self::getMessagesFileName( $code ) ) ||
265
			is_readable( self::getJsonMessagesFileName( $code ) );
266
	}
267
268
	/**
269
	 * Returns true if a language code string is a well-formed language tag
270
	 * according to RFC 5646.
271
	 * This function only checks well-formedness; it doesn't check that
272
	 * language, script or variant codes actually exist in the repositories.
273
	 *
274
	 * Based on regexes by Mark Davis of the Unicode Consortium:
275
	 * http://unicode.org/repos/cldr/trunk/tools/java/org/unicode/cldr/util/data/langtagRegex.txt
276
	 *
277
	 * @param string $code
278
	 * @param bool $lenient Whether to allow '_' as separator. The default is only '-'.
279
	 *
280
	 * @return bool
281
	 * @since 1.21
282
	 */
283
	public static function isWellFormedLanguageTag( $code, $lenient = false ) {
284
		$alpha = '[a-z]';
285
		$digit = '[0-9]';
286
		$alphanum = '[a-z0-9]';
287
		$x = 'x'; # private use singleton
288
		$singleton = '[a-wy-z]'; # other singleton
289
		$s = $lenient ? '[-_]' : '-';
290
291
		$language = "$alpha{2,8}|$alpha{2,3}$s$alpha{3}";
292
		$script = "$alpha{4}"; # ISO 15924
293
		$region = "(?:$alpha{2}|$digit{3})"; # ISO 3166-1 alpha-2 or UN M.49
294
		$variant = "(?:$alphanum{5,8}|$digit$alphanum{3})";
295
		$extension = "$singleton(?:$s$alphanum{2,8})+";
296
		$privateUse = "$x(?:$s$alphanum{1,8})+";
297
298
		# Define certain grandfathered codes, since otherwise the regex is pretty useless.
299
		# Since these are limited, this is safe even later changes to the registry --
300
		# the only oddity is that it might change the type of the tag, and thus
301
		# the results from the capturing groups.
302
		# http://www.iana.org/assignments/language-subtag-registry
303
304
		$grandfathered = "en{$s}GB{$s}oed"
305
			. "|i{$s}(?:ami|bnn|default|enochian|hak|klingon|lux|mingo|navajo|pwn|tao|tay|tsu)"
306
			. "|no{$s}(?:bok|nyn)"
307
			. "|sgn{$s}(?:BE{$s}(?:fr|nl)|CH{$s}de)"
308
			. "|zh{$s}min{$s}nan";
309
310
		$variantList = "$variant(?:$s$variant)*";
311
		$extensionList = "$extension(?:$s$extension)*";
312
313
		$langtag = "(?:($language)"
314
			. "(?:$s$script)?"
315
			. "(?:$s$region)?"
316
			. "(?:$s$variantList)?"
317
			. "(?:$s$extensionList)?"
318
			. "(?:$s$privateUse)?)";
319
320
		# The final breakdown, with capturing groups for each of these components
321
		# The variants, extensions, grandfathered, and private-use may have interior '-'
322
323
		$root = "^(?:$langtag|$privateUse|$grandfathered)$";
324
325
		return (bool)preg_match( "/$root/", strtolower( $code ) );
326
	}
327
328
	/**
329
	 * Returns true if a language code string is of a valid form, whether or
330
	 * not it exists. This includes codes which are used solely for
331
	 * customisation via the MediaWiki namespace.
332
	 *
333
	 * @param string $code
334
	 *
335
	 * @return bool
336
	 */
337
	public static function isValidCode( $code ) {
338
		static $cache = [];
339
		if ( !isset( $cache[$code] ) ) {
340
			// People think language codes are html safe, so enforce it.
341
			// Ideally we should only allow a-zA-Z0-9-
342
			// but, .+ and other chars are often used for {{int:}} hacks
343
			// see bugs 37564, 37587, 36938
344
			$cache[$code] =
345
				// Protect against path traversal
346
				strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code )
347
				&& !preg_match( MediaWikiTitleCodec::getTitleInvalidRegex(), $code );
348
		}
349
		return $cache[$code];
350
	}
351
352
	/**
353
	 * Returns true if a language code is of a valid form for the purposes of
354
	 * internal customisation of MediaWiki, via Messages*.php or *.json.
355
	 *
356
	 * @param string $code
357
	 *
358
	 * @throws MWException
359
	 * @since 1.18
360
	 * @return bool
361
	 */
362
	public static function isValidBuiltInCode( $code ) {
363
364
		if ( !is_string( $code ) ) {
365
			if ( is_object( $code ) ) {
366
				$addmsg = " of class " . get_class( $code );
367
			} else {
368
				$addmsg = '';
369
			}
370
			$type = gettype( $code );
371
			throw new MWException( __METHOD__ . " must be passed a string, $type given$addmsg" );
372
		}
373
374
		return (bool)preg_match( '/^[a-z0-9-]{2,}$/', $code );
375
	}
376
377
	/**
378
	 * Returns true if a language code is an IETF tag known to MediaWiki.
379
	 *
380
	 * @param string $tag
381
	 *
382
	 * @since 1.21
383
	 * @return bool
384
	 */
385
	public static function isKnownLanguageTag( $tag ) {
386
		// Quick escape for invalid input to avoid exceptions down the line
387
		// when code tries to process tags which are not valid at all.
388
		if ( !self::isValidBuiltInCode( $tag ) ) {
389
			return false;
390
		}
391
392
		if ( isset( MediaWiki\Languages\Data\Names::$names[$tag] )
393
			|| self::fetchLanguageName( $tag, $tag ) !== ''
394
		) {
395
			return true;
396
		}
397
398
		return false;
399
	}
400
401
	/**
402
	 * Get the LocalisationCache instance
403
	 *
404
	 * @return LocalisationCache
405
	 */
406
	public static function getLocalisationCache() {
407
		if ( is_null( self::$dataCache ) ) {
408
			global $wgLocalisationCacheConf;
409
			$class = $wgLocalisationCacheConf['class'];
410
			self::$dataCache = new $class( $wgLocalisationCacheConf );
411
		}
412
		return self::$dataCache;
413
	}
414
415
	function __construct() {
416
		$this->mConverter = new FakeConverter( $this );
0 ignored issues
show
Documentation Bug introduced by
It seems like new \FakeConverter($this) of type object<FakeConverter> is incompatible with the declared type object<LanguageConverter> of property $mConverter.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
417
		// Set the code to the name of the descendant
418
		if ( get_class( $this ) == 'Language' ) {
419
			$this->mCode = 'en';
420
		} else {
421
			$this->mCode = str_replace( '_', '-', strtolower( substr( get_class( $this ), 8 ) ) );
422
		}
423
		self::getLocalisationCache();
424
	}
425
426
	/**
427
	 * Reduce memory usage
428
	 */
429
	function __destruct() {
430
		foreach ( $this as $name => $value ) {
0 ignored issues
show
Bug introduced by
The expression $this of type this<Language> is not traversable.
Loading history...
431
			unset( $this->$name );
432
		}
433
	}
434
435
	/**
436
	 * Hook which will be called if this is the content language.
437
	 * Descendants can use this to register hook functions or modify globals
438
	 */
439
	function initContLang() {
440
	}
441
442
	/**
443
	 * @return array
444
	 * @since 1.19
445
	 */
446
	function getFallbackLanguages() {
447
		return self::getFallbacksFor( $this->mCode );
448
	}
449
450
	/**
451
	 * Exports $wgBookstoreListEn
452
	 * @return array
453
	 */
454
	function getBookstoreList() {
455
		return self::$dataCache->getItem( $this->mCode, 'bookstoreList' );
456
	}
457
458
	/**
459
	 * Returns an array of localised namespaces indexed by their numbers. If the namespace is not
460
	 * available in localised form, it will be included in English.
461
	 *
462
	 * @return array
463
	 */
464
	public function getNamespaces() {
465
		if ( is_null( $this->namespaceNames ) ) {
466
			global $wgMetaNamespace, $wgMetaNamespaceTalk, $wgExtraNamespaces;
467
468
			$this->namespaceNames = self::$dataCache->getItem( $this->mCode, 'namespaceNames' );
469
			$validNamespaces = MWNamespace::getCanonicalNamespaces();
470
471
			$this->namespaceNames = $wgExtraNamespaces + $this->namespaceNames + $validNamespaces;
472
473
			$this->namespaceNames[NS_PROJECT] = $wgMetaNamespace;
474
			if ( $wgMetaNamespaceTalk ) {
475
				$this->namespaceNames[NS_PROJECT_TALK] = $wgMetaNamespaceTalk;
476
			} else {
477
				$talk = $this->namespaceNames[NS_PROJECT_TALK];
478
				$this->namespaceNames[NS_PROJECT_TALK] =
479
					$this->fixVariableInNamespace( $talk );
480
			}
481
482
			# Sometimes a language will be localised but not actually exist on this wiki.
483
			foreach ( $this->namespaceNames as $key => $text ) {
484
				if ( !isset( $validNamespaces[$key] ) ) {
485
					unset( $this->namespaceNames[$key] );
486
				}
487
			}
488
489
			# The above mixing may leave namespaces out of canonical order.
490
			# Re-order by namespace ID number...
491
			ksort( $this->namespaceNames );
492
493
			Hooks::run( 'LanguageGetNamespaces', [ &$this->namespaceNames ] );
494
		}
495
496
		return $this->namespaceNames;
497
	}
498
499
	/**
500
	 * Arbitrarily set all of the namespace names at once. Mainly used for testing
501
	 * @param array $namespaces Array of namespaces (id => name)
502
	 */
503
	public function setNamespaces( array $namespaces ) {
504
		$this->namespaceNames = $namespaces;
505
		$this->mNamespaceIds = null;
506
	}
507
508
	/**
509
	 * Resets all of the namespace caches. Mainly used for testing
510
	 */
511
	public function resetNamespaces() {
512
		$this->namespaceNames = null;
513
		$this->mNamespaceIds = null;
514
		$this->namespaceAliases = null;
515
	}
516
517
	/**
518
	 * A convenience function that returns getNamespaces() with spaces instead of underscores
519
	 * in values. Useful for producing output to be displayed e.g. in `<select>` forms.
520
	 *
521
	 * @return array
522
	 */
523
	function getFormattedNamespaces() {
524
		$ns = $this->getNamespaces();
525
		foreach ( $ns as $k => $v ) {
526
			$ns[$k] = strtr( $v, '_', ' ' );
527
		}
528
		return $ns;
529
	}
530
531
	/**
532
	 * Get a namespace value by key
533
	 *
534
	 * <code>
535
	 * $mw_ns = $wgContLang->getNsText( NS_MEDIAWIKI );
536
	 * echo $mw_ns; // prints 'MediaWiki'
537
	 * </code>
538
	 *
539
	 * @param int $index The array key of the namespace to return
540
	 * @return string|bool String if the namespace value exists, otherwise false
541
	 */
542
	function getNsText( $index ) {
543
		$ns = $this->getNamespaces();
544
		return isset( $ns[$index] ) ? $ns[$index] : false;
545
	}
546
547
	/**
548
	 * A convenience function that returns the same thing as
549
	 * getNsText() except with '_' changed to ' ', useful for
550
	 * producing output.
551
	 *
552
	 * <code>
553
	 * $mw_ns = $wgContLang->getFormattedNsText( NS_MEDIAWIKI_TALK );
554
	 * echo $mw_ns; // prints 'MediaWiki talk'
555
	 * </code>
556
	 *
557
	 * @param int $index The array key of the namespace to return
558
	 * @return string Namespace name without underscores (empty string if namespace does not exist)
559
	 */
560
	function getFormattedNsText( $index ) {
561
		$ns = $this->getNsText( $index );
562
		return strtr( $ns, '_', ' ' );
0 ignored issues
show
Bug introduced by
It seems like $ns defined by $this->getNsText($index) on line 561 can also be of type boolean; however, strtr() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
563
	}
564
565
	/**
566
	 * Returns gender-dependent namespace alias if available.
567
	 * See https://www.mediawiki.org/wiki/Manual:$wgExtraGenderNamespaces
568
	 * @param int $index Namespace index
569
	 * @param string $gender Gender key (male, female... )
570
	 * @return string
571
	 * @since 1.18
572
	 */
573
	function getGenderNsText( $index, $gender ) {
574
		global $wgExtraGenderNamespaces;
575
576
		$ns = $wgExtraGenderNamespaces +
577
			(array)self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
578
579
		return isset( $ns[$index][$gender] ) ? $ns[$index][$gender] : $this->getNsText( $index );
580
	}
581
582
	/**
583
	 * Whether this language uses gender-dependent namespace aliases.
584
	 * See https://www.mediawiki.org/wiki/Manual:$wgExtraGenderNamespaces
585
	 * @return bool
586
	 * @since 1.18
587
	 */
588
	function needsGenderDistinction() {
589
		global $wgExtraGenderNamespaces, $wgExtraNamespaces;
590
		if ( count( $wgExtraGenderNamespaces ) > 0 ) {
591
			// $wgExtraGenderNamespaces overrides everything
592
			return true;
593
		} elseif ( isset( $wgExtraNamespaces[NS_USER] ) && isset( $wgExtraNamespaces[NS_USER_TALK] ) ) {
594
			/// @todo There may be other gender namespace than NS_USER & NS_USER_TALK in the future
595
			// $wgExtraNamespaces overrides any gender aliases specified in i18n files
596
			return false;
597
		} else {
598
			// Check what is in i18n files
599
			$aliases = self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
600
			return count( $aliases ) > 0;
601
		}
602
	}
603
604
	/**
605
	 * Get a namespace key by value, case insensitive.
606
	 * Only matches namespace names for the current language, not the
607
	 * canonical ones defined in Namespace.php.
608
	 *
609
	 * @param string $text
610
	 * @return int|bool An integer if $text is a valid value otherwise false
611
	 */
612
	function getLocalNsIndex( $text ) {
613
		$lctext = $this->lc( $text );
614
		$ids = $this->getNamespaceIds();
615
		return isset( $ids[$lctext] ) ? $ids[$lctext] : false;
616
	}
617
618
	/**
619
	 * @return array
620
	 */
621
	function getNamespaceAliases() {
622
		if ( is_null( $this->namespaceAliases ) ) {
623
			$aliases = self::$dataCache->getItem( $this->mCode, 'namespaceAliases' );
624
			if ( !$aliases ) {
625
				$aliases = [];
626
			} else {
627
				foreach ( $aliases as $name => $index ) {
628
					if ( $index === NS_PROJECT_TALK ) {
629
						unset( $aliases[$name] );
630
						$name = $this->fixVariableInNamespace( $name );
631
						$aliases[$name] = $index;
632
					}
633
				}
634
			}
635
636
			global $wgExtraGenderNamespaces;
637
			$genders = $wgExtraGenderNamespaces +
638
				(array)self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
639
			foreach ( $genders as $index => $forms ) {
640
				foreach ( $forms as $alias ) {
641
					$aliases[$alias] = $index;
642
				}
643
			}
644
645
			# Also add converted namespace names as aliases, to avoid confusion.
646
			$convertedNames = [];
647
			foreach ( $this->getVariants() as $variant ) {
648
				if ( $variant === $this->mCode ) {
649
					continue;
650
				}
651
				foreach ( $this->getNamespaces() as $ns => $_ ) {
652
					$convertedNames[$this->getConverter()->convertNamespace( $ns, $variant )] = $ns;
653
				}
654
			}
655
656
			$this->namespaceAliases = $aliases + $convertedNames;
657
		}
658
659
		return $this->namespaceAliases;
660
	}
661
662
	/**
663
	 * @return array
664
	 */
665
	function getNamespaceIds() {
666
		if ( is_null( $this->mNamespaceIds ) ) {
667
			global $wgNamespaceAliases;
668
			# Put namespace names and aliases into a hashtable.
669
			# If this is too slow, then we should arrange it so that it is done
670
			# before caching. The catch is that at pre-cache time, the above
671
			# class-specific fixup hasn't been done.
672
			$this->mNamespaceIds = [];
673
			foreach ( $this->getNamespaces() as $index => $name ) {
674
				$this->mNamespaceIds[$this->lc( $name )] = $index;
675
			}
676
			foreach ( $this->getNamespaceAliases() as $name => $index ) {
677
				$this->mNamespaceIds[$this->lc( $name )] = $index;
678
			}
679
			if ( $wgNamespaceAliases ) {
680
				foreach ( $wgNamespaceAliases as $name => $index ) {
681
					$this->mNamespaceIds[$this->lc( $name )] = $index;
682
				}
683
			}
684
		}
685
		return $this->mNamespaceIds;
686
	}
687
688
	/**
689
	 * Get a namespace key by value, case insensitive.  Canonical namespace
690
	 * names override custom ones defined for the current language.
691
	 *
692
	 * @param string $text
693
	 * @return int|bool An integer if $text is a valid value otherwise false
694
	 */
695
	function getNsIndex( $text ) {
696
		$lctext = $this->lc( $text );
697
		$ns = MWNamespace::getCanonicalIndex( $lctext );
698
		if ( $ns !== null ) {
699
			return $ns;
700
		}
701
		$ids = $this->getNamespaceIds();
702
		return isset( $ids[$lctext] ) ? $ids[$lctext] : false;
703
	}
704
705
	/**
706
	 * short names for language variants used for language conversion links.
707
	 *
708
	 * @param string $code
709
	 * @param bool $usemsg Use the "variantname-xyz" message if it exists
710
	 * @return string
711
	 */
712
	function getVariantname( $code, $usemsg = true ) {
713
		$msg = "variantname-$code";
714
		if ( $usemsg && wfMessage( $msg )->exists() ) {
715
			return $this->getMessageFromDB( $msg );
716
		}
717
		$name = self::fetchLanguageName( $code );
718
		if ( $name ) {
719
			return $name; # if it's defined as a language name, show that
720
		} else {
721
			# otherwise, output the language code
722
			return $code;
723
		}
724
	}
725
726
	/**
727
	 * @return array
728
	 */
729
	function getDatePreferences() {
730
		return self::$dataCache->getItem( $this->mCode, 'datePreferences' );
731
	}
732
733
	/**
734
	 * @return array
735
	 */
736
	function getDateFormats() {
737
		return self::$dataCache->getItem( $this->mCode, 'dateFormats' );
738
	}
739
740
	/**
741
	 * @return array|string
742
	 */
743
	function getDefaultDateFormat() {
744
		$df = self::$dataCache->getItem( $this->mCode, 'defaultDateFormat' );
745
		if ( $df === 'dmy or mdy' ) {
746
			global $wgAmericanDates;
747
			return $wgAmericanDates ? 'mdy' : 'dmy';
748
		} else {
749
			return $df;
750
		}
751
	}
752
753
	/**
754
	 * @return array
755
	 */
756
	function getDatePreferenceMigrationMap() {
757
		return self::$dataCache->getItem( $this->mCode, 'datePreferenceMigrationMap' );
758
	}
759
760
	/**
761
	 * @param string $image
762
	 * @return array|null
763
	 */
764
	function getImageFile( $image ) {
765
		return self::$dataCache->getSubitem( $this->mCode, 'imageFiles', $image );
766
	}
767
768
	/**
769
	 * @return array
770
	 * @since 1.24
771
	 */
772
	function getImageFiles() {
773
		return self::$dataCache->getItem( $this->mCode, 'imageFiles' );
774
	}
775
776
	/**
777
	 * @return array
778
	 */
779
	function getExtraUserToggles() {
780
		return (array)self::$dataCache->getItem( $this->mCode, 'extraUserToggles' );
781
	}
782
783
	/**
784
	 * @param string $tog
785
	 * @return string
786
	 */
787
	function getUserToggle( $tog ) {
788
		return $this->getMessageFromDB( "tog-$tog" );
789
	}
790
791
	/**
792
	 * Get an array of language names, indexed by code.
793
	 * @param null|string $inLanguage Code of language in which to return the names
794
	 *		Use null for autonyms (native names)
795
	 * @param string $include One of:
796
	 *		'all' all available languages
797
	 *		'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames (default)
798
	 *		'mwfile' only if the language is in 'mw' *and* has a message file
799
	 * @return array Language code => language name
800
	 * @since 1.20
801
	 */
802
	public static function fetchLanguageNames( $inLanguage = null, $include = 'mw' ) {
803
		$cacheKey = $inLanguage === null ? 'null' : $inLanguage;
804
		$cacheKey .= ":$include";
805
		if ( self::$languageNameCache === null ) {
806
			self::$languageNameCache = new HashBagOStuff( [ 'maxKeys' => 20 ] );
807
		}
808
809
		$ret = self::$languageNameCache->get( $cacheKey );
810
		if ( !$ret ) {
811
			$ret = self::fetchLanguageNamesUncached( $inLanguage, $include );
812
			self::$languageNameCache->set( $cacheKey, $ret );
813
		}
814
		return $ret;
815
	}
816
817
	/**
818
	 * Uncached helper for fetchLanguageNames
819
	 * @param null|string $inLanguage Code of language in which to return the names
820
	 *		Use null for autonyms (native names)
821
	 * @param string $include One of:
822
	 *		'all' all available languages
823
	 *		'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames (default)
824
	 *		'mwfile' only if the language is in 'mw' *and* has a message file
825
	 * @return array Language code => language name
826
	 */
827
	private static function fetchLanguageNamesUncached( $inLanguage = null, $include = 'mw' ) {
828
		global $wgExtraLanguageNames;
829
830
		// If passed an invalid language code to use, fallback to en
831
		if ( $inLanguage !== null && !Language::isValidCode( $inLanguage ) ) {
832
			$inLanguage = 'en';
833
		}
834
835
		$names = [];
836
837
		if ( $inLanguage ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $inLanguage of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
838
			# TODO: also include when $inLanguage is null, when this code is more efficient
839
			Hooks::run( 'LanguageGetTranslatedLanguageNames', [ &$names, $inLanguage ] );
840
		}
841
842
		$mwNames = $wgExtraLanguageNames + MediaWiki\Languages\Data\Names::$names;
843
		foreach ( $mwNames as $mwCode => $mwName ) {
844
			# - Prefer own MediaWiki native name when not using the hook
845
			# - For other names just add if not added through the hook
846
			if ( $mwCode === $inLanguage || !isset( $names[$mwCode] ) ) {
847
				$names[$mwCode] = $mwName;
848
			}
849
		}
850
851
		if ( $include === 'all' ) {
852
			ksort( $names );
853
			return $names;
854
		}
855
856
		$returnMw = [];
857
		$coreCodes = array_keys( $mwNames );
858
		foreach ( $coreCodes as $coreCode ) {
859
			$returnMw[$coreCode] = $names[$coreCode];
860
		}
861
862
		if ( $include === 'mwfile' ) {
863
			$namesMwFile = [];
864
			# We do this using a foreach over the codes instead of a directory
865
			# loop so that messages files in extensions will work correctly.
866
			foreach ( $returnMw as $code => $value ) {
867
				if ( is_readable( self::getMessagesFileName( $code ) )
868
					|| is_readable( self::getJsonMessagesFileName( $code ) )
869
				) {
870
					$namesMwFile[$code] = $names[$code];
871
				}
872
			}
873
874
			ksort( $namesMwFile );
875
			return $namesMwFile;
876
		}
877
878
		ksort( $returnMw );
879
		# 'mw' option; default if it's not one of the other two options (all/mwfile)
880
		return $returnMw;
881
	}
882
883
	/**
884
	 * @param string $code The code of the language for which to get the name
885
	 * @param null|string $inLanguage Code of language in which to return the name (null for autonyms)
886
	 * @param string $include 'all', 'mw' or 'mwfile'; see fetchLanguageNames()
887
	 * @return string Language name or empty
888
	 * @since 1.20
889
	 */
890
	public static function fetchLanguageName( $code, $inLanguage = null, $include = 'all' ) {
891
		$code = strtolower( $code );
892
		$array = self::fetchLanguageNames( $inLanguage, $include );
893
		return !array_key_exists( $code, $array ) ? '' : $array[$code];
894
	}
895
896
	/**
897
	 * Get a message from the MediaWiki namespace.
898
	 *
899
	 * @param string $msg Message name
900
	 * @return string
901
	 */
902
	function getMessageFromDB( $msg ) {
903
		return $this->msg( $msg )->text();
904
	}
905
906
	/**
907
	 * Get message object in this language. Only for use inside this class.
908
	 *
909
	 * @param string $msg Message name
910
	 * @return Message
911
	 */
912
	protected function msg( $msg ) {
913
		return wfMessage( $msg )->inLanguage( $this );
914
	}
915
916
	/**
917
	 * @param string $key
918
	 * @return string
919
	 */
920
	function getMonthName( $key ) {
921
		return $this->getMessageFromDB( self::$mMonthMsgs[$key - 1] );
922
	}
923
924
	/**
925
	 * @return array
926
	 */
927 View Code Duplication
	function getMonthNamesArray() {
928
		$monthNames = [ '' ];
929
		for ( $i = 1; $i < 13; $i++ ) {
930
			$monthNames[] = $this->getMonthName( $i );
931
		}
932
		return $monthNames;
933
	}
934
935
	/**
936
	 * @param string $key
937
	 * @return string
938
	 */
939
	function getMonthNameGen( $key ) {
940
		return $this->getMessageFromDB( self::$mMonthGenMsgs[$key - 1] );
941
	}
942
943
	/**
944
	 * @param string $key
945
	 * @return string
946
	 */
947
	function getMonthAbbreviation( $key ) {
948
		return $this->getMessageFromDB( self::$mMonthAbbrevMsgs[$key - 1] );
949
	}
950
951
	/**
952
	 * @return array
953
	 */
954 View Code Duplication
	function getMonthAbbreviationsArray() {
955
		$monthNames = [ '' ];
956
		for ( $i = 1; $i < 13; $i++ ) {
957
			$monthNames[] = $this->getMonthAbbreviation( $i );
958
		}
959
		return $monthNames;
960
	}
961
962
	/**
963
	 * @param string $key
964
	 * @return string
965
	 */
966
	function getWeekdayName( $key ) {
967
		return $this->getMessageFromDB( self::$mWeekdayMsgs[$key - 1] );
968
	}
969
970
	/**
971
	 * @param string $key
972
	 * @return string
973
	 */
974
	function getWeekdayAbbreviation( $key ) {
975
		return $this->getMessageFromDB( self::$mWeekdayAbbrevMsgs[$key - 1] );
976
	}
977
978
	/**
979
	 * @param string $key
980
	 * @return string
981
	 */
982
	function getIranianCalendarMonthName( $key ) {
983
		return $this->getMessageFromDB( self::$mIranianCalendarMonthMsgs[$key - 1] );
984
	}
985
986
	/**
987
	 * @param string $key
988
	 * @return string
989
	 */
990
	function getHebrewCalendarMonthName( $key ) {
991
		return $this->getMessageFromDB( self::$mHebrewCalendarMonthMsgs[$key - 1] );
992
	}
993
994
	/**
995
	 * @param string $key
996
	 * @return string
997
	 */
998
	function getHebrewCalendarMonthNameGen( $key ) {
999
		return $this->getMessageFromDB( self::$mHebrewCalendarMonthGenMsgs[$key - 1] );
1000
	}
1001
1002
	/**
1003
	 * @param string $key
1004
	 * @return string
1005
	 */
1006
	function getHijriCalendarMonthName( $key ) {
1007
		return $this->getMessageFromDB( self::$mHijriCalendarMonthMsgs[$key - 1] );
1008
	}
1009
1010
	/**
1011
	 * Pass through result from $dateTimeObj->format()
1012
	 * @param DateTime|bool|null &$dateTimeObj
1013
	 * @param string $ts
1014
	 * @param DateTimeZone|bool|null $zone
1015
	 * @param string $code
1016
	 * @return string
1017
	 */
1018
	private static function dateTimeObjFormat( &$dateTimeObj, $ts, $zone, $code ) {
1019
		if ( !$dateTimeObj ) {
1020
			$dateTimeObj = DateTime::createFromFormat(
1021
				'YmdHis', $ts, $zone ?: new DateTimeZone( 'UTC' )
1022
			);
1023
		}
1024
		return $dateTimeObj->format( $code );
1025
	}
1026
1027
	/**
1028
	 * This is a workalike of PHP's date() function, but with better
1029
	 * internationalisation, a reduced set of format characters, and a better
1030
	 * escaping format.
1031
	 *
1032
	 * Supported format characters are dDjlNwzWFmMntLoYyaAgGhHiscrUeIOPTZ. See
1033
	 * the PHP manual for definitions. There are a number of extensions, which
1034
	 * start with "x":
1035
	 *
1036
	 *    xn   Do not translate digits of the next numeric format character
1037
	 *    xN   Toggle raw digit (xn) flag, stays set until explicitly unset
1038
	 *    xr   Use roman numerals for the next numeric format character
1039
	 *    xh   Use hebrew numerals for the next numeric format character
1040
	 *    xx   Literal x
1041
	 *    xg   Genitive month name
1042
	 *
1043
	 *    xij  j (day number) in Iranian calendar
1044
	 *    xiF  F (month name) in Iranian calendar
1045
	 *    xin  n (month number) in Iranian calendar
1046
	 *    xiy  y (two digit year) in Iranian calendar
1047
	 *    xiY  Y (full year) in Iranian calendar
1048
	 *
1049
	 *    xjj  j (day number) in Hebrew calendar
1050
	 *    xjF  F (month name) in Hebrew calendar
1051
	 *    xjt  t (days in month) in Hebrew calendar
1052
	 *    xjx  xg (genitive month name) in Hebrew calendar
1053
	 *    xjn  n (month number) in Hebrew calendar
1054
	 *    xjY  Y (full year) in Hebrew calendar
1055
	 *
1056
	 *    xmj  j (day number) in Hijri calendar
1057
	 *    xmF  F (month name) in Hijri calendar
1058
	 *    xmn  n (month number) in Hijri calendar
1059
	 *    xmY  Y (full year) in Hijri calendar
1060
	 *
1061
	 *    xkY  Y (full year) in Thai solar calendar. Months and days are
1062
	 *                       identical to the Gregorian calendar
1063
	 *    xoY  Y (full year) in Minguo calendar or Juche year.
1064
	 *                       Months and days are identical to the
1065
	 *                       Gregorian calendar
1066
	 *    xtY  Y (full year) in Japanese nengo. Months and days are
1067
	 *                       identical to the Gregorian calendar
1068
	 *
1069
	 * Characters enclosed in double quotes will be considered literal (with
1070
	 * the quotes themselves removed). Unmatched quotes will be considered
1071
	 * literal quotes. Example:
1072
	 *
1073
	 * "The month is" F       => The month is January
1074
	 * i's"                   => 20'11"
1075
	 *
1076
	 * Backslash escaping is also supported.
1077
	 *
1078
	 * Input timestamp is assumed to be pre-normalized to the desired local
1079
	 * time zone, if any. Note that the format characters crUeIOPTZ will assume
1080
	 * $ts is UTC if $zone is not given.
1081
	 *
1082
	 * @param string $format
1083
	 * @param string $ts 14-character timestamp
1084
	 *      YYYYMMDDHHMMSS
1085
	 *      01234567890123
1086
	 * @param DateTimeZone $zone Timezone of $ts
1087
	 * @param[out] int $ttl The amount of time (in seconds) the output may be cached for.
1088
	 * Only makes sense if $ts is the current time.
1089
	 * @todo handling of "o" format character for Iranian, Hebrew, Hijri & Thai?
1090
	 *
1091
	 * @throws MWException
1092
	 * @return string
1093
	 */
1094
	function sprintfDate( $format, $ts, DateTimeZone $zone = null, &$ttl = null ) {
1095
		$s = '';
1096
		$raw = false;
1097
		$roman = false;
1098
		$hebrewNum = false;
1099
		$dateTimeObj = false;
1100
		$rawToggle = false;
1101
		$iranian = false;
1102
		$hebrew = false;
1103
		$hijri = false;
1104
		$thai = false;
1105
		$minguo = false;
1106
		$tenno = false;
1107
1108
		$usedSecond = false;
1109
		$usedMinute = false;
1110
		$usedHour = false;
1111
		$usedAMPM = false;
1112
		$usedDay = false;
1113
		$usedWeek = false;
1114
		$usedMonth = false;
1115
		$usedYear = false;
1116
		$usedISOYear = false;
1117
		$usedIsLeapYear = false;
1118
1119
		$usedHebrewMonth = false;
1120
		$usedIranianMonth = false;
1121
		$usedHijriMonth = false;
1122
		$usedHebrewYear = false;
1123
		$usedIranianYear = false;
1124
		$usedHijriYear = false;
1125
		$usedTennoYear = false;
1126
1127
		if ( strlen( $ts ) !== 14 ) {
1128
			throw new MWException( __METHOD__ . ": The timestamp $ts should have 14 characters" );
1129
		}
1130
1131
		if ( !ctype_digit( $ts ) ) {
1132
			throw new MWException( __METHOD__ . ": The timestamp $ts should be a number" );
1133
		}
1134
1135
		$formatLength = strlen( $format );
1136
		for ( $p = 0; $p < $formatLength; $p++ ) {
1137
			$num = false;
1138
			$code = $format[$p];
1139
			if ( $code == 'x' && $p < $formatLength - 1 ) {
1140
				$code .= $format[++$p];
1141
			}
1142
1143
			if ( ( $code === 'xi'
1144
					|| $code === 'xj'
1145
					|| $code === 'xk'
1146
					|| $code === 'xm'
1147
					|| $code === 'xo'
1148
					|| $code === 'xt' )
1149
				&& $p < $formatLength - 1 ) {
1150
				$code .= $format[++$p];
1151
			}
1152
1153
			switch ( $code ) {
1154
				case 'xx':
1155
					$s .= 'x';
1156
					break;
1157
				case 'xn':
1158
					$raw = true;
1159
					break;
1160
				case 'xN':
1161
					$rawToggle = !$rawToggle;
1162
					break;
1163
				case 'xr':
1164
					$roman = true;
1165
					break;
1166
				case 'xh':
1167
					$hebrewNum = true;
1168
					break;
1169
				case 'xg':
1170
					$usedMonth = true;
1171
					$s .= $this->getMonthNameGen( substr( $ts, 4, 2 ) );
1172
					break;
1173 View Code Duplication
				case 'xjx':
1174
					$usedHebrewMonth = true;
1175
					if ( !$hebrew ) {
1176
						$hebrew = self::tsToHebrew( $ts );
1177
					}
1178
					$s .= $this->getHebrewCalendarMonthNameGen( $hebrew[1] );
1179
					break;
1180
				case 'd':
1181
					$usedDay = true;
1182
					$num = substr( $ts, 6, 2 );
1183
					break;
1184
				case 'D':
1185
					$usedDay = true;
1186
					$s .= $this->getWeekdayAbbreviation(
1187
						Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1
1188
					);
1189
					break;
1190
				case 'j':
1191
					$usedDay = true;
1192
					$num = intval( substr( $ts, 6, 2 ) );
1193
					break;
1194
				case 'xij':
1195
					$usedDay = true;
1196
					if ( !$iranian ) {
1197
						$iranian = self::tsToIranian( $ts );
1198
					}
1199
					$num = $iranian[2];
1200
					break;
1201
				case 'xmj':
1202
					$usedDay = true;
1203
					if ( !$hijri ) {
1204
						$hijri = self::tsToHijri( $ts );
1205
					}
1206
					$num = $hijri[2];
1207
					break;
1208
				case 'xjj':
1209
					$usedDay = true;
1210
					if ( !$hebrew ) {
1211
						$hebrew = self::tsToHebrew( $ts );
1212
					}
1213
					$num = $hebrew[2];
1214
					break;
1215
				case 'l':
1216
					$usedDay = true;
1217
					$s .= $this->getWeekdayName(
1218
						Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1
1219
					);
1220
					break;
1221
				case 'F':
1222
					$usedMonth = true;
1223
					$s .= $this->getMonthName( substr( $ts, 4, 2 ) );
1224
					break;
1225 View Code Duplication
				case 'xiF':
1226
					$usedIranianMonth = true;
1227
					if ( !$iranian ) {
1228
						$iranian = self::tsToIranian( $ts );
1229
					}
1230
					$s .= $this->getIranianCalendarMonthName( $iranian[1] );
1231
					break;
1232 View Code Duplication
				case 'xmF':
1233
					$usedHijriMonth = true;
1234
					if ( !$hijri ) {
1235
						$hijri = self::tsToHijri( $ts );
1236
					}
1237
					$s .= $this->getHijriCalendarMonthName( $hijri[1] );
1238
					break;
1239 View Code Duplication
				case 'xjF':
1240
					$usedHebrewMonth = true;
1241
					if ( !$hebrew ) {
1242
						$hebrew = self::tsToHebrew( $ts );
1243
					}
1244
					$s .= $this->getHebrewCalendarMonthName( $hebrew[1] );
1245
					break;
1246
				case 'm':
1247
					$usedMonth = true;
1248
					$num = substr( $ts, 4, 2 );
1249
					break;
1250
				case 'M':
1251
					$usedMonth = true;
1252
					$s .= $this->getMonthAbbreviation( substr( $ts, 4, 2 ) );
1253
					break;
1254
				case 'n':
1255
					$usedMonth = true;
1256
					$num = intval( substr( $ts, 4, 2 ) );
1257
					break;
1258 View Code Duplication
				case 'xin':
1259
					$usedIranianMonth = true;
1260
					if ( !$iranian ) {
1261
						$iranian = self::tsToIranian( $ts );
1262
					}
1263
					$num = $iranian[1];
1264
					break;
1265 View Code Duplication
				case 'xmn':
1266
					$usedHijriMonth = true;
1267
					if ( !$hijri ) {
1268
						$hijri = self::tsToHijri( $ts );
1269
					}
1270
					$num = $hijri[1];
1271
					break;
1272 View Code Duplication
				case 'xjn':
1273
					$usedHebrewMonth = true;
1274
					if ( !$hebrew ) {
1275
						$hebrew = self::tsToHebrew( $ts );
1276
					}
1277
					$num = $hebrew[1];
1278
					break;
1279 View Code Duplication
				case 'xjt':
1280
					$usedHebrewMonth = true;
1281
					if ( !$hebrew ) {
1282
						$hebrew = self::tsToHebrew( $ts );
1283
					}
1284
					$num = $hebrew[3];
1285
					break;
1286
				case 'Y':
1287
					$usedYear = true;
1288
					$num = substr( $ts, 0, 4 );
1289
					break;
1290 View Code Duplication
				case 'xiY':
1291
					$usedIranianYear = true;
1292
					if ( !$iranian ) {
1293
						$iranian = self::tsToIranian( $ts );
1294
					}
1295
					$num = $iranian[0];
1296
					break;
1297
				case 'xmY':
1298
					$usedHijriYear = true;
1299
					if ( !$hijri ) {
1300
						$hijri = self::tsToHijri( $ts );
1301
					}
1302
					$num = $hijri[0];
1303
					break;
1304
				case 'xjY':
1305
					$usedHebrewYear = true;
1306
					if ( !$hebrew ) {
1307
						$hebrew = self::tsToHebrew( $ts );
1308
					}
1309
					$num = $hebrew[0];
1310
					break;
1311 View Code Duplication
				case 'xkY':
1312
					$usedYear = true;
1313
					if ( !$thai ) {
1314
						$thai = self::tsToYear( $ts, 'thai' );
1315
					}
1316
					$num = $thai[0];
1317
					break;
1318 View Code Duplication
				case 'xoY':
1319
					$usedYear = true;
1320
					if ( !$minguo ) {
1321
						$minguo = self::tsToYear( $ts, 'minguo' );
1322
					}
1323
					$num = $minguo[0];
1324
					break;
1325
				case 'xtY':
1326
					$usedTennoYear = true;
1327
					if ( !$tenno ) {
1328
						$tenno = self::tsToYear( $ts, 'tenno' );
1329
					}
1330
					$num = $tenno[0];
1331
					break;
1332
				case 'y':
1333
					$usedYear = true;
1334
					$num = substr( $ts, 2, 2 );
1335
					break;
1336 View Code Duplication
				case 'xiy':
1337
					$usedIranianYear = true;
1338
					if ( !$iranian ) {
1339
						$iranian = self::tsToIranian( $ts );
1340
					}
1341
					$num = substr( $iranian[0], -2 );
1342
					break;
1343 View Code Duplication
				case 'a':
1344
					$usedAMPM = true;
1345
					$s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'am' : 'pm';
1346
					break;
1347 View Code Duplication
				case 'A':
1348
					$usedAMPM = true;
1349
					$s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'AM' : 'PM';
1350
					break;
1351 View Code Duplication
				case 'g':
1352
					$usedHour = true;
1353
					$h = substr( $ts, 8, 2 );
1354
					$num = $h % 12 ? $h % 12 : 12;
1355
					break;
1356
				case 'G':
1357
					$usedHour = true;
1358
					$num = intval( substr( $ts, 8, 2 ) );
1359
					break;
1360 View Code Duplication
				case 'h':
1361
					$usedHour = true;
1362
					$h = substr( $ts, 8, 2 );
1363
					$num = sprintf( '%02d', $h % 12 ? $h % 12 : 12 );
1364
					break;
1365
				case 'H':
1366
					$usedHour = true;
1367
					$num = substr( $ts, 8, 2 );
1368
					break;
1369
				case 'i':
1370
					$usedMinute = true;
1371
					$num = substr( $ts, 10, 2 );
1372
					break;
1373
				case 's':
1374
					$usedSecond = true;
1375
					$num = substr( $ts, 12, 2 );
1376
					break;
1377
				case 'c':
1378
				case 'r':
1379
					$usedSecond = true;
1380
					// fall through
1381
				case 'e':
1382
				case 'O':
1383
				case 'P':
1384
				case 'T':
1385
					$s .= Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1386
					break;
1387
				case 'w':
1388
				case 'N':
1389
				case 'z':
1390
					$usedDay = true;
1391
					$num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1392
					break;
1393
				case 'W':
1394
					$usedWeek = true;
1395
					$num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1396
					break;
1397
				case 't':
1398
					$usedMonth = true;
1399
					$num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1400
					break;
1401
				case 'L':
1402
					$usedIsLeapYear = true;
1403
					$num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1404
					break;
1405
				case 'o':
1406
					$usedISOYear = true;
1407
					$num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1408
					break;
1409
				case 'U':
1410
					$usedSecond = true;
1411
					// fall through
1412
				case 'I':
1413
				case 'Z':
1414
					$num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1415
					break;
1416
				case '\\':
1417
					# Backslash escaping
1418
					if ( $p < $formatLength - 1 ) {
1419
						$s .= $format[++$p];
1420
					} else {
1421
						$s .= '\\';
1422
					}
1423
					break;
1424
				case '"':
1425
					# Quoted literal
1426
					if ( $p < $formatLength - 1 ) {
1427
						$endQuote = strpos( $format, '"', $p + 1 );
1428
						if ( $endQuote === false ) {
1429
							# No terminating quote, assume literal "
1430
							$s .= '"';
1431
						} else {
1432
							$s .= substr( $format, $p + 1, $endQuote - $p - 1 );
1433
							$p = $endQuote;
1434
						}
1435
					} else {
1436
						# Quote at end of string, assume literal "
1437
						$s .= '"';
1438
					}
1439
					break;
1440
				default:
1441
					$s .= $format[$p];
1442
			}
1443
			if ( $num !== false ) {
1444
				if ( $rawToggle || $raw ) {
1445
					$s .= $num;
1446
					$raw = false;
1447
				} elseif ( $roman ) {
1448
					$s .= Language::romanNumeral( $num );
1449
					$roman = false;
1450
				} elseif ( $hebrewNum ) {
1451
					$s .= self::hebrewNumeral( $num );
1452
					$hebrewNum = false;
1453
				} else {
1454
					$s .= $this->formatNum( $num, true );
1455
				}
1456
			}
1457
		}
1458
1459
		if ( $usedSecond ) {
1460
			$ttl = 1;
1461
		} elseif ( $usedMinute ) {
1462
			$ttl = 60 - substr( $ts, 12, 2 );
1463
		} elseif ( $usedHour ) {
1464
			$ttl = 3600 - substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1465
		} elseif ( $usedAMPM ) {
1466
			$ttl = 43200 - ( substr( $ts, 8, 2 ) % 12 ) * 3600 -
1467
				substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1468
		} elseif (
1469
			$usedDay ||
1470
			$usedHebrewMonth ||
1471
			$usedIranianMonth ||
1472
			$usedHijriMonth ||
1473
			$usedHebrewYear ||
1474
			$usedIranianYear ||
1475
			$usedHijriYear ||
1476
			$usedTennoYear
1477
		) {
1478
			// @todo Someone who understands the non-Gregorian calendars
1479
			// should write proper logic for them so that they don't need purged every day.
1480
			$ttl = 86400 - substr( $ts, 8, 2 ) * 3600 -
1481
				substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1482
		} else {
1483
			$possibleTtls = [];
1484
			$timeRemainingInDay = 86400 - substr( $ts, 8, 2 ) * 3600 -
1485
				substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1486
			if ( $usedWeek ) {
1487
				$possibleTtls[] =
1488
					( 7 - Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'N' ) ) * 86400 +
1489
					$timeRemainingInDay;
1490
			} elseif ( $usedISOYear ) {
1491
				// December 28th falls on the last ISO week of the year, every year.
1492
				// The last ISO week of a year can be 52 or 53.
1493
				$lastWeekOfISOYear = DateTime::createFromFormat(
1494
					'Ymd',
1495
					substr( $ts, 0, 4 ) . '1228',
1496
					$zone ?: new DateTimeZone( 'UTC' )
1497
				)->format( 'W' );
1498
				$currentISOWeek = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'W' );
1499
				$weeksRemaining = $lastWeekOfISOYear - $currentISOWeek;
1500
				$timeRemainingInWeek =
1501
					( 7 - Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'N' ) ) * 86400
1502
					+ $timeRemainingInDay;
1503
				$possibleTtls[] = $weeksRemaining * 604800 + $timeRemainingInWeek;
1504
			}
1505
1506
			if ( $usedMonth ) {
1507
				$possibleTtls[] =
1508
					( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 't' ) -
1509
						substr( $ts, 6, 2 ) ) * 86400
1510
					+ $timeRemainingInDay;
1511
			} elseif ( $usedYear ) {
1512
				$possibleTtls[] =
1513
					( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'L' ) + 364 -
1514
						Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'z' ) ) * 86400
1515
					+ $timeRemainingInDay;
1516
			} elseif ( $usedIsLeapYear ) {
1517
				$year = substr( $ts, 0, 4 );
1518
				$timeRemainingInYear =
1519
					( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'L' ) + 364 -
1520
						Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'z' ) ) * 86400
1521
					+ $timeRemainingInDay;
1522
				$mod = $year % 4;
1523
				if ( $mod || ( !( $year % 100 ) && $year % 400 ) ) {
1524
					// this isn't a leap year. see when the next one starts
1525
					$nextCandidate = $year - $mod + 4;
1526
					if ( $nextCandidate % 100 || !( $nextCandidate % 400 ) ) {
1527
						$possibleTtls[] = ( $nextCandidate - $year - 1 ) * 365 * 86400 +
1528
							$timeRemainingInYear;
1529
					} else {
1530
						$possibleTtls[] = ( $nextCandidate - $year + 3 ) * 365 * 86400 +
1531
							$timeRemainingInYear;
1532
					}
1533
				} else {
1534
					// this is a leap year, so the next year isn't
1535
					$possibleTtls[] = $timeRemainingInYear;
1536
				}
1537
			}
1538
1539
			if ( $possibleTtls ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $possibleTtls of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1540
				$ttl = min( $possibleTtls );
1541
			}
1542
		}
1543
1544
		return $s;
1545
	}
1546
1547
	private static $GREG_DAYS = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
1548
	private static $IRANIAN_DAYS = [ 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 ];
1549
1550
	/**
1551
	 * Algorithm by Roozbeh Pournader and Mohammad Toossi to convert
1552
	 * Gregorian dates to Iranian dates. Originally written in C, it
1553
	 * is released under the terms of GNU Lesser General Public
1554
	 * License. Conversion to PHP was performed by Niklas Laxström.
1555
	 *
1556
	 * Link: http://www.farsiweb.info/jalali/jalali.c
1557
	 *
1558
	 * @param string $ts
1559
	 *
1560
	 * @return string
1561
	 */
1562
	private static function tsToIranian( $ts ) {
1563
		$gy = substr( $ts, 0, 4 ) -1600;
1564
		$gm = substr( $ts, 4, 2 ) -1;
1565
		$gd = substr( $ts, 6, 2 ) -1;
1566
1567
		# Days passed from the beginning (including leap years)
1568
		$gDayNo = 365 * $gy
1569
			+ floor( ( $gy + 3 ) / 4 )
1570
			- floor( ( $gy + 99 ) / 100 )
1571
			+ floor( ( $gy + 399 ) / 400 );
1572
1573
		// Add days of the past months of this year
1574
		for ( $i = 0; $i < $gm; $i++ ) {
1575
			$gDayNo += self::$GREG_DAYS[$i];
1576
		}
1577
1578
		// Leap years
1579
		if ( $gm > 1 && ( ( $gy % 4 === 0 && $gy % 100 !== 0 || ( $gy % 400 == 0 ) ) ) ) {
1580
			$gDayNo++;
1581
		}
1582
1583
		// Days passed in current month
1584
		$gDayNo += (int)$gd;
1585
1586
		$jDayNo = $gDayNo - 79;
1587
1588
		$jNp = floor( $jDayNo / 12053 );
1589
		$jDayNo %= 12053;
1590
1591
		$jy = 979 + 33 * $jNp + 4 * floor( $jDayNo / 1461 );
1592
		$jDayNo %= 1461;
1593
1594
		if ( $jDayNo >= 366 ) {
1595
			$jy += floor( ( $jDayNo - 1 ) / 365 );
1596
			$jDayNo = floor( ( $jDayNo - 1 ) % 365 );
1597
		}
1598
1599
		for ( $i = 0; $i < 11 && $jDayNo >= self::$IRANIAN_DAYS[$i]; $i++ ) {
1600
			$jDayNo -= self::$IRANIAN_DAYS[$i];
1601
		}
1602
1603
		$jm = $i + 1;
1604
		$jd = $jDayNo + 1;
1605
1606
		return [ $jy, $jm, $jd ];
1607
	}
1608
1609
	/**
1610
	 * Converting Gregorian dates to Hijri dates.
1611
	 *
1612
	 * Based on a PHP-Nuke block by Sharjeel which is released under GNU/GPL license
1613
	 *
1614
	 * @see http://phpnuke.org/modules.php?name=News&file=article&sid=8234&mode=thread&order=0&thold=0
1615
	 *
1616
	 * @param string $ts
1617
	 *
1618
	 * @return string
1619
	 */
1620
	private static function tsToHijri( $ts ) {
1621
		$year = substr( $ts, 0, 4 );
1622
		$month = substr( $ts, 4, 2 );
1623
		$day = substr( $ts, 6, 2 );
1624
1625
		$zyr = $year;
1626
		$zd = $day;
1627
		$zm = $month;
1628
		$zy = $zyr;
1629
1630
		if (
1631
			( $zy > 1582 ) || ( ( $zy == 1582 ) && ( $zm > 10 ) ) ||
1632
			( ( $zy == 1582 ) && ( $zm == 10 ) && ( $zd > 14 ) )
1633
		) {
1634
			$zjd = (int)( ( 1461 * ( $zy + 4800 + (int)( ( $zm - 14 ) / 12 ) ) ) / 4 ) +
1635
					(int)( ( 367 * ( $zm - 2 - 12 * ( (int)( ( $zm - 14 ) / 12 ) ) ) ) / 12 ) -
1636
					(int)( ( 3 * (int)( ( ( $zy + 4900 + (int)( ( $zm - 14 ) / 12 ) ) / 100 ) ) ) / 4 ) +
1637
					$zd - 32075;
1638
		} else {
1639
			$zjd = 367 * $zy - (int)( ( 7 * ( $zy + 5001 + (int)( ( $zm - 9 ) / 7 ) ) ) / 4 ) +
1640
								(int)( ( 275 * $zm ) / 9 ) + $zd + 1729777;
1641
		}
1642
1643
		$zl = $zjd -1948440 + 10632;
1644
		$zn = (int)( ( $zl - 1 ) / 10631 );
1645
		$zl = $zl - 10631 * $zn + 354;
1646
		$zj = ( (int)( ( 10985 - $zl ) / 5316 ) ) * ( (int)( ( 50 * $zl ) / 17719 ) ) +
1647
			( (int)( $zl / 5670 ) ) * ( (int)( ( 43 * $zl ) / 15238 ) );
1648
		$zl = $zl - ( (int)( ( 30 - $zj ) / 15 ) ) * ( (int)( ( 17719 * $zj ) / 50 ) ) -
1649
			( (int)( $zj / 16 ) ) * ( (int)( ( 15238 * $zj ) / 43 ) ) + 29;
1650
		$zm = (int)( ( 24 * $zl ) / 709 );
1651
		$zd = $zl - (int)( ( 709 * $zm ) / 24 );
1652
		$zy = 30 * $zn + $zj - 30;
1653
1654
		return [ $zy, $zm, $zd ];
1655
	}
1656
1657
	/**
1658
	 * Converting Gregorian dates to Hebrew dates.
1659
	 *
1660
	 * Based on a JavaScript code by Abu Mami and Yisrael Hersch
1661
	 * ([email protected], http://www.kaluach.net), who permitted
1662
	 * to translate the relevant functions into PHP and release them under
1663
	 * GNU GPL.
1664
	 *
1665
	 * The months are counted from Tishrei = 1. In a leap year, Adar I is 13
1666
	 * and Adar II is 14. In a non-leap year, Adar is 6.
1667
	 *
1668
	 * @param string $ts
1669
	 *
1670
	 * @return string
1671
	 */
1672
	private static function tsToHebrew( $ts ) {
1673
		# Parse date
1674
		$year = substr( $ts, 0, 4 );
1675
		$month = substr( $ts, 4, 2 );
1676
		$day = substr( $ts, 6, 2 );
1677
1678
		# Calculate Hebrew year
1679
		$hebrewYear = $year + 3760;
1680
1681
		# Month number when September = 1, August = 12
1682
		$month += 4;
1683
		if ( $month > 12 ) {
1684
			# Next year
1685
			$month -= 12;
1686
			$year++;
1687
			$hebrewYear++;
1688
		}
1689
1690
		# Calculate day of year from 1 September
1691
		$dayOfYear = $day;
1692
		for ( $i = 1; $i < $month; $i++ ) {
1693
			if ( $i == 6 ) {
1694
				# February
1695
				$dayOfYear += 28;
1696
				# Check if the year is leap
1697 View Code Duplication
				if ( $year % 400 == 0 || ( $year % 4 == 0 && $year % 100 > 0 ) ) {
1698
					$dayOfYear++;
1699
				}
1700
			} elseif ( $i == 8 || $i == 10 || $i == 1 || $i == 3 ) {
1701
				$dayOfYear += 30;
1702
			} else {
1703
				$dayOfYear += 31;
1704
			}
1705
		}
1706
1707
		# Calculate the start of the Hebrew year
1708
		$start = self::hebrewYearStart( $hebrewYear );
1709
1710
		# Calculate next year's start
1711
		if ( $dayOfYear <= $start ) {
1712
			# Day is before the start of the year - it is the previous year
1713
			# Next year's start
1714
			$nextStart = $start;
1715
			# Previous year
1716
			$year--;
1717
			$hebrewYear--;
1718
			# Add days since previous year's 1 September
1719
			$dayOfYear += 365;
1720 View Code Duplication
			if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1721
				# Leap year
1722
				$dayOfYear++;
1723
			}
1724
			# Start of the new (previous) year
1725
			$start = self::hebrewYearStart( $hebrewYear );
1726
		} else {
1727
			# Next year's start
1728
			$nextStart = self::hebrewYearStart( $hebrewYear + 1 );
1729
		}
1730
1731
		# Calculate Hebrew day of year
1732
		$hebrewDayOfYear = $dayOfYear - $start;
1733
1734
		# Difference between year's days
1735
		$diff = $nextStart - $start;
1736
		# Add 12 (or 13 for leap years) days to ignore the difference between
1737
		# Hebrew and Gregorian year (353 at least vs. 365/6) - now the
1738
		# difference is only about the year type
1739
		if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1740
			$diff += 13;
1741
		} else {
1742
			$diff += 12;
1743
		}
1744
1745
		# Check the year pattern, and is leap year
1746
		# 0 means an incomplete year, 1 means a regular year, 2 means a complete year
1747
		# This is mod 30, to work on both leap years (which add 30 days of Adar I)
1748
		# and non-leap years
1749
		$yearPattern = $diff % 30;
1750
		# Check if leap year
1751
		$isLeap = $diff >= 30;
1752
1753
		# Calculate day in the month from number of day in the Hebrew year
1754
		# Don't check Adar - if the day is not in Adar, we will stop before;
1755
		# if it is in Adar, we will use it to check if it is Adar I or Adar II
1756
		$hebrewDay = $hebrewDayOfYear;
1757
		$hebrewMonth = 1;
1758
		$days = 0;
1759
		while ( $hebrewMonth <= 12 ) {
1760
			# Calculate days in this month
1761
			if ( $isLeap && $hebrewMonth == 6 ) {
1762
				# Adar in a leap year
1763
				if ( $isLeap ) {
1764
					# Leap year - has Adar I, with 30 days, and Adar II, with 29 days
1765
					$days = 30;
1766
					if ( $hebrewDay <= $days ) {
1767
						# Day in Adar I
1768
						$hebrewMonth = 13;
1769
					} else {
1770
						# Subtract the days of Adar I
1771
						$hebrewDay -= $days;
1772
						# Try Adar II
1773
						$days = 29;
1774
						if ( $hebrewDay <= $days ) {
1775
							# Day in Adar II
1776
							$hebrewMonth = 14;
1777
						}
1778
					}
1779
				}
1780
			} elseif ( $hebrewMonth == 2 && $yearPattern == 2 ) {
1781
				# Cheshvan in a complete year (otherwise as the rule below)
1782
				$days = 30;
1783
			} elseif ( $hebrewMonth == 3 && $yearPattern == 0 ) {
1784
				# Kislev in an incomplete year (otherwise as the rule below)
1785
				$days = 29;
1786
			} else {
1787
				# Odd months have 30 days, even have 29
1788
				$days = 30 - ( $hebrewMonth - 1 ) % 2;
1789
			}
1790
			if ( $hebrewDay <= $days ) {
1791
				# In the current month
1792
				break;
1793
			} else {
1794
				# Subtract the days of the current month
1795
				$hebrewDay -= $days;
1796
				# Try in the next month
1797
				$hebrewMonth++;
1798
			}
1799
		}
1800
1801
		return [ $hebrewYear, $hebrewMonth, $hebrewDay, $days ];
1802
	}
1803
1804
	/**
1805
	 * This calculates the Hebrew year start, as days since 1 September.
1806
	 * Based on Carl Friedrich Gauss algorithm for finding Easter date.
1807
	 * Used for Hebrew date.
1808
	 *
1809
	 * @param int $year
1810
	 *
1811
	 * @return string
1812
	 */
1813
	private static function hebrewYearStart( $year ) {
1814
		$a = intval( ( 12 * ( $year - 1 ) + 17 ) % 19 );
1815
		$b = intval( ( $year - 1 ) % 4 );
1816
		$m = 32.044093161144 + 1.5542417966212 * $a + $b / 4.0 - 0.0031777940220923 * ( $year - 1 );
1817
		if ( $m < 0 ) {
1818
			$m--;
1819
		}
1820
		$Mar = intval( $m );
1821
		if ( $m < 0 ) {
1822
			$m++;
1823
		}
1824
		$m -= $Mar;
1825
1826
		$c = intval( ( $Mar + 3 * ( $year - 1 ) + 5 * $b + 5 ) % 7 );
1827
		if ( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
1828
			$Mar++;
1829
		} elseif ( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
1830
			$Mar += 2;
1831
		} elseif ( $c == 2 || $c == 4 || $c == 6 ) {
1832
			$Mar++;
1833
		}
1834
1835
		$Mar += intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
1836
		return $Mar;
1837
	}
1838
1839
	/**
1840
	 * Algorithm to convert Gregorian dates to Thai solar dates,
1841
	 * Minguo dates or Minguo dates.
1842
	 *
1843
	 * Link: http://en.wikipedia.org/wiki/Thai_solar_calendar
1844
	 *       http://en.wikipedia.org/wiki/Minguo_calendar
1845
	 *       http://en.wikipedia.org/wiki/Japanese_era_name
1846
	 *
1847
	 * @param string $ts 14-character timestamp
1848
	 * @param string $cName Calender name
1849
	 * @return array Converted year, month, day
1850
	 */
1851
	private static function tsToYear( $ts, $cName ) {
1852
		$gy = substr( $ts, 0, 4 );
1853
		$gm = substr( $ts, 4, 2 );
1854
		$gd = substr( $ts, 6, 2 );
1855
1856
		if ( !strcmp( $cName, 'thai' ) ) {
1857
			# Thai solar dates
1858
			# Add 543 years to the Gregorian calendar
1859
			# Months and days are identical
1860
			$gy_offset = $gy + 543;
1861
		} elseif ( ( !strcmp( $cName, 'minguo' ) ) || !strcmp( $cName, 'juche' ) ) {
1862
			# Minguo dates
1863
			# Deduct 1911 years from the Gregorian calendar
1864
			# Months and days are identical
1865
			$gy_offset = $gy - 1911;
1866
		} elseif ( !strcmp( $cName, 'tenno' ) ) {
1867
			# Nengō dates up to Meiji period
1868
			# Deduct years from the Gregorian calendar
1869
			# depending on the nengo periods
1870
			# Months and days are identical
1871
			if ( ( $gy < 1912 )
1872
				|| ( ( $gy == 1912 ) && ( $gm < 7 ) )
1873
				|| ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd < 31 ) )
1874
			) {
1875
				# Meiji period
1876
				$gy_gannen = $gy - 1868 + 1;
1877
				$gy_offset = $gy_gannen;
1878
				if ( $gy_gannen == 1 ) {
1879
					$gy_offset = '元';
1880
				}
1881
				$gy_offset = '明治' . $gy_offset;
1882
			} elseif (
1883
				( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd == 31 ) ) ||
1884
				( ( $gy == 1912 ) && ( $gm >= 8 ) ) ||
1885
				( ( $gy > 1912 ) && ( $gy < 1926 ) ) ||
1886
				( ( $gy == 1926 ) && ( $gm < 12 ) ) ||
1887
				( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd < 26 ) )
1888
			) {
1889
				# Taishō period
1890
				$gy_gannen = $gy - 1912 + 1;
1891
				$gy_offset = $gy_gannen;
1892
				if ( $gy_gannen == 1 ) {
1893
					$gy_offset = '元';
1894
				}
1895
				$gy_offset = '大正' . $gy_offset;
1896
			} elseif (
1897
				( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd >= 26 ) ) ||
1898
				( ( $gy > 1926 ) && ( $gy < 1989 ) ) ||
1899
				( ( $gy == 1989 ) && ( $gm == 1 ) && ( $gd < 8 ) )
1900
			) {
1901
				# Shōwa period
1902
				$gy_gannen = $gy - 1926 + 1;
1903
				$gy_offset = $gy_gannen;
1904
				if ( $gy_gannen == 1 ) {
1905
					$gy_offset = '元';
1906
				}
1907
				$gy_offset = '昭和' . $gy_offset;
1908
			} else {
1909
				# Heisei period
1910
				$gy_gannen = $gy - 1989 + 1;
1911
				$gy_offset = $gy_gannen;
1912
				if ( $gy_gannen == 1 ) {
1913
					$gy_offset = '元';
1914
				}
1915
				$gy_offset = '平成' . $gy_offset;
1916
			}
1917
		} else {
1918
			$gy_offset = $gy;
1919
		}
1920
1921
		return [ $gy_offset, $gm, $gd ];
1922
	}
1923
1924
	/**
1925
	 * Gets directionality of the first strongly directional codepoint, for embedBidi()
1926
	 *
1927
	 * This is the rule the BIDI algorithm uses to determine the directionality of
1928
	 * paragraphs ( http://unicode.org/reports/tr9/#The_Paragraph_Level ) and
1929
	 * FSI isolates ( http://unicode.org/reports/tr9/#Explicit_Directional_Isolates ).
1930
	 *
1931
	 * TODO: Does not handle BIDI control characters inside the text.
1932
	 * TODO: Does not handle unallocated characters.
1933
	 *
1934
	 * @param string $text Text to test
1935
	 * @return null|string Directionality ('ltr' or 'rtl') or null
1936
	 */
1937
	private static function strongDirFromContent( $text = '' ) {
1938
		if ( !preg_match( self::$strongDirRegex, $text, $matches ) ) {
1939
			return null;
1940
		}
1941
		if ( $matches[1] === '' ) {
1942
			return 'rtl';
1943
		}
1944
		return 'ltr';
1945
	}
1946
1947
	/**
1948
	 * Roman number formatting up to 10000
1949
	 *
1950
	 * @param int $num
1951
	 *
1952
	 * @return string
1953
	 */
1954
	static function romanNumeral( $num ) {
1955
		static $table = [
1956
			[ '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ],
1957
			[ '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ],
1958
			[ '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ],
1959
			[ '', 'M', 'MM', 'MMM', 'MMMM', 'MMMMM', 'MMMMMM', 'MMMMMMM',
1960
				'MMMMMMMM', 'MMMMMMMMM', 'MMMMMMMMMM' ]
1961
		];
1962
1963
		$num = intval( $num );
1964
		if ( $num > 10000 || $num <= 0 ) {
1965
			return $num;
1966
		}
1967
1968
		$s = '';
1969
		for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1970
			if ( $num >= $pow10 ) {
1971
				$s .= $table[$i][(int)floor( $num / $pow10 )];
1972
			}
1973
			$num = $num % $pow10;
1974
		}
1975
		return $s;
1976
	}
1977
1978
	/**
1979
	 * Hebrew Gematria number formatting up to 9999
1980
	 *
1981
	 * @param int $num
1982
	 *
1983
	 * @return string
1984
	 */
1985
	static function hebrewNumeral( $num ) {
1986
		static $table = [
1987
			[ '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ],
1988
			[ '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ],
1989
			[ '',
1990
				[ 'ק' ],
1991
				[ 'ר' ],
1992
				[ 'ש' ],
1993
				[ 'ת' ],
1994
				[ 'ת', 'ק' ],
1995
				[ 'ת', 'ר' ],
1996
				[ 'ת', 'ש' ],
1997
				[ 'ת', 'ת' ],
1998
				[ 'ת', 'ת', 'ק' ],
1999
				[ 'ת', 'ת', 'ר' ],
2000
			],
2001
			[ '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ]
2002
		];
2003
2004
		$num = intval( $num );
2005
		if ( $num > 9999 || $num <= 0 ) {
2006
			return $num;
2007
		}
2008
2009
		// Round thousands have special notations
2010
		if ( $num === 1000 ) {
2011
			return "א' אלף";
2012
		} elseif ( $num % 1000 === 0 ) {
2013
			return $table[0][$num / 1000] . "' אלפים";
2014
		}
2015
2016
		$letters = [];
2017
2018
		for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
2019
			if ( $num >= $pow10 ) {
2020
				if ( $num === 15 || $num === 16 ) {
2021
					$letters[] = $table[0][9];
2022
					$letters[] = $table[0][$num - 9];
2023
					$num = 0;
2024
				} else {
2025
					$letters = array_merge(
2026
						$letters,
2027
						(array)$table[$i][intval( $num / $pow10 )]
2028
					);
2029
2030
					if ( $pow10 === 1000 ) {
2031
						$letters[] = "'";
2032
					}
2033
				}
2034
			}
2035
2036
			$num = $num % $pow10;
2037
		}
2038
2039
		$preTransformLength = count( $letters );
2040
		if ( $preTransformLength === 1 ) {
2041
			// Add geresh (single quote) to one-letter numbers
2042
			$letters[] = "'";
2043
		} else {
2044
			$lastIndex = $preTransformLength - 1;
2045
			$letters[$lastIndex] = str_replace(
2046
				[ 'כ', 'מ', 'נ', 'פ', 'צ' ],
2047
				[ 'ך', 'ם', 'ן', 'ף', 'ץ' ],
2048
				$letters[$lastIndex]
2049
			);
2050
2051
			// Add gershayim (double quote) to multiple-letter numbers,
2052
			// but exclude numbers with only one letter after the thousands
2053
			// (1001-1009, 1020, 1030, 2001-2009, etc.)
2054
			if ( $letters[1] === "'" && $preTransformLength === 3 ) {
2055
				$letters[] = "'";
2056
			} else {
2057
				array_splice( $letters, -1, 0, '"' );
2058
			}
2059
		}
2060
2061
		return implode( $letters );
2062
	}
2063
2064
	/**
2065
	 * Used by date() and time() to adjust the time output.
2066
	 *
2067
	 * @param string $ts The time in date('YmdHis') format
2068
	 * @param mixed $tz Adjust the time by this amount (default false, mean we
2069
	 *   get user timecorrection setting)
2070
	 * @return int
2071
	 */
2072
	function userAdjust( $ts, $tz = false ) {
2073
		global $wgUser, $wgLocalTZoffset;
2074
2075
		if ( $tz === false ) {
2076
			$tz = $wgUser->getOption( 'timecorrection' );
2077
		}
2078
2079
		$data = explode( '|', $tz, 3 );
2080
2081
		if ( $data[0] == 'ZoneInfo' ) {
2082
			MediaWiki\suppressWarnings();
2083
			$userTZ = timezone_open( $data[2] );
2084
			MediaWiki\restoreWarnings();
2085
			if ( $userTZ !== false ) {
2086
				$date = date_create( $ts, timezone_open( 'UTC' ) );
2087
				date_timezone_set( $date, $userTZ );
2088
				$date = date_format( $date, 'YmdHis' );
2089
				return $date;
2090
			}
2091
			# Unrecognized timezone, default to 'Offset' with the stored offset.
2092
			$data[0] = 'Offset';
2093
		}
2094
2095
		if ( $data[0] == 'System' || $tz == '' ) {
2096
			# Global offset in minutes.
2097
			$minDiff = $wgLocalTZoffset;
2098
		} elseif ( $data[0] == 'Offset' ) {
2099
			$minDiff = intval( $data[1] );
2100 View Code Duplication
		} else {
2101
			$data = explode( ':', $tz );
2102
			if ( count( $data ) == 2 ) {
2103
				$data[0] = intval( $data[0] );
2104
				$data[1] = intval( $data[1] );
2105
				$minDiff = abs( $data[0] ) * 60 + $data[1];
2106
				if ( $data[0] < 0 ) {
2107
					$minDiff = -$minDiff;
2108
				}
2109
			} else {
2110
				$minDiff = intval( $data[0] ) * 60;
2111
			}
2112
		}
2113
2114
		# No difference ? Return time unchanged
2115
		if ( 0 == $minDiff ) {
2116
			return $ts;
2117
		}
2118
2119
		MediaWiki\suppressWarnings(); // E_STRICT system time bitching
2120
		# Generate an adjusted date; take advantage of the fact that mktime
2121
		# will normalize out-of-range values so we don't have to split $minDiff
2122
		# into hours and minutes.
2123
		$t = mktime( (
2124
			(int)substr( $ts, 8, 2 ) ), # Hours
2125
			(int)substr( $ts, 10, 2 ) + $minDiff, # Minutes
2126
			(int)substr( $ts, 12, 2 ), # Seconds
2127
			(int)substr( $ts, 4, 2 ), # Month
2128
			(int)substr( $ts, 6, 2 ), # Day
2129
			(int)substr( $ts, 0, 4 ) ); # Year
2130
2131
		$date = date( 'YmdHis', $t );
2132
		MediaWiki\restoreWarnings();
2133
2134
		return $date;
2135
	}
2136
2137
	/**
2138
	 * This is meant to be used by time(), date(), and timeanddate() to get
2139
	 * the date preference they're supposed to use, it should be used in
2140
	 * all children.
2141
	 *
2142
	 *<code>
2143
	 * function timeanddate([...], $format = true) {
2144
	 * 	$datePreference = $this->dateFormat($format);
2145
	 * [...]
2146
	 * }
2147
	 *</code>
2148
	 *
2149
	 * @param int|string|bool $usePrefs If true, the user's preference is used
2150
	 *   if false, the site/language default is used
2151
	 *   if int/string, assumed to be a format.
2152
	 * @return string
2153
	 */
2154
	function dateFormat( $usePrefs = true ) {
2155
		global $wgUser;
2156
2157
		if ( is_bool( $usePrefs ) ) {
2158
			if ( $usePrefs ) {
2159
				$datePreference = $wgUser->getDatePreference();
2160
			} else {
2161
				$datePreference = (string)User::getDefaultOption( 'date' );
2162
			}
2163
		} else {
2164
			$datePreference = (string)$usePrefs;
2165
		}
2166
2167
		// return int
2168
		if ( $datePreference == '' ) {
2169
			return 'default';
2170
		}
2171
2172
		return $datePreference;
2173
	}
2174
2175
	/**
2176
	 * Get a format string for a given type and preference
2177
	 * @param string $type May be 'date', 'time', 'both', or 'pretty'.
2178
	 * @param string $pref The format name as it appears in Messages*.php under
2179
	 *  $datePreferences.
2180
	 *
2181
	 * @since 1.22 New type 'pretty' that provides a more readable timestamp format
2182
	 *
2183
	 * @return string
2184
	 */
2185
	function getDateFormatString( $type, $pref ) {
2186
		$wasDefault = false;
2187
		if ( $pref == 'default' ) {
2188
			$wasDefault = true;
2189
			$pref = $this->getDefaultDateFormat();
2190
		}
2191
2192
		if ( !isset( $this->dateFormatStrings[$type][$pref] ) ) {
2193
			$df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2194
2195
			if ( $type === 'pretty' && $df === null ) {
2196
				$df = $this->getDateFormatString( 'date', $pref );
0 ignored issues
show
Bug introduced by
It seems like $pref defined by $this->getDefaultDateFormat() on line 2189 can also be of type array; however, Language::getDateFormatString() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
2197
			}
2198
2199
			if ( !$wasDefault && $df === null ) {
2200
				$pref = $this->getDefaultDateFormat();
2201
				$df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2202
			}
2203
2204
			$this->dateFormatStrings[$type][$pref] = $df;
2205
		}
2206
		return $this->dateFormatStrings[$type][$pref];
2207
	}
2208
2209
	/**
2210
	 * @param string $ts The time format which needs to be turned into a
2211
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2212
	 * @param bool $adj Whether to adjust the time output according to the
2213
	 *   user configured offset ($timecorrection)
2214
	 * @param mixed $format True to use user's date format preference
2215
	 * @param string|bool $timecorrection The time offset as returned by
2216
	 *   validateTimeZone() in Special:Preferences
2217
	 * @return string
2218
	 */
2219 View Code Duplication
	function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
2220
		$ts = wfTimestamp( TS_MW, $ts );
2221
		if ( $adj ) {
2222
			$ts = $this->userAdjust( $ts, $timecorrection );
0 ignored issues
show
Security Bug introduced by
It seems like $ts defined by $this->userAdjust($ts, $timecorrection) on line 2222 can also be of type false; however, Language::userAdjust() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
2223
		}
2224
		$df = $this->getDateFormatString( 'date', $this->dateFormat( $format ) );
2225
		return $this->sprintfDate( $df, $ts );
2226
	}
2227
2228
	/**
2229
	 * @param string $ts The time format which needs to be turned into a
2230
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2231
	 * @param bool $adj Whether to adjust the time output according to the
2232
	 *   user configured offset ($timecorrection)
2233
	 * @param mixed $format True to use user's date format preference
2234
	 * @param string|bool $timecorrection The time offset as returned by
2235
	 *   validateTimeZone() in Special:Preferences
2236
	 * @return string
2237
	 */
2238 View Code Duplication
	function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
2239
		$ts = wfTimestamp( TS_MW, $ts );
2240
		if ( $adj ) {
2241
			$ts = $this->userAdjust( $ts, $timecorrection );
0 ignored issues
show
Security Bug introduced by
It seems like $ts defined by $this->userAdjust($ts, $timecorrection) on line 2241 can also be of type false; however, Language::userAdjust() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
2242
		}
2243
		$df = $this->getDateFormatString( 'time', $this->dateFormat( $format ) );
2244
		return $this->sprintfDate( $df, $ts );
2245
	}
2246
2247
	/**
2248
	 * @param string $ts The time format which needs to be turned into a
2249
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2250
	 * @param bool $adj Whether to adjust the time output according to the
2251
	 *   user configured offset ($timecorrection)
2252
	 * @param mixed $format What format to return, if it's false output the
2253
	 *   default one (default true)
2254
	 * @param string|bool $timecorrection The time offset as returned by
2255
	 *   validateTimeZone() in Special:Preferences
2256
	 * @return string
2257
	 */
2258 View Code Duplication
	function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false ) {
2259
		$ts = wfTimestamp( TS_MW, $ts );
2260
		if ( $adj ) {
2261
			$ts = $this->userAdjust( $ts, $timecorrection );
0 ignored issues
show
Security Bug introduced by
It seems like $ts defined by $this->userAdjust($ts, $timecorrection) on line 2261 can also be of type false; however, Language::userAdjust() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
2262
		}
2263
		$df = $this->getDateFormatString( 'both', $this->dateFormat( $format ) );
2264
		return $this->sprintfDate( $df, $ts );
2265
	}
2266
2267
	/**
2268
	 * Takes a number of seconds and turns it into a text using values such as hours and minutes.
2269
	 *
2270
	 * @since 1.20
2271
	 *
2272
	 * @param int $seconds The amount of seconds.
2273
	 * @param array $chosenIntervals The intervals to enable.
2274
	 *
2275
	 * @return string
2276
	 */
2277
	public function formatDuration( $seconds, array $chosenIntervals = [] ) {
2278
		$intervals = $this->getDurationIntervals( $seconds, $chosenIntervals );
2279
2280
		$segments = [];
2281
2282 View Code Duplication
		foreach ( $intervals as $intervalName => $intervalValue ) {
2283
			// Messages: duration-seconds, duration-minutes, duration-hours, duration-days, duration-weeks,
2284
			// duration-years, duration-decades, duration-centuries, duration-millennia
2285
			$message = wfMessage( 'duration-' . $intervalName )->numParams( $intervalValue );
2286
			$segments[] = $message->inLanguage( $this )->escaped();
2287
		}
2288
2289
		return $this->listToText( $segments );
2290
	}
2291
2292
	/**
2293
	 * Takes a number of seconds and returns an array with a set of corresponding intervals.
2294
	 * For example 65 will be turned into array( minutes => 1, seconds => 5 ).
2295
	 *
2296
	 * @since 1.20
2297
	 *
2298
	 * @param int $seconds The amount of seconds.
2299
	 * @param array $chosenIntervals The intervals to enable.
2300
	 *
2301
	 * @return array
2302
	 */
2303
	public function getDurationIntervals( $seconds, array $chosenIntervals = [] ) {
2304 View Code Duplication
		if ( empty( $chosenIntervals ) ) {
2305
			$chosenIntervals = [
2306
				'millennia',
2307
				'centuries',
2308
				'decades',
2309
				'years',
2310
				'days',
2311
				'hours',
2312
				'minutes',
2313
				'seconds'
2314
			];
2315
		}
2316
2317
		$intervals = array_intersect_key( self::$durationIntervals, array_flip( $chosenIntervals ) );
2318
		$sortedNames = array_keys( $intervals );
2319
		$smallestInterval = array_pop( $sortedNames );
2320
2321
		$segments = [];
2322
2323
		foreach ( $intervals as $name => $length ) {
2324
			$value = floor( $seconds / $length );
2325
2326
			if ( $value > 0 || ( $name == $smallestInterval && empty( $segments ) ) ) {
2327
				$seconds -= $value * $length;
2328
				$segments[$name] = $value;
2329
			}
2330
		}
2331
2332
		return $segments;
2333
	}
2334
2335
	/**
2336
	 * Internal helper function for userDate(), userTime() and userTimeAndDate()
2337
	 *
2338
	 * @param string $type Can be 'date', 'time' or 'both'
2339
	 * @param string $ts The time format which needs to be turned into a
2340
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2341
	 * @param User $user User object used to get preferences for timezone and format
2342
	 * @param array $options Array, can contain the following keys:
2343
	 *   - 'timecorrection': time correction, can have the following values:
2344
	 *     - true: use user's preference
2345
	 *     - false: don't use time correction
2346
	 *     - int: value of time correction in minutes
2347
	 *   - 'format': format to use, can have the following values:
2348
	 *     - true: use user's preference
2349
	 *     - false: use default preference
2350
	 *     - string: format to use
2351
	 * @since 1.19
2352
	 * @return string
2353
	 */
2354
	private function internalUserTimeAndDate( $type, $ts, User $user, array $options ) {
2355
		$ts = wfTimestamp( TS_MW, $ts );
2356
		$options += [ 'timecorrection' => true, 'format' => true ];
2357
		if ( $options['timecorrection'] !== false ) {
2358
			if ( $options['timecorrection'] === true ) {
2359
				$offset = $user->getOption( 'timecorrection' );
2360
			} else {
2361
				$offset = $options['timecorrection'];
2362
			}
2363
			$ts = $this->userAdjust( $ts, $offset );
0 ignored issues
show
Security Bug introduced by
It seems like $ts defined by $this->userAdjust($ts, $offset) on line 2363 can also be of type false; however, Language::userAdjust() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
2364
		}
2365
		if ( $options['format'] === true ) {
2366
			$format = $user->getDatePreference();
2367
		} else {
2368
			$format = $options['format'];
2369
		}
2370
		$df = $this->getDateFormatString( $type, $this->dateFormat( $format ) );
2371
		return $this->sprintfDate( $df, $ts );
2372
	}
2373
2374
	/**
2375
	 * Get the formatted date for the given timestamp and formatted for
2376
	 * the given user.
2377
	 *
2378
	 * @param mixed $ts Mixed: the time format which needs to be turned into a
2379
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2380
	 * @param User $user User object used to get preferences for timezone and format
2381
	 * @param array $options Array, can contain the following keys:
2382
	 *   - 'timecorrection': time correction, can have the following values:
2383
	 *     - true: use user's preference
2384
	 *     - false: don't use time correction
2385
	 *     - int: value of time correction in minutes
2386
	 *   - 'format': format to use, can have the following values:
2387
	 *     - true: use user's preference
2388
	 *     - false: use default preference
2389
	 *     - string: format to use
2390
	 * @since 1.19
2391
	 * @return string
2392
	 */
2393
	public function userDate( $ts, User $user, array $options = [] ) {
2394
		return $this->internalUserTimeAndDate( 'date', $ts, $user, $options );
2395
	}
2396
2397
	/**
2398
	 * Get the formatted time for the given timestamp and formatted for
2399
	 * the given user.
2400
	 *
2401
	 * @param mixed $ts The time format which needs to be turned into a
2402
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2403
	 * @param User $user User object used to get preferences for timezone and format
2404
	 * @param array $options Array, can contain the following keys:
2405
	 *   - 'timecorrection': time correction, can have the following values:
2406
	 *     - true: use user's preference
2407
	 *     - false: don't use time correction
2408
	 *     - int: value of time correction in minutes
2409
	 *   - 'format': format to use, can have the following values:
2410
	 *     - true: use user's preference
2411
	 *     - false: use default preference
2412
	 *     - string: format to use
2413
	 * @since 1.19
2414
	 * @return string
2415
	 */
2416
	public function userTime( $ts, User $user, array $options = [] ) {
2417
		return $this->internalUserTimeAndDate( 'time', $ts, $user, $options );
2418
	}
2419
2420
	/**
2421
	 * Get the formatted date and time for the given timestamp and formatted for
2422
	 * the given user.
2423
	 *
2424
	 * @param mixed $ts The time format which needs to be turned into a
2425
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2426
	 * @param User $user User object used to get preferences for timezone and format
2427
	 * @param array $options Array, can contain the following keys:
2428
	 *   - 'timecorrection': time correction, can have the following values:
2429
	 *     - true: use user's preference
2430
	 *     - false: don't use time correction
2431
	 *     - int: value of time correction in minutes
2432
	 *   - 'format': format to use, can have the following values:
2433
	 *     - true: use user's preference
2434
	 *     - false: use default preference
2435
	 *     - string: format to use
2436
	 * @since 1.19
2437
	 * @return string
2438
	 */
2439
	public function userTimeAndDate( $ts, User $user, array $options = [] ) {
2440
		return $this->internalUserTimeAndDate( 'both', $ts, $user, $options );
2441
	}
2442
2443
	/**
2444
	 * Get the timestamp in a human-friendly relative format, e.g., "3 days ago".
2445
	 *
2446
	 * Determine the difference between the timestamp and the current time, and
2447
	 * generate a readable timestamp by returning "<N> <units> ago", where the
2448
	 * largest possible unit is used.
2449
	 *
2450
	 * @since 1.26 (Prior to 1.26 method existed but was not meant to be used directly)
2451
	 *
2452
	 * @param MWTimestamp $time
2453
	 * @param MWTimestamp|null $relativeTo The base timestamp to compare to (defaults to now)
2454
	 * @param User|null $user User the timestamp is being generated for
2455
	 *  (or null to use main context's user)
2456
	 * @return string Formatted timestamp
2457
	 */
2458
	public function getHumanTimestamp(
2459
		MWTimestamp $time, MWTimestamp $relativeTo = null, User $user = null
2460
	) {
2461
		if ( $relativeTo === null ) {
2462
			$relativeTo = new MWTimestamp();
2463
		}
2464
		if ( $user === null ) {
2465
			$user = RequestContext::getMain()->getUser();
2466
		}
2467
2468
		// Adjust for the user's timezone.
2469
		$offsetThis = $time->offsetForUser( $user );
2470
		$offsetRel = $relativeTo->offsetForUser( $user );
2471
2472
		$ts = '';
2473
		if ( Hooks::run( 'GetHumanTimestamp', [ &$ts, $time, $relativeTo, $user, $this ] ) ) {
2474
			$ts = $this->getHumanTimestampInternal( $time, $relativeTo, $user );
2475
		}
2476
2477
		// Reset the timezone on the objects.
2478
		$time->timestamp->sub( $offsetThis );
2479
		$relativeTo->timestamp->sub( $offsetRel );
2480
2481
		return $ts;
2482
	}
2483
2484
	/**
2485
	 * Convert an MWTimestamp into a pretty human-readable timestamp using
2486
	 * the given user preferences and relative base time.
2487
	 *
2488
	 * @see Language::getHumanTimestamp
2489
	 * @param MWTimestamp $ts Timestamp to prettify
2490
	 * @param MWTimestamp $relativeTo Base timestamp
2491
	 * @param User $user User preferences to use
2492
	 * @return string Human timestamp
2493
	 * @since 1.26
2494
	 */
2495
	private function getHumanTimestampInternal(
2496
		MWTimestamp $ts, MWTimestamp $relativeTo, User $user
2497
	) {
2498
		$diff = $ts->diff( $relativeTo );
2499
		$diffDay = (bool)( (int)$ts->timestamp->format( 'w' ) -
2500
			(int)$relativeTo->timestamp->format( 'w' ) );
2501
		$days = $diff->days ?: (int)$diffDay;
2502
		if ( $diff->invert || $days > 5
2503
			&& $ts->timestamp->format( 'Y' ) !== $relativeTo->timestamp->format( 'Y' )
2504
		) {
2505
			// Timestamps are in different years: use full timestamp
2506
			// Also do full timestamp for future dates
2507
			/**
2508
			 * @todo FIXME: Add better handling of future timestamps.
2509
			 */
2510
			$format = $this->getDateFormatString( 'both', $user->getDatePreference() ?: 'default' );
2511
			$ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
2512
		} elseif ( $days > 5 ) {
2513
			// Timestamps are in same year,  but more than 5 days ago: show day and month only.
2514
			$format = $this->getDateFormatString( 'pretty', $user->getDatePreference() ?: 'default' );
2515
			$ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
2516
		} elseif ( $days > 1 ) {
2517
			// Timestamp within the past week: show the day of the week and time
2518
			$format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2519
			$weekday = self::$mWeekdayMsgs[$ts->timestamp->format( 'w' )];
2520
			// Messages:
2521
			// sunday-at, monday-at, tuesday-at, wednesday-at, thursday-at, friday-at, saturday-at
2522
			$ts = wfMessage( "$weekday-at" )
2523
				->inLanguage( $this )
2524
				->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2525
				->text();
2526
		} elseif ( $days == 1 ) {
2527
			// Timestamp was yesterday: say 'yesterday' and the time.
2528
			$format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2529
			$ts = wfMessage( 'yesterday-at' )
2530
				->inLanguage( $this )
2531
				->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2532
				->text();
2533
		} elseif ( $diff->h > 1 || $diff->h == 1 && $diff->i > 30 ) {
2534
			// Timestamp was today, but more than 90 minutes ago: say 'today' and the time.
2535
			$format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2536
			$ts = wfMessage( 'today-at' )
2537
				->inLanguage( $this )
2538
				->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2539
				->text();
2540
2541
		// From here on in, the timestamp was soon enough ago so that we can simply say
2542
		// XX units ago, e.g., "2 hours ago" or "5 minutes ago"
2543
		} elseif ( $diff->h == 1 ) {
2544
			// Less than 90 minutes, but more than an hour ago.
2545
			$ts = wfMessage( 'hours-ago' )->inLanguage( $this )->numParams( 1 )->text();
2546
		} elseif ( $diff->i >= 1 ) {
2547
			// A few minutes ago.
2548
			$ts = wfMessage( 'minutes-ago' )->inLanguage( $this )->numParams( $diff->i )->text();
2549
		} elseif ( $diff->s >= 30 ) {
2550
			// Less than a minute, but more than 30 sec ago.
2551
			$ts = wfMessage( 'seconds-ago' )->inLanguage( $this )->numParams( $diff->s )->text();
2552
		} else {
2553
			// Less than 30 seconds ago.
2554
			$ts = wfMessage( 'just-now' )->text();
2555
		}
2556
2557
		return $ts;
2558
	}
2559
2560
	/**
2561
	 * @param string $key
2562
	 * @return array|null
2563
	 */
2564
	function getMessage( $key ) {
2565
		return self::$dataCache->getSubitem( $this->mCode, 'messages', $key );
2566
	}
2567
2568
	/**
2569
	 * @return array
2570
	 */
2571
	function getAllMessages() {
2572
		return self::$dataCache->getItem( $this->mCode, 'messages' );
2573
	}
2574
2575
	/**
2576
	 * @param string $in
2577
	 * @param string $out
2578
	 * @param string $string
2579
	 * @return string
2580
	 */
2581
	function iconv( $in, $out, $string ) {
2582
		# This is a wrapper for iconv in all languages except esperanto,
2583
		# which does some nasty x-conversions beforehand
2584
2585
		# Even with //IGNORE iconv can whine about illegal characters in
2586
		# *input* string. We just ignore those too.
2587
		# REF: http://bugs.php.net/bug.php?id=37166
2588
		# REF: https://phabricator.wikimedia.org/T18885
2589
		MediaWiki\suppressWarnings();
2590
		$text = iconv( $in, $out . '//IGNORE', $string );
2591
		MediaWiki\restoreWarnings();
2592
		return $text;
2593
	}
2594
2595
	// callback functions for uc(), lc(), ucwords(), ucwordbreaks()
2596
2597
	/**
2598
	 * @param array $matches
2599
	 * @return mixed|string
2600
	 */
2601
	function ucwordbreaksCallbackAscii( $matches ) {
2602
		return $this->ucfirst( $matches[1] );
2603
	}
2604
2605
	/**
2606
	 * @param array $matches
2607
	 * @return string
2608
	 */
2609
	function ucwordbreaksCallbackMB( $matches ) {
2610
		return mb_strtoupper( $matches[0] );
2611
	}
2612
2613
	/**
2614
	 * @param array $matches
2615
	 * @return string
2616
	 */
2617
	function ucCallback( $matches ) {
2618
		list( $wikiUpperChars ) = self::getCaseMaps();
2619
		return strtr( $matches[1], $wikiUpperChars );
2620
	}
2621
2622
	/**
2623
	 * @param array $matches
2624
	 * @return string
2625
	 */
2626
	function lcCallback( $matches ) {
2627
		list( , $wikiLowerChars ) = self::getCaseMaps();
2628
		return strtr( $matches[1], $wikiLowerChars );
2629
	}
2630
2631
	/**
2632
	 * @param array $matches
2633
	 * @return string
2634
	 */
2635
	function ucwordsCallbackMB( $matches ) {
2636
		return mb_strtoupper( $matches[0] );
2637
	}
2638
2639
	/**
2640
	 * @param array $matches
2641
	 * @return string
2642
	 */
2643
	function ucwordsCallbackWiki( $matches ) {
2644
		list( $wikiUpperChars ) = self::getCaseMaps();
2645
		return strtr( $matches[0], $wikiUpperChars );
2646
	}
2647
2648
	/**
2649
	 * Make a string's first character uppercase
2650
	 *
2651
	 * @param string $str
2652
	 *
2653
	 * @return string
2654
	 */
2655
	function ucfirst( $str ) {
2656
		$o = ord( $str );
2657
		if ( $o < 96 ) { // if already uppercase...
2658
			return $str;
2659
		} elseif ( $o < 128 ) {
2660
			return ucfirst( $str ); // use PHP's ucfirst()
2661
		} else {
2662
			// fall back to more complex logic in case of multibyte strings
2663
			return $this->uc( $str, true );
2664
		}
2665
	}
2666
2667
	/**
2668
	 * Convert a string to uppercase
2669
	 *
2670
	 * @param string $str
2671
	 * @param bool $first
2672
	 *
2673
	 * @return string
2674
	 */
2675
	function uc( $str, $first = false ) {
2676
		if ( function_exists( 'mb_strtoupper' ) ) {
2677
			if ( $first ) {
2678
				if ( $this->isMultibyte( $str ) ) {
2679
					return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2680
				} else {
2681
					return ucfirst( $str );
2682
				}
2683
			} else {
2684
				return $this->isMultibyte( $str ) ? mb_strtoupper( $str ) : strtoupper( $str );
2685
			}
2686
		} else {
2687
			if ( $this->isMultibyte( $str ) ) {
2688
				$x = $first ? '^' : '';
2689
				return preg_replace_callback(
2690
					"/$x([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
2691
					[ $this, 'ucCallback' ],
2692
					$str
2693
				);
2694
			} else {
2695
				return $first ? ucfirst( $str ) : strtoupper( $str );
2696
			}
2697
		}
2698
	}
2699
2700
	/**
2701
	 * @param string $str
2702
	 * @return mixed|string
2703
	 */
2704
	function lcfirst( $str ) {
2705
		$o = ord( $str );
2706
		if ( !$o ) {
2707
			return strval( $str );
2708
		} elseif ( $o >= 128 ) {
2709
			return $this->lc( $str, true );
2710
		} elseif ( $o > 96 ) {
2711
			return $str;
2712
		} else {
2713
			$str[0] = strtolower( $str[0] );
2714
			return $str;
2715
		}
2716
	}
2717
2718
	/**
2719
	 * @param string $str
2720
	 * @param bool $first
2721
	 * @return mixed|string
2722
	 */
2723
	function lc( $str, $first = false ) {
2724
		if ( function_exists( 'mb_strtolower' ) ) {
2725
			if ( $first ) {
2726
				if ( $this->isMultibyte( $str ) ) {
2727
					return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2728
				} else {
2729
					return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 );
2730
				}
2731
			} else {
2732
				return $this->isMultibyte( $str ) ? mb_strtolower( $str ) : strtolower( $str );
2733
			}
2734
		} else {
2735
			if ( $this->isMultibyte( $str ) ) {
2736
				$x = $first ? '^' : '';
2737
				return preg_replace_callback(
2738
					"/$x([A-Z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
2739
					[ $this, 'lcCallback' ],
2740
					$str
2741
				);
2742
			} else {
2743
				return $first ? strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 ) : strtolower( $str );
2744
			}
2745
		}
2746
	}
2747
2748
	/**
2749
	 * @param string $str
2750
	 * @return bool
2751
	 */
2752
	function isMultibyte( $str ) {
2753
		return strlen( $str ) !== mb_strlen( $str );
2754
	}
2755
2756
	/**
2757
	 * @param string $str
2758
	 * @return mixed|string
2759
	 */
2760
	function ucwords( $str ) {
2761
		if ( $this->isMultibyte( $str ) ) {
2762
			$str = $this->lc( $str );
2763
2764
			// regexp to find first letter in each word (i.e. after each space)
2765
			$replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2766
2767
			// function to use to capitalize a single char
2768 View Code Duplication
			if ( function_exists( 'mb_strtoupper' ) ) {
2769
				return preg_replace_callback(
2770
					$replaceRegexp,
2771
					[ $this, 'ucwordsCallbackMB' ],
2772
					$str
2773
				);
2774
			} else {
2775
				return preg_replace_callback(
2776
					$replaceRegexp,
2777
					[ $this, 'ucwordsCallbackWiki' ],
2778
					$str
2779
				);
2780
			}
2781
		} else {
2782
			return ucwords( strtolower( $str ) );
2783
		}
2784
	}
2785
2786
	/**
2787
	 * capitalize words at word breaks
2788
	 *
2789
	 * @param string $str
2790
	 * @return mixed
2791
	 */
2792
	function ucwordbreaks( $str ) {
2793
		if ( $this->isMultibyte( $str ) ) {
2794
			$str = $this->lc( $str );
2795
2796
			// since \b doesn't work for UTF-8, we explicitely define word break chars
2797
			$breaks = "[ \-\(\)\}\{\.,\?!]";
2798
2799
			// find first letter after word break
2800
			$replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|" .
2801
				"$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2802
2803 View Code Duplication
			if ( function_exists( 'mb_strtoupper' ) ) {
2804
				return preg_replace_callback(
2805
					$replaceRegexp,
2806
					[ $this, 'ucwordbreaksCallbackMB' ],
2807
					$str
2808
				);
2809
			} else {
2810
				return preg_replace_callback(
2811
					$replaceRegexp,
2812
					[ $this, 'ucwordsCallbackWiki' ],
2813
					$str
2814
				);
2815
			}
2816
		} else {
2817
			return preg_replace_callback(
2818
				'/\b([\w\x80-\xff]+)\b/',
2819
				[ $this, 'ucwordbreaksCallbackAscii' ],
2820
				$str
2821
			);
2822
		}
2823
	}
2824
2825
	/**
2826
	 * Return a case-folded representation of $s
2827
	 *
2828
	 * This is a representation such that caseFold($s1)==caseFold($s2) if $s1
2829
	 * and $s2 are the same except for the case of their characters. It is not
2830
	 * necessary for the value returned to make sense when displayed.
2831
	 *
2832
	 * Do *not* perform any other normalisation in this function. If a caller
2833
	 * uses this function when it should be using a more general normalisation
2834
	 * function, then fix the caller.
2835
	 *
2836
	 * @param string $s
2837
	 *
2838
	 * @return string
2839
	 */
2840
	function caseFold( $s ) {
2841
		return $this->uc( $s );
2842
	}
2843
2844
	/**
2845
	 * @param string $s
2846
	 * @return string
2847
	 * @throws MWException
2848
	 */
2849
	function checkTitleEncoding( $s ) {
2850
		if ( is_array( $s ) ) {
2851
			throw new MWException( 'Given array to checkTitleEncoding.' );
2852
		}
2853
		if ( StringUtils::isUtf8( $s ) ) {
2854
			return $s;
2855
		}
2856
2857
		return $this->iconv( $this->fallback8bitEncoding(), 'utf-8', $s );
2858
	}
2859
2860
	/**
2861
	 * @return array
2862
	 */
2863
	function fallback8bitEncoding() {
2864
		return self::$dataCache->getItem( $this->mCode, 'fallback8bitEncoding' );
2865
	}
2866
2867
	/**
2868
	 * Most writing systems use whitespace to break up words.
2869
	 * Some languages such as Chinese don't conventionally do this,
2870
	 * which requires special handling when breaking up words for
2871
	 * searching etc.
2872
	 *
2873
	 * @return bool
2874
	 */
2875
	function hasWordBreaks() {
2876
		return true;
2877
	}
2878
2879
	/**
2880
	 * Some languages such as Chinese require word segmentation,
2881
	 * Specify such segmentation when overridden in derived class.
2882
	 *
2883
	 * @param string $string
2884
	 * @return string
2885
	 */
2886
	function segmentByWord( $string ) {
2887
		return $string;
2888
	}
2889
2890
	/**
2891
	 * Some languages have special punctuation need to be normalized.
2892
	 * Make such changes here.
2893
	 *
2894
	 * @param string $string
2895
	 * @return string
2896
	 */
2897
	function normalizeForSearch( $string ) {
2898
		return self::convertDoubleWidth( $string );
2899
	}
2900
2901
	/**
2902
	 * convert double-width roman characters to single-width.
2903
	 * range: ff00-ff5f ~= 0020-007f
2904
	 *
2905
	 * @param string $string
2906
	 *
2907
	 * @return string
2908
	 */
2909
	protected static function convertDoubleWidth( $string ) {
2910
		static $full = null;
2911
		static $half = null;
2912
2913
		if ( $full === null ) {
2914
			$fullWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2915
			$halfWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2916
			$full = str_split( $fullWidth, 3 );
2917
			$half = str_split( $halfWidth );
2918
		}
2919
2920
		$string = str_replace( $full, $half, $string );
2921
		return $string;
2922
	}
2923
2924
	/**
2925
	 * @param string $string
2926
	 * @param string $pattern
2927
	 * @return string
2928
	 */
2929
	protected static function insertSpace( $string, $pattern ) {
2930
		$string = preg_replace( $pattern, " $1 ", $string );
2931
		$string = preg_replace( '/ +/', ' ', $string );
2932
		return $string;
2933
	}
2934
2935
	/**
2936
	 * @param array $termsArray
2937
	 * @return array
2938
	 */
2939
	function convertForSearchResult( $termsArray ) {
2940
		# some languages, e.g. Chinese, need to do a conversion
2941
		# in order for search results to be displayed correctly
2942
		return $termsArray;
2943
	}
2944
2945
	/**
2946
	 * Get the first character of a string.
2947
	 *
2948
	 * @param string $s
2949
	 * @return string
2950
	 */
2951
	function firstChar( $s ) {
2952
		$matches = [];
2953
		preg_match(
2954
			'/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
2955
				'[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/',
2956
			$s,
2957
			$matches
2958
		);
2959
2960
		if ( isset( $matches[1] ) ) {
2961
			if ( strlen( $matches[1] ) != 3 ) {
2962
				return $matches[1];
2963
			}
2964
2965
			// Break down Hangul syllables to grab the first jamo
2966
			$code = UtfNormal\Utils::utf8ToCodepoint( $matches[1] );
2967
			if ( $code < 0xac00 || 0xd7a4 <= $code ) {
2968
				return $matches[1];
2969
			} elseif ( $code < 0xb098 ) {
2970
				return "\xe3\x84\xb1";
2971
			} elseif ( $code < 0xb2e4 ) {
2972
				return "\xe3\x84\xb4";
2973
			} elseif ( $code < 0xb77c ) {
2974
				return "\xe3\x84\xb7";
2975
			} elseif ( $code < 0xb9c8 ) {
2976
				return "\xe3\x84\xb9";
2977
			} elseif ( $code < 0xbc14 ) {
2978
				return "\xe3\x85\x81";
2979
			} elseif ( $code < 0xc0ac ) {
2980
				return "\xe3\x85\x82";
2981
			} elseif ( $code < 0xc544 ) {
2982
				return "\xe3\x85\x85";
2983
			} elseif ( $code < 0xc790 ) {
2984
				return "\xe3\x85\x87";
2985
			} elseif ( $code < 0xcc28 ) {
2986
				return "\xe3\x85\x88";
2987
			} elseif ( $code < 0xce74 ) {
2988
				return "\xe3\x85\x8a";
2989
			} elseif ( $code < 0xd0c0 ) {
2990
				return "\xe3\x85\x8b";
2991
			} elseif ( $code < 0xd30c ) {
2992
				return "\xe3\x85\x8c";
2993
			} elseif ( $code < 0xd558 ) {
2994
				return "\xe3\x85\x8d";
2995
			} else {
2996
				return "\xe3\x85\x8e";
2997
			}
2998
		} else {
2999
			return '';
3000
		}
3001
	}
3002
3003
	function initEncoding() {
3004
		# Some languages may have an alternate char encoding option
3005
		# (Esperanto X-coding, Japanese furigana conversion, etc)
3006
		# If this language is used as the primary content language,
3007
		# an override to the defaults can be set here on startup.
3008
	}
3009
3010
	/**
3011
	 * @param string $s
3012
	 * @return string
3013
	 */
3014
	function recodeForEdit( $s ) {
3015
		# For some languages we'll want to explicitly specify
3016
		# which characters make it into the edit box raw
3017
		# or are converted in some way or another.
3018
		global $wgEditEncoding;
3019
		if ( $wgEditEncoding == '' || $wgEditEncoding == 'UTF-8' ) {
3020
			return $s;
3021
		} else {
3022
			return $this->iconv( 'UTF-8', $wgEditEncoding, $s );
3023
		}
3024
	}
3025
3026
	/**
3027
	 * @param string $s
3028
	 * @return string
3029
	 */
3030
	function recodeInput( $s ) {
3031
		# Take the previous into account.
3032
		global $wgEditEncoding;
3033
		if ( $wgEditEncoding != '' ) {
3034
			$enc = $wgEditEncoding;
3035
		} else {
3036
			$enc = 'UTF-8';
3037
		}
3038
		if ( $enc == 'UTF-8' ) {
3039
			return $s;
3040
		} else {
3041
			return $this->iconv( $enc, 'UTF-8', $s );
3042
		}
3043
	}
3044
3045
	/**
3046
	 * Convert a UTF-8 string to normal form C. In Malayalam and Arabic, this
3047
	 * also cleans up certain backwards-compatible sequences, converting them
3048
	 * to the modern Unicode equivalent.
3049
	 *
3050
	 * This is language-specific for performance reasons only.
3051
	 *
3052
	 * @param string $s
3053
	 *
3054
	 * @return string
3055
	 */
3056
	function normalize( $s ) {
3057
		global $wgAllUnicodeFixes;
3058
		$s = UtfNormal\Validator::cleanUp( $s );
3059
		if ( $wgAllUnicodeFixes ) {
3060
			$s = $this->transformUsingPairFile( 'normalize-ar.ser', $s );
3061
			$s = $this->transformUsingPairFile( 'normalize-ml.ser', $s );
3062
		}
3063
3064
		return $s;
3065
	}
3066
3067
	/**
3068
	 * Transform a string using serialized data stored in the given file (which
3069
	 * must be in the serialized subdirectory of $IP). The file contains pairs
3070
	 * mapping source characters to destination characters.
3071
	 *
3072
	 * The data is cached in process memory. This will go faster if you have the
3073
	 * FastStringSearch extension.
3074
	 *
3075
	 * @param string $file
3076
	 * @param string $string
3077
	 *
3078
	 * @throws MWException
3079
	 * @return string
3080
	 */
3081
	function transformUsingPairFile( $file, $string ) {
3082
		if ( !isset( $this->transformData[$file] ) ) {
3083
			$data = wfGetPrecompiledData( $file );
3084
			if ( $data === false ) {
3085
				throw new MWException( __METHOD__ . ": The transformation file $file is missing" );
3086
			}
3087
			$this->transformData[$file] = new ReplacementArray( $data );
3088
		}
3089
		return $this->transformData[$file]->replace( $string );
3090
	}
3091
3092
	/**
3093
	 * For right-to-left language support
3094
	 *
3095
	 * @return bool
3096
	 */
3097
	function isRTL() {
3098
		return self::$dataCache->getItem( $this->mCode, 'rtl' );
3099
	}
3100
3101
	/**
3102
	 * Return the correct HTML 'dir' attribute value for this language.
3103
	 * @return string
3104
	 */
3105
	function getDir() {
3106
		return $this->isRTL() ? 'rtl' : 'ltr';
3107
	}
3108
3109
	/**
3110
	 * Return 'left' or 'right' as appropriate alignment for line-start
3111
	 * for this language's text direction.
3112
	 *
3113
	 * Should be equivalent to CSS3 'start' text-align value....
3114
	 *
3115
	 * @return string
3116
	 */
3117
	function alignStart() {
3118
		return $this->isRTL() ? 'right' : 'left';
3119
	}
3120
3121
	/**
3122
	 * Return 'right' or 'left' as appropriate alignment for line-end
3123
	 * for this language's text direction.
3124
	 *
3125
	 * Should be equivalent to CSS3 'end' text-align value....
3126
	 *
3127
	 * @return string
3128
	 */
3129
	function alignEnd() {
3130
		return $this->isRTL() ? 'left' : 'right';
3131
	}
3132
3133
	/**
3134
	 * A hidden direction mark (LRM or RLM), depending on the language direction.
3135
	 * Unlike getDirMark(), this function returns the character as an HTML entity.
3136
	 * This function should be used when the output is guaranteed to be HTML,
3137
	 * because it makes the output HTML source code more readable. When
3138
	 * the output is plain text or can be escaped, getDirMark() should be used.
3139
	 *
3140
	 * @param bool $opposite Get the direction mark opposite to your language
3141
	 * @return string
3142
	 * @since 1.20
3143
	 */
3144
	function getDirMarkEntity( $opposite = false ) {
3145
		if ( $opposite ) {
3146
			return $this->isRTL() ? '&lrm;' : '&rlm;';
3147
		}
3148
		return $this->isRTL() ? '&rlm;' : '&lrm;';
3149
	}
3150
3151
	/**
3152
	 * A hidden direction mark (LRM or RLM), depending on the language direction.
3153
	 * This function produces them as invisible Unicode characters and
3154
	 * the output may be hard to read and debug, so it should only be used
3155
	 * when the output is plain text or can be escaped. When the output is
3156
	 * HTML, use getDirMarkEntity() instead.
3157
	 *
3158
	 * @param bool $opposite Get the direction mark opposite to your language
3159
	 * @return string
3160
	 */
3161
	function getDirMark( $opposite = false ) {
3162
		$lrm = "\xE2\x80\x8E"; # LEFT-TO-RIGHT MARK, commonly abbreviated LRM
3163
		$rlm = "\xE2\x80\x8F"; # RIGHT-TO-LEFT MARK, commonly abbreviated RLM
3164
		if ( $opposite ) {
3165
			return $this->isRTL() ? $lrm : $rlm;
3166
		}
3167
		return $this->isRTL() ? $rlm : $lrm;
3168
	}
3169
3170
	/**
3171
	 * @return array
3172
	 */
3173
	function capitalizeAllNouns() {
3174
		return self::$dataCache->getItem( $this->mCode, 'capitalizeAllNouns' );
3175
	}
3176
3177
	/**
3178
	 * An arrow, depending on the language direction.
3179
	 *
3180
	 * @param string $direction The direction of the arrow: forwards (default),
3181
	 *   backwards, left, right, up, down.
3182
	 * @return string
3183
	 */
3184
	function getArrow( $direction = 'forwards' ) {
3185
		switch ( $direction ) {
3186
		case 'forwards':
3187
			return $this->isRTL() ? '←' : '→';
3188
		case 'backwards':
3189
			return $this->isRTL() ? '→' : '←';
3190
		case 'left':
3191
			return '←';
3192
		case 'right':
3193
			return '→';
3194
		case 'up':
3195
			return '↑';
3196
		case 'down':
3197
			return '↓';
3198
		}
3199
	}
3200
3201
	/**
3202
	 * To allow "foo[[bar]]" to extend the link over the whole word "foobar"
3203
	 *
3204
	 * @return bool
3205
	 */
3206
	function linkPrefixExtension() {
3207
		return self::$dataCache->getItem( $this->mCode, 'linkPrefixExtension' );
3208
	}
3209
3210
	/**
3211
	 * Get all magic words from cache.
3212
	 * @return array
3213
	 */
3214
	function getMagicWords() {
3215
		return self::$dataCache->getItem( $this->mCode, 'magicWords' );
3216
	}
3217
3218
	/**
3219
	 * Run the LanguageGetMagic hook once.
3220
	 */
3221
	protected function doMagicHook() {
3222
		if ( $this->mMagicHookDone ) {
3223
			return;
3224
		}
3225
		$this->mMagicHookDone = true;
3226
		Hooks::run( 'LanguageGetMagic', [ &$this->mMagicExtensions, $this->getCode() ] );
3227
	}
3228
3229
	/**
3230
	 * Fill a MagicWord object with data from here
3231
	 *
3232
	 * @param MagicWord $mw
3233
	 */
3234
	function getMagic( $mw ) {
3235
		// Saves a function call
3236
		if ( !$this->mMagicHookDone ) {
3237
			$this->doMagicHook();
3238
		}
3239
3240
		if ( isset( $this->mMagicExtensions[$mw->mId] ) ) {
3241
			$rawEntry = $this->mMagicExtensions[$mw->mId];
3242
		} else {
3243
			$rawEntry = self::$dataCache->getSubitem(
3244
				$this->mCode, 'magicWords', $mw->mId );
3245
		}
3246
3247
		if ( !is_array( $rawEntry ) ) {
3248
			wfWarn( "\"$rawEntry\" is not a valid magic word for \"$mw->mId\"" );
3249
		} else {
3250
			$mw->mCaseSensitive = $rawEntry[0];
3251
			$mw->mSynonyms = array_slice( $rawEntry, 1 );
3252
		}
3253
	}
3254
3255
	/**
3256
	 * Add magic words to the extension array
3257
	 *
3258
	 * @param array $newWords
3259
	 */
3260
	function addMagicWordsByLang( $newWords ) {
3261
		$fallbackChain = $this->getFallbackLanguages();
3262
		$fallbackChain = array_reverse( $fallbackChain );
3263
		foreach ( $fallbackChain as $code ) {
3264
			if ( isset( $newWords[$code] ) ) {
3265
				$this->mMagicExtensions = $newWords[$code] + $this->mMagicExtensions;
3266
			}
3267
		}
3268
	}
3269
3270
	/**
3271
	 * Get special page names, as an associative array
3272
	 *   canonical name => array of valid names, including aliases
3273
	 * @return array
3274
	 */
3275
	function getSpecialPageAliases() {
3276
		// Cache aliases because it may be slow to load them
3277
		if ( is_null( $this->mExtendedSpecialPageAliases ) ) {
3278
			// Initialise array
3279
			$this->mExtendedSpecialPageAliases =
3280
				self::$dataCache->getItem( $this->mCode, 'specialPageAliases' );
3281
			Hooks::run( 'LanguageGetSpecialPageAliases',
3282
				[ &$this->mExtendedSpecialPageAliases, $this->getCode() ] );
3283
		}
3284
3285
		return $this->mExtendedSpecialPageAliases;
3286
	}
3287
3288
	/**
3289
	 * Italic is unsuitable for some languages
3290
	 *
3291
	 * @param string $text The text to be emphasized.
3292
	 * @return string
3293
	 */
3294
	function emphasize( $text ) {
3295
		return "<em>$text</em>";
3296
	}
3297
3298
	/**
3299
	 * Normally we output all numbers in plain en_US style, that is
3300
	 * 293,291.235 for twohundredninetythreethousand-twohundredninetyone
3301
	 * point twohundredthirtyfive. However this is not suitable for all
3302
	 * languages, some such as Punjabi want ੨੯੩,੨੯੫.੨੩੫ and others such as
3303
	 * Icelandic just want to use commas instead of dots, and dots instead
3304
	 * of commas like "293.291,235".
3305
	 *
3306
	 * An example of this function being called:
3307
	 * <code>
3308
	 * wfMessage( 'message' )->numParams( $num )->text()
3309
	 * </code>
3310
	 *
3311
	 * See $separatorTransformTable on MessageIs.php for
3312
	 * the , => . and . => , implementation.
3313
	 *
3314
	 * @todo check if it's viable to use localeconv() for the decimal separator thing.
3315
	 * @param int|float $number The string to be formatted, should be an integer
3316
	 *   or a floating point number.
3317
	 * @param bool $nocommafy Set to true for special numbers like dates
3318
	 * @return string
3319
	 */
3320
	public function formatNum( $number, $nocommafy = false ) {
3321
		global $wgTranslateNumerals;
3322
		if ( !$nocommafy ) {
3323
			$number = $this->commafy( $number );
3324
			$s = $this->separatorTransformTable();
3325
			if ( $s ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $s of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
3326
				$number = strtr( $number, $s );
3327
			}
3328
		}
3329
3330
		if ( $wgTranslateNumerals ) {
3331
			$s = $this->digitTransformTable();
3332
			if ( $s ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $s of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
3333
				$number = strtr( $number, $s );
3334
			}
3335
		}
3336
3337
		return $number;
3338
	}
3339
3340
	/**
3341
	 * Front-end for non-commafied formatNum
3342
	 *
3343
	 * @param int|float $number The string to be formatted, should be an integer
3344
	 *        or a floating point number.
3345
	 * @since 1.21
3346
	 * @return string
3347
	 */
3348
	public function formatNumNoSeparators( $number ) {
3349
		return $this->formatNum( $number, true );
3350
	}
3351
3352
	/**
3353
	 * @param string $number
3354
	 * @return string
3355
	 */
3356
	public function parseFormattedNumber( $number ) {
3357
		$s = $this->digitTransformTable();
3358
		if ( $s ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $s of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
3359
			// eliminate empty array values such as ''. (bug 64347)
3360
			$s = array_filter( $s );
3361
			$number = strtr( $number, array_flip( $s ) );
3362
		}
3363
3364
		$s = $this->separatorTransformTable();
3365
		if ( $s ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $s of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
3366
			// eliminate empty array values such as ''. (bug 64347)
3367
			$s = array_filter( $s );
3368
			$number = strtr( $number, array_flip( $s ) );
3369
		}
3370
3371
		$number = strtr( $number, [ ',' => '' ] );
3372
		return $number;
3373
	}
3374
3375
	/**
3376
	 * Adds commas to a given number
3377
	 * @since 1.19
3378
	 * @param mixed $number
3379
	 * @return string
3380
	 */
3381
	function commafy( $number ) {
3382
		$digitGroupingPattern = $this->digitGroupingPattern();
3383
		if ( $number === null ) {
3384
			return '';
3385
		}
3386
3387
		if ( !$digitGroupingPattern || $digitGroupingPattern === "###,###,###" ) {
3388
			// default grouping is at thousands,  use the same for ###,###,### pattern too.
3389
			return strrev( (string)preg_replace( '/(\d{3})(?=\d)(?!\d*\.)/', '$1,', strrev( $number ) ) );
3390
		} else {
3391
			// Ref: http://cldr.unicode.org/translation/number-patterns
3392
			$sign = "";
3393
			if ( intval( $number ) < 0 ) {
3394
				// For negative numbers apply the algorithm like positive number and add sign.
3395
				$sign = "-";
3396
				$number = substr( $number, 1 );
3397
			}
3398
			$integerPart = [];
3399
			$decimalPart = [];
3400
			$numMatches = preg_match_all( "/(#+)/", $digitGroupingPattern, $matches );
3401
			preg_match( "/\d+/", $number, $integerPart );
3402
			preg_match( "/\.\d*/", $number, $decimalPart );
3403
			$groupedNumber = ( count( $decimalPart ) > 0 ) ? $decimalPart[0] : "";
3404
			if ( $groupedNumber === $number ) {
3405
				// the string does not have any number part. Eg: .12345
3406
				return $sign . $groupedNumber;
3407
			}
3408
			$start = $end = ( $integerPart ) ? strlen( $integerPart[0] ) : 0;
3409
			while ( $start > 0 ) {
3410
				$match = $matches[0][$numMatches - 1];
3411
				$matchLen = strlen( $match );
3412
				$start = $end - $matchLen;
3413
				if ( $start < 0 ) {
3414
					$start = 0;
3415
				}
3416
				$groupedNumber = substr( $number, $start, $end -$start ) . $groupedNumber;
3417
				$end = $start;
3418
				if ( $numMatches > 1 ) {
3419
					// use the last pattern for the rest of the number
3420
					$numMatches--;
3421
				}
3422
				if ( $start > 0 ) {
3423
					$groupedNumber = "," . $groupedNumber;
3424
				}
3425
			}
3426
			return $sign . $groupedNumber;
3427
		}
3428
	}
3429
3430
	/**
3431
	 * @return string
3432
	 */
3433
	function digitGroupingPattern() {
3434
		return self::$dataCache->getItem( $this->mCode, 'digitGroupingPattern' );
3435
	}
3436
3437
	/**
3438
	 * @return array
3439
	 */
3440
	function digitTransformTable() {
3441
		return self::$dataCache->getItem( $this->mCode, 'digitTransformTable' );
3442
	}
3443
3444
	/**
3445
	 * @return array
3446
	 */
3447
	function separatorTransformTable() {
3448
		return self::$dataCache->getItem( $this->mCode, 'separatorTransformTable' );
3449
	}
3450
3451
	/**
3452
	 * Take a list of strings and build a locale-friendly comma-separated
3453
	 * list, using the local comma-separator message.
3454
	 * The last two strings are chained with an "and".
3455
	 * NOTE: This function will only work with standard numeric array keys (0, 1, 2…)
3456
	 *
3457
	 * @param string[] $l
3458
	 * @return string
3459
	 */
3460
	function listToText( array $l ) {
3461
		$m = count( $l ) - 1;
3462
		if ( $m < 0 ) {
3463
			return '';
3464
		}
3465
		if ( $m > 0 ) {
3466
			$and = $this->msg( 'and' )->escaped();
3467
			$space = $this->msg( 'word-separator' )->escaped();
3468
			if ( $m > 1 ) {
3469
				$comma = $this->msg( 'comma-separator' )->escaped();
3470
			}
3471
		}
3472
		$s = $l[$m];
3473
		for ( $i = $m - 1; $i >= 0; $i-- ) {
3474
			if ( $i == $m - 1 ) {
3475
				$s = $l[$i] . $and . $space . $s;
0 ignored issues
show
Bug introduced by
The variable $and does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $space does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
3476
			} else {
3477
				$s = $l[$i] . $comma . $s;
0 ignored issues
show
Bug introduced by
The variable $comma does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
3478
			}
3479
		}
3480
		return $s;
3481
	}
3482
3483
	/**
3484
	 * Take a list of strings and build a locale-friendly comma-separated
3485
	 * list, using the local comma-separator message.
3486
	 * @param string[] $list Array of strings to put in a comma list
3487
	 * @return string
3488
	 */
3489
	function commaList( array $list ) {
3490
		return implode(
3491
			wfMessage( 'comma-separator' )->inLanguage( $this )->escaped(),
3492
			$list
3493
		);
3494
	}
3495
3496
	/**
3497
	 * Take a list of strings and build a locale-friendly semicolon-separated
3498
	 * list, using the local semicolon-separator message.
3499
	 * @param string[] $list Array of strings to put in a semicolon list
3500
	 * @return string
3501
	 */
3502
	function semicolonList( array $list ) {
3503
		return implode(
3504
			wfMessage( 'semicolon-separator' )->inLanguage( $this )->escaped(),
3505
			$list
3506
		);
3507
	}
3508
3509
	/**
3510
	 * Same as commaList, but separate it with the pipe instead.
3511
	 * @param string[] $list Array of strings to put in a pipe list
3512
	 * @return string
3513
	 */
3514
	function pipeList( array $list ) {
3515
		return implode(
3516
			wfMessage( 'pipe-separator' )->inLanguage( $this )->escaped(),
3517
			$list
3518
		);
3519
	}
3520
3521
	/**
3522
	 * Truncate a string to a specified length in bytes, appending an optional
3523
	 * string (e.g. for ellipses)
3524
	 *
3525
	 * The database offers limited byte lengths for some columns in the database;
3526
	 * multi-byte character sets mean we need to ensure that only whole characters
3527
	 * are included, otherwise broken characters can be passed to the user
3528
	 *
3529
	 * If $length is negative, the string will be truncated from the beginning
3530
	 *
3531
	 * @param string $string String to truncate
3532
	 * @param int $length Maximum length (including ellipses)
3533
	 * @param string $ellipsis String to append to the truncated text
3534
	 * @param bool $adjustLength Subtract length of ellipsis from $length.
3535
	 *	$adjustLength was introduced in 1.18, before that behaved as if false.
3536
	 * @return string
3537
	 */
3538
	function truncate( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3539
		# Use the localized ellipsis character
3540
		if ( $ellipsis == '...' ) {
3541
			$ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3542
		}
3543
		# Check if there is no need to truncate
3544
		if ( $length == 0 ) {
3545
			return $ellipsis; // convention
3546
		} elseif ( strlen( $string ) <= abs( $length ) ) {
3547
			return $string; // no need to truncate
3548
		}
3549
		$stringOriginal = $string;
3550
		# If ellipsis length is >= $length then we can't apply $adjustLength
3551
		if ( $adjustLength && strlen( $ellipsis ) >= abs( $length ) ) {
3552
			$string = $ellipsis; // this can be slightly unexpected
3553
		# Otherwise, truncate and add ellipsis...
3554
		} else {
3555
			$eLength = $adjustLength ? strlen( $ellipsis ) : 0;
3556
			if ( $length > 0 ) {
3557
				$length -= $eLength;
3558
				$string = substr( $string, 0, $length ); // xyz...
3559
				$string = $this->removeBadCharLast( $string );
3560
				$string = rtrim( $string );
3561
				$string = $string . $ellipsis;
3562
			} else {
3563
				$length += $eLength;
3564
				$string = substr( $string, $length ); // ...xyz
3565
				$string = $this->removeBadCharFirst( $string );
3566
				$string = ltrim( $string );
3567
				$string = $ellipsis . $string;
3568
			}
3569
		}
3570
		# Do not truncate if the ellipsis makes the string longer/equal (bug 22181).
3571
		# This check is *not* redundant if $adjustLength, due to the single case where
3572
		# LEN($ellipsis) > ABS($limit arg); $stringOriginal could be shorter than $string.
3573
		if ( strlen( $string ) < strlen( $stringOriginal ) ) {
3574
			return $string;
3575
		} else {
3576
			return $stringOriginal;
3577
		}
3578
	}
3579
3580
	/**
3581
	 * Remove bytes that represent an incomplete Unicode character
3582
	 * at the end of string (e.g. bytes of the char are missing)
3583
	 *
3584
	 * @param string $string
3585
	 * @return string
3586
	 */
3587
	protected function removeBadCharLast( $string ) {
3588
		if ( $string != '' ) {
3589
			$char = ord( $string[strlen( $string ) - 1] );
3590
			$m = [];
3591
			if ( $char >= 0xc0 ) {
3592
				# We got the first byte only of a multibyte char; remove it.
3593
				$string = substr( $string, 0, -1 );
3594
			} elseif ( $char >= 0x80 &&
3595
				// Use the /s modifier (PCRE_DOTALL) so (.*) also matches newlines
3596
				preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
3597
					'[\xf0-\xf7][\x80-\xbf]{1,2})$/s', $string, $m )
3598
			) {
3599
				# We chopped in the middle of a character; remove it
3600
				$string = $m[1];
3601
			}
3602
		}
3603
		return $string;
3604
	}
3605
3606
	/**
3607
	 * Remove bytes that represent an incomplete Unicode character
3608
	 * at the start of string (e.g. bytes of the char are missing)
3609
	 *
3610
	 * @param string $string
3611
	 * @return string
3612
	 */
3613
	protected function removeBadCharFirst( $string ) {
3614
		if ( $string != '' ) {
3615
			$char = ord( $string[0] );
3616
			if ( $char >= 0x80 && $char < 0xc0 ) {
3617
				# We chopped in the middle of a character; remove the whole thing
3618
				$string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
3619
			}
3620
		}
3621
		return $string;
3622
	}
3623
3624
	/**
3625
	 * Truncate a string of valid HTML to a specified length in bytes,
3626
	 * appending an optional string (e.g. for ellipses), and return valid HTML
3627
	 *
3628
	 * This is only intended for styled/linked text, such as HTML with
3629
	 * tags like <span> and <a>, were the tags are self-contained (valid HTML).
3630
	 * Also, this will not detect things like "display:none" CSS.
3631
	 *
3632
	 * Note: since 1.18 you do not need to leave extra room in $length for ellipses.
3633
	 *
3634
	 * @param string $text HTML string to truncate
3635
	 * @param int $length (zero/positive) Maximum length (including ellipses)
3636
	 * @param string $ellipsis String to append to the truncated text
3637
	 * @return string
3638
	 */
3639
	function truncateHtml( $text, $length, $ellipsis = '...' ) {
3640
		# Use the localized ellipsis character
3641
		if ( $ellipsis == '...' ) {
3642
			$ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3643
		}
3644
		# Check if there is clearly no need to truncate
3645
		if ( $length <= 0 ) {
3646
			return $ellipsis; // no text shown, nothing to format (convention)
3647
		} elseif ( strlen( $text ) <= $length ) {
3648
			return $text; // string short enough even *with* HTML (short-circuit)
3649
		}
3650
3651
		$dispLen = 0; // innerHTML legth so far
3652
		$testingEllipsis = false; // checking if ellipses will make string longer/equal?
3653
		$tagType = 0; // 0-open, 1-close
3654
		$bracketState = 0; // 1-tag start, 2-tag name, 0-neither
3655
		$entityState = 0; // 0-not entity, 1-entity
3656
		$tag = $ret = ''; // accumulated tag name, accumulated result string
3657
		$openTags = []; // open tag stack
3658
		$maybeState = null; // possible truncation state
3659
3660
		$textLen = strlen( $text );
3661
		$neLength = max( 0, $length - strlen( $ellipsis ) ); // non-ellipsis len if truncated
3662
		for ( $pos = 0; true; ++$pos ) {
3663
			# Consider truncation once the display length has reached the maximim.
3664
			# We check if $dispLen > 0 to grab tags for the $neLength = 0 case.
3665
			# Check that we're not in the middle of a bracket/entity...
3666
			if ( $dispLen && $dispLen >= $neLength && $bracketState == 0 && !$entityState ) {
3667
				if ( !$testingEllipsis ) {
3668
					$testingEllipsis = true;
3669
					# Save where we are; we will truncate here unless there turn out to
3670
					# be so few remaining characters that truncation is not necessary.
3671
					if ( !$maybeState ) { // already saved? ($neLength = 0 case)
3672
						$maybeState = [ $ret, $openTags ]; // save state
3673
					}
3674
				} elseif ( $dispLen > $length && $dispLen > strlen( $ellipsis ) ) {
3675
					# String in fact does need truncation, the truncation point was OK.
3676
					list( $ret, $openTags ) = $maybeState; // reload state
3677
					$ret = $this->removeBadCharLast( $ret ); // multi-byte char fix
3678
					$ret .= $ellipsis; // add ellipsis
3679
					break;
3680
				}
3681
			}
3682
			if ( $pos >= $textLen ) {
3683
				break; // extra iteration just for above checks
3684
			}
3685
3686
			# Read the next char...
3687
			$ch = $text[$pos];
3688
			$lastCh = $pos ? $text[$pos - 1] : '';
3689
			$ret .= $ch; // add to result string
3690
			if ( $ch == '<' ) {
3691
				$this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags ); // for bad HTML
3692
				$entityState = 0; // for bad HTML
3693
				$bracketState = 1; // tag started (checking for backslash)
3694
			} elseif ( $ch == '>' ) {
3695
				$this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags );
3696
				$entityState = 0; // for bad HTML
3697
				$bracketState = 0; // out of brackets
3698
			} elseif ( $bracketState == 1 ) {
3699
				if ( $ch == '/' ) {
3700
					$tagType = 1; // close tag (e.g. "</span>")
3701
				} else {
3702
					$tagType = 0; // open tag (e.g. "<span>")
3703
					$tag .= $ch;
3704
				}
3705
				$bracketState = 2; // building tag name
3706
			} elseif ( $bracketState == 2 ) {
3707
				if ( $ch != ' ' ) {
3708
					$tag .= $ch;
3709
				} else {
3710
					// Name found (e.g. "<a href=..."), add on tag attributes...
3711
					$pos += $this->truncate_skip( $ret, $text, "<>", $pos + 1 );
3712
				}
3713
			} elseif ( $bracketState == 0 ) {
3714
				if ( $entityState ) {
3715
					if ( $ch == ';' ) {
3716
						$entityState = 0;
3717
						$dispLen++; // entity is one displayed char
3718
					}
3719
				} else {
3720
					if ( $neLength == 0 && !$maybeState ) {
3721
						// Save state without $ch. We want to *hit* the first
3722
						// display char (to get tags) but not *use* it if truncating.
3723
						$maybeState = [ substr( $ret, 0, -1 ), $openTags ];
3724
					}
3725
					if ( $ch == '&' ) {
3726
						$entityState = 1; // entity found, (e.g. "&#160;")
3727
					} else {
3728
						$dispLen++; // this char is displayed
3729
						// Add the next $max display text chars after this in one swoop...
3730
						$max = ( $testingEllipsis ? $length : $neLength ) - $dispLen;
3731
						$skipped = $this->truncate_skip( $ret, $text, "<>&", $pos + 1, $max );
3732
						$dispLen += $skipped;
3733
						$pos += $skipped;
3734
					}
3735
				}
3736
			}
3737
		}
3738
		// Close the last tag if left unclosed by bad HTML
3739
		$this->truncate_endBracket( $tag, $text[$textLen - 1], $tagType, $openTags );
3740
		while ( count( $openTags ) > 0 ) {
3741
			$ret .= '</' . array_pop( $openTags ) . '>'; // close open tags
3742
		}
3743
		return $ret;
3744
	}
3745
3746
	/**
3747
	 * truncateHtml() helper function
3748
	 * like strcspn() but adds the skipped chars to $ret
3749
	 *
3750
	 * @param string $ret
3751
	 * @param string $text
3752
	 * @param string $search
3753
	 * @param int $start
3754
	 * @param null|int $len
3755
	 * @return int
3756
	 */
3757
	private function truncate_skip( &$ret, $text, $search, $start, $len = null ) {
3758
		if ( $len === null ) {
3759
			$len = -1; // -1 means "no limit" for strcspn
3760
		} elseif ( $len < 0 ) {
3761
			$len = 0; // sanity
3762
		}
3763
		$skipCount = 0;
3764
		if ( $start < strlen( $text ) ) {
3765
			$skipCount = strcspn( $text, $search, $start, $len );
3766
			$ret .= substr( $text, $start, $skipCount );
3767
		}
3768
		return $skipCount;
3769
	}
3770
3771
	/**
3772
	 * truncateHtml() helper function
3773
	 * (a) push or pop $tag from $openTags as needed
3774
	 * (b) clear $tag value
3775
	 * @param string &$tag Current HTML tag name we are looking at
3776
	 * @param int $tagType (0-open tag, 1-close tag)
3777
	 * @param string $lastCh Character before the '>' that ended this tag
3778
	 * @param array &$openTags Open tag stack (not accounting for $tag)
3779
	 */
3780
	private function truncate_endBracket( &$tag, $tagType, $lastCh, &$openTags ) {
3781
		$tag = ltrim( $tag );
3782
		if ( $tag != '' ) {
3783
			if ( $tagType == 0 && $lastCh != '/' ) {
3784
				$openTags[] = $tag; // tag opened (didn't close itself)
3785
			} elseif ( $tagType == 1 ) {
3786
				if ( $openTags && $tag == $openTags[count( $openTags ) - 1] ) {
3787
					array_pop( $openTags ); // tag closed
3788
				}
3789
			}
3790
			$tag = '';
3791
		}
3792
	}
3793
3794
	/**
3795
	 * Grammatical transformations, needed for inflected languages
3796
	 * Invoked by putting {{grammar:case|word}} in a message
3797
	 *
3798
	 * @param string $word
3799
	 * @param string $case
3800
	 * @return string
3801
	 */
3802
	function convertGrammar( $word, $case ) {
3803
		global $wgGrammarForms;
3804 View Code Duplication
		if ( isset( $wgGrammarForms[$this->getCode()][$case][$word] ) ) {
3805
			return $wgGrammarForms[$this->getCode()][$case][$word];
3806
		}
3807
3808
		return $word;
3809
	}
3810
	/**
3811
	 * Get the grammar forms for the content language
3812
	 * @return array Array of grammar forms
3813
	 * @since 1.20
3814
	 */
3815
	function getGrammarForms() {
3816
		global $wgGrammarForms;
3817
		if ( isset( $wgGrammarForms[$this->getCode()] )
3818
			&& is_array( $wgGrammarForms[$this->getCode()] )
3819
		) {
3820
			return $wgGrammarForms[$this->getCode()];
3821
		}
3822
3823
		return [];
3824
	}
3825
	/**
3826
	 * Provides an alternative text depending on specified gender.
3827
	 * Usage {{gender:username|masculine|feminine|unknown}}.
3828
	 * username is optional, in which case the gender of current user is used,
3829
	 * but only in (some) interface messages; otherwise default gender is used.
3830
	 *
3831
	 * If no forms are given, an empty string is returned. If only one form is
3832
	 * given, it will be returned unconditionally. These details are implied by
3833
	 * the caller and cannot be overridden in subclasses.
3834
	 *
3835
	 * If three forms are given, the default is to use the third (unknown) form.
3836
	 * If fewer than three forms are given, the default is to use the first (masculine) form.
3837
	 * These details can be overridden in subclasses.
3838
	 *
3839
	 * @param string $gender
3840
	 * @param array $forms
3841
	 *
3842
	 * @return string
3843
	 */
3844
	function gender( $gender, $forms ) {
3845
		if ( !count( $forms ) ) {
3846
			return '';
3847
		}
3848
		$forms = $this->preConvertPlural( $forms, 2 );
3849
		if ( $gender === 'male' ) {
3850
			return $forms[0];
3851
		}
3852
		if ( $gender === 'female' ) {
3853
			return $forms[1];
3854
		}
3855
		return isset( $forms[2] ) ? $forms[2] : $forms[0];
3856
	}
3857
3858
	/**
3859
	 * Plural form transformations, needed for some languages.
3860
	 * For example, there are 3 form of plural in Russian and Polish,
3861
	 * depending on "count mod 10". See [[w:Plural]]
3862
	 * For English it is pretty simple.
3863
	 *
3864
	 * Invoked by putting {{plural:count|wordform1|wordform2}}
3865
	 * or {{plural:count|wordform1|wordform2|wordform3}}
3866
	 *
3867
	 * Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}}
3868
	 *
3869
	 * @param int $count Non-localized number
3870
	 * @param array $forms Different plural forms
3871
	 * @return string Correct form of plural for $count in this language
3872
	 */
3873
	function convertPlural( $count, $forms ) {
3874
		// Handle explicit n=pluralform cases
3875
		$forms = $this->handleExplicitPluralForms( $count, $forms );
3876
		if ( is_string( $forms ) ) {
3877
			return $forms;
3878
		}
3879
		if ( !count( $forms ) ) {
3880
			return '';
3881
		}
3882
3883
		$pluralForm = $this->getPluralRuleIndexNumber( $count );
3884
		$pluralForm = min( $pluralForm, count( $forms ) - 1 );
3885
		return $forms[$pluralForm];
3886
	}
3887
3888
	/**
3889
	 * Handles explicit plural forms for Language::convertPlural()
3890
	 *
3891
	 * In {{PLURAL:$1|0=nothing|one|many}}, 0=nothing will be returned if $1 equals zero.
3892
	 * If an explicitly defined plural form matches the $count, then
3893
	 * string value returned, otherwise array returned for further consideration
3894
	 * by CLDR rules or overridden convertPlural().
3895
	 *
3896
	 * @since 1.23
3897
	 *
3898
	 * @param int $count Non-localized number
3899
	 * @param array $forms Different plural forms
3900
	 *
3901
	 * @return array|string
3902
	 */
3903
	protected function handleExplicitPluralForms( $count, array $forms ) {
3904
		foreach ( $forms as $index => $form ) {
3905
			if ( preg_match( '/\d+=/i', $form ) ) {
3906
				$pos = strpos( $form, '=' );
3907
				if ( substr( $form, 0, $pos ) === (string)$count ) {
3908
					return substr( $form, $pos + 1 );
3909
				}
3910
				unset( $forms[$index] );
3911
			}
3912
		}
3913
		return array_values( $forms );
3914
	}
3915
3916
	/**
3917
	 * Checks that convertPlural was given an array and pads it to requested
3918
	 * amount of forms by copying the last one.
3919
	 *
3920
	 * @param array $forms Array of forms given to convertPlural
3921
	 * @param int $count How many forms should there be at least
3922
	 * @return array Padded array of forms or an exception if not an array
3923
	 */
3924
	protected function preConvertPlural( /* Array */ $forms, $count ) {
3925
		while ( count( $forms ) < $count ) {
3926
			$forms[] = $forms[count( $forms ) - 1];
3927
		}
3928
		return $forms;
3929
	}
3930
3931
	/**
3932
	 * Wraps argument with unicode control characters for directionality safety
3933
	 *
3934
	 * This solves the problem where directionality-neutral characters at the edge of
3935
	 * the argument string get interpreted with the wrong directionality from the
3936
	 * enclosing context, giving renderings that look corrupted like "(Ben_(WMF".
3937
	 *
3938
	 * The wrapping is LRE...PDF or RLE...PDF, depending on the detected
3939
	 * directionality of the argument string, using the BIDI algorithm's own "First
3940
	 * strong directional codepoint" rule. Essentially, this works round the fact that
3941
	 * there is no embedding equivalent of U+2068 FSI (isolation with heuristic
3942
	 * direction inference). The latter is cleaner but still not widely supported.
3943
	 *
3944
	 * @param string $text Text to wrap
3945
	 * @return string Text, wrapped in LRE...PDF or RLE...PDF or nothing
3946
	 */
3947
	public function embedBidi( $text = '' ) {
3948
		$dir = Language::strongDirFromContent( $text );
3949
		if ( $dir === 'ltr' ) {
3950
			// Wrap in LEFT-TO-RIGHT EMBEDDING ... POP DIRECTIONAL FORMATTING
3951
			return self::$lre . $text . self::$pdf;
3952
		}
3953
		if ( $dir === 'rtl' ) {
3954
			// Wrap in RIGHT-TO-LEFT EMBEDDING ... POP DIRECTIONAL FORMATTING
3955
			return self::$rle . $text . self::$pdf;
3956
		}
3957
		// No strong directionality: do not wrap
3958
		return $text;
3959
	}
3960
3961
	/**
3962
	 * @todo Maybe translate block durations.  Note that this function is somewhat misnamed: it
3963
	 * deals with translating the *duration* ("1 week", "4 days", etc), not the expiry time
3964
	 * (which is an absolute timestamp). Please note: do NOT add this blindly, as it is used
3965
	 * on old expiry lengths recorded in log entries. You'd need to provide the start date to
3966
	 * match up with it.
3967
	 *
3968
	 * @param string $str The validated block duration in English
3969
	 * @return string Somehow translated block duration
3970
	 * @see LanguageFi.php for example implementation
3971
	 */
3972
	function translateBlockExpiry( $str ) {
3973
		$duration = SpecialBlock::getSuggestedDurations( $this );
3974
		foreach ( $duration as $show => $value ) {
3975
			if ( strcmp( $str, $value ) == 0 ) {
3976
				return htmlspecialchars( trim( $show ) );
3977
			}
3978
		}
3979
3980
		if ( wfIsInfinity( $str ) ) {
3981
			foreach ( $duration as $show => $value ) {
3982
				if ( wfIsInfinity( $value ) ) {
3983
					return htmlspecialchars( trim( $show ) );
3984
				}
3985
			}
3986
		}
3987
3988
		// If all else fails, return a standard duration or timestamp description.
3989
		$time = strtotime( $str, 0 );
3990
		if ( $time === false ) { // Unknown format. Return it as-is in case.
3991
			return $str;
3992
		} elseif ( $time !== strtotime( $str, 1 ) ) { // It's a relative timestamp.
3993
			// $time is relative to 0 so it's a duration length.
3994
			return $this->formatDuration( $time );
3995
		} else { // It's an absolute timestamp.
3996
			if ( $time === 0 ) {
3997
				// wfTimestamp() handles 0 as current time instead of epoch.
3998
				return $this->timeanddate( '19700101000000' );
3999
			} else {
4000
				return $this->timeanddate( $time );
4001
			}
4002
		}
4003
	}
4004
4005
	/**
4006
	 * languages like Chinese need to be segmented in order for the diff
4007
	 * to be of any use
4008
	 *
4009
	 * @param string $text
4010
	 * @return string
4011
	 */
4012
	public function segmentForDiff( $text ) {
4013
		return $text;
4014
	}
4015
4016
	/**
4017
	 * and unsegment to show the result
4018
	 *
4019
	 * @param string $text
4020
	 * @return string
4021
	 */
4022
	public function unsegmentForDiff( $text ) {
4023
		return $text;
4024
	}
4025
4026
	/**
4027
	 * Return the LanguageConverter used in the Language
4028
	 *
4029
	 * @since 1.19
4030
	 * @return LanguageConverter
4031
	 */
4032
	public function getConverter() {
4033
		return $this->mConverter;
4034
	}
4035
4036
	/**
4037
	 * convert text to all supported variants
4038
	 *
4039
	 * @param string $text
4040
	 * @return array
4041
	 */
4042
	public function autoConvertToAllVariants( $text ) {
4043
		return $this->mConverter->autoConvertToAllVariants( $text );
4044
	}
4045
4046
	/**
4047
	 * convert text to different variants of a language.
4048
	 *
4049
	 * @param string $text
4050
	 * @return string
4051
	 */
4052
	public function convert( $text ) {
4053
		return $this->mConverter->convert( $text );
4054
	}
4055
4056
	/**
4057
	 * Convert a Title object to a string in the preferred variant
4058
	 *
4059
	 * @param Title $title
4060
	 * @return string
4061
	 */
4062
	public function convertTitle( $title ) {
4063
		return $this->mConverter->convertTitle( $title );
4064
	}
4065
4066
	/**
4067
	 * Convert a namespace index to a string in the preferred variant
4068
	 *
4069
	 * @param int $ns
4070
	 * @return string
4071
	 */
4072
	public function convertNamespace( $ns ) {
4073
		return $this->mConverter->convertNamespace( $ns );
4074
	}
4075
4076
	/**
4077
	 * Check if this is a language with variants
4078
	 *
4079
	 * @return bool
4080
	 */
4081
	public function hasVariants() {
4082
		return count( $this->getVariants() ) > 1;
4083
	}
4084
4085
	/**
4086
	 * Check if the language has the specific variant
4087
	 *
4088
	 * @since 1.19
4089
	 * @param string $variant
4090
	 * @return bool
4091
	 */
4092
	public function hasVariant( $variant ) {
4093
		return (bool)$this->mConverter->validateVariant( $variant );
4094
	}
4095
4096
	/**
4097
	 * Perform output conversion on a string, and encode for safe HTML output.
4098
	 * @param string $text Text to be converted
4099
	 * @param bool $isTitle Whether this conversion is for the article title
4100
	 * @return string
4101
	 * @todo this should get integrated somewhere sane
4102
	 */
4103
	public function convertHtml( $text, $isTitle = false ) {
4104
		return htmlspecialchars( $this->convert( $text, $isTitle ) );
4105
	}
4106
4107
	/**
4108
	 * @param string $key
4109
	 * @return string
4110
	 */
4111
	public function convertCategoryKey( $key ) {
4112
		return $this->mConverter->convertCategoryKey( $key );
4113
	}
4114
4115
	/**
4116
	 * Get the list of variants supported by this language
4117
	 * see sample implementation in LanguageZh.php
4118
	 *
4119
	 * @return array An array of language codes
4120
	 */
4121
	public function getVariants() {
4122
		return $this->mConverter->getVariants();
4123
	}
4124
4125
	/**
4126
	 * @return string
4127
	 */
4128
	public function getPreferredVariant() {
4129
		return $this->mConverter->getPreferredVariant();
4130
	}
4131
4132
	/**
4133
	 * @return string
4134
	 */
4135
	public function getDefaultVariant() {
4136
		return $this->mConverter->getDefaultVariant();
4137
	}
4138
4139
	/**
4140
	 * @return string
4141
	 */
4142
	public function getURLVariant() {
4143
		return $this->mConverter->getURLVariant();
4144
	}
4145
4146
	/**
4147
	 * If a language supports multiple variants, it is
4148
	 * possible that non-existing link in one variant
4149
	 * actually exists in another variant. this function
4150
	 * tries to find it. See e.g. LanguageZh.php
4151
	 * The input parameters may be modified upon return
4152
	 *
4153
	 * @param string &$link The name of the link
4154
	 * @param Title &$nt The title object of the link
4155
	 * @param bool $ignoreOtherCond To disable other conditions when
4156
	 *   we need to transclude a template or update a category's link
4157
	 */
4158
	public function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
4159
		$this->mConverter->findVariantLink( $link, $nt, $ignoreOtherCond );
4160
	}
4161
4162
	/**
4163
	 * returns language specific options used by User::getPageRenderHash()
4164
	 * for example, the preferred language variant
4165
	 *
4166
	 * @return string
4167
	 */
4168
	function getExtraHashOptions() {
4169
		return $this->mConverter->getExtraHashOptions();
4170
	}
4171
4172
	/**
4173
	 * For languages that support multiple variants, the title of an
4174
	 * article may be displayed differently in different variants. this
4175
	 * function returns the apporiate title defined in the body of the article.
4176
	 *
4177
	 * @return string
4178
	 */
4179
	public function getParsedTitle() {
4180
		return $this->mConverter->getParsedTitle();
0 ignored issues
show
Bug introduced by
The method getParsedTitle() does not seem to exist on object<LanguageConverter>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
4181
	}
4182
4183
	/**
4184
	 * Refresh the cache of conversion tables when
4185
	 * MediaWiki:Conversiontable* is updated.
4186
	 *
4187
	 * @param Title $title The Title of the page being updated
4188
	 */
4189
	public function updateConversionTable( Title $title ) {
4190
		$this->mConverter->updateConversionTable( $title );
4191
	}
4192
4193
	/**
4194
	 * Prepare external link text for conversion. When the text is
4195
	 * a URL, it shouldn't be converted, and it'll be wrapped in
4196
	 * the "raw" tag (-{R| }-) to prevent conversion.
4197
	 *
4198
	 * This function is called "markNoConversion" for historical
4199
	 * reasons.
4200
	 *
4201
	 * @param string $text Text to be used for external link
4202
	 * @param bool $noParse Wrap it without confirming it's a real URL first
4203
	 * @return string The tagged text
4204
	 */
4205
	public function markNoConversion( $text, $noParse = false ) {
4206
		// Excluding protocal-relative URLs may avoid many false positives.
4207
		if ( $noParse || preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
4208
			return $this->mConverter->markNoConversion( $text );
4209
		} else {
4210
			return $text;
4211
		}
4212
	}
4213
4214
	/**
4215
	 * A regular expression to match legal word-trailing characters
4216
	 * which should be merged onto a link of the form [[foo]]bar.
4217
	 *
4218
	 * @return string
4219
	 */
4220
	public function linkTrail() {
4221
		return self::$dataCache->getItem( $this->mCode, 'linkTrail' );
4222
	}
4223
4224
	/**
4225
	 * A regular expression character set to match legal word-prefixing
4226
	 * characters which should be merged onto a link of the form foo[[bar]].
4227
	 *
4228
	 * @return string
4229
	 */
4230
	public function linkPrefixCharset() {
4231
		return self::$dataCache->getItem( $this->mCode, 'linkPrefixCharset' );
4232
	}
4233
4234
	/**
4235
	 * Get the "parent" language which has a converter to convert a "compatible" language
4236
	 * (in another variant) to this language (eg. zh for zh-cn, but not en for en-gb).
4237
	 *
4238
	 * @return Language|null
4239
	 * @since 1.22
4240
	 */
4241
	public function getParentLanguage() {
4242
		if ( $this->mParentLanguage !== false ) {
4243
			return $this->mParentLanguage;
4244
		}
4245
4246
		$code = explode( '-', $this->getCode() )[0];
4247
		if ( !in_array( $code, LanguageConverter::$languagesWithVariants ) ) {
4248
			$this->mParentLanguage = null;
4249
			return null;
4250
		}
4251
		$lang = Language::factory( $code );
4252
		if ( !$lang->hasVariant( $this->getCode() ) ) {
4253
			$this->mParentLanguage = null;
4254
			return null;
4255
		}
4256
4257
		$this->mParentLanguage = $lang;
0 ignored issues
show
Documentation Bug introduced by
It seems like $lang of type object<Language> is incompatible with the declared type boolean of property $mParentLanguage.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
4258
		return $lang;
4259
	}
4260
4261
	/**
4262
	 * Get the internal language code for this language object
4263
	 *
4264
	 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
4265
	 * htmlspecialchars() or similar
4266
	 *
4267
	 * @return string
4268
	 */
4269
	public function getCode() {
4270
		return $this->mCode;
4271
	}
4272
4273
	/**
4274
	 * Get the code in BCP 47 format which we can use
4275
	 * inside of html lang="" tags.
4276
	 *
4277
	 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
4278
	 * htmlspecialchars() or similar.
4279
	 *
4280
	 * @since 1.19
4281
	 * @return string
4282
	 */
4283
	public function getHtmlCode() {
4284
		if ( is_null( $this->mHtmlCode ) ) {
4285
			$this->mHtmlCode = wfBCP47( $this->getCode() );
4286
		}
4287
		return $this->mHtmlCode;
4288
	}
4289
4290
	/**
4291
	 * @param string $code
4292
	 */
4293
	public function setCode( $code ) {
4294
		$this->mCode = $code;
4295
		// Ensure we don't leave incorrect cached data lying around
4296
		$this->mHtmlCode = null;
4297
		$this->mParentLanguage = false;
4298
	}
4299
4300
	/**
4301
	 * Get the language code from a file name. Inverse of getFileName()
4302
	 * @param string $filename $prefix . $languageCode . $suffix
4303
	 * @param string $prefix Prefix before the language code
4304
	 * @param string $suffix Suffix after the language code
4305
	 * @return string Language code, or false if $prefix or $suffix isn't found
4306
	 */
4307
	public static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
4308
		$m = null;
4309
		preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
4310
			preg_quote( $suffix, '/' ) . '/', $filename, $m );
4311
		if ( !count( $m ) ) {
4312
			return false;
4313
		}
4314
		return str_replace( '_', '-', strtolower( $m[1] ) );
4315
	}
4316
4317
	/**
4318
	 * @param string $code
4319
	 * @return string Name of the language class
4320
	 */
4321
	public static function classFromCode( $code ) {
4322
		if ( $code == 'en' ) {
4323
			return 'Language';
4324
		} else {
4325
			return 'Language' . str_replace( '-', '_', ucfirst( $code ) );
4326
		}
4327
	}
4328
4329
	/**
4330
	 * Get the name of a file for a certain language code
4331
	 * @param string $prefix Prepend this to the filename
4332
	 * @param string $code Language code
4333
	 * @param string $suffix Append this to the filename
4334
	 * @throws MWException
4335
	 * @return string $prefix . $mangledCode . $suffix
4336
	 */
4337
	public static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) {
4338
		if ( !self::isValidBuiltInCode( $code ) ) {
4339
			throw new MWException( "Invalid language code \"$code\"" );
4340
		}
4341
4342
		return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
4343
	}
4344
4345
	/**
4346
	 * @param string $code
4347
	 * @return string
4348
	 */
4349
	public static function getMessagesFileName( $code ) {
4350
		global $IP;
4351
		$file = self::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
4352
		Hooks::run( 'Language::getMessagesFileName', [ $code, &$file ] );
4353
		return $file;
4354
	}
4355
4356
	/**
4357
	 * @param string $code
4358
	 * @return string
4359
	 * @throws MWException
4360
	 * @since 1.23
4361
	 */
4362
	public static function getJsonMessagesFileName( $code ) {
4363
		global $IP;
4364
4365
		if ( !self::isValidBuiltInCode( $code ) ) {
4366
			throw new MWException( "Invalid language code \"$code\"" );
4367
		}
4368
4369
		return "$IP/languages/i18n/$code.json";
4370
	}
4371
4372
	/**
4373
	 * Get the first fallback for a given language.
4374
	 *
4375
	 * @param string $code
4376
	 *
4377
	 * @return bool|string
4378
	 */
4379
	public static function getFallbackFor( $code ) {
4380
		$fallbacks = self::getFallbacksFor( $code );
4381
		if ( $fallbacks ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fallbacks of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
4382
			return $fallbacks[0];
4383
		}
4384
		return false;
4385
	}
4386
4387
	/**
4388
	 * Get the ordered list of fallback languages.
4389
	 *
4390
	 * @since 1.19
4391
	 * @param string $code Language code
4392
	 * @return array Non-empty array, ending in "en"
4393
	 */
4394
	public static function getFallbacksFor( $code ) {
4395
		if ( $code === 'en' || !Language::isValidBuiltInCode( $code ) ) {
4396
			return [];
4397
		}
4398
		// For unknown languages, fallbackSequence returns an empty array,
4399
		// hardcode fallback to 'en' in that case.
4400
		return self::getLocalisationCache()->getItem( $code, 'fallbackSequence' ) ?: [ 'en' ];
4401
	}
4402
4403
	/**
4404
	 * Get the ordered list of fallback languages, ending with the fallback
4405
	 * language chain for the site language.
4406
	 *
4407
	 * @since 1.22
4408
	 * @param string $code Language code
4409
	 * @return array Array( fallbacks, site fallbacks )
4410
	 */
4411
	public static function getFallbacksIncludingSiteLanguage( $code ) {
4412
		global $wgLanguageCode;
4413
4414
		// Usually, we will only store a tiny number of fallback chains, so we
4415
		// keep them in static memory.
4416
		$cacheKey = "{$code}-{$wgLanguageCode}";
4417
4418
		if ( !array_key_exists( $cacheKey, self::$fallbackLanguageCache ) ) {
4419
			$fallbacks = self::getFallbacksFor( $code );
4420
4421
			// Append the site's fallback chain, including the site language itself
4422
			$siteFallbacks = self::getFallbacksFor( $wgLanguageCode );
4423
			array_unshift( $siteFallbacks, $wgLanguageCode );
4424
4425
			// Eliminate any languages already included in the chain
4426
			$siteFallbacks = array_diff( $siteFallbacks, $fallbacks );
4427
4428
			self::$fallbackLanguageCache[$cacheKey] = [ $fallbacks, $siteFallbacks ];
4429
		}
4430
		return self::$fallbackLanguageCache[$cacheKey];
4431
	}
4432
4433
	/**
4434
	 * Get all messages for a given language
4435
	 * WARNING: this may take a long time. If you just need all message *keys*
4436
	 * but need the *contents* of only a few messages, consider using getMessageKeysFor().
4437
	 *
4438
	 * @param string $code
4439
	 *
4440
	 * @return array
4441
	 */
4442
	public static function getMessagesFor( $code ) {
4443
		return self::getLocalisationCache()->getItem( $code, 'messages' );
4444
	}
4445
4446
	/**
4447
	 * Get a message for a given language
4448
	 *
4449
	 * @param string $key
4450
	 * @param string $code
4451
	 *
4452
	 * @return string
4453
	 */
4454
	public static function getMessageFor( $key, $code ) {
4455
		return self::getLocalisationCache()->getSubitem( $code, 'messages', $key );
4456
	}
4457
4458
	/**
4459
	 * Get all message keys for a given language. This is a faster alternative to
4460
	 * array_keys( Language::getMessagesFor( $code ) )
4461
	 *
4462
	 * @since 1.19
4463
	 * @param string $code Language code
4464
	 * @return array Array of message keys (strings)
4465
	 */
4466
	public static function getMessageKeysFor( $code ) {
4467
		return self::getLocalisationCache()->getSubItemList( $code, 'messages' );
4468
	}
4469
4470
	/**
4471
	 * @param string $talk
4472
	 * @return mixed
4473
	 */
4474
	function fixVariableInNamespace( $talk ) {
4475
		if ( strpos( $talk, '$1' ) === false ) {
4476
			return $talk;
4477
		}
4478
4479
		global $wgMetaNamespace;
4480
		$talk = str_replace( '$1', $wgMetaNamespace, $talk );
4481
4482
		# Allow grammar transformations
4483
		# Allowing full message-style parsing would make simple requests
4484
		# such as action=raw much more expensive than they need to be.
4485
		# This will hopefully cover most cases.
4486
		$talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
4487
			[ &$this, 'replaceGrammarInNamespace' ], $talk );
4488
		return str_replace( ' ', '_', $talk );
4489
	}
4490
4491
	/**
4492
	 * @param string $m
4493
	 * @return string
4494
	 */
4495
	function replaceGrammarInNamespace( $m ) {
4496
		return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
4497
	}
4498
4499
	/**
4500
	 * @throws MWException
4501
	 * @return array
4502
	 */
4503
	static function getCaseMaps() {
4504
		static $wikiUpperChars, $wikiLowerChars;
4505
		if ( isset( $wikiUpperChars ) ) {
4506
			return [ $wikiUpperChars, $wikiLowerChars ];
4507
		}
4508
4509
		$arr = wfGetPrecompiledData( 'Utf8Case.ser' );
4510
		if ( $arr === false ) {
4511
			throw new MWException(
4512
				"Utf8Case.ser is missing, please run \"make\" in the serialized directory\n" );
4513
		}
4514
		$wikiUpperChars = $arr['wikiUpperChars'];
4515
		$wikiLowerChars = $arr['wikiLowerChars'];
4516
		return [ $wikiUpperChars, $wikiLowerChars ];
4517
	}
4518
4519
	/**
4520
	 * Decode an expiry (block, protection, etc) which has come from the DB
4521
	 *
4522
	 * @param string $expiry Database expiry String
4523
	 * @param bool|int $format True to process using language functions, or TS_ constant
4524
	 *     to return the expiry in a given timestamp
4525
	 * @param string $infinity If $format is not true, use this string for infinite expiry
4526
	 * @return string
4527
	 * @since 1.18
4528
	 */
4529
	public function formatExpiry( $expiry, $format = true, $infinity = 'infinity' ) {
4530
		static $dbInfinity;
4531
		if ( $dbInfinity === null ) {
4532
			$dbInfinity = wfGetDB( DB_SLAVE )->getInfinity();
4533
		}
4534
4535
		if ( $expiry == '' || $expiry === 'infinity' || $expiry == $dbInfinity ) {
4536
			return $format === true
4537
				? $this->getMessageFromDB( 'infiniteblock' )
4538
				: $infinity;
4539
		} else {
4540
			return $format === true
4541
				? $this->timeanddate( $expiry, /* User preference timezone */ true )
4542
				: wfTimestamp( $format, $expiry );
4543
		}
4544
	}
4545
4546
	/**
4547
	 * @todo Document
4548
	 * @param int|float $seconds
4549
	 * @param array $format Optional
4550
	 *   If $format['avoid'] === 'avoidseconds': don't mention seconds if $seconds >= 1 hour.
4551
	 *   If $format['avoid'] === 'avoidminutes': don't mention seconds/minutes if $seconds > 48 hours.
4552
	 *   If $format['noabbrevs'] is true: use 'seconds' and friends instead of 'seconds-abbrev'
4553
	 *     and friends.
4554
	 *   For backwards compatibility, $format may also be one of the strings 'avoidseconds'
4555
	 *     or 'avoidminutes'.
4556
	 * @return string
4557
	 */
4558
	function formatTimePeriod( $seconds, $format = [] ) {
4559
		if ( !is_array( $format ) ) {
4560
			$format = [ 'avoid' => $format ]; // For backwards compatibility
4561
		}
4562
		if ( !isset( $format['avoid'] ) ) {
4563
			$format['avoid'] = false;
4564
		}
4565
		if ( !isset( $format['noabbrevs'] ) ) {
4566
			$format['noabbrevs'] = false;
4567
		}
4568
		$secondsMsg = wfMessage(
4569
			$format['noabbrevs'] ? 'seconds' : 'seconds-abbrev' )->inLanguage( $this );
4570
		$minutesMsg = wfMessage(
4571
			$format['noabbrevs'] ? 'minutes' : 'minutes-abbrev' )->inLanguage( $this );
4572
		$hoursMsg = wfMessage(
4573
			$format['noabbrevs'] ? 'hours' : 'hours-abbrev' )->inLanguage( $this );
4574
		$daysMsg = wfMessage(
4575
			$format['noabbrevs'] ? 'days' : 'days-abbrev' )->inLanguage( $this );
4576
4577
		if ( round( $seconds * 10 ) < 100 ) {
4578
			$s = $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) );
4579
			$s = $secondsMsg->params( $s )->text();
4580
		} elseif ( round( $seconds ) < 60 ) {
4581
			$s = $this->formatNum( round( $seconds ) );
4582
			$s = $secondsMsg->params( $s )->text();
4583
		} elseif ( round( $seconds ) < 3600 ) {
4584
			$minutes = floor( $seconds / 60 );
4585
			$secondsPart = round( fmod( $seconds, 60 ) );
4586
			if ( $secondsPart == 60 ) {
4587
				$secondsPart = 0;
4588
				$minutes++;
4589
			}
4590
			$s = $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4591
			$s .= ' ';
4592
			$s .= $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4593
		} elseif ( round( $seconds ) <= 2 * 86400 ) {
4594
			$hours = floor( $seconds / 3600 );
4595
			$minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
4596
			$secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
4597
			if ( $secondsPart == 60 ) {
4598
				$secondsPart = 0;
4599
				$minutes++;
4600
			}
4601
			if ( $minutes == 60 ) {
4602
				$minutes = 0;
4603
				$hours++;
4604
			}
4605
			$s = $hoursMsg->params( $this->formatNum( $hours ) )->text();
4606
			$s .= ' ';
4607
			$s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4608
			if ( !in_array( $format['avoid'], [ 'avoidseconds', 'avoidminutes' ] ) ) {
4609
				$s .= ' ' . $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4610
			}
4611
		} else {
4612
			$days = floor( $seconds / 86400 );
4613
			if ( $format['avoid'] === 'avoidminutes' ) {
4614
				$hours = round( ( $seconds - $days * 86400 ) / 3600 );
4615
				if ( $hours == 24 ) {
4616
					$hours = 0;
4617
					$days++;
4618
				}
4619
				$s = $daysMsg->params( $this->formatNum( $days ) )->text();
4620
				$s .= ' ';
4621
				$s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4622
			} elseif ( $format['avoid'] === 'avoidseconds' ) {
4623
				$hours = floor( ( $seconds - $days * 86400 ) / 3600 );
4624
				$minutes = round( ( $seconds - $days * 86400 - $hours * 3600 ) / 60 );
4625
				if ( $minutes == 60 ) {
4626
					$minutes = 0;
4627
					$hours++;
4628
				}
4629
				if ( $hours == 24 ) {
4630
					$hours = 0;
4631
					$days++;
4632
				}
4633
				$s = $daysMsg->params( $this->formatNum( $days ) )->text();
4634
				$s .= ' ';
4635
				$s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4636
				$s .= ' ';
4637
				$s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4638
			} else {
4639
				$s = $daysMsg->params( $this->formatNum( $days ) )->text();
4640
				$s .= ' ';
4641
				$s .= $this->formatTimePeriod( $seconds - $days * 86400, $format );
4642
			}
4643
		}
4644
		return $s;
4645
	}
4646
4647
	/**
4648
	 * Format a bitrate for output, using an appropriate
4649
	 * unit (bps, kbps, Mbps, Gbps, Tbps, Pbps, Ebps, Zbps or Ybps) according to
4650
	 *   the magnitude in question.
4651
	 *
4652
	 * This use base 1000. For base 1024 use formatSize(), for another base
4653
	 * see formatComputingNumbers().
4654
	 *
4655
	 * @param int $bps
4656
	 * @return string
4657
	 */
4658
	function formatBitrate( $bps ) {
4659
		return $this->formatComputingNumbers( $bps, 1000, "bitrate-$1bits" );
4660
	}
4661
4662
	/**
4663
	 * @param int $size Size of the unit
4664
	 * @param int $boundary Size boundary (1000, or 1024 in most cases)
4665
	 * @param string $messageKey Message key to be uesd
4666
	 * @return string
4667
	 */
4668
	function formatComputingNumbers( $size, $boundary, $messageKey ) {
4669
		if ( $size <= 0 ) {
4670
			return str_replace( '$1', $this->formatNum( $size ),
4671
				$this->getMessageFromDB( str_replace( '$1', '', $messageKey ) )
4672
			);
4673
		}
4674
		$sizes = [ '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zeta', 'yotta' ];
4675
		$index = 0;
4676
4677
		$maxIndex = count( $sizes ) - 1;
4678
		while ( $size >= $boundary && $index < $maxIndex ) {
4679
			$index++;
4680
			$size /= $boundary;
4681
		}
4682
4683
		// For small sizes no decimal places necessary
4684
		$round = 0;
4685
		if ( $index > 1 ) {
4686
			// For MB and bigger two decimal places are smarter
4687
			$round = 2;
4688
		}
4689
		$msg = str_replace( '$1', $sizes[$index], $messageKey );
4690
4691
		$size = round( $size, $round );
4692
		$text = $this->getMessageFromDB( $msg );
4693
		return str_replace( '$1', $this->formatNum( $size ), $text );
4694
	}
4695
4696
	/**
4697
	 * Format a size in bytes for output, using an appropriate
4698
	 * unit (B, KB, MB, GB, TB, PB, EB, ZB or YB) according to the magnitude in question
4699
	 *
4700
	 * This method use base 1024. For base 1000 use formatBitrate(), for
4701
	 * another base see formatComputingNumbers()
4702
	 *
4703
	 * @param int $size Size to format
4704
	 * @return string Plain text (not HTML)
4705
	 */
4706
	function formatSize( $size ) {
4707
		return $this->formatComputingNumbers( $size, 1024, "size-$1bytes" );
4708
	}
4709
4710
	/**
4711
	 * Make a list item, used by various special pages
4712
	 *
4713
	 * @param string $page Page link
4714
	 * @param string $details HTML safe text between brackets
4715
	 * @param bool $oppositedm Add the direction mark opposite to your
4716
	 *   language, to display text properly
4717
	 * @return HTML escaped string
4718
	 */
4719
	function specialList( $page, $details, $oppositedm = true ) {
4720
		if ( !$details ) {
4721
			return $page;
4722
		}
4723
4724
		$dirmark = ( $oppositedm ? $this->getDirMark( true ) : '' ) . $this->getDirMark();
4725
		return
4726
			$page .
4727
			$dirmark .
4728
			$this->msg( 'word-separator' )->escaped() .
4729
			$this->msg( 'parentheses' )->rawParams( $details )->escaped();
4730
	}
4731
4732
	/**
4733
	 * Generate (prev x| next x) (20|50|100...) type links for paging
4734
	 *
4735
	 * @param Title $title Title object to link
4736
	 * @param int $offset
4737
	 * @param int $limit
4738
	 * @param array $query Optional URL query parameter string
4739
	 * @param bool $atend Optional param for specified if this is the last page
4740
	 * @return string
4741
	 */
4742
	public function viewPrevNext( Title $title, $offset, $limit,
4743
		array $query = [], $atend = false
4744
	) {
4745
		// @todo FIXME: Why on earth this needs one message for the text and another one for tooltip?
4746
4747
		# Make 'previous' link
4748
		$prev = wfMessage( 'prevn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4749 View Code Duplication
		if ( $offset > 0 ) {
4750
			$plink = $this->numLink( $title, max( $offset - $limit, 0 ), $limit,
4751
				$query, $prev, 'prevn-title', 'mw-prevlink' );
4752
		} else {
4753
			$plink = htmlspecialchars( $prev );
4754
		}
4755
4756
		# Make 'next' link
4757
		$next = wfMessage( 'nextn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4758 View Code Duplication
		if ( $atend ) {
4759
			$nlink = htmlspecialchars( $next );
4760
		} else {
4761
			$nlink = $this->numLink( $title, $offset + $limit, $limit,
4762
				$query, $next, 'nextn-title', 'mw-nextlink' );
4763
		}
4764
4765
		# Make links to set number of items per page
4766
		$numLinks = [];
4767
		foreach ( [ 20, 50, 100, 250, 500 ] as $num ) {
4768
			$numLinks[] = $this->numLink( $title, $offset, $num,
4769
				$query, $this->formatNum( $num ), 'shown-title', 'mw-numlink' );
4770
		}
4771
4772
		return wfMessage( 'viewprevnext' )->inLanguage( $this )->title( $title
4773
			)->rawParams( $plink, $nlink, $this->pipeList( $numLinks ) )->escaped();
4774
	}
4775
4776
	/**
4777
	 * Helper function for viewPrevNext() that generates links
4778
	 *
4779
	 * @param Title $title Title object to link
4780
	 * @param int $offset
4781
	 * @param int $limit
4782
	 * @param array $query Extra query parameters
4783
	 * @param string $link Text to use for the link; will be escaped
4784
	 * @param string $tooltipMsg Name of the message to use as tooltip
4785
	 * @param string $class Value of the "class" attribute of the link
4786
	 * @return string HTML fragment
4787
	 */
4788
	private function numLink( Title $title, $offset, $limit, array $query, $link,
4789
		$tooltipMsg, $class
4790
	) {
4791
		$query = [ 'limit' => $limit, 'offset' => $offset ] + $query;
4792
		$tooltip = wfMessage( $tooltipMsg )->inLanguage( $this )->title( $title )
4793
			->numParams( $limit )->text();
4794
4795
		return Html::element( 'a', [ 'href' => $title->getLocalURL( $query ),
4796
			'title' => $tooltip, 'class' => $class ], $link );
4797
	}
4798
4799
	/**
4800
	 * Get the conversion rule title, if any.
4801
	 *
4802
	 * @return string
4803
	 */
4804
	public function getConvRuleTitle() {
4805
		return $this->mConverter->getConvRuleTitle();
4806
	}
4807
4808
	/**
4809
	 * Get the compiled plural rules for the language
4810
	 * @since 1.20
4811
	 * @return array Associative array with plural form, and plural rule as key-value pairs
4812
	 */
4813 View Code Duplication
	public function getCompiledPluralRules() {
4814
		$pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'compiledPluralRules' );
4815
		$fallbacks = Language::getFallbacksFor( $this->mCode );
4816
		if ( !$pluralRules ) {
4817
			foreach ( $fallbacks as $fallbackCode ) {
4818
				$pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' );
4819
				if ( $pluralRules ) {
4820
					break;
4821
				}
4822
			}
4823
		}
4824
		return $pluralRules;
4825
	}
4826
4827
	/**
4828
	 * Get the plural rules for the language
4829
	 * @since 1.20
4830
	 * @return array Associative array with plural form number and plural rule as key-value pairs
4831
	 */
4832 View Code Duplication
	public function getPluralRules() {
4833
		$pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRules' );
4834
		$fallbacks = Language::getFallbacksFor( $this->mCode );
4835
		if ( !$pluralRules ) {
4836
			foreach ( $fallbacks as $fallbackCode ) {
4837
				$pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRules' );
4838
				if ( $pluralRules ) {
4839
					break;
4840
				}
4841
			}
4842
		}
4843
		return $pluralRules;
4844
	}
4845
4846
	/**
4847
	 * Get the plural rule types for the language
4848
	 * @since 1.22
4849
	 * @return array Associative array with plural form number and plural rule type as key-value pairs
4850
	 */
4851 View Code Duplication
	public function getPluralRuleTypes() {
4852
		$pluralRuleTypes = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRuleTypes' );
4853
		$fallbacks = Language::getFallbacksFor( $this->mCode );
4854
		if ( !$pluralRuleTypes ) {
4855
			foreach ( $fallbacks as $fallbackCode ) {
4856
				$pluralRuleTypes = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRuleTypes' );
4857
				if ( $pluralRuleTypes ) {
4858
					break;
4859
				}
4860
			}
4861
		}
4862
		return $pluralRuleTypes;
4863
	}
4864
4865
	/**
4866
	 * Find the index number of the plural rule appropriate for the given number
4867
	 * @param int $number
4868
	 * @return int The index number of the plural rule
4869
	 */
4870
	public function getPluralRuleIndexNumber( $number ) {
4871
		$pluralRules = $this->getCompiledPluralRules();
4872
		$form = Evaluator::evaluateCompiled( $number, $pluralRules );
4873
		return $form;
4874
	}
4875
4876
	/**
4877
	 * Find the plural rule type appropriate for the given number
4878
	 * For example, if the language is set to Arabic, getPluralType(5) should
4879
	 * return 'few'.
4880
	 * @since 1.22
4881
	 * @param int $number
4882
	 * @return string The name of the plural rule type, e.g. one, two, few, many
4883
	 */
4884
	public function getPluralRuleType( $number ) {
4885
		$index = $this->getPluralRuleIndexNumber( $number );
4886
		$pluralRuleTypes = $this->getPluralRuleTypes();
4887
		if ( isset( $pluralRuleTypes[$index] ) ) {
4888
			return $pluralRuleTypes[$index];
4889
		} else {
4890
			return 'other';
4891
		}
4892
	}
4893
}
4894