Completed
Branch master (537795)
by
unknown
33:10
created

Language::getGrammarTransformations()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 29
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 16
nc 8
nop 0
dl 0
loc 29
rs 8.439
c 0
b 0
f 0
1
<?php
2
/**
3
 * Internationalisation code.
4
 * See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more information.
5
 *
6
 * This program is free software; you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 2 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License along
17
 * with this program; if not, write to the Free Software Foundation, Inc.,
18
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
 * http://www.gnu.org/copyleft/gpl.html
20
 *
21
 * @file
22
 * @ingroup Language
23
 */
24
25
/**
26
 * @defgroup Language Language
27
 */
28
29
use CLDRPluralRuleParser\Evaluator;
30
31
/**
32
 * Internationalisation code
33
 * @ingroup Language
34
 */
35
class Language {
36
	/**
37
	 * @var LanguageConverter
38
	 */
39
	public $mConverter;
40
41
	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...
42
	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...
43
	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...
44
45
	public $dateFormatStrings = [];
46
	public $mExtendedSpecialPageAliases;
47
48
	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...
49
50
	/**
51
	 * ReplacementArray object caches
52
	 */
53
	public $transformData = [];
54
55
	/**
56
	 * @var LocalisationCache
57
	 */
58
	static public $dataCache;
59
60
	static public $mLangObjCache = [];
61
62
	static public $mWeekdayMsgs = [
63
		'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
64
		'friday', 'saturday'
65
	];
66
67
	static public $mWeekdayAbbrevMsgs = [
68
		'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
69
	];
70
71
	static public $mMonthMsgs = [
72
		'january', 'february', 'march', 'april', 'may_long', 'june',
73
		'july', 'august', 'september', 'october', 'november',
74
		'december'
75
	];
76
	static public $mMonthGenMsgs = [
77
		'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
78
		'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
79
		'december-gen'
80
	];
81
	static public $mMonthAbbrevMsgs = [
82
		'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
83
		'sep', 'oct', 'nov', 'dec'
84
	];
85
86
	static public $mIranianCalendarMonthMsgs = [
87
		'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
88
		'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
89
		'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
90
		'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
91
	];
92
93
	static public $mHebrewCalendarMonthMsgs = [
94
		'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
95
		'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
96
		'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
97
		'hebrew-calendar-m10', 'hebrew-calendar-m11', 'hebrew-calendar-m12',
98
		'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
99
	];
100
101
	static public $mHebrewCalendarMonthGenMsgs = [
102
		'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
103
		'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
104
		'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
105
		'hebrew-calendar-m10-gen', 'hebrew-calendar-m11-gen', 'hebrew-calendar-m12-gen',
106
		'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
107
	];
108
109
	static public $mHijriCalendarMonthMsgs = [
110
		'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
111
		'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
112
		'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
113
		'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12'
114
	];
115
116
	/**
117
	 * @since 1.20
118
	 * @var array
119
	 */
120
	static public $durationIntervals = [
121
		'millennia' => 31556952000,
122
		'centuries' => 3155695200,
123
		'decades' => 315569520,
124
		'years' => 31556952, // 86400 * ( 365 + ( 24 * 3 + 25 ) / 400 )
125
		'weeks' => 604800,
126
		'days' => 86400,
127
		'hours' => 3600,
128
		'minutes' => 60,
129
		'seconds' => 1,
130
	];
131
132
	/**
133
	 * Cache for language fallbacks.
134
	 * @see Language::getFallbacksIncludingSiteLanguage
135
	 * @since 1.21
136
	 * @var array
137
	 */
138
	static private $fallbackLanguageCache = [];
139
140
	/**
141
	 * Cache for grammar rules data
142
	 * @var MapCacheLRU|null
143
	 */
144
	static private $grammarTransformations;
145
146
	/**
147
	 * Cache for language names
148
	 * @var HashBagOStuff|null
149
	 */
150
	static private $languageNameCache;
151
152
	/**
153
	 * Unicode directional formatting characters, for embedBidi()
154
	 */
155
	static private $lre = "\xE2\x80\xAA"; // U+202A LEFT-TO-RIGHT EMBEDDING
156
	static private $rle = "\xE2\x80\xAB"; // U+202B RIGHT-TO-LEFT EMBEDDING
157
	static private $pdf = "\xE2\x80\xAC"; // U+202C POP DIRECTIONAL FORMATTING
158
159
	/**
160
	 * Directionality test regex for embedBidi(). Matches the first strong directionality codepoint:
161
	 * - in group 1 if it is LTR
162
	 * - in group 2 if it is RTL
163
	 * Does not match if there is no strong directionality codepoint.
164
	 *
165
	 * The form is '/(?:([strong ltr codepoint])|([strong rtl codepoint]))/u' .
166
	 *
167
	 * Generated by UnicodeJS (see tools/strongDir) from the UCD; see
168
	 * https://git.wikimedia.org/summary/unicodejs.git .
169
	 */
170
	// @codingStandardsIgnoreStart
171
	// @codeCoverageIgnoreStart
172
	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';
173
	// @codeCoverageIgnoreEnd
174
	// @codingStandardsIgnoreEnd
175
176
	/**
177
	 * Get a cached or new language object for a given language code
178
	 * @param string $code
179
	 * @return Language
180
	 */
181
	static function factory( $code ) {
182
		global $wgDummyLanguageCodes, $wgLangObjCacheSize;
183
184
		if ( isset( $wgDummyLanguageCodes[$code] ) ) {
185
			$code = $wgDummyLanguageCodes[$code];
186
		}
187
188
		// get the language object to process
189
		$langObj = isset( self::$mLangObjCache[$code] )
190
			? self::$mLangObjCache[$code]
191
			: self::newFromCode( $code );
192
193
		// merge the language object in to get it up front in the cache
194
		self::$mLangObjCache = array_merge( [ $code => $langObj ], self::$mLangObjCache );
195
		// get rid of the oldest ones in case we have an overflow
196
		self::$mLangObjCache = array_slice( self::$mLangObjCache, 0, $wgLangObjCacheSize, true );
197
198
		return $langObj;
199
	}
200
201
	/**
202
	 * Create a language object for a given language code
203
	 * @param string $code
204
	 * @throws MWException
205
	 * @return Language
206
	 */
207
	protected static function newFromCode( $code ) {
208
		if ( !Language::isValidCode( $code ) ) {
209
			throw new MWException( "Invalid language code \"$code\"" );
210
		}
211
212
		if ( !Language::isValidBuiltInCode( $code ) ) {
213
			// It's not possible to customise this code with class files, so
214
			// just return a Language object. This is to support uselang= hacks.
215
			$lang = new Language;
216
			$lang->setCode( $code );
217
			return $lang;
218
		}
219
220
		// Check if there is a language class for the code
221
		$class = self::classFromCode( $code );
222
		if ( class_exists( $class ) ) {
223
			$lang = new $class;
224
			return $lang;
225
		}
226
227
		// Keep trying the fallback list until we find an existing class
228
		$fallbacks = Language::getFallbacksFor( $code );
229
		foreach ( $fallbacks as $fallbackCode ) {
230
			if ( !Language::isValidBuiltInCode( $fallbackCode ) ) {
231
				throw new MWException( "Invalid fallback '$fallbackCode' in fallback sequence for '$code'" );
232
			}
233
234
			$class = self::classFromCode( $fallbackCode );
235
			if ( class_exists( $class ) ) {
236
				$lang = new $class;
237
				$lang->setCode( $code );
238
				return $lang;
239
			}
240
		}
241
242
		throw new MWException( "Invalid fallback sequence for language '$code'" );
243
	}
244
245
	/**
246
	 * Checks whether any localisation is available for that language tag
247
	 * in MediaWiki (MessagesXx.php exists).
248
	 *
249
	 * @param string $code Language tag (in lower case)
250
	 * @return bool Whether language is supported
251
	 * @since 1.21
252
	 */
253
	public static function isSupportedLanguage( $code ) {
254
		if ( !self::isValidBuiltInCode( $code ) ) {
255
			return false;
256
		}
257
258
		if ( $code === 'qqq' ) {
259
			return false;
260
		}
261
262
		return is_readable( self::getMessagesFileName( $code ) ) ||
263
			is_readable( self::getJsonMessagesFileName( $code ) );
264
	}
265
266
	/**
267
	 * Returns true if a language code string is a well-formed language tag
268
	 * according to RFC 5646.
269
	 * This function only checks well-formedness; it doesn't check that
270
	 * language, script or variant codes actually exist in the repositories.
271
	 *
272
	 * Based on regexes by Mark Davis of the Unicode Consortium:
273
	 * http://unicode.org/repos/cldr/trunk/tools/java/org/unicode/cldr/util/data/langtagRegex.txt
274
	 *
275
	 * @param string $code
276
	 * @param bool $lenient Whether to allow '_' as separator. The default is only '-'.
277
	 *
278
	 * @return bool
279
	 * @since 1.21
280
	 */
281
	public static function isWellFormedLanguageTag( $code, $lenient = false ) {
282
		$alpha = '[a-z]';
283
		$digit = '[0-9]';
284
		$alphanum = '[a-z0-9]';
285
		$x = 'x'; # private use singleton
286
		$singleton = '[a-wy-z]'; # other singleton
287
		$s = $lenient ? '[-_]' : '-';
288
289
		$language = "$alpha{2,8}|$alpha{2,3}$s$alpha{3}";
290
		$script = "$alpha{4}"; # ISO 15924
291
		$region = "(?:$alpha{2}|$digit{3})"; # ISO 3166-1 alpha-2 or UN M.49
292
		$variant = "(?:$alphanum{5,8}|$digit$alphanum{3})";
293
		$extension = "$singleton(?:$s$alphanum{2,8})+";
294
		$privateUse = "$x(?:$s$alphanum{1,8})+";
295
296
		# Define certain grandfathered codes, since otherwise the regex is pretty useless.
297
		# Since these are limited, this is safe even later changes to the registry --
298
		# the only oddity is that it might change the type of the tag, and thus
299
		# the results from the capturing groups.
300
		# https://www.iana.org/assignments/language-subtag-registry
301
302
		$grandfathered = "en{$s}GB{$s}oed"
303
			. "|i{$s}(?:ami|bnn|default|enochian|hak|klingon|lux|mingo|navajo|pwn|tao|tay|tsu)"
304
			. "|no{$s}(?:bok|nyn)"
305
			. "|sgn{$s}(?:BE{$s}(?:fr|nl)|CH{$s}de)"
306
			. "|zh{$s}min{$s}nan";
307
308
		$variantList = "$variant(?:$s$variant)*";
309
		$extensionList = "$extension(?:$s$extension)*";
310
311
		$langtag = "(?:($language)"
312
			. "(?:$s$script)?"
313
			. "(?:$s$region)?"
314
			. "(?:$s$variantList)?"
315
			. "(?:$s$extensionList)?"
316
			. "(?:$s$privateUse)?)";
317
318
		# The final breakdown, with capturing groups for each of these components
319
		# The variants, extensions, grandfathered, and private-use may have interior '-'
320
321
		$root = "^(?:$langtag|$privateUse|$grandfathered)$";
322
323
		return (bool)preg_match( "/$root/", strtolower( $code ) );
324
	}
325
326
	/**
327
	 * Returns true if a language code string is of a valid form, whether or
328
	 * not it exists. This includes codes which are used solely for
329
	 * customisation via the MediaWiki namespace.
330
	 *
331
	 * @param string $code
332
	 *
333
	 * @return bool
334
	 */
335
	public static function isValidCode( $code ) {
336
		static $cache = [];
337
		if ( !isset( $cache[$code] ) ) {
338
			// People think language codes are html safe, so enforce it.
339
			// Ideally we should only allow a-zA-Z0-9-
340
			// but, .+ and other chars are often used for {{int:}} hacks
341
			// see bugs T39564, T39587, T38938
342
			$cache[$code] =
343
				// Protect against path traversal
344
				strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code )
345
				&& !preg_match( MediaWikiTitleCodec::getTitleInvalidRegex(), $code );
346
		}
347
		return $cache[$code];
348
	}
349
350
	/**
351
	 * Returns true if a language code is of a valid form for the purposes of
352
	 * internal customisation of MediaWiki, via Messages*.php or *.json.
353
	 *
354
	 * @param string $code
355
	 *
356
	 * @throws MWException
357
	 * @since 1.18
358
	 * @return bool
359
	 */
360
	public static function isValidBuiltInCode( $code ) {
361
362
		if ( !is_string( $code ) ) {
363
			if ( is_object( $code ) ) {
364
				$addmsg = " of class " . get_class( $code );
365
			} else {
366
				$addmsg = '';
367
			}
368
			$type = gettype( $code );
369
			throw new MWException( __METHOD__ . " must be passed a string, $type given$addmsg" );
370
		}
371
372
		return (bool)preg_match( '/^[a-z0-9-]{2,}$/', $code );
373
	}
374
375
	/**
376
	 * Returns true if a language code is an IETF tag known to MediaWiki.
377
	 *
378
	 * @param string $tag
379
	 *
380
	 * @since 1.21
381
	 * @return bool
382
	 */
383
	public static function isKnownLanguageTag( $tag ) {
384
		// Quick escape for invalid input to avoid exceptions down the line
385
		// when code tries to process tags which are not valid at all.
386
		if ( !self::isValidBuiltInCode( $tag ) ) {
387
			return false;
388
		}
389
390
		if ( isset( MediaWiki\Languages\Data\Names::$names[$tag] )
391
			|| self::fetchLanguageName( $tag, $tag ) !== ''
392
		) {
393
			return true;
394
		}
395
396
		return false;
397
	}
398
399
	/**
400
	 * Get the LocalisationCache instance
401
	 *
402
	 * @return LocalisationCache
403
	 */
404
	public static function getLocalisationCache() {
405
		if ( is_null( self::$dataCache ) ) {
406
			global $wgLocalisationCacheConf;
407
			$class = $wgLocalisationCacheConf['class'];
408
			self::$dataCache = new $class( $wgLocalisationCacheConf );
409
		}
410
		return self::$dataCache;
411
	}
412
413
	function __construct() {
414
		$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...
415
		// Set the code to the name of the descendant
416
		if ( get_class( $this ) == 'Language' ) {
417
			$this->mCode = 'en';
418
		} else {
419
			$this->mCode = str_replace( '_', '-', strtolower( substr( get_class( $this ), 8 ) ) );
420
		}
421
		self::getLocalisationCache();
422
	}
423
424
	/**
425
	 * Reduce memory usage
426
	 */
427
	function __destruct() {
428
		foreach ( $this as $name => $value ) {
0 ignored issues
show
Bug introduced by
The expression $this of type this<Language> is not traversable.
Loading history...
429
			unset( $this->$name );
430
		}
431
	}
432
433
	/**
434
	 * Hook which will be called if this is the content language.
435
	 * Descendants can use this to register hook functions or modify globals
436
	 */
437
	function initContLang() {
438
	}
439
440
	/**
441
	 * @return array
442
	 * @since 1.19
443
	 */
444
	public function getFallbackLanguages() {
445
		return self::getFallbacksFor( $this->mCode );
446
	}
447
448
	/**
449
	 * Exports $wgBookstoreListEn
450
	 * @return array
451
	 */
452
	public function getBookstoreList() {
453
		return self::$dataCache->getItem( $this->mCode, 'bookstoreList' );
454
	}
455
456
	/**
457
	 * Returns an array of localised namespaces indexed by their numbers. If the namespace is not
458
	 * available in localised form, it will be included in English.
459
	 *
460
	 * @return array
461
	 */
462
	public function getNamespaces() {
463
		if ( is_null( $this->namespaceNames ) ) {
464
			global $wgMetaNamespace, $wgMetaNamespaceTalk, $wgExtraNamespaces;
465
466
			$this->namespaceNames = self::$dataCache->getItem( $this->mCode, 'namespaceNames' );
467
			$validNamespaces = MWNamespace::getCanonicalNamespaces();
468
469
			$this->namespaceNames = $wgExtraNamespaces + $this->namespaceNames + $validNamespaces;
470
471
			$this->namespaceNames[NS_PROJECT] = $wgMetaNamespace;
472
			if ( $wgMetaNamespaceTalk ) {
473
				$this->namespaceNames[NS_PROJECT_TALK] = $wgMetaNamespaceTalk;
474
			} else {
475
				$talk = $this->namespaceNames[NS_PROJECT_TALK];
476
				$this->namespaceNames[NS_PROJECT_TALK] =
477
					$this->fixVariableInNamespace( $talk );
478
			}
479
480
			# Sometimes a language will be localised but not actually exist on this wiki.
481
			foreach ( $this->namespaceNames as $key => $text ) {
482
				if ( !isset( $validNamespaces[$key] ) ) {
483
					unset( $this->namespaceNames[$key] );
484
				}
485
			}
486
487
			# The above mixing may leave namespaces out of canonical order.
488
			# Re-order by namespace ID number...
489
			ksort( $this->namespaceNames );
490
491
			Hooks::run( 'LanguageGetNamespaces', [ &$this->namespaceNames ] );
492
		}
493
494
		return $this->namespaceNames;
495
	}
496
497
	/**
498
	 * Arbitrarily set all of the namespace names at once. Mainly used for testing
499
	 * @param array $namespaces Array of namespaces (id => name)
500
	 */
501
	public function setNamespaces( array $namespaces ) {
502
		$this->namespaceNames = $namespaces;
503
		$this->mNamespaceIds = null;
504
	}
505
506
	/**
507
	 * Resets all of the namespace caches. Mainly used for testing
508
	 */
509
	public function resetNamespaces() {
510
		$this->namespaceNames = null;
511
		$this->mNamespaceIds = null;
512
		$this->namespaceAliases = null;
513
	}
514
515
	/**
516
	 * A convenience function that returns getNamespaces() with spaces instead of underscores
517
	 * in values. Useful for producing output to be displayed e.g. in `<select>` forms.
518
	 *
519
	 * @return array
520
	 */
521
	public function getFormattedNamespaces() {
522
		$ns = $this->getNamespaces();
523
		foreach ( $ns as $k => $v ) {
524
			$ns[$k] = strtr( $v, '_', ' ' );
525
		}
526
		return $ns;
527
	}
528
529
	/**
530
	 * Get a namespace value by key
531
	 *
532
	 * <code>
533
	 * $mw_ns = $wgContLang->getNsText( NS_MEDIAWIKI );
534
	 * echo $mw_ns; // prints 'MediaWiki'
535
	 * </code>
536
	 *
537
	 * @param int $index The array key of the namespace to return
538
	 * @return string|bool String if the namespace value exists, otherwise false
539
	 */
540
	public function getNsText( $index ) {
541
		$ns = $this->getNamespaces();
542
		return isset( $ns[$index] ) ? $ns[$index] : false;
543
	}
544
545
	/**
546
	 * A convenience function that returns the same thing as
547
	 * getNsText() except with '_' changed to ' ', useful for
548
	 * producing output.
549
	 *
550
	 * <code>
551
	 * $mw_ns = $wgContLang->getFormattedNsText( NS_MEDIAWIKI_TALK );
552
	 * echo $mw_ns; // prints 'MediaWiki talk'
553
	 * </code>
554
	 *
555
	 * @param int $index The array key of the namespace to return
556
	 * @return string Namespace name without underscores (empty string if namespace does not exist)
557
	 */
558
	public function getFormattedNsText( $index ) {
559
		$ns = $this->getNsText( $index );
560
		return strtr( $ns, '_', ' ' );
0 ignored issues
show
Bug introduced by
It seems like $ns defined by $this->getNsText($index) on line 559 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...
561
	}
562
563
	/**
564
	 * Returns gender-dependent namespace alias if available.
565
	 * See https://www.mediawiki.org/wiki/Manual:$wgExtraGenderNamespaces
566
	 * @param int $index Namespace index
567
	 * @param string $gender Gender key (male, female... )
568
	 * @return string
569
	 * @since 1.18
570
	 */
571
	public function getGenderNsText( $index, $gender ) {
572
		global $wgExtraGenderNamespaces;
573
574
		$ns = $wgExtraGenderNamespaces +
575
			(array)self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
576
577
		return isset( $ns[$index][$gender] ) ? $ns[$index][$gender] : $this->getNsText( $index );
578
	}
579
580
	/**
581
	 * Whether this language uses gender-dependent namespace aliases.
582
	 * See https://www.mediawiki.org/wiki/Manual:$wgExtraGenderNamespaces
583
	 * @return bool
584
	 * @since 1.18
585
	 */
586
	public function needsGenderDistinction() {
587
		global $wgExtraGenderNamespaces, $wgExtraNamespaces;
588
		if ( count( $wgExtraGenderNamespaces ) > 0 ) {
589
			// $wgExtraGenderNamespaces overrides everything
590
			return true;
591
		} elseif ( isset( $wgExtraNamespaces[NS_USER] ) && isset( $wgExtraNamespaces[NS_USER_TALK] ) ) {
592
			/// @todo There may be other gender namespace than NS_USER & NS_USER_TALK in the future
593
			// $wgExtraNamespaces overrides any gender aliases specified in i18n files
594
			return false;
595
		} else {
596
			// Check what is in i18n files
597
			$aliases = self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
598
			return count( $aliases ) > 0;
599
		}
600
	}
601
602
	/**
603
	 * Get a namespace key by value, case insensitive.
604
	 * Only matches namespace names for the current language, not the
605
	 * canonical ones defined in Namespace.php.
606
	 *
607
	 * @param string $text
608
	 * @return int|bool An integer if $text is a valid value otherwise false
609
	 */
610
	function getLocalNsIndex( $text ) {
611
		$lctext = $this->lc( $text );
612
		$ids = $this->getNamespaceIds();
613
		return isset( $ids[$lctext] ) ? $ids[$lctext] : false;
614
	}
615
616
	/**
617
	 * @return array
618
	 */
619
	public function getNamespaceAliases() {
620
		if ( is_null( $this->namespaceAliases ) ) {
621
			$aliases = self::$dataCache->getItem( $this->mCode, 'namespaceAliases' );
622
			if ( !$aliases ) {
623
				$aliases = [];
624
			} else {
625
				foreach ( $aliases as $name => $index ) {
626
					if ( $index === NS_PROJECT_TALK ) {
627
						unset( $aliases[$name] );
628
						$name = $this->fixVariableInNamespace( $name );
629
						$aliases[$name] = $index;
630
					}
631
				}
632
			}
633
634
			global $wgExtraGenderNamespaces;
635
			$genders = $wgExtraGenderNamespaces +
636
				(array)self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
637
			foreach ( $genders as $index => $forms ) {
638
				foreach ( $forms as $alias ) {
639
					$aliases[$alias] = $index;
640
				}
641
			}
642
643
			# Also add converted namespace names as aliases, to avoid confusion.
644
			$convertedNames = [];
645
			foreach ( $this->getVariants() as $variant ) {
646
				if ( $variant === $this->mCode ) {
647
					continue;
648
				}
649
				foreach ( $this->getNamespaces() as $ns => $_ ) {
650
					$convertedNames[$this->getConverter()->convertNamespace( $ns, $variant )] = $ns;
651
				}
652
			}
653
654
			$this->namespaceAliases = $aliases + $convertedNames;
655
		}
656
657
		return $this->namespaceAliases;
658
	}
659
660
	/**
661
	 * @return array
662
	 */
663
	public function getNamespaceIds() {
664
		if ( is_null( $this->mNamespaceIds ) ) {
665
			global $wgNamespaceAliases;
666
			# Put namespace names and aliases into a hashtable.
667
			# If this is too slow, then we should arrange it so that it is done
668
			# before caching. The catch is that at pre-cache time, the above
669
			# class-specific fixup hasn't been done.
670
			$this->mNamespaceIds = [];
671
			foreach ( $this->getNamespaces() as $index => $name ) {
672
				$this->mNamespaceIds[$this->lc( $name )] = $index;
673
			}
674
			foreach ( $this->getNamespaceAliases() as $name => $index ) {
675
				$this->mNamespaceIds[$this->lc( $name )] = $index;
676
			}
677
			if ( $wgNamespaceAliases ) {
678
				foreach ( $wgNamespaceAliases as $name => $index ) {
679
					$this->mNamespaceIds[$this->lc( $name )] = $index;
680
				}
681
			}
682
		}
683
		return $this->mNamespaceIds;
684
	}
685
686
	/**
687
	 * Get a namespace key by value, case insensitive.  Canonical namespace
688
	 * names override custom ones defined for the current language.
689
	 *
690
	 * @param string $text
691
	 * @return int|bool An integer if $text is a valid value otherwise false
692
	 */
693
	public function getNsIndex( $text ) {
694
		$lctext = $this->lc( $text );
695
		$ns = MWNamespace::getCanonicalIndex( $lctext );
696
		if ( $ns !== null ) {
697
			return $ns;
698
		}
699
		$ids = $this->getNamespaceIds();
700
		return isset( $ids[$lctext] ) ? $ids[$lctext] : false;
701
	}
702
703
	/**
704
	 * short names for language variants used for language conversion links.
705
	 *
706
	 * @param string $code
707
	 * @param bool $usemsg Use the "variantname-xyz" message if it exists
708
	 * @return string
709
	 */
710
	public function getVariantname( $code, $usemsg = true ) {
711
		$msg = "variantname-$code";
712
		if ( $usemsg && wfMessage( $msg )->exists() ) {
713
			return $this->getMessageFromDB( $msg );
714
		}
715
		$name = self::fetchLanguageName( $code );
716
		if ( $name ) {
717
			return $name; # if it's defined as a language name, show that
718
		} else {
719
			# otherwise, output the language code
720
			return $code;
721
		}
722
	}
723
724
	/**
725
	 * @return array
726
	 */
727
	public function getDatePreferences() {
728
		return self::$dataCache->getItem( $this->mCode, 'datePreferences' );
729
	}
730
731
	/**
732
	 * @return array
733
	 */
734
	function getDateFormats() {
735
		return self::$dataCache->getItem( $this->mCode, 'dateFormats' );
736
	}
737
738
	/**
739
	 * @return array|string
740
	 */
741
	public function getDefaultDateFormat() {
742
		$df = self::$dataCache->getItem( $this->mCode, 'defaultDateFormat' );
743
		if ( $df === 'dmy or mdy' ) {
744
			global $wgAmericanDates;
745
			return $wgAmericanDates ? 'mdy' : 'dmy';
746
		} else {
747
			return $df;
748
		}
749
	}
750
751
	/**
752
	 * @return array
753
	 */
754
	public function getDatePreferenceMigrationMap() {
755
		return self::$dataCache->getItem( $this->mCode, 'datePreferenceMigrationMap' );
756
	}
757
758
	/**
759
	 * @param string $image
760
	 * @return array|null
761
	 */
762
	function getImageFile( $image ) {
763
		return self::$dataCache->getSubitem( $this->mCode, 'imageFiles', $image );
764
	}
765
766
	/**
767
	 * @return array
768
	 * @since 1.24
769
	 */
770
	public function getImageFiles() {
771
		return self::$dataCache->getItem( $this->mCode, 'imageFiles' );
772
	}
773
774
	/**
775
	 * @return array
776
	 */
777
	public function getExtraUserToggles() {
778
		return (array)self::$dataCache->getItem( $this->mCode, 'extraUserToggles' );
779
	}
780
781
	/**
782
	 * @param string $tog
783
	 * @return string
784
	 */
785
	function getUserToggle( $tog ) {
786
		return $this->getMessageFromDB( "tog-$tog" );
787
	}
788
789
	/**
790
	 * Get an array of language names, indexed by code.
791
	 * @param null|string $inLanguage Code of language in which to return the names
792
	 *		Use null for autonyms (native names)
793
	 * @param string $include One of:
794
	 *		'all' all available languages
795
	 *		'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames (default)
796
	 *		'mwfile' only if the language is in 'mw' *and* has a message file
797
	 * @return array Language code => language name
798
	 * @since 1.20
799
	 */
800
	public static function fetchLanguageNames( $inLanguage = null, $include = 'mw' ) {
801
		$cacheKey = $inLanguage === null ? 'null' : $inLanguage;
802
		$cacheKey .= ":$include";
803
		if ( self::$languageNameCache === null ) {
804
			self::$languageNameCache = new HashBagOStuff( [ 'maxKeys' => 20 ] );
805
		}
806
807
		$ret = self::$languageNameCache->get( $cacheKey );
808
		if ( !$ret ) {
809
			$ret = self::fetchLanguageNamesUncached( $inLanguage, $include );
810
			self::$languageNameCache->set( $cacheKey, $ret );
811
		}
812
		return $ret;
813
	}
814
815
	/**
816
	 * Uncached helper for fetchLanguageNames
817
	 * @param null|string $inLanguage Code of language in which to return the names
818
	 *		Use null for autonyms (native names)
819
	 * @param string $include One of:
820
	 *		'all' all available languages
821
	 *		'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames (default)
822
	 *		'mwfile' only if the language is in 'mw' *and* has a message file
823
	 * @return array Language code => language name
824
	 */
825
	private static function fetchLanguageNamesUncached( $inLanguage = null, $include = 'mw' ) {
826
		global $wgExtraLanguageNames;
827
828
		// If passed an invalid language code to use, fallback to en
829
		if ( $inLanguage !== null && !Language::isValidCode( $inLanguage ) ) {
830
			$inLanguage = 'en';
831
		}
832
833
		$names = [];
834
835
		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...
836
			# TODO: also include when $inLanguage is null, when this code is more efficient
837
			Hooks::run( 'LanguageGetTranslatedLanguageNames', [ &$names, $inLanguage ] );
838
		}
839
840
		$mwNames = $wgExtraLanguageNames + MediaWiki\Languages\Data\Names::$names;
841
		foreach ( $mwNames as $mwCode => $mwName ) {
842
			# - Prefer own MediaWiki native name when not using the hook
843
			# - For other names just add if not added through the hook
844
			if ( $mwCode === $inLanguage || !isset( $names[$mwCode] ) ) {
845
				$names[$mwCode] = $mwName;
846
			}
847
		}
848
849
		if ( $include === 'all' ) {
850
			ksort( $names );
851
			return $names;
852
		}
853
854
		$returnMw = [];
855
		$coreCodes = array_keys( $mwNames );
856
		foreach ( $coreCodes as $coreCode ) {
857
			$returnMw[$coreCode] = $names[$coreCode];
858
		}
859
860
		if ( $include === 'mwfile' ) {
861
			$namesMwFile = [];
862
			# We do this using a foreach over the codes instead of a directory
863
			# loop so that messages files in extensions will work correctly.
864
			foreach ( $returnMw as $code => $value ) {
865
				if ( is_readable( self::getMessagesFileName( $code ) )
866
					|| is_readable( self::getJsonMessagesFileName( $code ) )
867
				) {
868
					$namesMwFile[$code] = $names[$code];
869
				}
870
			}
871
872
			ksort( $namesMwFile );
873
			return $namesMwFile;
874
		}
875
876
		ksort( $returnMw );
877
		# 'mw' option; default if it's not one of the other two options (all/mwfile)
878
		return $returnMw;
879
	}
880
881
	/**
882
	 * @param string $code The code of the language for which to get the name
883
	 * @param null|string $inLanguage Code of language in which to return the name (null for autonyms)
884
	 * @param string $include 'all', 'mw' or 'mwfile'; see fetchLanguageNames()
885
	 * @return string Language name or empty
886
	 * @since 1.20
887
	 */
888
	public static function fetchLanguageName( $code, $inLanguage = null, $include = 'all' ) {
889
		$code = strtolower( $code );
890
		$array = self::fetchLanguageNames( $inLanguage, $include );
891
		return !array_key_exists( $code, $array ) ? '' : $array[$code];
892
	}
893
894
	/**
895
	 * Get a message from the MediaWiki namespace.
896
	 *
897
	 * @param string $msg Message name
898
	 * @return string
899
	 */
900
	public function getMessageFromDB( $msg ) {
901
		return $this->msg( $msg )->text();
902
	}
903
904
	/**
905
	 * Get message object in this language. Only for use inside this class.
906
	 *
907
	 * @param string $msg Message name
908
	 * @return Message
909
	 */
910
	protected function msg( $msg ) {
911
		return wfMessage( $msg )->inLanguage( $this );
912
	}
913
914
	/**
915
	 * @param string $key
916
	 * @return string
917
	 */
918
	public function getMonthName( $key ) {
919
		return $this->getMessageFromDB( self::$mMonthMsgs[$key - 1] );
920
	}
921
922
	/**
923
	 * @return array
924
	 */
925 View Code Duplication
	public function getMonthNamesArray() {
926
		$monthNames = [ '' ];
927
		for ( $i = 1; $i < 13; $i++ ) {
928
			$monthNames[] = $this->getMonthName( $i );
929
		}
930
		return $monthNames;
931
	}
932
933
	/**
934
	 * @param string $key
935
	 * @return string
936
	 */
937
	public function getMonthNameGen( $key ) {
938
		return $this->getMessageFromDB( self::$mMonthGenMsgs[$key - 1] );
939
	}
940
941
	/**
942
	 * @param string $key
943
	 * @return string
944
	 */
945
	public function getMonthAbbreviation( $key ) {
946
		return $this->getMessageFromDB( self::$mMonthAbbrevMsgs[$key - 1] );
947
	}
948
949
	/**
950
	 * @return array
951
	 */
952 View Code Duplication
	public function getMonthAbbreviationsArray() {
953
		$monthNames = [ '' ];
954
		for ( $i = 1; $i < 13; $i++ ) {
955
			$monthNames[] = $this->getMonthAbbreviation( $i );
956
		}
957
		return $monthNames;
958
	}
959
960
	/**
961
	 * @param string $key
962
	 * @return string
963
	 */
964
	public function getWeekdayName( $key ) {
965
		return $this->getMessageFromDB( self::$mWeekdayMsgs[$key - 1] );
966
	}
967
968
	/**
969
	 * @param string $key
970
	 * @return string
971
	 */
972
	function getWeekdayAbbreviation( $key ) {
973
		return $this->getMessageFromDB( self::$mWeekdayAbbrevMsgs[$key - 1] );
974
	}
975
976
	/**
977
	 * @param string $key
978
	 * @return string
979
	 */
980
	function getIranianCalendarMonthName( $key ) {
981
		return $this->getMessageFromDB( self::$mIranianCalendarMonthMsgs[$key - 1] );
982
	}
983
984
	/**
985
	 * @param string $key
986
	 * @return string
987
	 */
988
	function getHebrewCalendarMonthName( $key ) {
989
		return $this->getMessageFromDB( self::$mHebrewCalendarMonthMsgs[$key - 1] );
990
	}
991
992
	/**
993
	 * @param string $key
994
	 * @return string
995
	 */
996
	function getHebrewCalendarMonthNameGen( $key ) {
997
		return $this->getMessageFromDB( self::$mHebrewCalendarMonthGenMsgs[$key - 1] );
998
	}
999
1000
	/**
1001
	 * @param string $key
1002
	 * @return string
1003
	 */
1004
	function getHijriCalendarMonthName( $key ) {
1005
		return $this->getMessageFromDB( self::$mHijriCalendarMonthMsgs[$key - 1] );
1006
	}
1007
1008
	/**
1009
	 * Pass through result from $dateTimeObj->format()
1010
	 * @param DateTime|bool|null &$dateTimeObj
1011
	 * @param string $ts
1012
	 * @param DateTimeZone|bool|null $zone
1013
	 * @param string $code
1014
	 * @return string
1015
	 */
1016
	private static function dateTimeObjFormat( &$dateTimeObj, $ts, $zone, $code ) {
1017
		if ( !$dateTimeObj ) {
1018
			$dateTimeObj = DateTime::createFromFormat(
1019
				'YmdHis', $ts, $zone ?: new DateTimeZone( 'UTC' )
1020
			);
1021
		}
1022
		return $dateTimeObj->format( $code );
1023
	}
1024
1025
	/**
1026
	 * This is a workalike of PHP's date() function, but with better
1027
	 * internationalisation, a reduced set of format characters, and a better
1028
	 * escaping format.
1029
	 *
1030
	 * Supported format characters are dDjlNwzWFmMntLoYyaAgGhHiscrUeIOPTZ. See
1031
	 * the PHP manual for definitions. There are a number of extensions, which
1032
	 * start with "x":
1033
	 *
1034
	 *    xn   Do not translate digits of the next numeric format character
1035
	 *    xN   Toggle raw digit (xn) flag, stays set until explicitly unset
1036
	 *    xr   Use roman numerals for the next numeric format character
1037
	 *    xh   Use hebrew numerals for the next numeric format character
1038
	 *    xx   Literal x
1039
	 *    xg   Genitive month name
1040
	 *
1041
	 *    xij  j (day number) in Iranian calendar
1042
	 *    xiF  F (month name) in Iranian calendar
1043
	 *    xin  n (month number) in Iranian calendar
1044
	 *    xiy  y (two digit year) in Iranian calendar
1045
	 *    xiY  Y (full year) in Iranian calendar
1046
	 *    xit  t (days in month) in Iranian calendar
1047
	 *    xiz  z (day of the 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
	public function sprintfDate( $format, $ts, DateTimeZone $zone = null, &$ttl = 'unused' ) {
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 'xit':
1344
					$usedIranianYear = true;
1345
					if ( !$iranian ) {
1346
						$iranian = self::tsToIranian( $ts );
1347
					}
1348
					$num = self::$IRANIAN_DAYS[$iranian[1] - 1];
1349
					break;
1350 View Code Duplication
				case 'xiz':
1351
					$usedIranianYear = true;
1352
					if ( !$iranian ) {
1353
						$iranian = self::tsToIranian( $ts );
1354
					}
1355
					$num = $iranian[3];
1356
					break;
1357 View Code Duplication
				case 'a':
1358
					$usedAMPM = true;
1359
					$s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'am' : 'pm';
1360
					break;
1361 View Code Duplication
				case 'A':
1362
					$usedAMPM = true;
1363
					$s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'AM' : 'PM';
1364
					break;
1365 View Code Duplication
				case 'g':
1366
					$usedHour = true;
1367
					$h = substr( $ts, 8, 2 );
1368
					$num = $h % 12 ? $h % 12 : 12;
1369
					break;
1370
				case 'G':
1371
					$usedHour = true;
1372
					$num = intval( substr( $ts, 8, 2 ) );
1373
					break;
1374 View Code Duplication
				case 'h':
1375
					$usedHour = true;
1376
					$h = substr( $ts, 8, 2 );
1377
					$num = sprintf( '%02d', $h % 12 ? $h % 12 : 12 );
1378
					break;
1379
				case 'H':
1380
					$usedHour = true;
1381
					$num = substr( $ts, 8, 2 );
1382
					break;
1383
				case 'i':
1384
					$usedMinute = true;
1385
					$num = substr( $ts, 10, 2 );
1386
					break;
1387
				case 's':
1388
					$usedSecond = true;
1389
					$num = substr( $ts, 12, 2 );
1390
					break;
1391
				case 'c':
1392
				case 'r':
1393
					$usedSecond = true;
1394
					// fall through
1395
				case 'e':
1396
				case 'O':
1397
				case 'P':
1398
				case 'T':
1399
					$s .= Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1400
					break;
1401
				case 'w':
1402
				case 'N':
1403
				case 'z':
1404
					$usedDay = true;
1405
					$num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1406
					break;
1407
				case 'W':
1408
					$usedWeek = true;
1409
					$num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1410
					break;
1411
				case 't':
1412
					$usedMonth = true;
1413
					$num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1414
					break;
1415
				case 'L':
1416
					$usedIsLeapYear = true;
1417
					$num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1418
					break;
1419
				case 'o':
1420
					$usedISOYear = true;
1421
					$num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1422
					break;
1423
				case 'U':
1424
					$usedSecond = true;
1425
					// fall through
1426
				case 'I':
1427
				case 'Z':
1428
					$num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1429
					break;
1430
				case '\\':
1431
					# Backslash escaping
1432
					if ( $p < $formatLength - 1 ) {
1433
						$s .= $format[++$p];
1434
					} else {
1435
						$s .= '\\';
1436
					}
1437
					break;
1438
				case '"':
1439
					# Quoted literal
1440
					if ( $p < $formatLength - 1 ) {
1441
						$endQuote = strpos( $format, '"', $p + 1 );
1442
						if ( $endQuote === false ) {
1443
							# No terminating quote, assume literal "
1444
							$s .= '"';
1445
						} else {
1446
							$s .= substr( $format, $p + 1, $endQuote - $p - 1 );
1447
							$p = $endQuote;
1448
						}
1449
					} else {
1450
						# Quote at end of string, assume literal "
1451
						$s .= '"';
1452
					}
1453
					break;
1454
				default:
1455
					$s .= $format[$p];
1456
			}
1457
			if ( $num !== false ) {
1458
				if ( $rawToggle || $raw ) {
1459
					$s .= $num;
1460
					$raw = false;
1461
				} elseif ( $roman ) {
1462
					$s .= Language::romanNumeral( $num );
1463
					$roman = false;
1464
				} elseif ( $hebrewNum ) {
1465
					$s .= self::hebrewNumeral( $num );
1466
					$hebrewNum = false;
1467
				} else {
1468
					$s .= $this->formatNum( $num, true );
1469
				}
1470
			}
1471
		}
1472
1473
		if ( $ttl === 'unused' ) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
1474
			// No need to calculate the TTL, the caller wont use it anyway.
1475
		} elseif ( $usedSecond ) {
1476
			$ttl = 1;
1477
		} elseif ( $usedMinute ) {
1478
			$ttl = 60 - substr( $ts, 12, 2 );
1479
		} elseif ( $usedHour ) {
1480
			$ttl = 3600 - substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1481
		} elseif ( $usedAMPM ) {
1482
			$ttl = 43200 - ( substr( $ts, 8, 2 ) % 12 ) * 3600 -
1483
				substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1484
		} elseif (
1485
			$usedDay ||
1486
			$usedHebrewMonth ||
1487
			$usedIranianMonth ||
1488
			$usedHijriMonth ||
1489
			$usedHebrewYear ||
1490
			$usedIranianYear ||
1491
			$usedHijriYear ||
1492
			$usedTennoYear
1493
		) {
1494
			// @todo Someone who understands the non-Gregorian calendars
1495
			// should write proper logic for them so that they don't need purged every day.
1496
			$ttl = 86400 - substr( $ts, 8, 2 ) * 3600 -
1497
				substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1498
		} else {
1499
			$possibleTtls = [];
1500
			$timeRemainingInDay = 86400 - substr( $ts, 8, 2 ) * 3600 -
1501
				substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1502
			if ( $usedWeek ) {
1503
				$possibleTtls[] =
1504
					( 7 - Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'N' ) ) * 86400 +
1505
					$timeRemainingInDay;
1506
			} elseif ( $usedISOYear ) {
1507
				// December 28th falls on the last ISO week of the year, every year.
1508
				// The last ISO week of a year can be 52 or 53.
1509
				$lastWeekOfISOYear = DateTime::createFromFormat(
1510
					'Ymd',
1511
					substr( $ts, 0, 4 ) . '1228',
1512
					$zone ?: new DateTimeZone( 'UTC' )
1513
				)->format( 'W' );
1514
				$currentISOWeek = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'W' );
1515
				$weeksRemaining = $lastWeekOfISOYear - $currentISOWeek;
1516
				$timeRemainingInWeek =
1517
					( 7 - Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'N' ) ) * 86400
1518
					+ $timeRemainingInDay;
1519
				$possibleTtls[] = $weeksRemaining * 604800 + $timeRemainingInWeek;
1520
			}
1521
1522
			if ( $usedMonth ) {
1523
				$possibleTtls[] =
1524
					( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 't' ) -
1525
						substr( $ts, 6, 2 ) ) * 86400
1526
					+ $timeRemainingInDay;
1527
			} elseif ( $usedYear ) {
1528
				$possibleTtls[] =
1529
					( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'L' ) + 364 -
1530
						Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'z' ) ) * 86400
1531
					+ $timeRemainingInDay;
1532
			} elseif ( $usedIsLeapYear ) {
1533
				$year = substr( $ts, 0, 4 );
1534
				$timeRemainingInYear =
1535
					( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'L' ) + 364 -
1536
						Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'z' ) ) * 86400
1537
					+ $timeRemainingInDay;
1538
				$mod = $year % 4;
1539
				if ( $mod || ( !( $year % 100 ) && $year % 400 ) ) {
1540
					// this isn't a leap year. see when the next one starts
1541
					$nextCandidate = $year - $mod + 4;
1542
					if ( $nextCandidate % 100 || !( $nextCandidate % 400 ) ) {
1543
						$possibleTtls[] = ( $nextCandidate - $year - 1 ) * 365 * 86400 +
1544
							$timeRemainingInYear;
1545
					} else {
1546
						$possibleTtls[] = ( $nextCandidate - $year + 3 ) * 365 * 86400 +
1547
							$timeRemainingInYear;
1548
					}
1549
				} else {
1550
					// this is a leap year, so the next year isn't
1551
					$possibleTtls[] = $timeRemainingInYear;
1552
				}
1553
			}
1554
1555
			if ( $possibleTtls ) {
1556
				$ttl = min( $possibleTtls );
1557
			}
1558
		}
1559
1560
		return $s;
1561
	}
1562
1563
	private static $GREG_DAYS = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
1564
	private static $IRANIAN_DAYS = [ 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 ];
1565
1566
	/**
1567
	 * Algorithm by Roozbeh Pournader and Mohammad Toossi to convert
1568
	 * Gregorian dates to Iranian dates. Originally written in C, it
1569
	 * is released under the terms of GNU Lesser General Public
1570
	 * License. Conversion to PHP was performed by Niklas Laxström.
1571
	 *
1572
	 * Link: http://www.farsiweb.info/jalali/jalali.c
1573
	 *
1574
	 * @param string $ts
1575
	 *
1576
	 * @return int[]
1577
	 */
1578
	private static function tsToIranian( $ts ) {
1579
		$gy = substr( $ts, 0, 4 ) -1600;
1580
		$gm = substr( $ts, 4, 2 ) -1;
1581
		$gd = substr( $ts, 6, 2 ) -1;
1582
1583
		# Days passed from the beginning (including leap years)
1584
		$gDayNo = 365 * $gy
1585
			+ floor( ( $gy + 3 ) / 4 )
1586
			- floor( ( $gy + 99 ) / 100 )
1587
			+ floor( ( $gy + 399 ) / 400 );
1588
1589
		// Add days of the past months of this year
1590
		for ( $i = 0; $i < $gm; $i++ ) {
1591
			$gDayNo += self::$GREG_DAYS[$i];
1592
		}
1593
1594
		// Leap years
1595
		if ( $gm > 1 && ( ( $gy % 4 === 0 && $gy % 100 !== 0 || ( $gy % 400 == 0 ) ) ) ) {
1596
			$gDayNo++;
1597
		}
1598
1599
		// Days passed in current month
1600
		$gDayNo += (int)$gd;
1601
1602
		$jDayNo = $gDayNo - 79;
1603
1604
		$jNp = floor( $jDayNo / 12053 );
1605
		$jDayNo %= 12053;
1606
1607
		$jy = 979 + 33 * $jNp + 4 * floor( $jDayNo / 1461 );
1608
		$jDayNo %= 1461;
1609
1610
		if ( $jDayNo >= 366 ) {
1611
			$jy += floor( ( $jDayNo - 1 ) / 365 );
1612
			$jDayNo = floor( ( $jDayNo - 1 ) % 365 );
1613
		}
1614
1615
		$jz = $jDayNo;
1616
1617
		for ( $i = 0; $i < 11 && $jDayNo >= self::$IRANIAN_DAYS[$i]; $i++ ) {
1618
			$jDayNo -= self::$IRANIAN_DAYS[$i];
1619
		}
1620
1621
		$jm = $i + 1;
1622
		$jd = $jDayNo + 1;
1623
1624
		return [ $jy, $jm, $jd, $jz ];
1625
	}
1626
1627
	/**
1628
	 * Converting Gregorian dates to Hijri dates.
1629
	 *
1630
	 * Based on a PHP-Nuke block by Sharjeel which is released under GNU/GPL license
1631
	 *
1632
	 * @see https://phpnuke.org/modules.php?name=News&file=article&sid=8234&mode=thread&order=0&thold=0
1633
	 *
1634
	 * @param string $ts
1635
	 *
1636
	 * @return int[]
1637
	 */
1638
	private static function tsToHijri( $ts ) {
1639
		$year = substr( $ts, 0, 4 );
1640
		$month = substr( $ts, 4, 2 );
1641
		$day = substr( $ts, 6, 2 );
1642
1643
		$zyr = $year;
1644
		$zd = $day;
1645
		$zm = $month;
1646
		$zy = $zyr;
1647
1648
		if (
1649
			( $zy > 1582 ) || ( ( $zy == 1582 ) && ( $zm > 10 ) ) ||
1650
			( ( $zy == 1582 ) && ( $zm == 10 ) && ( $zd > 14 ) )
1651
		) {
1652
			$zjd = (int)( ( 1461 * ( $zy + 4800 + (int)( ( $zm - 14 ) / 12 ) ) ) / 4 ) +
1653
					(int)( ( 367 * ( $zm - 2 - 12 * ( (int)( ( $zm - 14 ) / 12 ) ) ) ) / 12 ) -
1654
					(int)( ( 3 * (int)( ( ( $zy + 4900 + (int)( ( $zm - 14 ) / 12 ) ) / 100 ) ) ) / 4 ) +
1655
					$zd - 32075;
1656
		} else {
1657
			$zjd = 367 * $zy - (int)( ( 7 * ( $zy + 5001 + (int)( ( $zm - 9 ) / 7 ) ) ) / 4 ) +
1658
								(int)( ( 275 * $zm ) / 9 ) + $zd + 1729777;
1659
		}
1660
1661
		$zl = $zjd -1948440 + 10632;
1662
		$zn = (int)( ( $zl - 1 ) / 10631 );
1663
		$zl = $zl - 10631 * $zn + 354;
1664
		$zj = ( (int)( ( 10985 - $zl ) / 5316 ) ) * ( (int)( ( 50 * $zl ) / 17719 ) ) +
1665
			( (int)( $zl / 5670 ) ) * ( (int)( ( 43 * $zl ) / 15238 ) );
1666
		$zl = $zl - ( (int)( ( 30 - $zj ) / 15 ) ) * ( (int)( ( 17719 * $zj ) / 50 ) ) -
1667
			( (int)( $zj / 16 ) ) * ( (int)( ( 15238 * $zj ) / 43 ) ) + 29;
1668
		$zm = (int)( ( 24 * $zl ) / 709 );
1669
		$zd = $zl - (int)( ( 709 * $zm ) / 24 );
1670
		$zy = 30 * $zn + $zj - 30;
1671
1672
		return [ $zy, $zm, $zd ];
1673
	}
1674
1675
	/**
1676
	 * Converting Gregorian dates to Hebrew dates.
1677
	 *
1678
	 * Based on a JavaScript code by Abu Mami and Yisrael Hersch
1679
	 * ([email protected], http://www.kaluach.net), who permitted
1680
	 * to translate the relevant functions into PHP and release them under
1681
	 * GNU GPL.
1682
	 *
1683
	 * The months are counted from Tishrei = 1. In a leap year, Adar I is 13
1684
	 * and Adar II is 14. In a non-leap year, Adar is 6.
1685
	 *
1686
	 * @param string $ts
1687
	 *
1688
	 * @return int[]
1689
	 */
1690
	private static function tsToHebrew( $ts ) {
1691
		# Parse date
1692
		$year = substr( $ts, 0, 4 );
1693
		$month = substr( $ts, 4, 2 );
1694
		$day = substr( $ts, 6, 2 );
1695
1696
		# Calculate Hebrew year
1697
		$hebrewYear = $year + 3760;
1698
1699
		# Month number when September = 1, August = 12
1700
		$month += 4;
1701
		if ( $month > 12 ) {
1702
			# Next year
1703
			$month -= 12;
1704
			$year++;
1705
			$hebrewYear++;
1706
		}
1707
1708
		# Calculate day of year from 1 September
1709
		$dayOfYear = $day;
1710
		for ( $i = 1; $i < $month; $i++ ) {
1711
			if ( $i == 6 ) {
1712
				# February
1713
				$dayOfYear += 28;
1714
				# Check if the year is leap
1715 View Code Duplication
				if ( $year % 400 == 0 || ( $year % 4 == 0 && $year % 100 > 0 ) ) {
1716
					$dayOfYear++;
1717
				}
1718
			} elseif ( $i == 8 || $i == 10 || $i == 1 || $i == 3 ) {
1719
				$dayOfYear += 30;
1720
			} else {
1721
				$dayOfYear += 31;
1722
			}
1723
		}
1724
1725
		# Calculate the start of the Hebrew year
1726
		$start = self::hebrewYearStart( $hebrewYear );
1727
1728
		# Calculate next year's start
1729
		if ( $dayOfYear <= $start ) {
1730
			# Day is before the start of the year - it is the previous year
1731
			# Next year's start
1732
			$nextStart = $start;
1733
			# Previous year
1734
			$year--;
1735
			$hebrewYear--;
1736
			# Add days since previous year's 1 September
1737
			$dayOfYear += 365;
1738 View Code Duplication
			if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1739
				# Leap year
1740
				$dayOfYear++;
1741
			}
1742
			# Start of the new (previous) year
1743
			$start = self::hebrewYearStart( $hebrewYear );
1744
		} else {
1745
			# Next year's start
1746
			$nextStart = self::hebrewYearStart( $hebrewYear + 1 );
1747
		}
1748
1749
		# Calculate Hebrew day of year
1750
		$hebrewDayOfYear = $dayOfYear - $start;
1751
1752
		# Difference between year's days
1753
		$diff = $nextStart - $start;
1754
		# Add 12 (or 13 for leap years) days to ignore the difference between
1755
		# Hebrew and Gregorian year (353 at least vs. 365/6) - now the
1756
		# difference is only about the year type
1757
		if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1758
			$diff += 13;
1759
		} else {
1760
			$diff += 12;
1761
		}
1762
1763
		# Check the year pattern, and is leap year
1764
		# 0 means an incomplete year, 1 means a regular year, 2 means a complete year
1765
		# This is mod 30, to work on both leap years (which add 30 days of Adar I)
1766
		# and non-leap years
1767
		$yearPattern = $diff % 30;
1768
		# Check if leap year
1769
		$isLeap = $diff >= 30;
1770
1771
		# Calculate day in the month from number of day in the Hebrew year
1772
		# Don't check Adar - if the day is not in Adar, we will stop before;
1773
		# if it is in Adar, we will use it to check if it is Adar I or Adar II
1774
		$hebrewDay = $hebrewDayOfYear;
1775
		$hebrewMonth = 1;
1776
		$days = 0;
1777
		while ( $hebrewMonth <= 12 ) {
1778
			# Calculate days in this month
1779
			if ( $isLeap && $hebrewMonth == 6 ) {
1780
				# Adar in a leap year
1781
				if ( $isLeap ) {
1782
					# Leap year - has Adar I, with 30 days, and Adar II, with 29 days
1783
					$days = 30;
1784
					if ( $hebrewDay <= $days ) {
1785
						# Day in Adar I
1786
						$hebrewMonth = 13;
1787
					} else {
1788
						# Subtract the days of Adar I
1789
						$hebrewDay -= $days;
1790
						# Try Adar II
1791
						$days = 29;
1792
						if ( $hebrewDay <= $days ) {
1793
							# Day in Adar II
1794
							$hebrewMonth = 14;
1795
						}
1796
					}
1797
				}
1798
			} elseif ( $hebrewMonth == 2 && $yearPattern == 2 ) {
1799
				# Cheshvan in a complete year (otherwise as the rule below)
1800
				$days = 30;
1801
			} elseif ( $hebrewMonth == 3 && $yearPattern == 0 ) {
1802
				# Kislev in an incomplete year (otherwise as the rule below)
1803
				$days = 29;
1804
			} else {
1805
				# Odd months have 30 days, even have 29
1806
				$days = 30 - ( $hebrewMonth - 1 ) % 2;
1807
			}
1808
			if ( $hebrewDay <= $days ) {
1809
				# In the current month
1810
				break;
1811
			} else {
1812
				# Subtract the days of the current month
1813
				$hebrewDay -= $days;
1814
				# Try in the next month
1815
				$hebrewMonth++;
1816
			}
1817
		}
1818
1819
		return [ $hebrewYear, $hebrewMonth, $hebrewDay, $days ];
1820
	}
1821
1822
	/**
1823
	 * This calculates the Hebrew year start, as days since 1 September.
1824
	 * Based on Carl Friedrich Gauss algorithm for finding Easter date.
1825
	 * Used for Hebrew date.
1826
	 *
1827
	 * @param int $year
1828
	 *
1829
	 * @return string
1830
	 */
1831
	private static function hebrewYearStart( $year ) {
1832
		$a = intval( ( 12 * ( $year - 1 ) + 17 ) % 19 );
1833
		$b = intval( ( $year - 1 ) % 4 );
1834
		$m = 32.044093161144 + 1.5542417966212 * $a + $b / 4.0 - 0.0031777940220923 * ( $year - 1 );
1835
		if ( $m < 0 ) {
1836
			$m--;
1837
		}
1838
		$Mar = intval( $m );
1839
		if ( $m < 0 ) {
1840
			$m++;
1841
		}
1842
		$m -= $Mar;
1843
1844
		$c = intval( ( $Mar + 3 * ( $year - 1 ) + 5 * $b + 5 ) % 7 );
1845
		if ( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
1846
			$Mar++;
1847
		} elseif ( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
1848
			$Mar += 2;
1849
		} elseif ( $c == 2 || $c == 4 || $c == 6 ) {
1850
			$Mar++;
1851
		}
1852
1853
		$Mar += intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
1854
		return $Mar;
1855
	}
1856
1857
	/**
1858
	 * Algorithm to convert Gregorian dates to Thai solar dates,
1859
	 * Minguo dates or Minguo dates.
1860
	 *
1861
	 * Link: https://en.wikipedia.org/wiki/Thai_solar_calendar
1862
	 *       https://en.wikipedia.org/wiki/Minguo_calendar
1863
	 *       https://en.wikipedia.org/wiki/Japanese_era_name
1864
	 *
1865
	 * @param string $ts 14-character timestamp
1866
	 * @param string $cName Calender name
1867
	 * @return array Converted year, month, day
1868
	 */
1869
	private static function tsToYear( $ts, $cName ) {
1870
		$gy = substr( $ts, 0, 4 );
1871
		$gm = substr( $ts, 4, 2 );
1872
		$gd = substr( $ts, 6, 2 );
1873
1874
		if ( !strcmp( $cName, 'thai' ) ) {
1875
			# Thai solar dates
1876
			# Add 543 years to the Gregorian calendar
1877
			# Months and days are identical
1878
			$gy_offset = $gy + 543;
1879
		} elseif ( ( !strcmp( $cName, 'minguo' ) ) || !strcmp( $cName, 'juche' ) ) {
1880
			# Minguo dates
1881
			# Deduct 1911 years from the Gregorian calendar
1882
			# Months and days are identical
1883
			$gy_offset = $gy - 1911;
1884
		} elseif ( !strcmp( $cName, 'tenno' ) ) {
1885
			# Nengō dates up to Meiji period
1886
			# Deduct years from the Gregorian calendar
1887
			# depending on the nengo periods
1888
			# Months and days are identical
1889
			if ( ( $gy < 1912 )
1890
				|| ( ( $gy == 1912 ) && ( $gm < 7 ) )
1891
				|| ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd < 31 ) )
1892
			) {
1893
				# Meiji period
1894
				$gy_gannen = $gy - 1868 + 1;
1895
				$gy_offset = $gy_gannen;
1896
				if ( $gy_gannen == 1 ) {
1897
					$gy_offset = '元';
1898
				}
1899
				$gy_offset = '明治' . $gy_offset;
1900
			} elseif (
1901
				( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd == 31 ) ) ||
1902
				( ( $gy == 1912 ) && ( $gm >= 8 ) ) ||
1903
				( ( $gy > 1912 ) && ( $gy < 1926 ) ) ||
1904
				( ( $gy == 1926 ) && ( $gm < 12 ) ) ||
1905
				( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd < 26 ) )
1906
			) {
1907
				# Taishō period
1908
				$gy_gannen = $gy - 1912 + 1;
1909
				$gy_offset = $gy_gannen;
1910
				if ( $gy_gannen == 1 ) {
1911
					$gy_offset = '元';
1912
				}
1913
				$gy_offset = '大正' . $gy_offset;
1914
			} elseif (
1915
				( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd >= 26 ) ) ||
1916
				( ( $gy > 1926 ) && ( $gy < 1989 ) ) ||
1917
				( ( $gy == 1989 ) && ( $gm == 1 ) && ( $gd < 8 ) )
1918
			) {
1919
				# Shōwa period
1920
				$gy_gannen = $gy - 1926 + 1;
1921
				$gy_offset = $gy_gannen;
1922
				if ( $gy_gannen == 1 ) {
1923
					$gy_offset = '元';
1924
				}
1925
				$gy_offset = '昭和' . $gy_offset;
1926
			} else {
1927
				# Heisei period
1928
				$gy_gannen = $gy - 1989 + 1;
1929
				$gy_offset = $gy_gannen;
1930
				if ( $gy_gannen == 1 ) {
1931
					$gy_offset = '元';
1932
				}
1933
				$gy_offset = '平成' . $gy_offset;
1934
			}
1935
		} else {
1936
			$gy_offset = $gy;
1937
		}
1938
1939
		return [ $gy_offset, $gm, $gd ];
1940
	}
1941
1942
	/**
1943
	 * Gets directionality of the first strongly directional codepoint, for embedBidi()
1944
	 *
1945
	 * This is the rule the BIDI algorithm uses to determine the directionality of
1946
	 * paragraphs ( http://unicode.org/reports/tr9/#The_Paragraph_Level ) and
1947
	 * FSI isolates ( http://unicode.org/reports/tr9/#Explicit_Directional_Isolates ).
1948
	 *
1949
	 * TODO: Does not handle BIDI control characters inside the text.
1950
	 * TODO: Does not handle unallocated characters.
1951
	 *
1952
	 * @param string $text Text to test
1953
	 * @return null|string Directionality ('ltr' or 'rtl') or null
1954
	 */
1955
	private static function strongDirFromContent( $text = '' ) {
1956
		if ( !preg_match( self::$strongDirRegex, $text, $matches ) ) {
1957
			return null;
1958
		}
1959
		if ( $matches[1] === '' ) {
1960
			return 'rtl';
1961
		}
1962
		return 'ltr';
1963
	}
1964
1965
	/**
1966
	 * Roman number formatting up to 10000
1967
	 *
1968
	 * @param int $num
1969
	 *
1970
	 * @return string
1971
	 */
1972
	static function romanNumeral( $num ) {
1973
		static $table = [
1974
			[ '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ],
1975
			[ '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ],
1976
			[ '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ],
1977
			[ '', 'M', 'MM', 'MMM', 'MMMM', 'MMMMM', 'MMMMMM', 'MMMMMMM',
1978
				'MMMMMMMM', 'MMMMMMMMM', 'MMMMMMMMMM' ]
1979
		];
1980
1981
		$num = intval( $num );
1982
		if ( $num > 10000 || $num <= 0 ) {
1983
			return $num;
1984
		}
1985
1986
		$s = '';
1987
		for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1988
			if ( $num >= $pow10 ) {
1989
				$s .= $table[$i][(int)floor( $num / $pow10 )];
1990
			}
1991
			$num = $num % $pow10;
1992
		}
1993
		return $s;
1994
	}
1995
1996
	/**
1997
	 * Hebrew Gematria number formatting up to 9999
1998
	 *
1999
	 * @param int $num
2000
	 *
2001
	 * @return string
2002
	 */
2003
	static function hebrewNumeral( $num ) {
2004
		static $table = [
2005
			[ '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ],
2006
			[ '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ],
2007
			[ '',
2008
				[ 'ק' ],
2009
				[ 'ר' ],
2010
				[ 'ש' ],
2011
				[ 'ת' ],
2012
				[ 'ת', 'ק' ],
2013
				[ 'ת', 'ר' ],
2014
				[ 'ת', 'ש' ],
2015
				[ 'ת', 'ת' ],
2016
				[ 'ת', 'ת', 'ק' ],
2017
				[ 'ת', 'ת', 'ר' ],
2018
			],
2019
			[ '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ]
2020
		];
2021
2022
		$num = intval( $num );
2023
		if ( $num > 9999 || $num <= 0 ) {
2024
			return $num;
2025
		}
2026
2027
		// Round thousands have special notations
2028
		if ( $num === 1000 ) {
2029
			return "א' אלף";
2030
		} elseif ( $num % 1000 === 0 ) {
2031
			return $table[0][$num / 1000] . "' אלפים";
2032
		}
2033
2034
		$letters = [];
2035
2036
		for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
2037
			if ( $num >= $pow10 ) {
2038
				if ( $num === 15 || $num === 16 ) {
2039
					$letters[] = $table[0][9];
2040
					$letters[] = $table[0][$num - 9];
2041
					$num = 0;
2042
				} else {
2043
					$letters = array_merge(
2044
						$letters,
2045
						(array)$table[$i][intval( $num / $pow10 )]
2046
					);
2047
2048
					if ( $pow10 === 1000 ) {
2049
						$letters[] = "'";
2050
					}
2051
				}
2052
			}
2053
2054
			$num = $num % $pow10;
2055
		}
2056
2057
		$preTransformLength = count( $letters );
2058
		if ( $preTransformLength === 1 ) {
2059
			// Add geresh (single quote) to one-letter numbers
2060
			$letters[] = "'";
2061
		} else {
2062
			$lastIndex = $preTransformLength - 1;
2063
			$letters[$lastIndex] = str_replace(
2064
				[ 'כ', 'מ', 'נ', 'פ', 'צ' ],
2065
				[ 'ך', 'ם', 'ן', 'ף', 'ץ' ],
2066
				$letters[$lastIndex]
2067
			);
2068
2069
			// Add gershayim (double quote) to multiple-letter numbers,
2070
			// but exclude numbers with only one letter after the thousands
2071
			// (1001-1009, 1020, 1030, 2001-2009, etc.)
2072
			if ( $letters[1] === "'" && $preTransformLength === 3 ) {
2073
				$letters[] = "'";
2074
			} else {
2075
				array_splice( $letters, -1, 0, '"' );
2076
			}
2077
		}
2078
2079
		return implode( $letters );
2080
	}
2081
2082
	/**
2083
	 * Used by date() and time() to adjust the time output.
2084
	 *
2085
	 * @param string $ts The time in date('YmdHis') format
2086
	 * @param mixed $tz Adjust the time by this amount (default false, mean we
2087
	 *   get user timecorrection setting)
2088
	 * @return int
2089
	 */
2090
	public function userAdjust( $ts, $tz = false ) {
2091
		global $wgUser, $wgLocalTZoffset;
2092
2093
		if ( $tz === false ) {
2094
			$tz = $wgUser->getOption( 'timecorrection' );
2095
		}
2096
2097
		$data = explode( '|', $tz, 3 );
2098
2099
		if ( $data[0] == 'ZoneInfo' ) {
2100
			MediaWiki\suppressWarnings();
2101
			$userTZ = timezone_open( $data[2] );
2102
			MediaWiki\restoreWarnings();
2103
			if ( $userTZ !== false ) {
2104
				$date = date_create( $ts, timezone_open( 'UTC' ) );
2105
				date_timezone_set( $date, $userTZ );
2106
				$date = date_format( $date, 'YmdHis' );
2107
				return $date;
2108
			}
2109
			# Unrecognized timezone, default to 'Offset' with the stored offset.
2110
			$data[0] = 'Offset';
2111
		}
2112
2113
		if ( $data[0] == 'System' || $tz == '' ) {
2114
			# Global offset in minutes.
2115
			$minDiff = $wgLocalTZoffset;
2116
		} elseif ( $data[0] == 'Offset' ) {
2117
			$minDiff = intval( $data[1] );
2118 View Code Duplication
		} else {
2119
			$data = explode( ':', $tz );
2120
			if ( count( $data ) == 2 ) {
2121
				$data[0] = intval( $data[0] );
2122
				$data[1] = intval( $data[1] );
2123
				$minDiff = abs( $data[0] ) * 60 + $data[1];
2124
				if ( $data[0] < 0 ) {
2125
					$minDiff = -$minDiff;
2126
				}
2127
			} else {
2128
				$minDiff = intval( $data[0] ) * 60;
2129
			}
2130
		}
2131
2132
		# No difference ? Return time unchanged
2133
		if ( 0 == $minDiff ) {
2134
			return $ts;
2135
		}
2136
2137
		MediaWiki\suppressWarnings(); // E_STRICT system time bitching
2138
		# Generate an adjusted date; take advantage of the fact that mktime
2139
		# will normalize out-of-range values so we don't have to split $minDiff
2140
		# into hours and minutes.
2141
		$t = mktime( (
2142
			(int)substr( $ts, 8, 2 ) ), # Hours
2143
			(int)substr( $ts, 10, 2 ) + $minDiff, # Minutes
2144
			(int)substr( $ts, 12, 2 ), # Seconds
2145
			(int)substr( $ts, 4, 2 ), # Month
2146
			(int)substr( $ts, 6, 2 ), # Day
2147
			(int)substr( $ts, 0, 4 ) ); # Year
2148
2149
		$date = date( 'YmdHis', $t );
2150
		MediaWiki\restoreWarnings();
2151
2152
		return $date;
2153
	}
2154
2155
	/**
2156
	 * This is meant to be used by time(), date(), and timeanddate() to get
2157
	 * the date preference they're supposed to use, it should be used in
2158
	 * all children.
2159
	 *
2160
	 *<code>
2161
	 * function timeanddate([...], $format = true) {
2162
	 * 	$datePreference = $this->dateFormat($format);
2163
	 * [...]
2164
	 * }
2165
	 *</code>
2166
	 *
2167
	 * @param int|string|bool $usePrefs If true, the user's preference is used
2168
	 *   if false, the site/language default is used
2169
	 *   if int/string, assumed to be a format.
2170
	 * @return string
2171
	 */
2172
	function dateFormat( $usePrefs = true ) {
2173
		global $wgUser;
2174
2175
		if ( is_bool( $usePrefs ) ) {
2176
			if ( $usePrefs ) {
2177
				$datePreference = $wgUser->getDatePreference();
2178
			} else {
2179
				$datePreference = (string)User::getDefaultOption( 'date' );
2180
			}
2181
		} else {
2182
			$datePreference = (string)$usePrefs;
2183
		}
2184
2185
		// return int
2186
		if ( $datePreference == '' ) {
2187
			return 'default';
2188
		}
2189
2190
		return $datePreference;
2191
	}
2192
2193
	/**
2194
	 * Get a format string for a given type and preference
2195
	 * @param string $type May be 'date', 'time', 'both', or 'pretty'.
2196
	 * @param string $pref The format name as it appears in Messages*.php under
2197
	 *  $datePreferences.
2198
	 *
2199
	 * @since 1.22 New type 'pretty' that provides a more readable timestamp format
2200
	 *
2201
	 * @return string
2202
	 */
2203
	function getDateFormatString( $type, $pref ) {
2204
		$wasDefault = false;
2205
		if ( $pref == 'default' ) {
2206
			$wasDefault = true;
2207
			$pref = $this->getDefaultDateFormat();
2208
		}
2209
2210
		if ( !isset( $this->dateFormatStrings[$type][$pref] ) ) {
2211
			$df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2212
2213
			if ( $type === 'pretty' && $df === null ) {
2214
				$df = $this->getDateFormatString( 'date', $pref );
0 ignored issues
show
Bug introduced by
It seems like $pref defined by $this->getDefaultDateFormat() on line 2207 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...
2215
			}
2216
2217
			if ( !$wasDefault && $df === null ) {
2218
				$pref = $this->getDefaultDateFormat();
2219
				$df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2220
			}
2221
2222
			$this->dateFormatStrings[$type][$pref] = $df;
2223
		}
2224
		return $this->dateFormatStrings[$type][$pref];
2225
	}
2226
2227
	/**
2228
	 * @param string $ts The time format which needs to be turned into a
2229
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2230
	 * @param bool $adj Whether to adjust the time output according to the
2231
	 *   user configured offset ($timecorrection)
2232
	 * @param mixed $format True to use user's date format preference
2233
	 * @param string|bool $timecorrection The time offset as returned by
2234
	 *   validateTimeZone() in Special:Preferences
2235
	 * @return string
2236
	 */
2237 View Code Duplication
	public function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
2238
		$ts = wfTimestamp( TS_MW, $ts );
2239
		if ( $adj ) {
2240
			$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 2240 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...
2241
		}
2242
		$df = $this->getDateFormatString( 'date', $this->dateFormat( $format ) );
2243
		return $this->sprintfDate( $df, $ts );
2244
	}
2245
2246
	/**
2247
	 * @param string $ts The time format which needs to be turned into a
2248
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2249
	 * @param bool $adj Whether to adjust the time output according to the
2250
	 *   user configured offset ($timecorrection)
2251
	 * @param mixed $format True to use user's date format preference
2252
	 * @param string|bool $timecorrection The time offset as returned by
2253
	 *   validateTimeZone() in Special:Preferences
2254
	 * @return string
2255
	 */
2256 View Code Duplication
	public function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
2257
		$ts = wfTimestamp( TS_MW, $ts );
2258
		if ( $adj ) {
2259
			$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 2259 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...
2260
		}
2261
		$df = $this->getDateFormatString( 'time', $this->dateFormat( $format ) );
2262
		return $this->sprintfDate( $df, $ts );
2263
	}
2264
2265
	/**
2266
	 * @param string $ts The time format which needs to be turned into a
2267
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2268
	 * @param bool $adj Whether to adjust the time output according to the
2269
	 *   user configured offset ($timecorrection)
2270
	 * @param mixed $format What format to return, if it's false output the
2271
	 *   default one (default true)
2272
	 * @param string|bool $timecorrection The time offset as returned by
2273
	 *   validateTimeZone() in Special:Preferences
2274
	 * @return string
2275
	 */
2276 View Code Duplication
	public function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false ) {
2277
		$ts = wfTimestamp( TS_MW, $ts );
2278
		if ( $adj ) {
2279
			$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 2279 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...
2280
		}
2281
		$df = $this->getDateFormatString( 'both', $this->dateFormat( $format ) );
2282
		return $this->sprintfDate( $df, $ts );
2283
	}
2284
2285
	/**
2286
	 * Takes a number of seconds and turns it into a text using values such as hours and minutes.
2287
	 *
2288
	 * @since 1.20
2289
	 *
2290
	 * @param int $seconds The amount of seconds.
2291
	 * @param array $chosenIntervals The intervals to enable.
2292
	 *
2293
	 * @return string
2294
	 */
2295
	public function formatDuration( $seconds, array $chosenIntervals = [] ) {
2296
		$intervals = $this->getDurationIntervals( $seconds, $chosenIntervals );
2297
2298
		$segments = [];
2299
2300 View Code Duplication
		foreach ( $intervals as $intervalName => $intervalValue ) {
2301
			// Messages: duration-seconds, duration-minutes, duration-hours, duration-days, duration-weeks,
2302
			// duration-years, duration-decades, duration-centuries, duration-millennia
2303
			$message = wfMessage( 'duration-' . $intervalName )->numParams( $intervalValue );
2304
			$segments[] = $message->inLanguage( $this )->escaped();
2305
		}
2306
2307
		return $this->listToText( $segments );
2308
	}
2309
2310
	/**
2311
	 * Takes a number of seconds and returns an array with a set of corresponding intervals.
2312
	 * For example 65 will be turned into [ minutes => 1, seconds => 5 ].
2313
	 *
2314
	 * @since 1.20
2315
	 *
2316
	 * @param int $seconds The amount of seconds.
2317
	 * @param array $chosenIntervals The intervals to enable.
2318
	 *
2319
	 * @return array
2320
	 */
2321
	public function getDurationIntervals( $seconds, array $chosenIntervals = [] ) {
2322 View Code Duplication
		if ( empty( $chosenIntervals ) ) {
2323
			$chosenIntervals = [
2324
				'millennia',
2325
				'centuries',
2326
				'decades',
2327
				'years',
2328
				'days',
2329
				'hours',
2330
				'minutes',
2331
				'seconds'
2332
			];
2333
		}
2334
2335
		$intervals = array_intersect_key( self::$durationIntervals, array_flip( $chosenIntervals ) );
2336
		$sortedNames = array_keys( $intervals );
2337
		$smallestInterval = array_pop( $sortedNames );
2338
2339
		$segments = [];
2340
2341
		foreach ( $intervals as $name => $length ) {
2342
			$value = floor( $seconds / $length );
2343
2344
			if ( $value > 0 || ( $name == $smallestInterval && empty( $segments ) ) ) {
2345
				$seconds -= $value * $length;
2346
				$segments[$name] = $value;
2347
			}
2348
		}
2349
2350
		return $segments;
2351
	}
2352
2353
	/**
2354
	 * Internal helper function for userDate(), userTime() and userTimeAndDate()
2355
	 *
2356
	 * @param string $type Can be 'date', 'time' or 'both'
2357
	 * @param string $ts The time format which needs to be turned into a
2358
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2359
	 * @param User $user User object used to get preferences for timezone and format
2360
	 * @param array $options Array, can contain the following keys:
2361
	 *   - 'timecorrection': time correction, can have the following values:
2362
	 *     - true: use user's preference
2363
	 *     - false: don't use time correction
2364
	 *     - int: value of time correction in minutes
2365
	 *   - 'format': format to use, can have the following values:
2366
	 *     - true: use user's preference
2367
	 *     - false: use default preference
2368
	 *     - string: format to use
2369
	 * @since 1.19
2370
	 * @return string
2371
	 */
2372
	private function internalUserTimeAndDate( $type, $ts, User $user, array $options ) {
2373
		$ts = wfTimestamp( TS_MW, $ts );
2374
		$options += [ 'timecorrection' => true, 'format' => true ];
2375
		if ( $options['timecorrection'] !== false ) {
2376
			if ( $options['timecorrection'] === true ) {
2377
				$offset = $user->getOption( 'timecorrection' );
2378
			} else {
2379
				$offset = $options['timecorrection'];
2380
			}
2381
			$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 2381 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...
2382
		}
2383
		if ( $options['format'] === true ) {
2384
			$format = $user->getDatePreference();
2385
		} else {
2386
			$format = $options['format'];
2387
		}
2388
		$df = $this->getDateFormatString( $type, $this->dateFormat( $format ) );
2389
		return $this->sprintfDate( $df, $ts );
2390
	}
2391
2392
	/**
2393
	 * Get the formatted date for the given timestamp and formatted for
2394
	 * the given user.
2395
	 *
2396
	 * @param mixed $ts Mixed: the time format which needs to be turned into a
2397
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2398
	 * @param User $user User object used to get preferences for timezone and format
2399
	 * @param array $options Array, can contain the following keys:
2400
	 *   - 'timecorrection': time correction, can have the following values:
2401
	 *     - true: use user's preference
2402
	 *     - false: don't use time correction
2403
	 *     - int: value of time correction in minutes
2404
	 *   - 'format': format to use, can have the following values:
2405
	 *     - true: use user's preference
2406
	 *     - false: use default preference
2407
	 *     - string: format to use
2408
	 * @since 1.19
2409
	 * @return string
2410
	 */
2411
	public function userDate( $ts, User $user, array $options = [] ) {
2412
		return $this->internalUserTimeAndDate( 'date', $ts, $user, $options );
2413
	}
2414
2415
	/**
2416
	 * Get the formatted time for the given timestamp and formatted for
2417
	 * the given user.
2418
	 *
2419
	 * @param mixed $ts The time format which needs to be turned into a
2420
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2421
	 * @param User $user User object used to get preferences for timezone and format
2422
	 * @param array $options Array, can contain the following keys:
2423
	 *   - 'timecorrection': time correction, can have the following values:
2424
	 *     - true: use user's preference
2425
	 *     - false: don't use time correction
2426
	 *     - int: value of time correction in minutes
2427
	 *   - 'format': format to use, can have the following values:
2428
	 *     - true: use user's preference
2429
	 *     - false: use default preference
2430
	 *     - string: format to use
2431
	 * @since 1.19
2432
	 * @return string
2433
	 */
2434
	public function userTime( $ts, User $user, array $options = [] ) {
2435
		return $this->internalUserTimeAndDate( 'time', $ts, $user, $options );
2436
	}
2437
2438
	/**
2439
	 * Get the formatted date and time for the given timestamp and formatted for
2440
	 * the given user.
2441
	 *
2442
	 * @param mixed $ts The time format which needs to be turned into a
2443
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2444
	 * @param User $user User object used to get preferences for timezone and format
2445
	 * @param array $options Array, can contain the following keys:
2446
	 *   - 'timecorrection': time correction, can have the following values:
2447
	 *     - true: use user's preference
2448
	 *     - false: don't use time correction
2449
	 *     - int: value of time correction in minutes
2450
	 *   - 'format': format to use, can have the following values:
2451
	 *     - true: use user's preference
2452
	 *     - false: use default preference
2453
	 *     - string: format to use
2454
	 * @since 1.19
2455
	 * @return string
2456
	 */
2457
	public function userTimeAndDate( $ts, User $user, array $options = [] ) {
2458
		return $this->internalUserTimeAndDate( 'both', $ts, $user, $options );
2459
	}
2460
2461
	/**
2462
	 * Get the timestamp in a human-friendly relative format, e.g., "3 days ago".
2463
	 *
2464
	 * Determine the difference between the timestamp and the current time, and
2465
	 * generate a readable timestamp by returning "<N> <units> ago", where the
2466
	 * largest possible unit is used.
2467
	 *
2468
	 * @since 1.26 (Prior to 1.26 method existed but was not meant to be used directly)
2469
	 *
2470
	 * @param MWTimestamp $time
2471
	 * @param MWTimestamp|null $relativeTo The base timestamp to compare to (defaults to now)
2472
	 * @param User|null $user User the timestamp is being generated for
2473
	 *  (or null to use main context's user)
2474
	 * @return string Formatted timestamp
2475
	 */
2476
	public function getHumanTimestamp(
2477
		MWTimestamp $time, MWTimestamp $relativeTo = null, User $user = null
2478
	) {
2479
		if ( $relativeTo === null ) {
2480
			$relativeTo = new MWTimestamp();
2481
		}
2482
		if ( $user === null ) {
2483
			$user = RequestContext::getMain()->getUser();
2484
		}
2485
2486
		// Adjust for the user's timezone.
2487
		$offsetThis = $time->offsetForUser( $user );
2488
		$offsetRel = $relativeTo->offsetForUser( $user );
2489
2490
		$ts = '';
2491
		if ( Hooks::run( 'GetHumanTimestamp', [ &$ts, $time, $relativeTo, $user, $this ] ) ) {
2492
			$ts = $this->getHumanTimestampInternal( $time, $relativeTo, $user );
2493
		}
2494
2495
		// Reset the timezone on the objects.
2496
		$time->timestamp->sub( $offsetThis );
2497
		$relativeTo->timestamp->sub( $offsetRel );
2498
2499
		return $ts;
2500
	}
2501
2502
	/**
2503
	 * Convert an MWTimestamp into a pretty human-readable timestamp using
2504
	 * the given user preferences and relative base time.
2505
	 *
2506
	 * @see Language::getHumanTimestamp
2507
	 * @param MWTimestamp $ts Timestamp to prettify
2508
	 * @param MWTimestamp $relativeTo Base timestamp
2509
	 * @param User $user User preferences to use
2510
	 * @return string Human timestamp
2511
	 * @since 1.26
2512
	 */
2513
	private function getHumanTimestampInternal(
2514
		MWTimestamp $ts, MWTimestamp $relativeTo, User $user
2515
	) {
2516
		$diff = $ts->diff( $relativeTo );
2517
		$diffDay = (bool)( (int)$ts->timestamp->format( 'w' ) -
2518
			(int)$relativeTo->timestamp->format( 'w' ) );
2519
		$days = $diff->days ?: (int)$diffDay;
2520
		if ( $diff->invert || $days > 5
2521
			&& $ts->timestamp->format( 'Y' ) !== $relativeTo->timestamp->format( 'Y' )
2522
		) {
2523
			// Timestamps are in different years: use full timestamp
2524
			// Also do full timestamp for future dates
2525
			/**
2526
			 * @todo FIXME: Add better handling of future timestamps.
2527
			 */
2528
			$format = $this->getDateFormatString( 'both', $user->getDatePreference() ?: 'default' );
2529
			$ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
2530
		} elseif ( $days > 5 ) {
2531
			// Timestamps are in same year,  but more than 5 days ago: show day and month only.
2532
			$format = $this->getDateFormatString( 'pretty', $user->getDatePreference() ?: 'default' );
2533
			$ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
2534
		} elseif ( $days > 1 ) {
2535
			// Timestamp within the past week: show the day of the week and time
2536
			$format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2537
			$weekday = self::$mWeekdayMsgs[$ts->timestamp->format( 'w' )];
2538
			// Messages:
2539
			// sunday-at, monday-at, tuesday-at, wednesday-at, thursday-at, friday-at, saturday-at
2540
			$ts = wfMessage( "$weekday-at" )
2541
				->inLanguage( $this )
2542
				->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2543
				->text();
2544
		} elseif ( $days == 1 ) {
2545
			// Timestamp was yesterday: say 'yesterday' and the time.
2546
			$format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2547
			$ts = wfMessage( 'yesterday-at' )
2548
				->inLanguage( $this )
2549
				->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2550
				->text();
2551
		} elseif ( $diff->h > 1 || $diff->h == 1 && $diff->i > 30 ) {
2552
			// Timestamp was today, but more than 90 minutes ago: say 'today' and the time.
2553
			$format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2554
			$ts = wfMessage( 'today-at' )
2555
				->inLanguage( $this )
2556
				->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2557
				->text();
2558
2559
		// From here on in, the timestamp was soon enough ago so that we can simply say
2560
		// XX units ago, e.g., "2 hours ago" or "5 minutes ago"
2561
		} elseif ( $diff->h == 1 ) {
2562
			// Less than 90 minutes, but more than an hour ago.
2563
			$ts = wfMessage( 'hours-ago' )->inLanguage( $this )->numParams( 1 )->text();
2564
		} elseif ( $diff->i >= 1 ) {
2565
			// A few minutes ago.
2566
			$ts = wfMessage( 'minutes-ago' )->inLanguage( $this )->numParams( $diff->i )->text();
2567
		} elseif ( $diff->s >= 30 ) {
2568
			// Less than a minute, but more than 30 sec ago.
2569
			$ts = wfMessage( 'seconds-ago' )->inLanguage( $this )->numParams( $diff->s )->text();
2570
		} else {
2571
			// Less than 30 seconds ago.
2572
			$ts = wfMessage( 'just-now' )->text();
2573
		}
2574
2575
		return $ts;
2576
	}
2577
2578
	/**
2579
	 * @param string $key
2580
	 * @return array|null
2581
	 */
2582
	public function getMessage( $key ) {
2583
		return self::$dataCache->getSubitem( $this->mCode, 'messages', $key );
2584
	}
2585
2586
	/**
2587
	 * @return array
2588
	 */
2589
	function getAllMessages() {
2590
		return self::$dataCache->getItem( $this->mCode, 'messages' );
2591
	}
2592
2593
	/**
2594
	 * @param string $in
2595
	 * @param string $out
2596
	 * @param string $string
2597
	 * @return string
2598
	 */
2599
	public function iconv( $in, $out, $string ) {
2600
		# Even with //IGNORE iconv can whine about illegal characters in
2601
		# *input* string. We just ignore those too.
2602
		# REF: https://bugs.php.net/bug.php?id=37166
2603
		# REF: https://phabricator.wikimedia.org/T18885
2604
		MediaWiki\suppressWarnings();
2605
		$text = iconv( $in, $out . '//IGNORE', $string );
2606
		MediaWiki\restoreWarnings();
2607
		return $text;
2608
	}
2609
2610
	// callback functions for ucwords(), ucwordbreaks()
2611
2612
	/**
2613
	 * @param array $matches
2614
	 * @return mixed|string
2615
	 */
2616
	function ucwordbreaksCallbackAscii( $matches ) {
2617
		return $this->ucfirst( $matches[1] );
2618
	}
2619
2620
	/**
2621
	 * @param array $matches
2622
	 * @return string
2623
	 */
2624
	function ucwordbreaksCallbackMB( $matches ) {
2625
		return mb_strtoupper( $matches[0] );
2626
	}
2627
2628
	/**
2629
	 * @param array $matches
2630
	 * @return string
2631
	 */
2632
	function ucwordsCallbackMB( $matches ) {
2633
		return mb_strtoupper( $matches[0] );
2634
	}
2635
2636
	/**
2637
	 * Make a string's first character uppercase
2638
	 *
2639
	 * @param string $str
2640
	 *
2641
	 * @return string
2642
	 */
2643
	public function ucfirst( $str ) {
2644
		$o = ord( $str );
2645
		if ( $o < 96 ) { // if already uppercase...
2646
			return $str;
2647
		} elseif ( $o < 128 ) {
2648
			return ucfirst( $str ); // use PHP's ucfirst()
2649
		} else {
2650
			// fall back to more complex logic in case of multibyte strings
2651
			return $this->uc( $str, true );
2652
		}
2653
	}
2654
2655
	/**
2656
	 * Convert a string to uppercase
2657
	 *
2658
	 * @param string $str
2659
	 * @param bool $first
2660
	 *
2661
	 * @return string
2662
	 */
2663
	public function uc( $str, $first = false ) {
2664
		if ( $first ) {
2665
			if ( $this->isMultibyte( $str ) ) {
2666
				return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2667
			} else {
2668
				return ucfirst( $str );
2669
			}
2670
		} else {
2671
			return $this->isMultibyte( $str ) ? mb_strtoupper( $str ) : strtoupper( $str );
2672
		}
2673
	}
2674
2675
	/**
2676
	 * @param string $str
2677
	 * @return mixed|string
2678
	 */
2679
	function lcfirst( $str ) {
2680
		$o = ord( $str );
2681
		if ( !$o ) {
2682
			return strval( $str );
2683
		} elseif ( $o >= 128 ) {
2684
			return $this->lc( $str, true );
2685
		} elseif ( $o > 96 ) {
2686
			return $str;
2687
		} else {
2688
			$str[0] = strtolower( $str[0] );
2689
			return $str;
2690
		}
2691
	}
2692
2693
	/**
2694
	 * @param string $str
2695
	 * @param bool $first
2696
	 * @return mixed|string
2697
	 */
2698
	function lc( $str, $first = false ) {
2699
		if ( $first ) {
2700
			if ( $this->isMultibyte( $str ) ) {
2701
				return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2702
			} else {
2703
				return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 );
2704
			}
2705
		} else {
2706
			return $this->isMultibyte( $str ) ? mb_strtolower( $str ) : strtolower( $str );
2707
		}
2708
	}
2709
2710
	/**
2711
	 * @param string $str
2712
	 * @return bool
2713
	 */
2714
	function isMultibyte( $str ) {
2715
		return strlen( $str ) !== mb_strlen( $str );
2716
	}
2717
2718
	/**
2719
	 * @param string $str
2720
	 * @return mixed|string
2721
	 */
2722
	function ucwords( $str ) {
2723
		if ( $this->isMultibyte( $str ) ) {
2724
			$str = $this->lc( $str );
2725
2726
			// regexp to find first letter in each word (i.e. after each space)
2727
			$replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2728
2729
			// function to use to capitalize a single char
2730
			return preg_replace_callback(
2731
				$replaceRegexp,
2732
				[ $this, 'ucwordsCallbackMB' ],
2733
				$str
2734
			);
2735
		} else {
2736
			return ucwords( strtolower( $str ) );
2737
		}
2738
	}
2739
2740
	/**
2741
	 * capitalize words at word breaks
2742
	 *
2743
	 * @param string $str
2744
	 * @return mixed
2745
	 */
2746
	function ucwordbreaks( $str ) {
2747
		if ( $this->isMultibyte( $str ) ) {
2748
			$str = $this->lc( $str );
2749
2750
			// since \b doesn't work for UTF-8, we explicitely define word break chars
2751
			$breaks = "[ \-\(\)\}\{\.,\?!]";
2752
2753
			// find first letter after word break
2754
			$replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|" .
2755
				"$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2756
2757
			return preg_replace_callback(
2758
				$replaceRegexp,
2759
				[ $this, 'ucwordbreaksCallbackMB' ],
2760
				$str
2761
			);
2762
		} else {
2763
			return preg_replace_callback(
2764
				'/\b([\w\x80-\xff]+)\b/',
2765
				[ $this, 'ucwordbreaksCallbackAscii' ],
2766
				$str
2767
			);
2768
		}
2769
	}
2770
2771
	/**
2772
	 * Return a case-folded representation of $s
2773
	 *
2774
	 * This is a representation such that caseFold($s1)==caseFold($s2) if $s1
2775
	 * and $s2 are the same except for the case of their characters. It is not
2776
	 * necessary for the value returned to make sense when displayed.
2777
	 *
2778
	 * Do *not* perform any other normalisation in this function. If a caller
2779
	 * uses this function when it should be using a more general normalisation
2780
	 * function, then fix the caller.
2781
	 *
2782
	 * @param string $s
2783
	 *
2784
	 * @return string
2785
	 */
2786
	function caseFold( $s ) {
2787
		return $this->uc( $s );
2788
	}
2789
2790
	/**
2791
	 * @param string $s
2792
	 * @return string
2793
	 * @throws MWException
2794
	 */
2795
	function checkTitleEncoding( $s ) {
2796
		if ( is_array( $s ) ) {
2797
			throw new MWException( 'Given array to checkTitleEncoding.' );
2798
		}
2799
		if ( StringUtils::isUtf8( $s ) ) {
2800
			return $s;
2801
		}
2802
2803
		return $this->iconv( $this->fallback8bitEncoding(), 'utf-8', $s );
2804
	}
2805
2806
	/**
2807
	 * @return array
2808
	 */
2809
	function fallback8bitEncoding() {
2810
		return self::$dataCache->getItem( $this->mCode, 'fallback8bitEncoding' );
2811
	}
2812
2813
	/**
2814
	 * Most writing systems use whitespace to break up words.
2815
	 * Some languages such as Chinese don't conventionally do this,
2816
	 * which requires special handling when breaking up words for
2817
	 * searching etc.
2818
	 *
2819
	 * @return bool
2820
	 */
2821
	function hasWordBreaks() {
2822
		return true;
2823
	}
2824
2825
	/**
2826
	 * Some languages such as Chinese require word segmentation,
2827
	 * Specify such segmentation when overridden in derived class.
2828
	 *
2829
	 * @param string $string
2830
	 * @return string
2831
	 */
2832
	function segmentByWord( $string ) {
2833
		return $string;
2834
	}
2835
2836
	/**
2837
	 * Some languages have special punctuation need to be normalized.
2838
	 * Make such changes here.
2839
	 *
2840
	 * @param string $string
2841
	 * @return string
2842
	 */
2843
	function normalizeForSearch( $string ) {
2844
		return self::convertDoubleWidth( $string );
2845
	}
2846
2847
	/**
2848
	 * convert double-width roman characters to single-width.
2849
	 * range: ff00-ff5f ~= 0020-007f
2850
	 *
2851
	 * @param string $string
2852
	 *
2853
	 * @return string
2854
	 */
2855
	protected static function convertDoubleWidth( $string ) {
2856
		static $full = null;
2857
		static $half = null;
2858
2859
		if ( $full === null ) {
2860
			$fullWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2861
			$halfWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2862
			$full = str_split( $fullWidth, 3 );
2863
			$half = str_split( $halfWidth );
2864
		}
2865
2866
		$string = str_replace( $full, $half, $string );
2867
		return $string;
2868
	}
2869
2870
	/**
2871
	 * @param string $string
2872
	 * @param string $pattern
2873
	 * @return string
2874
	 */
2875
	protected static function insertSpace( $string, $pattern ) {
2876
		$string = preg_replace( $pattern, " $1 ", $string );
2877
		$string = preg_replace( '/ +/', ' ', $string );
2878
		return $string;
2879
	}
2880
2881
	/**
2882
	 * @param array $termsArray
2883
	 * @return array
2884
	 */
2885
	function convertForSearchResult( $termsArray ) {
2886
		# some languages, e.g. Chinese, need to do a conversion
2887
		# in order for search results to be displayed correctly
2888
		return $termsArray;
2889
	}
2890
2891
	/**
2892
	 * Get the first character of a string.
2893
	 *
2894
	 * @param string $s
2895
	 * @return string
2896
	 */
2897
	function firstChar( $s ) {
2898
		$matches = [];
2899
		preg_match(
2900
			'/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
2901
				'[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/',
2902
			$s,
2903
			$matches
2904
		);
2905
2906
		if ( isset( $matches[1] ) ) {
2907
			if ( strlen( $matches[1] ) != 3 ) {
2908
				return $matches[1];
2909
			}
2910
2911
			// Break down Hangul syllables to grab the first jamo
2912
			$code = UtfNormal\Utils::utf8ToCodepoint( $matches[1] );
2913
			if ( $code < 0xac00 || 0xd7a4 <= $code ) {
2914
				return $matches[1];
2915
			} elseif ( $code < 0xb098 ) {
2916
				return "\xe3\x84\xb1";
2917
			} elseif ( $code < 0xb2e4 ) {
2918
				return "\xe3\x84\xb4";
2919
			} elseif ( $code < 0xb77c ) {
2920
				return "\xe3\x84\xb7";
2921
			} elseif ( $code < 0xb9c8 ) {
2922
				return "\xe3\x84\xb9";
2923
			} elseif ( $code < 0xbc14 ) {
2924
				return "\xe3\x85\x81";
2925
			} elseif ( $code < 0xc0ac ) {
2926
				return "\xe3\x85\x82";
2927
			} elseif ( $code < 0xc544 ) {
2928
				return "\xe3\x85\x85";
2929
			} elseif ( $code < 0xc790 ) {
2930
				return "\xe3\x85\x87";
2931
			} elseif ( $code < 0xcc28 ) {
2932
				return "\xe3\x85\x88";
2933
			} elseif ( $code < 0xce74 ) {
2934
				return "\xe3\x85\x8a";
2935
			} elseif ( $code < 0xd0c0 ) {
2936
				return "\xe3\x85\x8b";
2937
			} elseif ( $code < 0xd30c ) {
2938
				return "\xe3\x85\x8c";
2939
			} elseif ( $code < 0xd558 ) {
2940
				return "\xe3\x85\x8d";
2941
			} else {
2942
				return "\xe3\x85\x8e";
2943
			}
2944
		} else {
2945
			return '';
2946
		}
2947
	}
2948
2949
	/**
2950
	 * @deprecated No-op since 1.28
2951
	 */
2952
	function initEncoding() {
2953
		// No-op.
2954
	}
2955
2956
	/**
2957
	 * @param string $s
2958
	 * @return string
2959
	 * @deprecated No-op since 1.28
2960
	 */
2961
	function recodeForEdit( $s ) {
2962
		return $s;
2963
	}
2964
2965
	/**
2966
	 * @param string $s
2967
	 * @return string
2968
	 * @deprecated No-op since 1.28
2969
	 */
2970
	function recodeInput( $s ) {
2971
		return $s;
2972
	}
2973
2974
	/**
2975
	 * Convert a UTF-8 string to normal form C. In Malayalam and Arabic, this
2976
	 * also cleans up certain backwards-compatible sequences, converting them
2977
	 * to the modern Unicode equivalent.
2978
	 *
2979
	 * This is language-specific for performance reasons only.
2980
	 *
2981
	 * @param string $s
2982
	 *
2983
	 * @return string
2984
	 */
2985
	function normalize( $s ) {
2986
		global $wgAllUnicodeFixes;
2987
		$s = UtfNormal\Validator::cleanUp( $s );
2988
		if ( $wgAllUnicodeFixes ) {
2989
			$s = $this->transformUsingPairFile( 'normalize-ar.ser', $s );
2990
			$s = $this->transformUsingPairFile( 'normalize-ml.ser', $s );
2991
		}
2992
2993
		return $s;
2994
	}
2995
2996
	/**
2997
	 * Transform a string using serialized data stored in the given file (which
2998
	 * must be in the serialized subdirectory of $IP). The file contains pairs
2999
	 * mapping source characters to destination characters.
3000
	 *
3001
	 * The data is cached in process memory. This will go faster if you have the
3002
	 * FastStringSearch extension.
3003
	 *
3004
	 * @param string $file
3005
	 * @param string $string
3006
	 *
3007
	 * @throws MWException
3008
	 * @return string
3009
	 */
3010
	function transformUsingPairFile( $file, $string ) {
3011
		if ( !isset( $this->transformData[$file] ) ) {
3012
			$data = wfGetPrecompiledData( $file );
3013
			if ( $data === false ) {
3014
				throw new MWException( __METHOD__ . ": The transformation file $file is missing" );
3015
			}
3016
			$this->transformData[$file] = new ReplacementArray( $data );
3017
		}
3018
		return $this->transformData[$file]->replace( $string );
3019
	}
3020
3021
	/**
3022
	 * For right-to-left language support
3023
	 *
3024
	 * @return bool
3025
	 */
3026
	function isRTL() {
3027
		return self::$dataCache->getItem( $this->mCode, 'rtl' );
3028
	}
3029
3030
	/**
3031
	 * Return the correct HTML 'dir' attribute value for this language.
3032
	 * @return string
3033
	 */
3034
	function getDir() {
3035
		return $this->isRTL() ? 'rtl' : 'ltr';
3036
	}
3037
3038
	/**
3039
	 * Return 'left' or 'right' as appropriate alignment for line-start
3040
	 * for this language's text direction.
3041
	 *
3042
	 * Should be equivalent to CSS3 'start' text-align value....
3043
	 *
3044
	 * @return string
3045
	 */
3046
	function alignStart() {
3047
		return $this->isRTL() ? 'right' : 'left';
3048
	}
3049
3050
	/**
3051
	 * Return 'right' or 'left' as appropriate alignment for line-end
3052
	 * for this language's text direction.
3053
	 *
3054
	 * Should be equivalent to CSS3 'end' text-align value....
3055
	 *
3056
	 * @return string
3057
	 */
3058
	function alignEnd() {
3059
		return $this->isRTL() ? 'left' : 'right';
3060
	}
3061
3062
	/**
3063
	 * A hidden direction mark (LRM or RLM), depending on the language direction.
3064
	 * Unlike getDirMark(), this function returns the character as an HTML entity.
3065
	 * This function should be used when the output is guaranteed to be HTML,
3066
	 * because it makes the output HTML source code more readable. When
3067
	 * the output is plain text or can be escaped, getDirMark() should be used.
3068
	 *
3069
	 * @param bool $opposite Get the direction mark opposite to your language
3070
	 * @return string
3071
	 * @since 1.20
3072
	 */
3073
	function getDirMarkEntity( $opposite = false ) {
3074
		if ( $opposite ) {
3075
			return $this->isRTL() ? '&lrm;' : '&rlm;';
3076
		}
3077
		return $this->isRTL() ? '&rlm;' : '&lrm;';
3078
	}
3079
3080
	/**
3081
	 * A hidden direction mark (LRM or RLM), depending on the language direction.
3082
	 * This function produces them as invisible Unicode characters and
3083
	 * the output may be hard to read and debug, so it should only be used
3084
	 * when the output is plain text or can be escaped. When the output is
3085
	 * HTML, use getDirMarkEntity() instead.
3086
	 *
3087
	 * @param bool $opposite Get the direction mark opposite to your language
3088
	 * @return string
3089
	 */
3090
	function getDirMark( $opposite = false ) {
3091
		$lrm = "\xE2\x80\x8E"; # LEFT-TO-RIGHT MARK, commonly abbreviated LRM
3092
		$rlm = "\xE2\x80\x8F"; # RIGHT-TO-LEFT MARK, commonly abbreviated RLM
3093
		if ( $opposite ) {
3094
			return $this->isRTL() ? $lrm : $rlm;
3095
		}
3096
		return $this->isRTL() ? $rlm : $lrm;
3097
	}
3098
3099
	/**
3100
	 * @return array
3101
	 */
3102
	function capitalizeAllNouns() {
3103
		return self::$dataCache->getItem( $this->mCode, 'capitalizeAllNouns' );
3104
	}
3105
3106
	/**
3107
	 * An arrow, depending on the language direction.
3108
	 *
3109
	 * @param string $direction The direction of the arrow: forwards (default),
3110
	 *   backwards, left, right, up, down.
3111
	 * @return string
3112
	 */
3113
	function getArrow( $direction = 'forwards' ) {
3114
		switch ( $direction ) {
3115
		case 'forwards':
3116
			return $this->isRTL() ? '←' : '→';
3117
		case 'backwards':
3118
			return $this->isRTL() ? '→' : '←';
3119
		case 'left':
3120
			return '←';
3121
		case 'right':
3122
			return '→';
3123
		case 'up':
3124
			return '↑';
3125
		case 'down':
3126
			return '↓';
3127
		}
3128
	}
3129
3130
	/**
3131
	 * To allow "foo[[bar]]" to extend the link over the whole word "foobar"
3132
	 *
3133
	 * @return bool
3134
	 */
3135
	function linkPrefixExtension() {
3136
		return self::$dataCache->getItem( $this->mCode, 'linkPrefixExtension' );
3137
	}
3138
3139
	/**
3140
	 * Get all magic words from cache.
3141
	 * @return array
3142
	 */
3143
	function getMagicWords() {
3144
		return self::$dataCache->getItem( $this->mCode, 'magicWords' );
3145
	}
3146
3147
	/**
3148
	 * Run the LanguageGetMagic hook once.
3149
	 */
3150
	protected function doMagicHook() {
3151
		if ( $this->mMagicHookDone ) {
3152
			return;
3153
		}
3154
		$this->mMagicHookDone = true;
3155
		Hooks::run( 'LanguageGetMagic', [ &$this->mMagicExtensions, $this->getCode() ] );
3156
	}
3157
3158
	/**
3159
	 * Fill a MagicWord object with data from here
3160
	 *
3161
	 * @param MagicWord $mw
3162
	 */
3163
	function getMagic( $mw ) {
3164
		// Saves a function call
3165
		if ( !$this->mMagicHookDone ) {
3166
			$this->doMagicHook();
3167
		}
3168
3169
		if ( isset( $this->mMagicExtensions[$mw->mId] ) ) {
3170
			$rawEntry = $this->mMagicExtensions[$mw->mId];
3171
		} else {
3172
			$rawEntry = self::$dataCache->getSubitem(
3173
				$this->mCode, 'magicWords', $mw->mId );
3174
		}
3175
3176
		if ( !is_array( $rawEntry ) ) {
3177
			wfWarn( "\"$rawEntry\" is not a valid magic word for \"$mw->mId\"" );
3178
		} else {
3179
			$mw->mCaseSensitive = $rawEntry[0];
3180
			$mw->mSynonyms = array_slice( $rawEntry, 1 );
3181
		}
3182
	}
3183
3184
	/**
3185
	 * Add magic words to the extension array
3186
	 *
3187
	 * @param array $newWords
3188
	 */
3189
	function addMagicWordsByLang( $newWords ) {
3190
		$fallbackChain = $this->getFallbackLanguages();
3191
		$fallbackChain = array_reverse( $fallbackChain );
3192
		foreach ( $fallbackChain as $code ) {
3193
			if ( isset( $newWords[$code] ) ) {
3194
				$this->mMagicExtensions = $newWords[$code] + $this->mMagicExtensions;
3195
			}
3196
		}
3197
	}
3198
3199
	/**
3200
	 * Get special page names, as an associative array
3201
	 *   canonical name => array of valid names, including aliases
3202
	 * @return array
3203
	 */
3204
	function getSpecialPageAliases() {
3205
		// Cache aliases because it may be slow to load them
3206
		if ( is_null( $this->mExtendedSpecialPageAliases ) ) {
3207
			// Initialise array
3208
			$this->mExtendedSpecialPageAliases =
3209
				self::$dataCache->getItem( $this->mCode, 'specialPageAliases' );
3210
			Hooks::run( 'LanguageGetSpecialPageAliases',
3211
				[ &$this->mExtendedSpecialPageAliases, $this->getCode() ] );
3212
		}
3213
3214
		return $this->mExtendedSpecialPageAliases;
3215
	}
3216
3217
	/**
3218
	 * Italic is unsuitable for some languages
3219
	 *
3220
	 * @param string $text The text to be emphasized.
3221
	 * @return string
3222
	 */
3223
	function emphasize( $text ) {
3224
		return "<em>$text</em>";
3225
	}
3226
3227
	/**
3228
	 * Normally we output all numbers in plain en_US style, that is
3229
	 * 293,291.235 for twohundredninetythreethousand-twohundredninetyone
3230
	 * point twohundredthirtyfive. However this is not suitable for all
3231
	 * languages, some such as Bengali (bn) want ২,৯৩,২৯১.২৩৫ and others such as
3232
	 * Icelandic just want to use commas instead of dots, and dots instead
3233
	 * of commas like "293.291,235".
3234
	 *
3235
	 * An example of this function being called:
3236
	 * <code>
3237
	 * wfMessage( 'message' )->numParams( $num )->text()
3238
	 * </code>
3239
	 *
3240
	 * See $separatorTransformTable on MessageIs.php for
3241
	 * the , => . and . => , implementation.
3242
	 *
3243
	 * @todo check if it's viable to use localeconv() for the decimal separator thing.
3244
	 * @param int|float $number The string to be formatted, should be an integer
3245
	 *   or a floating point number.
3246
	 * @param bool $nocommafy Set to true for special numbers like dates
3247
	 * @return string
3248
	 */
3249
	public function formatNum( $number, $nocommafy = false ) {
3250
		global $wgTranslateNumerals;
3251
		if ( !$nocommafy ) {
3252
			$number = $this->commafy( $number );
3253
			$s = $this->separatorTransformTable();
3254
			if ( $s ) {
3255
				$number = strtr( $number, $s );
3256
			}
3257
		}
3258
3259
		if ( $wgTranslateNumerals ) {
3260
			$s = $this->digitTransformTable();
3261
			if ( $s ) {
3262
				$number = strtr( $number, $s );
3263
			}
3264
		}
3265
3266
		return $number;
3267
	}
3268
3269
	/**
3270
	 * Front-end for non-commafied formatNum
3271
	 *
3272
	 * @param int|float $number The string to be formatted, should be an integer
3273
	 *        or a floating point number.
3274
	 * @since 1.21
3275
	 * @return string
3276
	 */
3277
	public function formatNumNoSeparators( $number ) {
3278
		return $this->formatNum( $number, true );
3279
	}
3280
3281
	/**
3282
	 * @param string $number
3283
	 * @return string
3284
	 */
3285
	public function parseFormattedNumber( $number ) {
3286
		$s = $this->digitTransformTable();
3287
		if ( $s ) {
3288
			// eliminate empty array values such as ''. (bug 64347)
3289
			$s = array_filter( $s );
3290
			$number = strtr( $number, array_flip( $s ) );
3291
		}
3292
3293
		$s = $this->separatorTransformTable();
3294
		if ( $s ) {
3295
			// eliminate empty array values such as ''. (bug 64347)
3296
			$s = array_filter( $s );
3297
			$number = strtr( $number, array_flip( $s ) );
3298
		}
3299
3300
		$number = strtr( $number, [ ',' => '' ] );
3301
		return $number;
3302
	}
3303
3304
	/**
3305
	 * Adds commas to a given number
3306
	 * @since 1.19
3307
	 * @param mixed $number
3308
	 * @return string
3309
	 */
3310
	function commafy( $number ) {
3311
		$digitGroupingPattern = $this->digitGroupingPattern();
3312
		if ( $number === null ) {
3313
			return '';
3314
		}
3315
3316
		if ( !$digitGroupingPattern || $digitGroupingPattern === "###,###,###" ) {
3317
			// default grouping is at thousands,  use the same for ###,###,### pattern too.
3318
			return strrev( (string)preg_replace( '/(\d{3})(?=\d)(?!\d*\.)/', '$1,', strrev( $number ) ) );
3319
		} else {
3320
			// Ref: http://cldr.unicode.org/translation/number-patterns
3321
			$sign = "";
3322
			if ( intval( $number ) < 0 ) {
3323
				// For negative numbers apply the algorithm like positive number and add sign.
3324
				$sign = "-";
3325
				$number = substr( $number, 1 );
3326
			}
3327
			$integerPart = [];
3328
			$decimalPart = [];
3329
			$numMatches = preg_match_all( "/(#+)/", $digitGroupingPattern, $matches );
3330
			preg_match( "/\d+/", $number, $integerPart );
3331
			preg_match( "/\.\d*/", $number, $decimalPart );
3332
			$groupedNumber = ( count( $decimalPart ) > 0 ) ? $decimalPart[0] : "";
3333
			if ( $groupedNumber === $number ) {
3334
				// the string does not have any number part. Eg: .12345
3335
				return $sign . $groupedNumber;
3336
			}
3337
			$start = $end = ( $integerPart ) ? strlen( $integerPart[0] ) : 0;
3338
			while ( $start > 0 ) {
3339
				$match = $matches[0][$numMatches - 1];
3340
				$matchLen = strlen( $match );
3341
				$start = $end - $matchLen;
3342
				if ( $start < 0 ) {
3343
					$start = 0;
3344
				}
3345
				$groupedNumber = substr( $number, $start, $end -$start ) . $groupedNumber;
3346
				$end = $start;
3347
				if ( $numMatches > 1 ) {
3348
					// use the last pattern for the rest of the number
3349
					$numMatches--;
3350
				}
3351
				if ( $start > 0 ) {
3352
					$groupedNumber = "," . $groupedNumber;
3353
				}
3354
			}
3355
			return $sign . $groupedNumber;
3356
		}
3357
	}
3358
3359
	/**
3360
	 * @return string
3361
	 */
3362
	function digitGroupingPattern() {
3363
		return self::$dataCache->getItem( $this->mCode, 'digitGroupingPattern' );
3364
	}
3365
3366
	/**
3367
	 * @return array
3368
	 */
3369
	function digitTransformTable() {
3370
		return self::$dataCache->getItem( $this->mCode, 'digitTransformTable' );
3371
	}
3372
3373
	/**
3374
	 * @return array
3375
	 */
3376
	function separatorTransformTable() {
3377
		return self::$dataCache->getItem( $this->mCode, 'separatorTransformTable' );
3378
	}
3379
3380
	/**
3381
	 * Take a list of strings and build a locale-friendly comma-separated
3382
	 * list, using the local comma-separator message.
3383
	 * The last two strings are chained with an "and".
3384
	 * NOTE: This function will only work with standard numeric array keys (0, 1, 2…)
3385
	 *
3386
	 * @param string[] $l
3387
	 * @return string
3388
	 */
3389
	function listToText( array $l ) {
3390
		$m = count( $l ) - 1;
3391
		if ( $m < 0 ) {
3392
			return '';
3393
		}
3394
		if ( $m > 0 ) {
3395
			$and = $this->msg( 'and' )->escaped();
3396
			$space = $this->msg( 'word-separator' )->escaped();
3397
			if ( $m > 1 ) {
3398
				$comma = $this->msg( 'comma-separator' )->escaped();
3399
			}
3400
		}
3401
		$s = $l[$m];
3402
		for ( $i = $m - 1; $i >= 0; $i-- ) {
3403
			if ( $i == $m - 1 ) {
3404
				$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...
3405
			} else {
3406
				$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...
3407
			}
3408
		}
3409
		return $s;
3410
	}
3411
3412
	/**
3413
	 * Take a list of strings and build a locale-friendly comma-separated
3414
	 * list, using the local comma-separator message.
3415
	 * @param string[] $list Array of strings to put in a comma list
3416
	 * @return string
3417
	 */
3418
	function commaList( array $list ) {
3419
		return implode(
3420
			wfMessage( 'comma-separator' )->inLanguage( $this )->escaped(),
3421
			$list
3422
		);
3423
	}
3424
3425
	/**
3426
	 * Take a list of strings and build a locale-friendly semicolon-separated
3427
	 * list, using the local semicolon-separator message.
3428
	 * @param string[] $list Array of strings to put in a semicolon list
3429
	 * @return string
3430
	 */
3431
	function semicolonList( array $list ) {
3432
		return implode(
3433
			wfMessage( 'semicolon-separator' )->inLanguage( $this )->escaped(),
3434
			$list
3435
		);
3436
	}
3437
3438
	/**
3439
	 * Same as commaList, but separate it with the pipe instead.
3440
	 * @param string[] $list Array of strings to put in a pipe list
3441
	 * @return string
3442
	 */
3443
	function pipeList( array $list ) {
3444
		return implode(
3445
			wfMessage( 'pipe-separator' )->inLanguage( $this )->escaped(),
3446
			$list
3447
		);
3448
	}
3449
3450
	/**
3451
	 * Truncate a string to a specified length in bytes, appending an optional
3452
	 * string (e.g. for ellipses)
3453
	 *
3454
	 * The database offers limited byte lengths for some columns in the database;
3455
	 * multi-byte character sets mean we need to ensure that only whole characters
3456
	 * are included, otherwise broken characters can be passed to the user
3457
	 *
3458
	 * If $length is negative, the string will be truncated from the beginning
3459
	 *
3460
	 * @param string $string String to truncate
3461
	 * @param int $length Maximum length (including ellipses)
3462
	 * @param string $ellipsis String to append to the truncated text
3463
	 * @param bool $adjustLength Subtract length of ellipsis from $length.
3464
	 *	$adjustLength was introduced in 1.18, before that behaved as if false.
3465
	 * @return string
3466
	 */
3467
	function truncate( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3468
		# Use the localized ellipsis character
3469
		if ( $ellipsis == '...' ) {
3470
			$ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3471
		}
3472
		# Check if there is no need to truncate
3473
		if ( $length == 0 ) {
3474
			return $ellipsis; // convention
3475
		} elseif ( strlen( $string ) <= abs( $length ) ) {
3476
			return $string; // no need to truncate
3477
		}
3478
		$stringOriginal = $string;
3479
		# If ellipsis length is >= $length then we can't apply $adjustLength
3480
		if ( $adjustLength && strlen( $ellipsis ) >= abs( $length ) ) {
3481
			$string = $ellipsis; // this can be slightly unexpected
3482
		# Otherwise, truncate and add ellipsis...
3483
		} else {
3484
			$eLength = $adjustLength ? strlen( $ellipsis ) : 0;
3485
			if ( $length > 0 ) {
3486
				$length -= $eLength;
3487
				$string = substr( $string, 0, $length ); // xyz...
3488
				$string = $this->removeBadCharLast( $string );
3489
				$string = rtrim( $string );
3490
				$string = $string . $ellipsis;
3491
			} else {
3492
				$length += $eLength;
3493
				$string = substr( $string, $length ); // ...xyz
3494
				$string = $this->removeBadCharFirst( $string );
3495
				$string = ltrim( $string );
3496
				$string = $ellipsis . $string;
3497
			}
3498
		}
3499
		# Do not truncate if the ellipsis makes the string longer/equal (bug 22181).
3500
		# This check is *not* redundant if $adjustLength, due to the single case where
3501
		# LEN($ellipsis) > ABS($limit arg); $stringOriginal could be shorter than $string.
3502
		if ( strlen( $string ) < strlen( $stringOriginal ) ) {
3503
			return $string;
3504
		} else {
3505
			return $stringOriginal;
3506
		}
3507
	}
3508
3509
	/**
3510
	 * Remove bytes that represent an incomplete Unicode character
3511
	 * at the end of string (e.g. bytes of the char are missing)
3512
	 *
3513
	 * @param string $string
3514
	 * @return string
3515
	 */
3516
	protected function removeBadCharLast( $string ) {
3517
		if ( $string != '' ) {
3518
			$char = ord( $string[strlen( $string ) - 1] );
3519
			$m = [];
3520
			if ( $char >= 0xc0 ) {
3521
				# We got the first byte only of a multibyte char; remove it.
3522
				$string = substr( $string, 0, -1 );
3523
			} elseif ( $char >= 0x80 &&
3524
				// Use the /s modifier (PCRE_DOTALL) so (.*) also matches newlines
3525
				preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
3526
					'[\xf0-\xf7][\x80-\xbf]{1,2})$/s', $string, $m )
3527
			) {
3528
				# We chopped in the middle of a character; remove it
3529
				$string = $m[1];
3530
			}
3531
		}
3532
		return $string;
3533
	}
3534
3535
	/**
3536
	 * Remove bytes that represent an incomplete Unicode character
3537
	 * at the start of string (e.g. bytes of the char are missing)
3538
	 *
3539
	 * @param string $string
3540
	 * @return string
3541
	 */
3542
	protected function removeBadCharFirst( $string ) {
3543
		if ( $string != '' ) {
3544
			$char = ord( $string[0] );
3545
			if ( $char >= 0x80 && $char < 0xc0 ) {
3546
				# We chopped in the middle of a character; remove the whole thing
3547
				$string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
3548
			}
3549
		}
3550
		return $string;
3551
	}
3552
3553
	/**
3554
	 * Truncate a string of valid HTML to a specified length in bytes,
3555
	 * appending an optional string (e.g. for ellipses), and return valid HTML
3556
	 *
3557
	 * This is only intended for styled/linked text, such as HTML with
3558
	 * tags like <span> and <a>, were the tags are self-contained (valid HTML).
3559
	 * Also, this will not detect things like "display:none" CSS.
3560
	 *
3561
	 * Note: since 1.18 you do not need to leave extra room in $length for ellipses.
3562
	 *
3563
	 * @param string $text HTML string to truncate
3564
	 * @param int $length (zero/positive) Maximum length (including ellipses)
3565
	 * @param string $ellipsis String to append to the truncated text
3566
	 * @return string
3567
	 */
3568
	function truncateHtml( $text, $length, $ellipsis = '...' ) {
3569
		# Use the localized ellipsis character
3570
		if ( $ellipsis == '...' ) {
3571
			$ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3572
		}
3573
		# Check if there is clearly no need to truncate
3574
		if ( $length <= 0 ) {
3575
			return $ellipsis; // no text shown, nothing to format (convention)
3576
		} elseif ( strlen( $text ) <= $length ) {
3577
			return $text; // string short enough even *with* HTML (short-circuit)
3578
		}
3579
3580
		$dispLen = 0; // innerHTML legth so far
3581
		$testingEllipsis = false; // checking if ellipses will make string longer/equal?
3582
		$tagType = 0; // 0-open, 1-close
3583
		$bracketState = 0; // 1-tag start, 2-tag name, 0-neither
3584
		$entityState = 0; // 0-not entity, 1-entity
3585
		$tag = $ret = ''; // accumulated tag name, accumulated result string
3586
		$openTags = []; // open tag stack
3587
		$maybeState = null; // possible truncation state
3588
3589
		$textLen = strlen( $text );
3590
		$neLength = max( 0, $length - strlen( $ellipsis ) ); // non-ellipsis len if truncated
3591
		for ( $pos = 0; true; ++$pos ) {
3592
			# Consider truncation once the display length has reached the maximim.
3593
			# We check if $dispLen > 0 to grab tags for the $neLength = 0 case.
3594
			# Check that we're not in the middle of a bracket/entity...
3595
			if ( $dispLen && $dispLen >= $neLength && $bracketState == 0 && !$entityState ) {
3596
				if ( !$testingEllipsis ) {
3597
					$testingEllipsis = true;
3598
					# Save where we are; we will truncate here unless there turn out to
3599
					# be so few remaining characters that truncation is not necessary.
3600
					if ( !$maybeState ) { // already saved? ($neLength = 0 case)
3601
						$maybeState = [ $ret, $openTags ]; // save state
3602
					}
3603
				} elseif ( $dispLen > $length && $dispLen > strlen( $ellipsis ) ) {
3604
					# String in fact does need truncation, the truncation point was OK.
3605
					list( $ret, $openTags ) = $maybeState; // reload state
3606
					$ret = $this->removeBadCharLast( $ret ); // multi-byte char fix
3607
					$ret .= $ellipsis; // add ellipsis
3608
					break;
3609
				}
3610
			}
3611
			if ( $pos >= $textLen ) {
3612
				break; // extra iteration just for above checks
3613
			}
3614
3615
			# Read the next char...
3616
			$ch = $text[$pos];
3617
			$lastCh = $pos ? $text[$pos - 1] : '';
3618
			$ret .= $ch; // add to result string
3619
			if ( $ch == '<' ) {
3620
				$this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags ); // for bad HTML
3621
				$entityState = 0; // for bad HTML
3622
				$bracketState = 1; // tag started (checking for backslash)
3623
			} elseif ( $ch == '>' ) {
3624
				$this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags );
3625
				$entityState = 0; // for bad HTML
3626
				$bracketState = 0; // out of brackets
3627
			} elseif ( $bracketState == 1 ) {
3628
				if ( $ch == '/' ) {
3629
					$tagType = 1; // close tag (e.g. "</span>")
3630
				} else {
3631
					$tagType = 0; // open tag (e.g. "<span>")
3632
					$tag .= $ch;
3633
				}
3634
				$bracketState = 2; // building tag name
3635
			} elseif ( $bracketState == 2 ) {
3636
				if ( $ch != ' ' ) {
3637
					$tag .= $ch;
3638
				} else {
3639
					// Name found (e.g. "<a href=..."), add on tag attributes...
3640
					$pos += $this->truncate_skip( $ret, $text, "<>", $pos + 1 );
3641
				}
3642
			} elseif ( $bracketState == 0 ) {
3643
				if ( $entityState ) {
3644
					if ( $ch == ';' ) {
3645
						$entityState = 0;
3646
						$dispLen++; // entity is one displayed char
3647
					}
3648
				} else {
3649
					if ( $neLength == 0 && !$maybeState ) {
3650
						// Save state without $ch. We want to *hit* the first
3651
						// display char (to get tags) but not *use* it if truncating.
3652
						$maybeState = [ substr( $ret, 0, -1 ), $openTags ];
3653
					}
3654
					if ( $ch == '&' ) {
3655
						$entityState = 1; // entity found, (e.g. "&#160;")
3656
					} else {
3657
						$dispLen++; // this char is displayed
3658
						// Add the next $max display text chars after this in one swoop...
3659
						$max = ( $testingEllipsis ? $length : $neLength ) - $dispLen;
3660
						$skipped = $this->truncate_skip( $ret, $text, "<>&", $pos + 1, $max );
3661
						$dispLen += $skipped;
3662
						$pos += $skipped;
3663
					}
3664
				}
3665
			}
3666
		}
3667
		// Close the last tag if left unclosed by bad HTML
3668
		$this->truncate_endBracket( $tag, $text[$textLen - 1], $tagType, $openTags );
3669
		while ( count( $openTags ) > 0 ) {
3670
			$ret .= '</' . array_pop( $openTags ) . '>'; // close open tags
3671
		}
3672
		return $ret;
3673
	}
3674
3675
	/**
3676
	 * truncateHtml() helper function
3677
	 * like strcspn() but adds the skipped chars to $ret
3678
	 *
3679
	 * @param string $ret
3680
	 * @param string $text
3681
	 * @param string $search
3682
	 * @param int $start
3683
	 * @param null|int $len
3684
	 * @return int
3685
	 */
3686
	private function truncate_skip( &$ret, $text, $search, $start, $len = null ) {
3687
		if ( $len === null ) {
3688
			$len = -1; // -1 means "no limit" for strcspn
3689
		} elseif ( $len < 0 ) {
3690
			$len = 0; // sanity
3691
		}
3692
		$skipCount = 0;
3693
		if ( $start < strlen( $text ) ) {
3694
			$skipCount = strcspn( $text, $search, $start, $len );
3695
			$ret .= substr( $text, $start, $skipCount );
3696
		}
3697
		return $skipCount;
3698
	}
3699
3700
	/**
3701
	 * truncateHtml() helper function
3702
	 * (a) push or pop $tag from $openTags as needed
3703
	 * (b) clear $tag value
3704
	 * @param string &$tag Current HTML tag name we are looking at
3705
	 * @param int $tagType (0-open tag, 1-close tag)
3706
	 * @param string $lastCh Character before the '>' that ended this tag
3707
	 * @param array &$openTags Open tag stack (not accounting for $tag)
3708
	 */
3709
	private function truncate_endBracket( &$tag, $tagType, $lastCh, &$openTags ) {
3710
		$tag = ltrim( $tag );
3711
		if ( $tag != '' ) {
3712
			if ( $tagType == 0 && $lastCh != '/' ) {
3713
				$openTags[] = $tag; // tag opened (didn't close itself)
3714
			} elseif ( $tagType == 1 ) {
3715
				if ( $openTags && $tag == $openTags[count( $openTags ) - 1] ) {
3716
					array_pop( $openTags ); // tag closed
3717
				}
3718
			}
3719
			$tag = '';
3720
		}
3721
	}
3722
3723
	/**
3724
	 * Grammatical transformations, needed for inflected languages
3725
	 * Invoked by putting {{grammar:case|word}} in a message
3726
	 *
3727
	 * @param string $word
3728
	 * @param string $case
3729
	 * @return string
3730
	 */
3731
	function convertGrammar( $word, $case ) {
3732
		global $wgGrammarForms;
3733 View Code Duplication
		if ( isset( $wgGrammarForms[$this->getCode()][$case][$word] ) ) {
3734
			return $wgGrammarForms[$this->getCode()][$case][$word];
3735
		}
3736
3737
		return $word;
3738
	}
3739
3740
	/**
3741
	 * Get the grammar forms for the content language
3742
	 * @return array Array of grammar forms
3743
	 * @since 1.20
3744
	 */
3745
	function getGrammarForms() {
3746
		global $wgGrammarForms;
3747
		if ( isset( $wgGrammarForms[$this->getCode()] )
3748
			&& is_array( $wgGrammarForms[$this->getCode()] )
3749
		) {
3750
			return $wgGrammarForms[$this->getCode()];
3751
		}
3752
3753
		return [];
3754
	}
3755
3756
	/**
3757
	 * Get the grammar transformations data for the language.
3758
	 * Used like grammar forms, with {{GRAMMAR}} and cases,
3759
	 * but uses pairs of regexes and replacements instead of code.
3760
	 *
3761
	 * @return array[] Array of grammar transformations.
3762
	 * @throws MWException
3763
	 * @since 1.28
3764
	 */
3765
	public function getGrammarTransformations() {
3766
		$languageCode = $this->getCode();
3767
3768
		if ( self::$grammarTransformations === null ) {
3769
			self::$grammarTransformations = new MapCacheLRU( 10 );
3770
		}
3771
3772
		if ( self::$grammarTransformations->has( $languageCode ) ) {
3773
			return self::$grammarTransformations->get( $languageCode );
3774
		}
3775
3776
		$data = [];
3777
3778
		$grammarDataFile = __DIR__ . "/data/grammarTransformations/$languageCode.json";
3779
		if ( is_readable( $grammarDataFile ) ) {
3780
			$data = FormatJson::decode(
3781
				file_get_contents( $grammarDataFile ),
3782
				true
3783
			);
3784
3785
			if ( $data === null ) {
3786
				throw new MWException( "Invalid grammar data for \"$languageCode\"." );
3787
			}
3788
3789
			self::$grammarTransformations->set( $languageCode, $data );
3790
		}
3791
3792
		return $data;
3793
	}
3794
3795
	/**
3796
	 * Provides an alternative text depending on specified gender.
3797
	 * Usage {{gender:username|masculine|feminine|unknown}}.
3798
	 * username is optional, in which case the gender of current user is used,
3799
	 * but only in (some) interface messages; otherwise default gender is used.
3800
	 *
3801
	 * If no forms are given, an empty string is returned. If only one form is
3802
	 * given, it will be returned unconditionally. These details are implied by
3803
	 * the caller and cannot be overridden in subclasses.
3804
	 *
3805
	 * If three forms are given, the default is to use the third (unknown) form.
3806
	 * If fewer than three forms are given, the default is to use the first (masculine) form.
3807
	 * These details can be overridden in subclasses.
3808
	 *
3809
	 * @param string $gender
3810
	 * @param array $forms
3811
	 *
3812
	 * @return string
3813
	 */
3814
	function gender( $gender, $forms ) {
3815
		if ( !count( $forms ) ) {
3816
			return '';
3817
		}
3818
		$forms = $this->preConvertPlural( $forms, 2 );
3819
		if ( $gender === 'male' ) {
3820
			return $forms[0];
3821
		}
3822
		if ( $gender === 'female' ) {
3823
			return $forms[1];
3824
		}
3825
		return isset( $forms[2] ) ? $forms[2] : $forms[0];
3826
	}
3827
3828
	/**
3829
	 * Plural form transformations, needed for some languages.
3830
	 * For example, there are 3 form of plural in Russian and Polish,
3831
	 * depending on "count mod 10". See [[w:Plural]]
3832
	 * For English it is pretty simple.
3833
	 *
3834
	 * Invoked by putting {{plural:count|wordform1|wordform2}}
3835
	 * or {{plural:count|wordform1|wordform2|wordform3}}
3836
	 *
3837
	 * Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}}
3838
	 *
3839
	 * @param int $count Non-localized number
3840
	 * @param array $forms Different plural forms
3841
	 * @return string Correct form of plural for $count in this language
3842
	 */
3843
	function convertPlural( $count, $forms ) {
3844
		// Handle explicit n=pluralform cases
3845
		$forms = $this->handleExplicitPluralForms( $count, $forms );
3846
		if ( is_string( $forms ) ) {
3847
			return $forms;
3848
		}
3849
		if ( !count( $forms ) ) {
3850
			return '';
3851
		}
3852
3853
		$pluralForm = $this->getPluralRuleIndexNumber( $count );
3854
		$pluralForm = min( $pluralForm, count( $forms ) - 1 );
3855
		return $forms[$pluralForm];
3856
	}
3857
3858
	/**
3859
	 * Handles explicit plural forms for Language::convertPlural()
3860
	 *
3861
	 * In {{PLURAL:$1|0=nothing|one|many}}, 0=nothing will be returned if $1 equals zero.
3862
	 * If an explicitly defined plural form matches the $count, then
3863
	 * string value returned, otherwise array returned for further consideration
3864
	 * by CLDR rules or overridden convertPlural().
3865
	 *
3866
	 * @since 1.23
3867
	 *
3868
	 * @param int $count Non-localized number
3869
	 * @param array $forms Different plural forms
3870
	 *
3871
	 * @return array|string
3872
	 */
3873
	protected function handleExplicitPluralForms( $count, array $forms ) {
3874
		foreach ( $forms as $index => $form ) {
3875
			if ( preg_match( '/\d+=/i', $form ) ) {
3876
				$pos = strpos( $form, '=' );
3877
				if ( substr( $form, 0, $pos ) === (string)$count ) {
3878
					return substr( $form, $pos + 1 );
3879
				}
3880
				unset( $forms[$index] );
3881
			}
3882
		}
3883
		return array_values( $forms );
3884
	}
3885
3886
	/**
3887
	 * Checks that convertPlural was given an array and pads it to requested
3888
	 * amount of forms by copying the last one.
3889
	 *
3890
	 * @param array $forms Array of forms given to convertPlural
3891
	 * @param int $count How many forms should there be at least
3892
	 * @return array Padded array of forms or an exception if not an array
3893
	 */
3894
	protected function preConvertPlural( /* Array */ $forms, $count ) {
3895
		while ( count( $forms ) < $count ) {
3896
			$forms[] = $forms[count( $forms ) - 1];
3897
		}
3898
		return $forms;
3899
	}
3900
3901
	/**
3902
	 * Wraps argument with unicode control characters for directionality safety
3903
	 *
3904
	 * This solves the problem where directionality-neutral characters at the edge of
3905
	 * the argument string get interpreted with the wrong directionality from the
3906
	 * enclosing context, giving renderings that look corrupted like "(Ben_(WMF".
3907
	 *
3908
	 * The wrapping is LRE...PDF or RLE...PDF, depending on the detected
3909
	 * directionality of the argument string, using the BIDI algorithm's own "First
3910
	 * strong directional codepoint" rule. Essentially, this works round the fact that
3911
	 * there is no embedding equivalent of U+2068 FSI (isolation with heuristic
3912
	 * direction inference). The latter is cleaner but still not widely supported.
3913
	 *
3914
	 * @param string $text Text to wrap
3915
	 * @return string Text, wrapped in LRE...PDF or RLE...PDF or nothing
3916
	 */
3917
	public function embedBidi( $text = '' ) {
3918
		$dir = Language::strongDirFromContent( $text );
3919
		if ( $dir === 'ltr' ) {
3920
			// Wrap in LEFT-TO-RIGHT EMBEDDING ... POP DIRECTIONAL FORMATTING
3921
			return self::$lre . $text . self::$pdf;
3922
		}
3923
		if ( $dir === 'rtl' ) {
3924
			// Wrap in RIGHT-TO-LEFT EMBEDDING ... POP DIRECTIONAL FORMATTING
3925
			return self::$rle . $text . self::$pdf;
3926
		}
3927
		// No strong directionality: do not wrap
3928
		return $text;
3929
	}
3930
3931
	/**
3932
	 * @todo Maybe translate block durations.  Note that this function is somewhat misnamed: it
3933
	 * deals with translating the *duration* ("1 week", "4 days", etc), not the expiry time
3934
	 * (which is an absolute timestamp). Please note: do NOT add this blindly, as it is used
3935
	 * on old expiry lengths recorded in log entries. You'd need to provide the start date to
3936
	 * match up with it.
3937
	 *
3938
	 * @param string $str The validated block duration in English
3939
	 * @param User $user User object to use timezone from or null for $wgUser
3940
	 * @return string Somehow translated block duration
3941
	 * @see LanguageFi.php for example implementation
3942
	 */
3943
	function translateBlockExpiry( $str, User $user = null ) {
3944
		$duration = SpecialBlock::getSuggestedDurations( $this );
3945
		foreach ( $duration as $show => $value ) {
3946
			if ( strcmp( $str, $value ) == 0 ) {
3947
				return htmlspecialchars( trim( $show ) );
3948
			}
3949
		}
3950
3951
		if ( wfIsInfinity( $str ) ) {
3952
			foreach ( $duration as $show => $value ) {
3953
				if ( wfIsInfinity( $value ) ) {
3954
					return htmlspecialchars( trim( $show ) );
3955
				}
3956
			}
3957
		}
3958
3959
		// If all else fails, return a standard duration or timestamp description.
3960
		$time = strtotime( $str, 0 );
3961
		if ( $time === false ) { // Unknown format. Return it as-is in case.
3962
			return $str;
3963
		} elseif ( $time !== strtotime( $str, 1 ) ) { // It's a relative timestamp.
3964
			// $time is relative to 0 so it's a duration length.
3965
			return $this->formatDuration( $time );
3966
		} else { // It's an absolute timestamp.
3967
			if ( $time === 0 ) {
3968
				// wfTimestamp() handles 0 as current time instead of epoch.
3969
				$time = '19700101000000';
3970
			}
3971
			if ( $user ) {
3972
				return $this->userTimeAndDate( $time, $user );
3973
			}
3974
			return $this->timeanddate( $time );
3975
		}
3976
	}
3977
3978
	/**
3979
	 * languages like Chinese need to be segmented in order for the diff
3980
	 * to be of any use
3981
	 *
3982
	 * @param string $text
3983
	 * @return string
3984
	 */
3985
	public function segmentForDiff( $text ) {
3986
		return $text;
3987
	}
3988
3989
	/**
3990
	 * and unsegment to show the result
3991
	 *
3992
	 * @param string $text
3993
	 * @return string
3994
	 */
3995
	public function unsegmentForDiff( $text ) {
3996
		return $text;
3997
	}
3998
3999
	/**
4000
	 * Return the LanguageConverter used in the Language
4001
	 *
4002
	 * @since 1.19
4003
	 * @return LanguageConverter
4004
	 */
4005
	public function getConverter() {
4006
		return $this->mConverter;
4007
	}
4008
4009
	/**
4010
	 * convert text to all supported variants
4011
	 *
4012
	 * @param string $text
4013
	 * @return array
4014
	 */
4015
	public function autoConvertToAllVariants( $text ) {
4016
		return $this->mConverter->autoConvertToAllVariants( $text );
4017
	}
4018
4019
	/**
4020
	 * convert text to different variants of a language.
4021
	 *
4022
	 * @param string $text
4023
	 * @return string
4024
	 */
4025
	public function convert( $text ) {
4026
		return $this->mConverter->convert( $text );
4027
	}
4028
4029
	/**
4030
	 * Convert a Title object to a string in the preferred variant
4031
	 *
4032
	 * @param Title $title
4033
	 * @return string
4034
	 */
4035
	public function convertTitle( $title ) {
4036
		return $this->mConverter->convertTitle( $title );
4037
	}
4038
4039
	/**
4040
	 * Convert a namespace index to a string in the preferred variant
4041
	 *
4042
	 * @param int $ns
4043
	 * @return string
4044
	 */
4045
	public function convertNamespace( $ns ) {
4046
		return $this->mConverter->convertNamespace( $ns );
4047
	}
4048
4049
	/**
4050
	 * Check if this is a language with variants
4051
	 *
4052
	 * @return bool
4053
	 */
4054
	public function hasVariants() {
4055
		return count( $this->getVariants() ) > 1;
4056
	}
4057
4058
	/**
4059
	 * Check if the language has the specific variant
4060
	 *
4061
	 * @since 1.19
4062
	 * @param string $variant
4063
	 * @return bool
4064
	 */
4065
	public function hasVariant( $variant ) {
4066
		return (bool)$this->mConverter->validateVariant( $variant );
4067
	}
4068
4069
	/**
4070
	 * Perform output conversion on a string, and encode for safe HTML output.
4071
	 * @param string $text Text to be converted
4072
	 * @param bool $isTitle Whether this conversion is for the article title
4073
	 * @return string
4074
	 * @todo this should get integrated somewhere sane
4075
	 */
4076
	public function convertHtml( $text, $isTitle = false ) {
4077
		return htmlspecialchars( $this->convert( $text, $isTitle ) );
4078
	}
4079
4080
	/**
4081
	 * @param string $key
4082
	 * @return string
4083
	 */
4084
	public function convertCategoryKey( $key ) {
4085
		return $this->mConverter->convertCategoryKey( $key );
4086
	}
4087
4088
	/**
4089
	 * Get the list of variants supported by this language
4090
	 * see sample implementation in LanguageZh.php
4091
	 *
4092
	 * @return array An array of language codes
4093
	 */
4094
	public function getVariants() {
4095
		return $this->mConverter->getVariants();
4096
	}
4097
4098
	/**
4099
	 * @return string
4100
	 */
4101
	public function getPreferredVariant() {
4102
		return $this->mConverter->getPreferredVariant();
4103
	}
4104
4105
	/**
4106
	 * @return string
4107
	 */
4108
	public function getDefaultVariant() {
4109
		return $this->mConverter->getDefaultVariant();
4110
	}
4111
4112
	/**
4113
	 * @return string
4114
	 */
4115
	public function getURLVariant() {
4116
		return $this->mConverter->getURLVariant();
4117
	}
4118
4119
	/**
4120
	 * If a language supports multiple variants, it is
4121
	 * possible that non-existing link in one variant
4122
	 * actually exists in another variant. this function
4123
	 * tries to find it. See e.g. LanguageZh.php
4124
	 * The input parameters may be modified upon return
4125
	 *
4126
	 * @param string &$link The name of the link
4127
	 * @param Title &$nt The title object of the link
4128
	 * @param bool $ignoreOtherCond To disable other conditions when
4129
	 *   we need to transclude a template or update a category's link
4130
	 */
4131
	public function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
4132
		$this->mConverter->findVariantLink( $link, $nt, $ignoreOtherCond );
4133
	}
4134
4135
	/**
4136
	 * returns language specific options used by User::getPageRenderHash()
4137
	 * for example, the preferred language variant
4138
	 *
4139
	 * @return string
4140
	 */
4141
	function getExtraHashOptions() {
4142
		return $this->mConverter->getExtraHashOptions();
4143
	}
4144
4145
	/**
4146
	 * For languages that support multiple variants, the title of an
4147
	 * article may be displayed differently in different variants. this
4148
	 * function returns the apporiate title defined in the body of the article.
4149
	 *
4150
	 * @return string
4151
	 */
4152
	public function getParsedTitle() {
4153
		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...
4154
	}
4155
4156
	/**
4157
	 * Refresh the cache of conversion tables when
4158
	 * MediaWiki:Conversiontable* is updated.
4159
	 *
4160
	 * @param Title $title The Title of the page being updated
4161
	 */
4162
	public function updateConversionTable( Title $title ) {
4163
		$this->mConverter->updateConversionTable( $title );
4164
	}
4165
4166
	/**
4167
	 * Prepare external link text for conversion. When the text is
4168
	 * a URL, it shouldn't be converted, and it'll be wrapped in
4169
	 * the "raw" tag (-{R| }-) to prevent conversion.
4170
	 *
4171
	 * This function is called "markNoConversion" for historical
4172
	 * reasons.
4173
	 *
4174
	 * @param string $text Text to be used for external link
4175
	 * @param bool $noParse Wrap it without confirming it's a real URL first
4176
	 * @return string The tagged text
4177
	 */
4178
	public function markNoConversion( $text, $noParse = false ) {
4179
		// Excluding protocal-relative URLs may avoid many false positives.
4180
		if ( $noParse || preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
4181
			return $this->mConverter->markNoConversion( $text );
4182
		} else {
4183
			return $text;
4184
		}
4185
	}
4186
4187
	/**
4188
	 * A regular expression to match legal word-trailing characters
4189
	 * which should be merged onto a link of the form [[foo]]bar.
4190
	 *
4191
	 * @return string
4192
	 */
4193
	public function linkTrail() {
4194
		return self::$dataCache->getItem( $this->mCode, 'linkTrail' );
4195
	}
4196
4197
	/**
4198
	 * A regular expression character set to match legal word-prefixing
4199
	 * characters which should be merged onto a link of the form foo[[bar]].
4200
	 *
4201
	 * @return string
4202
	 */
4203
	public function linkPrefixCharset() {
4204
		return self::$dataCache->getItem( $this->mCode, 'linkPrefixCharset' );
4205
	}
4206
4207
	/**
4208
	 * Get the "parent" language which has a converter to convert a "compatible" language
4209
	 * (in another variant) to this language (eg. zh for zh-cn, but not en for en-gb).
4210
	 *
4211
	 * @return Language|null
4212
	 * @since 1.22
4213
	 */
4214
	public function getParentLanguage() {
4215
		if ( $this->mParentLanguage !== false ) {
4216
			return $this->mParentLanguage;
4217
		}
4218
4219
		$code = explode( '-', $this->getCode() )[0];
4220
		if ( !in_array( $code, LanguageConverter::$languagesWithVariants ) ) {
4221
			$this->mParentLanguage = null;
4222
			return null;
4223
		}
4224
		$lang = Language::factory( $code );
4225
		if ( !$lang->hasVariant( $this->getCode() ) ) {
4226
			$this->mParentLanguage = null;
4227
			return null;
4228
		}
4229
4230
		$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...
4231
		return $lang;
4232
	}
4233
4234
	/**
4235
	 * Compare with an other language object
4236
	 *
4237
	 * @since 1.28
4238
	 * @param Language $lang
4239
	 * @return boolean
4240
	 */
4241
	public function equals( Language $lang ) {
4242
		return $lang->getCode() === $this->mCode;
4243
	}
4244
4245
	/**
4246
	 * Get the internal language code for this language object
4247
	 *
4248
	 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
4249
	 * htmlspecialchars() or similar
4250
	 *
4251
	 * @return string
4252
	 */
4253
	public function getCode() {
4254
		return $this->mCode;
4255
	}
4256
4257
	/**
4258
	 * Get the code in BCP 47 format which we can use
4259
	 * inside of html lang="" tags.
4260
	 *
4261
	 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
4262
	 * htmlspecialchars() or similar.
4263
	 *
4264
	 * @since 1.19
4265
	 * @return string
4266
	 */
4267
	public function getHtmlCode() {
4268
		if ( is_null( $this->mHtmlCode ) ) {
4269
			$this->mHtmlCode = wfBCP47( $this->getCode() );
4270
		}
4271
		return $this->mHtmlCode;
4272
	}
4273
4274
	/**
4275
	 * @param string $code
4276
	 */
4277
	public function setCode( $code ) {
4278
		$this->mCode = $code;
4279
		// Ensure we don't leave incorrect cached data lying around
4280
		$this->mHtmlCode = null;
4281
		$this->mParentLanguage = false;
4282
	}
4283
4284
	/**
4285
	 * Get the language code from a file name. Inverse of getFileName()
4286
	 * @param string $filename $prefix . $languageCode . $suffix
4287
	 * @param string $prefix Prefix before the language code
4288
	 * @param string $suffix Suffix after the language code
4289
	 * @return string Language code, or false if $prefix or $suffix isn't found
4290
	 */
4291
	public static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
4292
		$m = null;
4293
		preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
4294
			preg_quote( $suffix, '/' ) . '/', $filename, $m );
4295
		if ( !count( $m ) ) {
4296
			return false;
4297
		}
4298
		return str_replace( '_', '-', strtolower( $m[1] ) );
4299
	}
4300
4301
	/**
4302
	 * @param string $code
4303
	 * @return string Name of the language class
4304
	 */
4305
	public static function classFromCode( $code ) {
4306
		if ( $code == 'en' ) {
4307
			return 'Language';
4308
		} else {
4309
			return 'Language' . str_replace( '-', '_', ucfirst( $code ) );
4310
		}
4311
	}
4312
4313
	/**
4314
	 * Get the name of a file for a certain language code
4315
	 * @param string $prefix Prepend this to the filename
4316
	 * @param string $code Language code
4317
	 * @param string $suffix Append this to the filename
4318
	 * @throws MWException
4319
	 * @return string $prefix . $mangledCode . $suffix
4320
	 */
4321
	public static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) {
4322
		if ( !self::isValidBuiltInCode( $code ) ) {
4323
			throw new MWException( "Invalid language code \"$code\"" );
4324
		}
4325
4326
		return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
4327
	}
4328
4329
	/**
4330
	 * @param string $code
4331
	 * @return string
4332
	 */
4333
	public static function getMessagesFileName( $code ) {
4334
		global $IP;
4335
		$file = self::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
4336
		Hooks::run( 'Language::getMessagesFileName', [ $code, &$file ] );
4337
		return $file;
4338
	}
4339
4340
	/**
4341
	 * @param string $code
4342
	 * @return string
4343
	 * @throws MWException
4344
	 * @since 1.23
4345
	 */
4346
	public static function getJsonMessagesFileName( $code ) {
4347
		global $IP;
4348
4349
		if ( !self::isValidBuiltInCode( $code ) ) {
4350
			throw new MWException( "Invalid language code \"$code\"" );
4351
		}
4352
4353
		return "$IP/languages/i18n/$code.json";
4354
	}
4355
4356
	/**
4357
	 * Get the first fallback for a given language.
4358
	 *
4359
	 * @param string $code
4360
	 *
4361
	 * @return bool|string
4362
	 */
4363
	public static function getFallbackFor( $code ) {
4364
		$fallbacks = self::getFallbacksFor( $code );
4365
		if ( $fallbacks ) {
4366
			return $fallbacks[0];
4367
		}
4368
		return false;
4369
	}
4370
4371
	/**
4372
	 * Get the ordered list of fallback languages.
4373
	 *
4374
	 * @since 1.19
4375
	 * @param string $code Language code
4376
	 * @return array Non-empty array, ending in "en"
4377
	 */
4378
	public static function getFallbacksFor( $code ) {
4379
		if ( $code === 'en' || !Language::isValidBuiltInCode( $code ) ) {
4380
			return [];
4381
		}
4382
		// For unknown languages, fallbackSequence returns an empty array,
4383
		// hardcode fallback to 'en' in that case.
4384
		return self::getLocalisationCache()->getItem( $code, 'fallbackSequence' ) ?: [ 'en' ];
4385
	}
4386
4387
	/**
4388
	 * Get the ordered list of fallback languages, ending with the fallback
4389
	 * language chain for the site language.
4390
	 *
4391
	 * @since 1.22
4392
	 * @param string $code Language code
4393
	 * @return array Array( fallbacks, site fallbacks )
4394
	 */
4395
	public static function getFallbacksIncludingSiteLanguage( $code ) {
4396
		global $wgLanguageCode;
4397
4398
		// Usually, we will only store a tiny number of fallback chains, so we
4399
		// keep them in static memory.
4400
		$cacheKey = "{$code}-{$wgLanguageCode}";
4401
4402
		if ( !array_key_exists( $cacheKey, self::$fallbackLanguageCache ) ) {
4403
			$fallbacks = self::getFallbacksFor( $code );
4404
4405
			// Append the site's fallback chain, including the site language itself
4406
			$siteFallbacks = self::getFallbacksFor( $wgLanguageCode );
4407
			array_unshift( $siteFallbacks, $wgLanguageCode );
4408
4409
			// Eliminate any languages already included in the chain
4410
			$siteFallbacks = array_diff( $siteFallbacks, $fallbacks );
4411
4412
			self::$fallbackLanguageCache[$cacheKey] = [ $fallbacks, $siteFallbacks ];
4413
		}
4414
		return self::$fallbackLanguageCache[$cacheKey];
4415
	}
4416
4417
	/**
4418
	 * Get all messages for a given language
4419
	 * WARNING: this may take a long time. If you just need all message *keys*
4420
	 * but need the *contents* of only a few messages, consider using getMessageKeysFor().
4421
	 *
4422
	 * @param string $code
4423
	 *
4424
	 * @return array
4425
	 */
4426
	public static function getMessagesFor( $code ) {
4427
		return self::getLocalisationCache()->getItem( $code, 'messages' );
4428
	}
4429
4430
	/**
4431
	 * Get a message for a given language
4432
	 *
4433
	 * @param string $key
4434
	 * @param string $code
4435
	 *
4436
	 * @return string
4437
	 */
4438
	public static function getMessageFor( $key, $code ) {
4439
		return self::getLocalisationCache()->getSubitem( $code, 'messages', $key );
4440
	}
4441
4442
	/**
4443
	 * Get all message keys for a given language. This is a faster alternative to
4444
	 * array_keys( Language::getMessagesFor( $code ) )
4445
	 *
4446
	 * @since 1.19
4447
	 * @param string $code Language code
4448
	 * @return array Array of message keys (strings)
4449
	 */
4450
	public static function getMessageKeysFor( $code ) {
4451
		return self::getLocalisationCache()->getSubitemList( $code, 'messages' );
4452
	}
4453
4454
	/**
4455
	 * @param string $talk
4456
	 * @return mixed
4457
	 */
4458
	function fixVariableInNamespace( $talk ) {
4459
		if ( strpos( $talk, '$1' ) === false ) {
4460
			return $talk;
4461
		}
4462
4463
		global $wgMetaNamespace;
4464
		$talk = str_replace( '$1', $wgMetaNamespace, $talk );
4465
4466
		# Allow grammar transformations
4467
		# Allowing full message-style parsing would make simple requests
4468
		# such as action=raw much more expensive than they need to be.
4469
		# This will hopefully cover most cases.
4470
		$talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
4471
			[ &$this, 'replaceGrammarInNamespace' ], $talk );
4472
		return str_replace( ' ', '_', $talk );
4473
	}
4474
4475
	/**
4476
	 * @param string $m
4477
	 * @return string
4478
	 */
4479
	function replaceGrammarInNamespace( $m ) {
4480
		return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
4481
	}
4482
4483
	/**
4484
	 * Decode an expiry (block, protection, etc) which has come from the DB
4485
	 *
4486
	 * @param string $expiry Database expiry String
4487
	 * @param bool|int $format True to process using language functions, or TS_ constant
4488
	 *     to return the expiry in a given timestamp
4489
	 * @param string $infinity If $format is not true, use this string for infinite expiry
4490
	 * @return string
4491
	 * @since 1.18
4492
	 */
4493
	public function formatExpiry( $expiry, $format = true, $infinity = 'infinity' ) {
4494
		static $dbInfinity;
4495
		if ( $dbInfinity === null ) {
4496
			$dbInfinity = wfGetDB( DB_SLAVE )->getInfinity();
4497
		}
4498
4499
		if ( $expiry == '' || $expiry === 'infinity' || $expiry == $dbInfinity ) {
4500
			return $format === true
4501
				? $this->getMessageFromDB( 'infiniteblock' )
4502
				: $infinity;
4503
		} else {
4504
			return $format === true
4505
				? $this->timeanddate( $expiry, /* User preference timezone */ true )
4506
				: wfTimestamp( $format, $expiry );
4507
		}
4508
	}
4509
4510
	/**
4511
	 * Formats a time given in seconds into a string representation of that time.
4512
	 *
4513
	 * @param int|float $seconds
4514
	 * @param array $format An optional argument that formats the returned string in different ways:
4515
	 *   If $format['avoid'] === 'avoidseconds': don't show seconds if $seconds >= 1 hour,
4516
	 *   If $format['avoid'] === 'avoidminutes': don't show seconds/minutes if $seconds > 48 hours,
4517
	 *   If $format['noabbrevs'] is true: use 'seconds' and friends instead of 'seconds-abbrev'
4518
	 *     and friends.
4519
	 * @note For backwards compatibility, $format may also be one of the strings 'avoidseconds'
4520
	 *     or 'avoidminutes'.
4521
	 * @return string
4522
	 */
4523
	function formatTimePeriod( $seconds, $format = [] ) {
4524
		if ( !is_array( $format ) ) {
4525
			$format = [ 'avoid' => $format ]; // For backwards compatibility
4526
		}
4527
		if ( !isset( $format['avoid'] ) ) {
4528
			$format['avoid'] = false;
4529
		}
4530
		if ( !isset( $format['noabbrevs'] ) ) {
4531
			$format['noabbrevs'] = false;
4532
		}
4533
		$secondsMsg = wfMessage(
4534
			$format['noabbrevs'] ? 'seconds' : 'seconds-abbrev' )->inLanguage( $this );
4535
		$minutesMsg = wfMessage(
4536
			$format['noabbrevs'] ? 'minutes' : 'minutes-abbrev' )->inLanguage( $this );
4537
		$hoursMsg = wfMessage(
4538
			$format['noabbrevs'] ? 'hours' : 'hours-abbrev' )->inLanguage( $this );
4539
		$daysMsg = wfMessage(
4540
			$format['noabbrevs'] ? 'days' : 'days-abbrev' )->inLanguage( $this );
4541
4542
		if ( round( $seconds * 10 ) < 100 ) {
4543
			$s = $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) );
4544
			$s = $secondsMsg->params( $s )->text();
4545
		} elseif ( round( $seconds ) < 60 ) {
4546
			$s = $this->formatNum( round( $seconds ) );
4547
			$s = $secondsMsg->params( $s )->text();
4548
		} elseif ( round( $seconds ) < 3600 ) {
4549
			$minutes = floor( $seconds / 60 );
4550
			$secondsPart = round( fmod( $seconds, 60 ) );
4551
			if ( $secondsPart == 60 ) {
4552
				$secondsPart = 0;
4553
				$minutes++;
4554
			}
4555
			$s = $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4556
			$s .= ' ';
4557
			$s .= $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4558
		} elseif ( round( $seconds ) <= 2 * 86400 ) {
4559
			$hours = floor( $seconds / 3600 );
4560
			$minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
4561
			$secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
4562
			if ( $secondsPart == 60 ) {
4563
				$secondsPart = 0;
4564
				$minutes++;
4565
			}
4566
			if ( $minutes == 60 ) {
4567
				$minutes = 0;
4568
				$hours++;
4569
			}
4570
			$s = $hoursMsg->params( $this->formatNum( $hours ) )->text();
4571
			$s .= ' ';
4572
			$s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4573
			if ( !in_array( $format['avoid'], [ 'avoidseconds', 'avoidminutes' ] ) ) {
4574
				$s .= ' ' . $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4575
			}
4576
		} else {
4577
			$days = floor( $seconds / 86400 );
4578
			if ( $format['avoid'] === 'avoidminutes' ) {
4579
				$hours = round( ( $seconds - $days * 86400 ) / 3600 );
4580
				if ( $hours == 24 ) {
4581
					$hours = 0;
4582
					$days++;
4583
				}
4584
				$s = $daysMsg->params( $this->formatNum( $days ) )->text();
4585
				$s .= ' ';
4586
				$s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4587
			} elseif ( $format['avoid'] === 'avoidseconds' ) {
4588
				$hours = floor( ( $seconds - $days * 86400 ) / 3600 );
4589
				$minutes = round( ( $seconds - $days * 86400 - $hours * 3600 ) / 60 );
4590
				if ( $minutes == 60 ) {
4591
					$minutes = 0;
4592
					$hours++;
4593
				}
4594
				if ( $hours == 24 ) {
4595
					$hours = 0;
4596
					$days++;
4597
				}
4598
				$s = $daysMsg->params( $this->formatNum( $days ) )->text();
4599
				$s .= ' ';
4600
				$s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4601
				$s .= ' ';
4602
				$s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4603
			} else {
4604
				$s = $daysMsg->params( $this->formatNum( $days ) )->text();
4605
				$s .= ' ';
4606
				$s .= $this->formatTimePeriod( $seconds - $days * 86400, $format );
4607
			}
4608
		}
4609
		return $s;
4610
	}
4611
4612
	/**
4613
	 * Format a bitrate for output, using an appropriate
4614
	 * unit (bps, kbps, Mbps, Gbps, Tbps, Pbps, Ebps, Zbps or Ybps) according to
4615
	 *   the magnitude in question.
4616
	 *
4617
	 * This use base 1000. For base 1024 use formatSize(), for another base
4618
	 * see formatComputingNumbers().
4619
	 *
4620
	 * @param int $bps
4621
	 * @return string
4622
	 */
4623
	function formatBitrate( $bps ) {
4624
		return $this->formatComputingNumbers( $bps, 1000, "bitrate-$1bits" );
4625
	}
4626
4627
	/**
4628
	 * @param int $size Size of the unit
4629
	 * @param int $boundary Size boundary (1000, or 1024 in most cases)
4630
	 * @param string $messageKey Message key to be uesd
4631
	 * @return string
4632
	 */
4633
	function formatComputingNumbers( $size, $boundary, $messageKey ) {
4634
		if ( $size <= 0 ) {
4635
			return str_replace( '$1', $this->formatNum( $size ),
4636
				$this->getMessageFromDB( str_replace( '$1', '', $messageKey ) )
4637
			);
4638
		}
4639
		$sizes = [ '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zeta', 'yotta' ];
4640
		$index = 0;
4641
4642
		$maxIndex = count( $sizes ) - 1;
4643
		while ( $size >= $boundary && $index < $maxIndex ) {
4644
			$index++;
4645
			$size /= $boundary;
4646
		}
4647
4648
		// For small sizes no decimal places necessary
4649
		$round = 0;
4650
		if ( $index > 1 ) {
4651
			// For MB and bigger two decimal places are smarter
4652
			$round = 2;
4653
		}
4654
		$msg = str_replace( '$1', $sizes[$index], $messageKey );
4655
4656
		$size = round( $size, $round );
4657
		$text = $this->getMessageFromDB( $msg );
4658
		return str_replace( '$1', $this->formatNum( $size ), $text );
4659
	}
4660
4661
	/**
4662
	 * Format a size in bytes for output, using an appropriate
4663
	 * unit (B, KB, MB, GB, TB, PB, EB, ZB or YB) according to the magnitude in question
4664
	 *
4665
	 * This method use base 1024. For base 1000 use formatBitrate(), for
4666
	 * another base see formatComputingNumbers()
4667
	 *
4668
	 * @param int $size Size to format
4669
	 * @return string Plain text (not HTML)
4670
	 */
4671
	function formatSize( $size ) {
4672
		return $this->formatComputingNumbers( $size, 1024, "size-$1bytes" );
4673
	}
4674
4675
	/**
4676
	 * Make a list item, used by various special pages
4677
	 *
4678
	 * @param string $page Page link
4679
	 * @param string $details HTML safe text between brackets
4680
	 * @param bool $oppositedm Add the direction mark opposite to your
4681
	 *   language, to display text properly
4682
	 * @return HTML escaped string
4683
	 */
4684
	function specialList( $page, $details, $oppositedm = true ) {
4685
		if ( !$details ) {
4686
			return $page;
4687
		}
4688
4689
		$dirmark = ( $oppositedm ? $this->getDirMark( true ) : '' ) . $this->getDirMark();
4690
		return
4691
			$page .
4692
			$dirmark .
4693
			$this->msg( 'word-separator' )->escaped() .
4694
			$this->msg( 'parentheses' )->rawParams( $details )->escaped();
4695
	}
4696
4697
	/**
4698
	 * Generate (prev x| next x) (20|50|100...) type links for paging
4699
	 *
4700
	 * @param Title $title Title object to link
4701
	 * @param int $offset
4702
	 * @param int $limit
4703
	 * @param array $query Optional URL query parameter string
4704
	 * @param bool $atend Optional param for specified if this is the last page
4705
	 * @return string
4706
	 */
4707
	public function viewPrevNext( Title $title, $offset, $limit,
4708
		array $query = [], $atend = false
4709
	) {
4710
		// @todo FIXME: Why on earth this needs one message for the text and another one for tooltip?
4711
4712
		# Make 'previous' link
4713
		$prev = wfMessage( 'prevn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4714 View Code Duplication
		if ( $offset > 0 ) {
4715
			$plink = $this->numLink( $title, max( $offset - $limit, 0 ), $limit,
4716
				$query, $prev, 'prevn-title', 'mw-prevlink' );
4717
		} else {
4718
			$plink = htmlspecialchars( $prev );
4719
		}
4720
4721
		# Make 'next' link
4722
		$next = wfMessage( 'nextn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4723 View Code Duplication
		if ( $atend ) {
4724
			$nlink = htmlspecialchars( $next );
4725
		} else {
4726
			$nlink = $this->numLink( $title, $offset + $limit, $limit,
4727
				$query, $next, 'nextn-title', 'mw-nextlink' );
4728
		}
4729
4730
		# Make links to set number of items per page
4731
		$numLinks = [];
4732
		foreach ( [ 20, 50, 100, 250, 500 ] as $num ) {
4733
			$numLinks[] = $this->numLink( $title, $offset, $num,
4734
				$query, $this->formatNum( $num ), 'shown-title', 'mw-numlink' );
4735
		}
4736
4737
		return wfMessage( 'viewprevnext' )->inLanguage( $this )->title( $title
4738
			)->rawParams( $plink, $nlink, $this->pipeList( $numLinks ) )->escaped();
4739
	}
4740
4741
	/**
4742
	 * Helper function for viewPrevNext() that generates links
4743
	 *
4744
	 * @param Title $title Title object to link
4745
	 * @param int $offset
4746
	 * @param int $limit
4747
	 * @param array $query Extra query parameters
4748
	 * @param string $link Text to use for the link; will be escaped
4749
	 * @param string $tooltipMsg Name of the message to use as tooltip
4750
	 * @param string $class Value of the "class" attribute of the link
4751
	 * @return string HTML fragment
4752
	 */
4753
	private function numLink( Title $title, $offset, $limit, array $query, $link,
4754
		$tooltipMsg, $class
4755
	) {
4756
		$query = [ 'limit' => $limit, 'offset' => $offset ] + $query;
4757
		$tooltip = wfMessage( $tooltipMsg )->inLanguage( $this )->title( $title )
4758
			->numParams( $limit )->text();
4759
4760
		return Html::element( 'a', [ 'href' => $title->getLocalURL( $query ),
4761
			'title' => $tooltip, 'class' => $class ], $link );
4762
	}
4763
4764
	/**
4765
	 * Get the conversion rule title, if any.
4766
	 *
4767
	 * @return string
4768
	 */
4769
	public function getConvRuleTitle() {
4770
		return $this->mConverter->getConvRuleTitle();
4771
	}
4772
4773
	/**
4774
	 * Get the compiled plural rules for the language
4775
	 * @since 1.20
4776
	 * @return array Associative array with plural form, and plural rule as key-value pairs
4777
	 */
4778 View Code Duplication
	public function getCompiledPluralRules() {
4779
		$pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'compiledPluralRules' );
4780
		$fallbacks = Language::getFallbacksFor( $this->mCode );
4781
		if ( !$pluralRules ) {
4782
			foreach ( $fallbacks as $fallbackCode ) {
4783
				$pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' );
4784
				if ( $pluralRules ) {
4785
					break;
4786
				}
4787
			}
4788
		}
4789
		return $pluralRules;
4790
	}
4791
4792
	/**
4793
	 * Get the plural rules for the language
4794
	 * @since 1.20
4795
	 * @return array Associative array with plural form number and plural rule as key-value pairs
4796
	 */
4797 View Code Duplication
	public function getPluralRules() {
4798
		$pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRules' );
4799
		$fallbacks = Language::getFallbacksFor( $this->mCode );
4800
		if ( !$pluralRules ) {
4801
			foreach ( $fallbacks as $fallbackCode ) {
4802
				$pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRules' );
4803
				if ( $pluralRules ) {
4804
					break;
4805
				}
4806
			}
4807
		}
4808
		return $pluralRules;
4809
	}
4810
4811
	/**
4812
	 * Get the plural rule types for the language
4813
	 * @since 1.22
4814
	 * @return array Associative array with plural form number and plural rule type as key-value pairs
4815
	 */
4816 View Code Duplication
	public function getPluralRuleTypes() {
4817
		$pluralRuleTypes = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRuleTypes' );
4818
		$fallbacks = Language::getFallbacksFor( $this->mCode );
4819
		if ( !$pluralRuleTypes ) {
4820
			foreach ( $fallbacks as $fallbackCode ) {
4821
				$pluralRuleTypes = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRuleTypes' );
4822
				if ( $pluralRuleTypes ) {
4823
					break;
4824
				}
4825
			}
4826
		}
4827
		return $pluralRuleTypes;
4828
	}
4829
4830
	/**
4831
	 * Find the index number of the plural rule appropriate for the given number
4832
	 * @param int $number
4833
	 * @return int The index number of the plural rule
4834
	 */
4835
	public function getPluralRuleIndexNumber( $number ) {
4836
		$pluralRules = $this->getCompiledPluralRules();
4837
		$form = Evaluator::evaluateCompiled( $number, $pluralRules );
4838
		return $form;
4839
	}
4840
4841
	/**
4842
	 * Find the plural rule type appropriate for the given number
4843
	 * For example, if the language is set to Arabic, getPluralType(5) should
4844
	 * return 'few'.
4845
	 * @since 1.22
4846
	 * @param int $number
4847
	 * @return string The name of the plural rule type, e.g. one, two, few, many
4848
	 */
4849
	public function getPluralRuleType( $number ) {
4850
		$index = $this->getPluralRuleIndexNumber( $number );
4851
		$pluralRuleTypes = $this->getPluralRuleTypes();
4852
		if ( isset( $pluralRuleTypes[$index] ) ) {
4853
			return $pluralRuleTypes[$index];
4854
		} else {
4855
			return 'other';
4856
		}
4857
	}
4858
}
4859