Completed
Branch master (9259dd)
by
unknown
27:26
created

Language::sprintfDate()   F

Complexity

Conditions 128
Paths > 20000

Size

Total Lines 454
Code Lines 391

Duplication

Lines 102
Ratio 22.47 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 128
eloc 391
c 1
b 0
f 0
nc 71444
nop 4
dl 102
loc 454
rs 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 39 and the first side effect is on line 29.

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

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

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

Loading history...
2
/**
3
 * Internationalisation code.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup Language
22
 */
23
24
/**
25
 * @defgroup Language Language
26
 */
27
28
if ( !defined( 'MEDIAWIKI' ) ) {
29
	echo "This file is part of MediaWiki, it is not a valid entry point.\n";
30
	exit( 1 );
31
}
32
33
use CLDRPluralRuleParser\Evaluator;
34
35
/**
36
 * Internationalisation code
37
 * @ingroup Language
38
 */
39
class Language {
40
	/**
41
	 * @var LanguageConverter
42
	 */
43
	public $mConverter;
44
45
	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...
46
	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...
47
	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...
48
49
	public $dateFormatStrings = [];
50
	public $mExtendedSpecialPageAliases;
51
52
	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...
53
54
	/**
55
	 * ReplacementArray object caches
56
	 */
57
	public $transformData = [];
58
59
	/**
60
	 * @var LocalisationCache
61
	 */
62
	static public $dataCache;
63
64
	static public $mLangObjCache = [];
65
66
	static public $mWeekdayMsgs = [
67
		'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
68
		'friday', 'saturday'
69
	];
70
71
	static public $mWeekdayAbbrevMsgs = [
72
		'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
73
	];
74
75
	static public $mMonthMsgs = [
76
		'january', 'february', 'march', 'april', 'may_long', 'june',
77
		'july', 'august', 'september', 'october', 'november',
78
		'december'
79
	];
80
	static public $mMonthGenMsgs = [
81
		'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
82
		'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
83
		'december-gen'
84
	];
85
	static public $mMonthAbbrevMsgs = [
86
		'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
87
		'sep', 'oct', 'nov', 'dec'
88
	];
89
90
	static public $mIranianCalendarMonthMsgs = [
91
		'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
92
		'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
93
		'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
94
		'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
95
	];
96
97
	static public $mHebrewCalendarMonthMsgs = [
98
		'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
99
		'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
100
		'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
101
		'hebrew-calendar-m10', 'hebrew-calendar-m11', 'hebrew-calendar-m12',
102
		'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
103
	];
104
105
	static public $mHebrewCalendarMonthGenMsgs = [
106
		'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
107
		'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
108
		'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
109
		'hebrew-calendar-m10-gen', 'hebrew-calendar-m11-gen', 'hebrew-calendar-m12-gen',
110
		'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
111
	];
112
113
	static public $mHijriCalendarMonthMsgs = [
114
		'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
115
		'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
116
		'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
117
		'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12'
118
	];
119
120
	/**
121
	 * @since 1.20
122
	 * @var array
123
	 */
124
	static public $durationIntervals = [
125
		'millennia' => 31556952000,
126
		'centuries' => 3155695200,
127
		'decades' => 315569520,
128
		'years' => 31556952, // 86400 * ( 365 + ( 24 * 3 + 25 ) / 400 )
129
		'weeks' => 604800,
130
		'days' => 86400,
131
		'hours' => 3600,
132
		'minutes' => 60,
133
		'seconds' => 1,
134
	];
135
136
	/**
137
	 * Cache for language fallbacks.
138
	 * @see Language::getFallbacksIncludingSiteLanguage
139
	 * @since 1.21
140
	 * @var array
141
	 */
142
	static private $fallbackLanguageCache = [];
143
144
	/**
145
	 * Cache for language names
146
	 * @var HashBagOStuff|null
147
	 */
148
	static private $languageNameCache;
149
150
	/**
151
	 * Unicode directional formatting characters, for embedBidi()
152
	 */
153
	static private $lre = "\xE2\x80\xAA"; // U+202A LEFT-TO-RIGHT EMBEDDING
154
	static private $rle = "\xE2\x80\xAB"; // U+202B RIGHT-TO-LEFT EMBEDDING
155
	static private $pdf = "\xE2\x80\xAC"; // U+202C POP DIRECTIONAL FORMATTING
156
157
	/**
158
	 * Directionality test regex for embedBidi(). Matches the first strong directionality codepoint:
159
	 * - in group 1 if it is LTR
160
	 * - in group 2 if it is RTL
161
	 * Does not match if there is no strong directionality codepoint.
162
	 *
163
	 * The form is '/(?:([strong ltr codepoint])|([strong rtl codepoint]))/u' .
164
	 *
165
	 * Generated by UnicodeJS (see tools/strongDir) from the UCD; see
166
	 * https://git.wikimedia.org/summary/unicodejs.git .
167
	 */
168
	// @codingStandardsIgnoreStart
169
	// @codeCoverageIgnoreStart
170
	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';
171
	// @codeCoverageIgnoreEnd
172
	// @codingStandardsIgnoreEnd
173
174
	/**
175
	 * Get a cached or new language object for a given language code
176
	 * @param string $code
177
	 * @return Language
178
	 */
179
	static function factory( $code ) {
180
		global $wgDummyLanguageCodes, $wgLangObjCacheSize;
181
182
		if ( isset( $wgDummyLanguageCodes[$code] ) ) {
183
			$code = $wgDummyLanguageCodes[$code];
184
		}
185
186
		// get the language object to process
187
		$langObj = isset( self::$mLangObjCache[$code] )
188
			? self::$mLangObjCache[$code]
189
			: self::newFromCode( $code );
190
191
		// merge the language object in to get it up front in the cache
192
		self::$mLangObjCache = array_merge( [ $code => $langObj ], self::$mLangObjCache );
193
		// get rid of the oldest ones in case we have an overflow
194
		self::$mLangObjCache = array_slice( self::$mLangObjCache, 0, $wgLangObjCacheSize, true );
195
196
		return $langObj;
197
	}
198
199
	/**
200
	 * Create a language object for a given language code
201
	 * @param string $code
202
	 * @throws MWException
203
	 * @return Language
204
	 */
205
	protected static function newFromCode( $code ) {
206
		if ( !Language::isValidCode( $code ) ) {
207
			throw new MWException( "Invalid language code \"$code\"" );
208
		}
209
210
		if ( !Language::isValidBuiltInCode( $code ) ) {
211
			// It's not possible to customise this code with class files, so
212
			// just return a Language object. This is to support uselang= hacks.
213
			$lang = new Language;
214
			$lang->setCode( $code );
215
			return $lang;
216
		}
217
218
		// Check if there is a language class for the code
219
		$class = self::classFromCode( $code );
220
		if ( class_exists( $class ) ) {
221
			$lang = new $class;
222
			return $lang;
223
		}
224
225
		// Keep trying the fallback list until we find an existing class
226
		$fallbacks = Language::getFallbacksFor( $code );
227
		foreach ( $fallbacks as $fallbackCode ) {
228
			if ( !Language::isValidBuiltInCode( $fallbackCode ) ) {
229
				throw new MWException( "Invalid fallback '$fallbackCode' in fallback sequence for '$code'" );
230
			}
231
232
			$class = self::classFromCode( $fallbackCode );
233
			if ( class_exists( $class ) ) {
234
				$lang = new $class;
235
				$lang->setCode( $code );
236
				return $lang;
237
			}
238
		}
239
240
		throw new MWException( "Invalid fallback sequence for language '$code'" );
241
	}
242
243
	/**
244
	 * Checks whether any localisation is available for that language tag
245
	 * in MediaWiki (MessagesXx.php exists).
246
	 *
247
	 * @param string $code Language tag (in lower case)
248
	 * @return bool Whether language is supported
249
	 * @since 1.21
250
	 */
251
	public static function isSupportedLanguage( $code ) {
252
		if ( !self::isValidBuiltInCode( $code ) ) {
253
			return false;
254
		}
255
256
		if ( $code === 'qqq' ) {
257
			return false;
258
		}
259
260
		return is_readable( self::getMessagesFileName( $code ) ) ||
261
			is_readable( self::getJsonMessagesFileName( $code ) );
262
	}
263
264
	/**
265
	 * Returns true if a language code string is a well-formed language tag
266
	 * according to RFC 5646.
267
	 * This function only checks well-formedness; it doesn't check that
268
	 * language, script or variant codes actually exist in the repositories.
269
	 *
270
	 * Based on regexes by Mark Davis of the Unicode Consortium:
271
	 * http://unicode.org/repos/cldr/trunk/tools/java/org/unicode/cldr/util/data/langtagRegex.txt
272
	 *
273
	 * @param string $code
274
	 * @param bool $lenient Whether to allow '_' as separator. The default is only '-'.
275
	 *
276
	 * @return bool
277
	 * @since 1.21
278
	 */
279
	public static function isWellFormedLanguageTag( $code, $lenient = false ) {
280
		$alpha = '[a-z]';
281
		$digit = '[0-9]';
282
		$alphanum = '[a-z0-9]';
283
		$x = 'x'; # private use singleton
284
		$singleton = '[a-wy-z]'; # other singleton
285
		$s = $lenient ? '[-_]' : '-';
286
287
		$language = "$alpha{2,8}|$alpha{2,3}$s$alpha{3}";
288
		$script = "$alpha{4}"; # ISO 15924
289
		$region = "(?:$alpha{2}|$digit{3})"; # ISO 3166-1 alpha-2 or UN M.49
290
		$variant = "(?:$alphanum{5,8}|$digit$alphanum{3})";
291
		$extension = "$singleton(?:$s$alphanum{2,8})+";
292
		$privateUse = "$x(?:$s$alphanum{1,8})+";
293
294
		# Define certain grandfathered codes, since otherwise the regex is pretty useless.
295
		# Since these are limited, this is safe even later changes to the registry --
296
		# the only oddity is that it might change the type of the tag, and thus
297
		# the results from the capturing groups.
298
		# http://www.iana.org/assignments/language-subtag-registry
299
300
		$grandfathered = "en{$s}GB{$s}oed"
301
			. "|i{$s}(?:ami|bnn|default|enochian|hak|klingon|lux|mingo|navajo|pwn|tao|tay|tsu)"
302
			. "|no{$s}(?:bok|nyn)"
303
			. "|sgn{$s}(?:BE{$s}(?:fr|nl)|CH{$s}de)"
304
			. "|zh{$s}min{$s}nan";
305
306
		$variantList = "$variant(?:$s$variant)*";
307
		$extensionList = "$extension(?:$s$extension)*";
308
309
		$langtag = "(?:($language)"
310
			. "(?:$s$script)?"
311
			. "(?:$s$region)?"
312
			. "(?:$s$variantList)?"
313
			. "(?:$s$extensionList)?"
314
			. "(?:$s$privateUse)?)";
315
316
		# The final breakdown, with capturing groups for each of these components
317
		# The variants, extensions, grandfathered, and private-use may have interior '-'
318
319
		$root = "^(?:$langtag|$privateUse|$grandfathered)$";
320
321
		return (bool)preg_match( "/$root/", strtolower( $code ) );
322
	}
323
324
	/**
325
	 * Returns true if a language code string is of a valid form, whether or
326
	 * not it exists. This includes codes which are used solely for
327
	 * customisation via the MediaWiki namespace.
328
	 *
329
	 * @param string $code
330
	 *
331
	 * @return bool
332
	 */
333
	public static function isValidCode( $code ) {
334
		static $cache = [];
335
		if ( !isset( $cache[$code] ) ) {
336
			// People think language codes are html safe, so enforce it.
337
			// Ideally we should only allow a-zA-Z0-9-
338
			// but, .+ and other chars are often used for {{int:}} hacks
339
			// see bugs 37564, 37587, 36938
340
			$cache[$code] =
341
				// Protect against path traversal
342
				strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code )
343
				&& !preg_match( MediaWikiTitleCodec::getTitleInvalidRegex(), $code );
344
		}
345
		return $cache[$code];
346
	}
347
348
	/**
349
	 * Returns true if a language code is of a valid form for the purposes of
350
	 * internal customisation of MediaWiki, via Messages*.php or *.json.
351
	 *
352
	 * @param string $code
353
	 *
354
	 * @throws MWException
355
	 * @since 1.18
356
	 * @return bool
357
	 */
358
	public static function isValidBuiltInCode( $code ) {
359
360
		if ( !is_string( $code ) ) {
361
			if ( is_object( $code ) ) {
362
				$addmsg = " of class " . get_class( $code );
363
			} else {
364
				$addmsg = '';
365
			}
366
			$type = gettype( $code );
367
			throw new MWException( __METHOD__ . " must be passed a string, $type given$addmsg" );
368
		}
369
370
		return (bool)preg_match( '/^[a-z0-9-]{2,}$/', $code );
371
	}
372
373
	/**
374
	 * Returns true if a language code is an IETF tag known to MediaWiki.
375
	 *
376
	 * @param string $tag
377
	 *
378
	 * @since 1.21
379
	 * @return bool
380
	 */
381
	public static function isKnownLanguageTag( $tag ) {
382
		// Quick escape for invalid input to avoid exceptions down the line
383
		// when code tries to process tags which are not valid at all.
384
		if ( !self::isValidBuiltInCode( $tag ) ) {
385
			return false;
386
		}
387
388
		if ( isset( MediaWiki\Languages\Data\Names::$names[$tag] )
389
			|| self::fetchLanguageName( $tag, $tag ) !== ''
390
		) {
391
			return true;
392
		}
393
394
		return false;
395
	}
396
397
	/**
398
	 * Get the LocalisationCache instance
399
	 *
400
	 * @return LocalisationCache
401
	 */
402
	public static function getLocalisationCache() {
403
		if ( is_null( self::$dataCache ) ) {
404
			global $wgLocalisationCacheConf;
405
			$class = $wgLocalisationCacheConf['class'];
406
			self::$dataCache = new $class( $wgLocalisationCacheConf );
407
		}
408
		return self::$dataCache;
409
	}
410
411
	function __construct() {
412
		$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...
413
		// Set the code to the name of the descendant
414
		if ( get_class( $this ) == 'Language' ) {
415
			$this->mCode = 'en';
416
		} else {
417
			$this->mCode = str_replace( '_', '-', strtolower( substr( get_class( $this ), 8 ) ) );
418
		}
419
		self::getLocalisationCache();
420
	}
421
422
	/**
423
	 * Reduce memory usage
424
	 */
425
	function __destruct() {
426
		foreach ( $this as $name => $value ) {
0 ignored issues
show
Bug introduced by
The expression $this of type this<Language> is not traversable.
Loading history...
427
			unset( $this->$name );
428
		}
429
	}
430
431
	/**
432
	 * Hook which will be called if this is the content language.
433
	 * Descendants can use this to register hook functions or modify globals
434
	 */
435
	function initContLang() {
436
	}
437
438
	/**
439
	 * @return array
440
	 * @since 1.19
441
	 */
442
	public function getFallbackLanguages() {
443
		return self::getFallbacksFor( $this->mCode );
444
	}
445
446
	/**
447
	 * Exports $wgBookstoreListEn
448
	 * @return array
449
	 */
450
	public function getBookstoreList() {
451
		return self::$dataCache->getItem( $this->mCode, 'bookstoreList' );
452
	}
453
454
	/**
455
	 * Returns an array of localised namespaces indexed by their numbers. If the namespace is not
456
	 * available in localised form, it will be included in English.
457
	 *
458
	 * @return array
459
	 */
460
	public function getNamespaces() {
461
		if ( is_null( $this->namespaceNames ) ) {
462
			global $wgMetaNamespace, $wgMetaNamespaceTalk, $wgExtraNamespaces;
463
464
			$this->namespaceNames = self::$dataCache->getItem( $this->mCode, 'namespaceNames' );
465
			$validNamespaces = MWNamespace::getCanonicalNamespaces();
466
467
			$this->namespaceNames = $wgExtraNamespaces + $this->namespaceNames + $validNamespaces;
468
469
			$this->namespaceNames[NS_PROJECT] = $wgMetaNamespace;
470
			if ( $wgMetaNamespaceTalk ) {
471
				$this->namespaceNames[NS_PROJECT_TALK] = $wgMetaNamespaceTalk;
472
			} else {
473
				$talk = $this->namespaceNames[NS_PROJECT_TALK];
474
				$this->namespaceNames[NS_PROJECT_TALK] =
475
					$this->fixVariableInNamespace( $talk );
476
			}
477
478
			# Sometimes a language will be localised but not actually exist on this wiki.
479
			foreach ( $this->namespaceNames as $key => $text ) {
480
				if ( !isset( $validNamespaces[$key] ) ) {
481
					unset( $this->namespaceNames[$key] );
482
				}
483
			}
484
485
			# The above mixing may leave namespaces out of canonical order.
486
			# Re-order by namespace ID number...
487
			ksort( $this->namespaceNames );
488
489
			Hooks::run( 'LanguageGetNamespaces', [ &$this->namespaceNames ] );
490
		}
491
492
		return $this->namespaceNames;
493
	}
494
495
	/**
496
	 * Arbitrarily set all of the namespace names at once. Mainly used for testing
497
	 * @param array $namespaces Array of namespaces (id => name)
498
	 */
499
	public function setNamespaces( array $namespaces ) {
500
		$this->namespaceNames = $namespaces;
501
		$this->mNamespaceIds = null;
502
	}
503
504
	/**
505
	 * Resets all of the namespace caches. Mainly used for testing
506
	 */
507
	public function resetNamespaces() {
508
		$this->namespaceNames = null;
509
		$this->mNamespaceIds = null;
510
		$this->namespaceAliases = null;
511
	}
512
513
	/**
514
	 * A convenience function that returns getNamespaces() with spaces instead of underscores
515
	 * in values. Useful for producing output to be displayed e.g. in `<select>` forms.
516
	 *
517
	 * @return array
518
	 */
519
	public function getFormattedNamespaces() {
520
		$ns = $this->getNamespaces();
521
		foreach ( $ns as $k => $v ) {
522
			$ns[$k] = strtr( $v, '_', ' ' );
523
		}
524
		return $ns;
525
	}
526
527
	/**
528
	 * Get a namespace value by key
529
	 *
530
	 * <code>
531
	 * $mw_ns = $wgContLang->getNsText( NS_MEDIAWIKI );
532
	 * echo $mw_ns; // prints 'MediaWiki'
533
	 * </code>
534
	 *
535
	 * @param int $index The array key of the namespace to return
536
	 * @return string|bool String if the namespace value exists, otherwise false
537
	 */
538
	public function getNsText( $index ) {
539
		$ns = $this->getNamespaces();
540
		return isset( $ns[$index] ) ? $ns[$index] : false;
541
	}
542
543
	/**
544
	 * A convenience function that returns the same thing as
545
	 * getNsText() except with '_' changed to ' ', useful for
546
	 * producing output.
547
	 *
548
	 * <code>
549
	 * $mw_ns = $wgContLang->getFormattedNsText( NS_MEDIAWIKI_TALK );
550
	 * echo $mw_ns; // prints 'MediaWiki talk'
551
	 * </code>
552
	 *
553
	 * @param int $index The array key of the namespace to return
554
	 * @return string Namespace name without underscores (empty string if namespace does not exist)
555
	 */
556
	public function getFormattedNsText( $index ) {
557
		$ns = $this->getNsText( $index );
558
		return strtr( $ns, '_', ' ' );
0 ignored issues
show
Bug introduced by
It seems like $ns defined by $this->getNsText($index) on line 557 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...
559
	}
560
561
	/**
562
	 * Returns gender-dependent namespace alias if available.
563
	 * See https://www.mediawiki.org/wiki/Manual:$wgExtraGenderNamespaces
564
	 * @param int $index Namespace index
565
	 * @param string $gender Gender key (male, female... )
566
	 * @return string
567
	 * @since 1.18
568
	 */
569
	public function getGenderNsText( $index, $gender ) {
570
		global $wgExtraGenderNamespaces;
571
572
		$ns = $wgExtraGenderNamespaces +
573
			(array)self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
574
575
		return isset( $ns[$index][$gender] ) ? $ns[$index][$gender] : $this->getNsText( $index );
576
	}
577
578
	/**
579
	 * Whether this language uses gender-dependent namespace aliases.
580
	 * See https://www.mediawiki.org/wiki/Manual:$wgExtraGenderNamespaces
581
	 * @return bool
582
	 * @since 1.18
583
	 */
584
	public function needsGenderDistinction() {
585
		global $wgExtraGenderNamespaces, $wgExtraNamespaces;
586
		if ( count( $wgExtraGenderNamespaces ) > 0 ) {
587
			// $wgExtraGenderNamespaces overrides everything
588
			return true;
589
		} elseif ( isset( $wgExtraNamespaces[NS_USER] ) && isset( $wgExtraNamespaces[NS_USER_TALK] ) ) {
590
			/// @todo There may be other gender namespace than NS_USER & NS_USER_TALK in the future
591
			// $wgExtraNamespaces overrides any gender aliases specified in i18n files
592
			return false;
593
		} else {
594
			// Check what is in i18n files
595
			$aliases = self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
596
			return count( $aliases ) > 0;
597
		}
598
	}
599
600
	/**
601
	 * Get a namespace key by value, case insensitive.
602
	 * Only matches namespace names for the current language, not the
603
	 * canonical ones defined in Namespace.php.
604
	 *
605
	 * @param string $text
606
	 * @return int|bool An integer if $text is a valid value otherwise false
607
	 */
608
	function getLocalNsIndex( $text ) {
609
		$lctext = $this->lc( $text );
610
		$ids = $this->getNamespaceIds();
611
		return isset( $ids[$lctext] ) ? $ids[$lctext] : false;
612
	}
613
614
	/**
615
	 * @return array
616
	 */
617
	public function getNamespaceAliases() {
618
		if ( is_null( $this->namespaceAliases ) ) {
619
			$aliases = self::$dataCache->getItem( $this->mCode, 'namespaceAliases' );
620
			if ( !$aliases ) {
621
				$aliases = [];
622
			} else {
623
				foreach ( $aliases as $name => $index ) {
624
					if ( $index === NS_PROJECT_TALK ) {
625
						unset( $aliases[$name] );
626
						$name = $this->fixVariableInNamespace( $name );
627
						$aliases[$name] = $index;
628
					}
629
				}
630
			}
631
632
			global $wgExtraGenderNamespaces;
633
			$genders = $wgExtraGenderNamespaces +
634
				(array)self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
635
			foreach ( $genders as $index => $forms ) {
636
				foreach ( $forms as $alias ) {
637
					$aliases[$alias] = $index;
638
				}
639
			}
640
641
			# Also add converted namespace names as aliases, to avoid confusion.
642
			$convertedNames = [];
643
			foreach ( $this->getVariants() as $variant ) {
644
				if ( $variant === $this->mCode ) {
645
					continue;
646
				}
647
				foreach ( $this->getNamespaces() as $ns => $_ ) {
648
					$convertedNames[$this->getConverter()->convertNamespace( $ns, $variant )] = $ns;
649
				}
650
			}
651
652
			$this->namespaceAliases = $aliases + $convertedNames;
653
		}
654
655
		return $this->namespaceAliases;
656
	}
657
658
	/**
659
	 * @return array
660
	 */
661
	public function getNamespaceIds() {
662
		if ( is_null( $this->mNamespaceIds ) ) {
663
			global $wgNamespaceAliases;
664
			# Put namespace names and aliases into a hashtable.
665
			# If this is too slow, then we should arrange it so that it is done
666
			# before caching. The catch is that at pre-cache time, the above
667
			# class-specific fixup hasn't been done.
668
			$this->mNamespaceIds = [];
669
			foreach ( $this->getNamespaces() as $index => $name ) {
670
				$this->mNamespaceIds[$this->lc( $name )] = $index;
671
			}
672
			foreach ( $this->getNamespaceAliases() as $name => $index ) {
673
				$this->mNamespaceIds[$this->lc( $name )] = $index;
674
			}
675
			if ( $wgNamespaceAliases ) {
676
				foreach ( $wgNamespaceAliases as $name => $index ) {
677
					$this->mNamespaceIds[$this->lc( $name )] = $index;
678
				}
679
			}
680
		}
681
		return $this->mNamespaceIds;
682
	}
683
684
	/**
685
	 * Get a namespace key by value, case insensitive.  Canonical namespace
686
	 * names override custom ones defined for the current language.
687
	 *
688
	 * @param string $text
689
	 * @return int|bool An integer if $text is a valid value otherwise false
690
	 */
691
	public function getNsIndex( $text ) {
692
		$lctext = $this->lc( $text );
693
		$ns = MWNamespace::getCanonicalIndex( $lctext );
694
		if ( $ns !== null ) {
695
			return $ns;
696
		}
697
		$ids = $this->getNamespaceIds();
698
		return isset( $ids[$lctext] ) ? $ids[$lctext] : false;
699
	}
700
701
	/**
702
	 * short names for language variants used for language conversion links.
703
	 *
704
	 * @param string $code
705
	 * @param bool $usemsg Use the "variantname-xyz" message if it exists
706
	 * @return string
707
	 */
708
	public function getVariantname( $code, $usemsg = true ) {
709
		$msg = "variantname-$code";
710
		if ( $usemsg && wfMessage( $msg )->exists() ) {
711
			return $this->getMessageFromDB( $msg );
712
		}
713
		$name = self::fetchLanguageName( $code );
714
		if ( $name ) {
715
			return $name; # if it's defined as a language name, show that
716
		} else {
717
			# otherwise, output the language code
718
			return $code;
719
		}
720
	}
721
722
	/**
723
	 * @return array
724
	 */
725
	public function getDatePreferences() {
726
		return self::$dataCache->getItem( $this->mCode, 'datePreferences' );
727
	}
728
729
	/**
730
	 * @return array
731
	 */
732
	function getDateFormats() {
733
		return self::$dataCache->getItem( $this->mCode, 'dateFormats' );
734
	}
735
736
	/**
737
	 * @return array|string
738
	 */
739
	public function getDefaultDateFormat() {
740
		$df = self::$dataCache->getItem( $this->mCode, 'defaultDateFormat' );
741
		if ( $df === 'dmy or mdy' ) {
742
			global $wgAmericanDates;
743
			return $wgAmericanDates ? 'mdy' : 'dmy';
744
		} else {
745
			return $df;
746
		}
747
	}
748
749
	/**
750
	 * @return array
751
	 */
752
	public function getDatePreferenceMigrationMap() {
753
		return self::$dataCache->getItem( $this->mCode, 'datePreferenceMigrationMap' );
754
	}
755
756
	/**
757
	 * @param string $image
758
	 * @return array|null
759
	 */
760
	function getImageFile( $image ) {
761
		return self::$dataCache->getSubitem( $this->mCode, 'imageFiles', $image );
762
	}
763
764
	/**
765
	 * @return array
766
	 * @since 1.24
767
	 */
768
	public function getImageFiles() {
769
		return self::$dataCache->getItem( $this->mCode, 'imageFiles' );
770
	}
771
772
	/**
773
	 * @return array
774
	 */
775
	public function getExtraUserToggles() {
776
		return (array)self::$dataCache->getItem( $this->mCode, 'extraUserToggles' );
777
	}
778
779
	/**
780
	 * @param string $tog
781
	 * @return string
782
	 */
783
	function getUserToggle( $tog ) {
784
		return $this->getMessageFromDB( "tog-$tog" );
785
	}
786
787
	/**
788
	 * Get an array of language names, indexed by code.
789
	 * @param null|string $inLanguage Code of language in which to return the names
790
	 *		Use null for autonyms (native names)
791
	 * @param string $include One of:
792
	 *		'all' all available languages
793
	 *		'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames (default)
794
	 *		'mwfile' only if the language is in 'mw' *and* has a message file
795
	 * @return array Language code => language name
796
	 * @since 1.20
797
	 */
798
	public static function fetchLanguageNames( $inLanguage = null, $include = 'mw' ) {
799
		$cacheKey = $inLanguage === null ? 'null' : $inLanguage;
800
		$cacheKey .= ":$include";
801
		if ( self::$languageNameCache === null ) {
802
			self::$languageNameCache = new HashBagOStuff( [ 'maxKeys' => 20 ] );
803
		}
804
805
		$ret = self::$languageNameCache->get( $cacheKey );
806
		if ( !$ret ) {
807
			$ret = self::fetchLanguageNamesUncached( $inLanguage, $include );
808
			self::$languageNameCache->set( $cacheKey, $ret );
809
		}
810
		return $ret;
811
	}
812
813
	/**
814
	 * Uncached helper for fetchLanguageNames
815
	 * @param null|string $inLanguage Code of language in which to return the names
816
	 *		Use null for autonyms (native names)
817
	 * @param string $include One of:
818
	 *		'all' all available languages
819
	 *		'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames (default)
820
	 *		'mwfile' only if the language is in 'mw' *and* has a message file
821
	 * @return array Language code => language name
822
	 */
823
	private static function fetchLanguageNamesUncached( $inLanguage = null, $include = 'mw' ) {
824
		global $wgExtraLanguageNames;
825
826
		// If passed an invalid language code to use, fallback to en
827
		if ( $inLanguage !== null && !Language::isValidCode( $inLanguage ) ) {
828
			$inLanguage = 'en';
829
		}
830
831
		$names = [];
832
833
		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...
834
			# TODO: also include when $inLanguage is null, when this code is more efficient
835
			Hooks::run( 'LanguageGetTranslatedLanguageNames', [ &$names, $inLanguage ] );
836
		}
837
838
		$mwNames = $wgExtraLanguageNames + MediaWiki\Languages\Data\Names::$names;
839
		foreach ( $mwNames as $mwCode => $mwName ) {
840
			# - Prefer own MediaWiki native name when not using the hook
841
			# - For other names just add if not added through the hook
842
			if ( $mwCode === $inLanguage || !isset( $names[$mwCode] ) ) {
843
				$names[$mwCode] = $mwName;
844
			}
845
		}
846
847
		if ( $include === 'all' ) {
848
			ksort( $names );
849
			return $names;
850
		}
851
852
		$returnMw = [];
853
		$coreCodes = array_keys( $mwNames );
854
		foreach ( $coreCodes as $coreCode ) {
855
			$returnMw[$coreCode] = $names[$coreCode];
856
		}
857
858
		if ( $include === 'mwfile' ) {
859
			$namesMwFile = [];
860
			# We do this using a foreach over the codes instead of a directory
861
			# loop so that messages files in extensions will work correctly.
862
			foreach ( $returnMw as $code => $value ) {
863
				if ( is_readable( self::getMessagesFileName( $code ) )
864
					|| is_readable( self::getJsonMessagesFileName( $code ) )
865
				) {
866
					$namesMwFile[$code] = $names[$code];
867
				}
868
			}
869
870
			ksort( $namesMwFile );
871
			return $namesMwFile;
872
		}
873
874
		ksort( $returnMw );
875
		# 'mw' option; default if it's not one of the other two options (all/mwfile)
876
		return $returnMw;
877
	}
878
879
	/**
880
	 * @param string $code The code of the language for which to get the name
881
	 * @param null|string $inLanguage Code of language in which to return the name (null for autonyms)
882
	 * @param string $include 'all', 'mw' or 'mwfile'; see fetchLanguageNames()
883
	 * @return string Language name or empty
884
	 * @since 1.20
885
	 */
886
	public static function fetchLanguageName( $code, $inLanguage = null, $include = 'all' ) {
887
		$code = strtolower( $code );
888
		$array = self::fetchLanguageNames( $inLanguage, $include );
889
		return !array_key_exists( $code, $array ) ? '' : $array[$code];
890
	}
891
892
	/**
893
	 * Get a message from the MediaWiki namespace.
894
	 *
895
	 * @param string $msg Message name
896
	 * @return string
897
	 */
898
	public function getMessageFromDB( $msg ) {
899
		return $this->msg( $msg )->text();
900
	}
901
902
	/**
903
	 * Get message object in this language. Only for use inside this class.
904
	 *
905
	 * @param string $msg Message name
906
	 * @return Message
907
	 */
908
	protected function msg( $msg ) {
909
		return wfMessage( $msg )->inLanguage( $this );
910
	}
911
912
	/**
913
	 * @param string $key
914
	 * @return string
915
	 */
916
	public function getMonthName( $key ) {
917
		return $this->getMessageFromDB( self::$mMonthMsgs[$key - 1] );
918
	}
919
920
	/**
921
	 * @return array
922
	 */
923 View Code Duplication
	public function getMonthNamesArray() {
924
		$monthNames = [ '' ];
925
		for ( $i = 1; $i < 13; $i++ ) {
926
			$monthNames[] = $this->getMonthName( $i );
927
		}
928
		return $monthNames;
929
	}
930
931
	/**
932
	 * @param string $key
933
	 * @return string
934
	 */
935
	public function getMonthNameGen( $key ) {
936
		return $this->getMessageFromDB( self::$mMonthGenMsgs[$key - 1] );
937
	}
938
939
	/**
940
	 * @param string $key
941
	 * @return string
942
	 */
943
	public function getMonthAbbreviation( $key ) {
944
		return $this->getMessageFromDB( self::$mMonthAbbrevMsgs[$key - 1] );
945
	}
946
947
	/**
948
	 * @return array
949
	 */
950 View Code Duplication
	public function getMonthAbbreviationsArray() {
951
		$monthNames = [ '' ];
952
		for ( $i = 1; $i < 13; $i++ ) {
953
			$monthNames[] = $this->getMonthAbbreviation( $i );
954
		}
955
		return $monthNames;
956
	}
957
958
	/**
959
	 * @param string $key
960
	 * @return string
961
	 */
962
	public function getWeekdayName( $key ) {
963
		return $this->getMessageFromDB( self::$mWeekdayMsgs[$key - 1] );
964
	}
965
966
	/**
967
	 * @param string $key
968
	 * @return string
969
	 */
970
	function getWeekdayAbbreviation( $key ) {
971
		return $this->getMessageFromDB( self::$mWeekdayAbbrevMsgs[$key - 1] );
972
	}
973
974
	/**
975
	 * @param string $key
976
	 * @return string
977
	 */
978
	function getIranianCalendarMonthName( $key ) {
979
		return $this->getMessageFromDB( self::$mIranianCalendarMonthMsgs[$key - 1] );
980
	}
981
982
	/**
983
	 * @param string $key
984
	 * @return string
985
	 */
986
	function getHebrewCalendarMonthName( $key ) {
987
		return $this->getMessageFromDB( self::$mHebrewCalendarMonthMsgs[$key - 1] );
988
	}
989
990
	/**
991
	 * @param string $key
992
	 * @return string
993
	 */
994
	function getHebrewCalendarMonthNameGen( $key ) {
995
		return $this->getMessageFromDB( self::$mHebrewCalendarMonthGenMsgs[$key - 1] );
996
	}
997
998
	/**
999
	 * @param string $key
1000
	 * @return string
1001
	 */
1002
	function getHijriCalendarMonthName( $key ) {
1003
		return $this->getMessageFromDB( self::$mHijriCalendarMonthMsgs[$key - 1] );
1004
	}
1005
1006
	/**
1007
	 * Pass through result from $dateTimeObj->format()
1008
	 * @param DateTime|bool|null &$dateTimeObj
1009
	 * @param string $ts
1010
	 * @param DateTimeZone|bool|null $zone
1011
	 * @param string $code
1012
	 * @return string
1013
	 */
1014
	private static function dateTimeObjFormat( &$dateTimeObj, $ts, $zone, $code ) {
1015
		if ( !$dateTimeObj ) {
1016
			$dateTimeObj = DateTime::createFromFormat(
1017
				'YmdHis', $ts, $zone ?: new DateTimeZone( 'UTC' )
1018
			);
1019
		}
1020
		return $dateTimeObj->format( $code );
1021
	}
1022
1023
	/**
1024
	 * This is a workalike of PHP's date() function, but with better
1025
	 * internationalisation, a reduced set of format characters, and a better
1026
	 * escaping format.
1027
	 *
1028
	 * Supported format characters are dDjlNwzWFmMntLoYyaAgGhHiscrUeIOPTZ. See
1029
	 * the PHP manual for definitions. There are a number of extensions, which
1030
	 * start with "x":
1031
	 *
1032
	 *    xn   Do not translate digits of the next numeric format character
1033
	 *    xN   Toggle raw digit (xn) flag, stays set until explicitly unset
1034
	 *    xr   Use roman numerals for the next numeric format character
1035
	 *    xh   Use hebrew numerals for the next numeric format character
1036
	 *    xx   Literal x
1037
	 *    xg   Genitive month name
1038
	 *
1039
	 *    xij  j (day number) in Iranian calendar
1040
	 *    xiF  F (month name) in Iranian calendar
1041
	 *    xin  n (month number) in Iranian calendar
1042
	 *    xiy  y (two digit year) in Iranian calendar
1043
	 *    xiY  Y (full year) in Iranian calendar
1044
	 *
1045
	 *    xjj  j (day number) in Hebrew calendar
1046
	 *    xjF  F (month name) in Hebrew calendar
1047
	 *    xjt  t (days in month) in Hebrew calendar
1048
	 *    xjx  xg (genitive month name) in Hebrew calendar
1049
	 *    xjn  n (month number) in Hebrew calendar
1050
	 *    xjY  Y (full year) in Hebrew calendar
1051
	 *
1052
	 *    xmj  j (day number) in Hijri calendar
1053
	 *    xmF  F (month name) in Hijri calendar
1054
	 *    xmn  n (month number) in Hijri calendar
1055
	 *    xmY  Y (full year) in Hijri calendar
1056
	 *
1057
	 *    xkY  Y (full year) in Thai solar calendar. Months and days are
1058
	 *                       identical to the Gregorian calendar
1059
	 *    xoY  Y (full year) in Minguo calendar or Juche year.
1060
	 *                       Months and days are identical to the
1061
	 *                       Gregorian calendar
1062
	 *    xtY  Y (full year) in Japanese nengo. Months and days are
1063
	 *                       identical to the Gregorian calendar
1064
	 *
1065
	 * Characters enclosed in double quotes will be considered literal (with
1066
	 * the quotes themselves removed). Unmatched quotes will be considered
1067
	 * literal quotes. Example:
1068
	 *
1069
	 * "The month is" F       => The month is January
1070
	 * i's"                   => 20'11"
1071
	 *
1072
	 * Backslash escaping is also supported.
1073
	 *
1074
	 * Input timestamp is assumed to be pre-normalized to the desired local
1075
	 * time zone, if any. Note that the format characters crUeIOPTZ will assume
1076
	 * $ts is UTC if $zone is not given.
1077
	 *
1078
	 * @param string $format
1079
	 * @param string $ts 14-character timestamp
1080
	 *      YYYYMMDDHHMMSS
1081
	 *      01234567890123
1082
	 * @param DateTimeZone $zone Timezone of $ts
1083
	 * @param[out] int $ttl The amount of time (in seconds) the output may be cached for.
1084
	 * Only makes sense if $ts is the current time.
1085
	 * @todo handling of "o" format character for Iranian, Hebrew, Hijri & Thai?
1086
	 *
1087
	 * @throws MWException
1088
	 * @return string
1089
	 */
1090
	public function sprintfDate( $format, $ts, DateTimeZone $zone = null, &$ttl = 'unused' ) {
1091
		$s = '';
1092
		$raw = false;
1093
		$roman = false;
1094
		$hebrewNum = false;
1095
		$dateTimeObj = false;
1096
		$rawToggle = false;
1097
		$iranian = false;
1098
		$hebrew = false;
1099
		$hijri = false;
1100
		$thai = false;
1101
		$minguo = false;
1102
		$tenno = false;
1103
1104
		$usedSecond = false;
1105
		$usedMinute = false;
1106
		$usedHour = false;
1107
		$usedAMPM = false;
1108
		$usedDay = false;
1109
		$usedWeek = false;
1110
		$usedMonth = false;
1111
		$usedYear = false;
1112
		$usedISOYear = false;
1113
		$usedIsLeapYear = false;
1114
1115
		$usedHebrewMonth = false;
1116
		$usedIranianMonth = false;
1117
		$usedHijriMonth = false;
1118
		$usedHebrewYear = false;
1119
		$usedIranianYear = false;
1120
		$usedHijriYear = false;
1121
		$usedTennoYear = false;
1122
1123
		if ( strlen( $ts ) !== 14 ) {
1124
			throw new MWException( __METHOD__ . ": The timestamp $ts should have 14 characters" );
1125
		}
1126
1127
		if ( !ctype_digit( $ts ) ) {
1128
			throw new MWException( __METHOD__ . ": The timestamp $ts should be a number" );
1129
		}
1130
1131
		$formatLength = strlen( $format );
1132
		for ( $p = 0; $p < $formatLength; $p++ ) {
1133
			$num = false;
1134
			$code = $format[$p];
1135
			if ( $code == 'x' && $p < $formatLength - 1 ) {
1136
				$code .= $format[++$p];
1137
			}
1138
1139
			if ( ( $code === 'xi'
1140
					|| $code === 'xj'
1141
					|| $code === 'xk'
1142
					|| $code === 'xm'
1143
					|| $code === 'xo'
1144
					|| $code === 'xt' )
1145
				&& $p < $formatLength - 1 ) {
1146
				$code .= $format[++$p];
1147
			}
1148
1149
			switch ( $code ) {
1150
				case 'xx':
1151
					$s .= 'x';
1152
					break;
1153
				case 'xn':
1154
					$raw = true;
1155
					break;
1156
				case 'xN':
1157
					$rawToggle = !$rawToggle;
1158
					break;
1159
				case 'xr':
1160
					$roman = true;
1161
					break;
1162
				case 'xh':
1163
					$hebrewNum = true;
1164
					break;
1165
				case 'xg':
1166
					$usedMonth = true;
1167
					$s .= $this->getMonthNameGen( substr( $ts, 4, 2 ) );
1168
					break;
1169 View Code Duplication
				case 'xjx':
1170
					$usedHebrewMonth = true;
1171
					if ( !$hebrew ) {
1172
						$hebrew = self::tsToHebrew( $ts );
1173
					}
1174
					$s .= $this->getHebrewCalendarMonthNameGen( $hebrew[1] );
1175
					break;
1176
				case 'd':
1177
					$usedDay = true;
1178
					$num = substr( $ts, 6, 2 );
1179
					break;
1180
				case 'D':
1181
					$usedDay = true;
1182
					$s .= $this->getWeekdayAbbreviation(
1183
						Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1
1184
					);
1185
					break;
1186
				case 'j':
1187
					$usedDay = true;
1188
					$num = intval( substr( $ts, 6, 2 ) );
1189
					break;
1190
				case 'xij':
1191
					$usedDay = true;
1192
					if ( !$iranian ) {
1193
						$iranian = self::tsToIranian( $ts );
1194
					}
1195
					$num = $iranian[2];
1196
					break;
1197
				case 'xmj':
1198
					$usedDay = true;
1199
					if ( !$hijri ) {
1200
						$hijri = self::tsToHijri( $ts );
1201
					}
1202
					$num = $hijri[2];
1203
					break;
1204
				case 'xjj':
1205
					$usedDay = true;
1206
					if ( !$hebrew ) {
1207
						$hebrew = self::tsToHebrew( $ts );
1208
					}
1209
					$num = $hebrew[2];
1210
					break;
1211
				case 'l':
1212
					$usedDay = true;
1213
					$s .= $this->getWeekdayName(
1214
						Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1
1215
					);
1216
					break;
1217
				case 'F':
1218
					$usedMonth = true;
1219
					$s .= $this->getMonthName( substr( $ts, 4, 2 ) );
1220
					break;
1221 View Code Duplication
				case 'xiF':
1222
					$usedIranianMonth = true;
1223
					if ( !$iranian ) {
1224
						$iranian = self::tsToIranian( $ts );
1225
					}
1226
					$s .= $this->getIranianCalendarMonthName( $iranian[1] );
1227
					break;
1228 View Code Duplication
				case 'xmF':
1229
					$usedHijriMonth = true;
1230
					if ( !$hijri ) {
1231
						$hijri = self::tsToHijri( $ts );
1232
					}
1233
					$s .= $this->getHijriCalendarMonthName( $hijri[1] );
1234
					break;
1235 View Code Duplication
				case 'xjF':
1236
					$usedHebrewMonth = true;
1237
					if ( !$hebrew ) {
1238
						$hebrew = self::tsToHebrew( $ts );
1239
					}
1240
					$s .= $this->getHebrewCalendarMonthName( $hebrew[1] );
1241
					break;
1242
				case 'm':
1243
					$usedMonth = true;
1244
					$num = substr( $ts, 4, 2 );
1245
					break;
1246
				case 'M':
1247
					$usedMonth = true;
1248
					$s .= $this->getMonthAbbreviation( substr( $ts, 4, 2 ) );
1249
					break;
1250
				case 'n':
1251
					$usedMonth = true;
1252
					$num = intval( substr( $ts, 4, 2 ) );
1253
					break;
1254 View Code Duplication
				case 'xin':
1255
					$usedIranianMonth = true;
1256
					if ( !$iranian ) {
1257
						$iranian = self::tsToIranian( $ts );
1258
					}
1259
					$num = $iranian[1];
1260
					break;
1261 View Code Duplication
				case 'xmn':
1262
					$usedHijriMonth = true;
1263
					if ( !$hijri ) {
1264
						$hijri = self::tsToHijri( $ts );
1265
					}
1266
					$num = $hijri[1];
1267
					break;
1268 View Code Duplication
				case 'xjn':
1269
					$usedHebrewMonth = true;
1270
					if ( !$hebrew ) {
1271
						$hebrew = self::tsToHebrew( $ts );
1272
					}
1273
					$num = $hebrew[1];
1274
					break;
1275 View Code Duplication
				case 'xjt':
1276
					$usedHebrewMonth = true;
1277
					if ( !$hebrew ) {
1278
						$hebrew = self::tsToHebrew( $ts );
1279
					}
1280
					$num = $hebrew[3];
1281
					break;
1282
				case 'Y':
1283
					$usedYear = true;
1284
					$num = substr( $ts, 0, 4 );
1285
					break;
1286 View Code Duplication
				case 'xiY':
1287
					$usedIranianYear = true;
1288
					if ( !$iranian ) {
1289
						$iranian = self::tsToIranian( $ts );
1290
					}
1291
					$num = $iranian[0];
1292
					break;
1293
				case 'xmY':
1294
					$usedHijriYear = true;
1295
					if ( !$hijri ) {
1296
						$hijri = self::tsToHijri( $ts );
1297
					}
1298
					$num = $hijri[0];
1299
					break;
1300
				case 'xjY':
1301
					$usedHebrewYear = true;
1302
					if ( !$hebrew ) {
1303
						$hebrew = self::tsToHebrew( $ts );
1304
					}
1305
					$num = $hebrew[0];
1306
					break;
1307 View Code Duplication
				case 'xkY':
1308
					$usedYear = true;
1309
					if ( !$thai ) {
1310
						$thai = self::tsToYear( $ts, 'thai' );
1311
					}
1312
					$num = $thai[0];
1313
					break;
1314 View Code Duplication
				case 'xoY':
1315
					$usedYear = true;
1316
					if ( !$minguo ) {
1317
						$minguo = self::tsToYear( $ts, 'minguo' );
1318
					}
1319
					$num = $minguo[0];
1320
					break;
1321
				case 'xtY':
1322
					$usedTennoYear = true;
1323
					if ( !$tenno ) {
1324
						$tenno = self::tsToYear( $ts, 'tenno' );
1325
					}
1326
					$num = $tenno[0];
1327
					break;
1328
				case 'y':
1329
					$usedYear = true;
1330
					$num = substr( $ts, 2, 2 );
1331
					break;
1332 View Code Duplication
				case 'xiy':
1333
					$usedIranianYear = true;
1334
					if ( !$iranian ) {
1335
						$iranian = self::tsToIranian( $ts );
1336
					}
1337
					$num = substr( $iranian[0], -2 );
1338
					break;
1339 View Code Duplication
				case 'a':
1340
					$usedAMPM = true;
1341
					$s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'am' : 'pm';
1342
					break;
1343 View Code Duplication
				case 'A':
1344
					$usedAMPM = true;
1345
					$s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'AM' : 'PM';
1346
					break;
1347 View Code Duplication
				case 'g':
1348
					$usedHour = true;
1349
					$h = substr( $ts, 8, 2 );
1350
					$num = $h % 12 ? $h % 12 : 12;
1351
					break;
1352
				case 'G':
1353
					$usedHour = true;
1354
					$num = intval( substr( $ts, 8, 2 ) );
1355
					break;
1356 View Code Duplication
				case 'h':
1357
					$usedHour = true;
1358
					$h = substr( $ts, 8, 2 );
1359
					$num = sprintf( '%02d', $h % 12 ? $h % 12 : 12 );
1360
					break;
1361
				case 'H':
1362
					$usedHour = true;
1363
					$num = substr( $ts, 8, 2 );
1364
					break;
1365
				case 'i':
1366
					$usedMinute = true;
1367
					$num = substr( $ts, 10, 2 );
1368
					break;
1369
				case 's':
1370
					$usedSecond = true;
1371
					$num = substr( $ts, 12, 2 );
1372
					break;
1373
				case 'c':
1374
				case 'r':
1375
					$usedSecond = true;
1376
					// fall through
1377
				case 'e':
1378
				case 'O':
1379
				case 'P':
1380
				case 'T':
1381
					$s .= Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1382
					break;
1383
				case 'w':
1384
				case 'N':
1385
				case 'z':
1386
					$usedDay = true;
1387
					$num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1388
					break;
1389
				case 'W':
1390
					$usedWeek = true;
1391
					$num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1392
					break;
1393
				case 't':
1394
					$usedMonth = true;
1395
					$num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1396
					break;
1397
				case 'L':
1398
					$usedIsLeapYear = true;
1399
					$num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1400
					break;
1401
				case 'o':
1402
					$usedISOYear = true;
1403
					$num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1404
					break;
1405
				case 'U':
1406
					$usedSecond = true;
1407
					// fall through
1408
				case 'I':
1409
				case 'Z':
1410
					$num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1411
					break;
1412
				case '\\':
1413
					# Backslash escaping
1414
					if ( $p < $formatLength - 1 ) {
1415
						$s .= $format[++$p];
1416
					} else {
1417
						$s .= '\\';
1418
					}
1419
					break;
1420
				case '"':
1421
					# Quoted literal
1422
					if ( $p < $formatLength - 1 ) {
1423
						$endQuote = strpos( $format, '"', $p + 1 );
1424
						if ( $endQuote === false ) {
1425
							# No terminating quote, assume literal "
1426
							$s .= '"';
1427
						} else {
1428
							$s .= substr( $format, $p + 1, $endQuote - $p - 1 );
1429
							$p = $endQuote;
1430
						}
1431
					} else {
1432
						# Quote at end of string, assume literal "
1433
						$s .= '"';
1434
					}
1435
					break;
1436
				default:
1437
					$s .= $format[$p];
1438
			}
1439
			if ( $num !== false ) {
1440
				if ( $rawToggle || $raw ) {
1441
					$s .= $num;
1442
					$raw = false;
1443
				} elseif ( $roman ) {
1444
					$s .= Language::romanNumeral( $num );
1445
					$roman = false;
1446
				} elseif ( $hebrewNum ) {
1447
					$s .= self::hebrewNumeral( $num );
1448
					$hebrewNum = false;
1449
				} else {
1450
					$s .= $this->formatNum( $num, true );
1451
				}
1452
			}
1453
		}
1454
1455
		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...
1456
			// No need to calculate the TTL, the caller wont use it anyway.
1457
		} elseif ( $usedSecond ) {
1458
			$ttl = 1;
1459
		} elseif ( $usedMinute ) {
1460
			$ttl = 60 - substr( $ts, 12, 2 );
1461
		} elseif ( $usedHour ) {
1462
			$ttl = 3600 - substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1463
		} elseif ( $usedAMPM ) {
1464
			$ttl = 43200 - ( substr( $ts, 8, 2 ) % 12 ) * 3600 -
1465
				substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1466
		} elseif (
1467
			$usedDay ||
1468
			$usedHebrewMonth ||
1469
			$usedIranianMonth ||
1470
			$usedHijriMonth ||
1471
			$usedHebrewYear ||
1472
			$usedIranianYear ||
1473
			$usedHijriYear ||
1474
			$usedTennoYear
1475
		) {
1476
			// @todo Someone who understands the non-Gregorian calendars
1477
			// should write proper logic for them so that they don't need purged every day.
1478
			$ttl = 86400 - substr( $ts, 8, 2 ) * 3600 -
1479
				substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1480
		} else {
1481
			$possibleTtls = [];
1482
			$timeRemainingInDay = 86400 - substr( $ts, 8, 2 ) * 3600 -
1483
				substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1484
			if ( $usedWeek ) {
1485
				$possibleTtls[] =
1486
					( 7 - Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'N' ) ) * 86400 +
1487
					$timeRemainingInDay;
1488
			} elseif ( $usedISOYear ) {
1489
				// December 28th falls on the last ISO week of the year, every year.
1490
				// The last ISO week of a year can be 52 or 53.
1491
				$lastWeekOfISOYear = DateTime::createFromFormat(
1492
					'Ymd',
1493
					substr( $ts, 0, 4 ) . '1228',
1494
					$zone ?: new DateTimeZone( 'UTC' )
1495
				)->format( 'W' );
1496
				$currentISOWeek = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'W' );
1497
				$weeksRemaining = $lastWeekOfISOYear - $currentISOWeek;
1498
				$timeRemainingInWeek =
1499
					( 7 - Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'N' ) ) * 86400
1500
					+ $timeRemainingInDay;
1501
				$possibleTtls[] = $weeksRemaining * 604800 + $timeRemainingInWeek;
1502
			}
1503
1504
			if ( $usedMonth ) {
1505
				$possibleTtls[] =
1506
					( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 't' ) -
1507
						substr( $ts, 6, 2 ) ) * 86400
1508
					+ $timeRemainingInDay;
1509
			} elseif ( $usedYear ) {
1510
				$possibleTtls[] =
1511
					( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'L' ) + 364 -
1512
						Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'z' ) ) * 86400
1513
					+ $timeRemainingInDay;
1514
			} elseif ( $usedIsLeapYear ) {
1515
				$year = substr( $ts, 0, 4 );
1516
				$timeRemainingInYear =
1517
					( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'L' ) + 364 -
1518
						Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'z' ) ) * 86400
1519
					+ $timeRemainingInDay;
1520
				$mod = $year % 4;
1521
				if ( $mod || ( !( $year % 100 ) && $year % 400 ) ) {
1522
					// this isn't a leap year. see when the next one starts
1523
					$nextCandidate = $year - $mod + 4;
1524
					if ( $nextCandidate % 100 || !( $nextCandidate % 400 ) ) {
1525
						$possibleTtls[] = ( $nextCandidate - $year - 1 ) * 365 * 86400 +
1526
							$timeRemainingInYear;
1527
					} else {
1528
						$possibleTtls[] = ( $nextCandidate - $year + 3 ) * 365 * 86400 +
1529
							$timeRemainingInYear;
1530
					}
1531
				} else {
1532
					// this is a leap year, so the next year isn't
1533
					$possibleTtls[] = $timeRemainingInYear;
1534
				}
1535
			}
1536
1537
			if ( $possibleTtls ) {
1538
				$ttl = min( $possibleTtls );
1539
			}
1540
		}
1541
1542
		return $s;
1543
	}
1544
1545
	private static $GREG_DAYS = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
1546
	private static $IRANIAN_DAYS = [ 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 ];
1547
1548
	/**
1549
	 * Algorithm by Roozbeh Pournader and Mohammad Toossi to convert
1550
	 * Gregorian dates to Iranian dates. Originally written in C, it
1551
	 * is released under the terms of GNU Lesser General Public
1552
	 * License. Conversion to PHP was performed by Niklas Laxström.
1553
	 *
1554
	 * Link: http://www.farsiweb.info/jalali/jalali.c
1555
	 *
1556
	 * @param string $ts
1557
	 *
1558
	 * @return int[]
1559
	 */
1560
	private static function tsToIranian( $ts ) {
1561
		$gy = substr( $ts, 0, 4 ) -1600;
1562
		$gm = substr( $ts, 4, 2 ) -1;
1563
		$gd = substr( $ts, 6, 2 ) -1;
1564
1565
		# Days passed from the beginning (including leap years)
1566
		$gDayNo = 365 * $gy
1567
			+ floor( ( $gy + 3 ) / 4 )
1568
			- floor( ( $gy + 99 ) / 100 )
1569
			+ floor( ( $gy + 399 ) / 400 );
1570
1571
		// Add days of the past months of this year
1572
		for ( $i = 0; $i < $gm; $i++ ) {
1573
			$gDayNo += self::$GREG_DAYS[$i];
1574
		}
1575
1576
		// Leap years
1577
		if ( $gm > 1 && ( ( $gy % 4 === 0 && $gy % 100 !== 0 || ( $gy % 400 == 0 ) ) ) ) {
1578
			$gDayNo++;
1579
		}
1580
1581
		// Days passed in current month
1582
		$gDayNo += (int)$gd;
1583
1584
		$jDayNo = $gDayNo - 79;
1585
1586
		$jNp = floor( $jDayNo / 12053 );
1587
		$jDayNo %= 12053;
1588
1589
		$jy = 979 + 33 * $jNp + 4 * floor( $jDayNo / 1461 );
1590
		$jDayNo %= 1461;
1591
1592
		if ( $jDayNo >= 366 ) {
1593
			$jy += floor( ( $jDayNo - 1 ) / 365 );
1594
			$jDayNo = floor( ( $jDayNo - 1 ) % 365 );
1595
		}
1596
1597
		for ( $i = 0; $i < 11 && $jDayNo >= self::$IRANIAN_DAYS[$i]; $i++ ) {
1598
			$jDayNo -= self::$IRANIAN_DAYS[$i];
1599
		}
1600
1601
		$jm = $i + 1;
1602
		$jd = $jDayNo + 1;
1603
1604
		return [ $jy, $jm, $jd ];
1605
	}
1606
1607
	/**
1608
	 * Converting Gregorian dates to Hijri dates.
1609
	 *
1610
	 * Based on a PHP-Nuke block by Sharjeel which is released under GNU/GPL license
1611
	 *
1612
	 * @see http://phpnuke.org/modules.php?name=News&file=article&sid=8234&mode=thread&order=0&thold=0
1613
	 *
1614
	 * @param string $ts
1615
	 *
1616
	 * @return int[]
1617
	 */
1618
	private static function tsToHijri( $ts ) {
1619
		$year = substr( $ts, 0, 4 );
1620
		$month = substr( $ts, 4, 2 );
1621
		$day = substr( $ts, 6, 2 );
1622
1623
		$zyr = $year;
1624
		$zd = $day;
1625
		$zm = $month;
1626
		$zy = $zyr;
1627
1628
		if (
1629
			( $zy > 1582 ) || ( ( $zy == 1582 ) && ( $zm > 10 ) ) ||
1630
			( ( $zy == 1582 ) && ( $zm == 10 ) && ( $zd > 14 ) )
1631
		) {
1632
			$zjd = (int)( ( 1461 * ( $zy + 4800 + (int)( ( $zm - 14 ) / 12 ) ) ) / 4 ) +
1633
					(int)( ( 367 * ( $zm - 2 - 12 * ( (int)( ( $zm - 14 ) / 12 ) ) ) ) / 12 ) -
1634
					(int)( ( 3 * (int)( ( ( $zy + 4900 + (int)( ( $zm - 14 ) / 12 ) ) / 100 ) ) ) / 4 ) +
1635
					$zd - 32075;
1636
		} else {
1637
			$zjd = 367 * $zy - (int)( ( 7 * ( $zy + 5001 + (int)( ( $zm - 9 ) / 7 ) ) ) / 4 ) +
1638
								(int)( ( 275 * $zm ) / 9 ) + $zd + 1729777;
1639
		}
1640
1641
		$zl = $zjd -1948440 + 10632;
1642
		$zn = (int)( ( $zl - 1 ) / 10631 );
1643
		$zl = $zl - 10631 * $zn + 354;
1644
		$zj = ( (int)( ( 10985 - $zl ) / 5316 ) ) * ( (int)( ( 50 * $zl ) / 17719 ) ) +
1645
			( (int)( $zl / 5670 ) ) * ( (int)( ( 43 * $zl ) / 15238 ) );
1646
		$zl = $zl - ( (int)( ( 30 - $zj ) / 15 ) ) * ( (int)( ( 17719 * $zj ) / 50 ) ) -
1647
			( (int)( $zj / 16 ) ) * ( (int)( ( 15238 * $zj ) / 43 ) ) + 29;
1648
		$zm = (int)( ( 24 * $zl ) / 709 );
1649
		$zd = $zl - (int)( ( 709 * $zm ) / 24 );
1650
		$zy = 30 * $zn + $zj - 30;
1651
1652
		return [ $zy, $zm, $zd ];
1653
	}
1654
1655
	/**
1656
	 * Converting Gregorian dates to Hebrew dates.
1657
	 *
1658
	 * Based on a JavaScript code by Abu Mami and Yisrael Hersch
1659
	 * ([email protected], http://www.kaluach.net), who permitted
1660
	 * to translate the relevant functions into PHP and release them under
1661
	 * GNU GPL.
1662
	 *
1663
	 * The months are counted from Tishrei = 1. In a leap year, Adar I is 13
1664
	 * and Adar II is 14. In a non-leap year, Adar is 6.
1665
	 *
1666
	 * @param string $ts
1667
	 *
1668
	 * @return int[]
1669
	 */
1670
	private static function tsToHebrew( $ts ) {
1671
		# Parse date
1672
		$year = substr( $ts, 0, 4 );
1673
		$month = substr( $ts, 4, 2 );
1674
		$day = substr( $ts, 6, 2 );
1675
1676
		# Calculate Hebrew year
1677
		$hebrewYear = $year + 3760;
1678
1679
		# Month number when September = 1, August = 12
1680
		$month += 4;
1681
		if ( $month > 12 ) {
1682
			# Next year
1683
			$month -= 12;
1684
			$year++;
1685
			$hebrewYear++;
1686
		}
1687
1688
		# Calculate day of year from 1 September
1689
		$dayOfYear = $day;
1690
		for ( $i = 1; $i < $month; $i++ ) {
1691
			if ( $i == 6 ) {
1692
				# February
1693
				$dayOfYear += 28;
1694
				# Check if the year is leap
1695 View Code Duplication
				if ( $year % 400 == 0 || ( $year % 4 == 0 && $year % 100 > 0 ) ) {
1696
					$dayOfYear++;
1697
				}
1698
			} elseif ( $i == 8 || $i == 10 || $i == 1 || $i == 3 ) {
1699
				$dayOfYear += 30;
1700
			} else {
1701
				$dayOfYear += 31;
1702
			}
1703
		}
1704
1705
		# Calculate the start of the Hebrew year
1706
		$start = self::hebrewYearStart( $hebrewYear );
1707
1708
		# Calculate next year's start
1709
		if ( $dayOfYear <= $start ) {
1710
			# Day is before the start of the year - it is the previous year
1711
			# Next year's start
1712
			$nextStart = $start;
1713
			# Previous year
1714
			$year--;
1715
			$hebrewYear--;
1716
			# Add days since previous year's 1 September
1717
			$dayOfYear += 365;
1718 View Code Duplication
			if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1719
				# Leap year
1720
				$dayOfYear++;
1721
			}
1722
			# Start of the new (previous) year
1723
			$start = self::hebrewYearStart( $hebrewYear );
1724
		} else {
1725
			# Next year's start
1726
			$nextStart = self::hebrewYearStart( $hebrewYear + 1 );
1727
		}
1728
1729
		# Calculate Hebrew day of year
1730
		$hebrewDayOfYear = $dayOfYear - $start;
1731
1732
		# Difference between year's days
1733
		$diff = $nextStart - $start;
1734
		# Add 12 (or 13 for leap years) days to ignore the difference between
1735
		# Hebrew and Gregorian year (353 at least vs. 365/6) - now the
1736
		# difference is only about the year type
1737
		if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1738
			$diff += 13;
1739
		} else {
1740
			$diff += 12;
1741
		}
1742
1743
		# Check the year pattern, and is leap year
1744
		# 0 means an incomplete year, 1 means a regular year, 2 means a complete year
1745
		# This is mod 30, to work on both leap years (which add 30 days of Adar I)
1746
		# and non-leap years
1747
		$yearPattern = $diff % 30;
1748
		# Check if leap year
1749
		$isLeap = $diff >= 30;
1750
1751
		# Calculate day in the month from number of day in the Hebrew year
1752
		# Don't check Adar - if the day is not in Adar, we will stop before;
1753
		# if it is in Adar, we will use it to check if it is Adar I or Adar II
1754
		$hebrewDay = $hebrewDayOfYear;
1755
		$hebrewMonth = 1;
1756
		$days = 0;
1757
		while ( $hebrewMonth <= 12 ) {
1758
			# Calculate days in this month
1759
			if ( $isLeap && $hebrewMonth == 6 ) {
1760
				# Adar in a leap year
1761
				if ( $isLeap ) {
1762
					# Leap year - has Adar I, with 30 days, and Adar II, with 29 days
1763
					$days = 30;
1764
					if ( $hebrewDay <= $days ) {
1765
						# Day in Adar I
1766
						$hebrewMonth = 13;
1767
					} else {
1768
						# Subtract the days of Adar I
1769
						$hebrewDay -= $days;
1770
						# Try Adar II
1771
						$days = 29;
1772
						if ( $hebrewDay <= $days ) {
1773
							# Day in Adar II
1774
							$hebrewMonth = 14;
1775
						}
1776
					}
1777
				}
1778
			} elseif ( $hebrewMonth == 2 && $yearPattern == 2 ) {
1779
				# Cheshvan in a complete year (otherwise as the rule below)
1780
				$days = 30;
1781
			} elseif ( $hebrewMonth == 3 && $yearPattern == 0 ) {
1782
				# Kislev in an incomplete year (otherwise as the rule below)
1783
				$days = 29;
1784
			} else {
1785
				# Odd months have 30 days, even have 29
1786
				$days = 30 - ( $hebrewMonth - 1 ) % 2;
1787
			}
1788
			if ( $hebrewDay <= $days ) {
1789
				# In the current month
1790
				break;
1791
			} else {
1792
				# Subtract the days of the current month
1793
				$hebrewDay -= $days;
1794
				# Try in the next month
1795
				$hebrewMonth++;
1796
			}
1797
		}
1798
1799
		return [ $hebrewYear, $hebrewMonth, $hebrewDay, $days ];
1800
	}
1801
1802
	/**
1803
	 * This calculates the Hebrew year start, as days since 1 September.
1804
	 * Based on Carl Friedrich Gauss algorithm for finding Easter date.
1805
	 * Used for Hebrew date.
1806
	 *
1807
	 * @param int $year
1808
	 *
1809
	 * @return string
1810
	 */
1811
	private static function hebrewYearStart( $year ) {
1812
		$a = intval( ( 12 * ( $year - 1 ) + 17 ) % 19 );
1813
		$b = intval( ( $year - 1 ) % 4 );
1814
		$m = 32.044093161144 + 1.5542417966212 * $a + $b / 4.0 - 0.0031777940220923 * ( $year - 1 );
1815
		if ( $m < 0 ) {
1816
			$m--;
1817
		}
1818
		$Mar = intval( $m );
1819
		if ( $m < 0 ) {
1820
			$m++;
1821
		}
1822
		$m -= $Mar;
1823
1824
		$c = intval( ( $Mar + 3 * ( $year - 1 ) + 5 * $b + 5 ) % 7 );
1825
		if ( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
1826
			$Mar++;
1827
		} elseif ( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
1828
			$Mar += 2;
1829
		} elseif ( $c == 2 || $c == 4 || $c == 6 ) {
1830
			$Mar++;
1831
		}
1832
1833
		$Mar += intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
1834
		return $Mar;
1835
	}
1836
1837
	/**
1838
	 * Algorithm to convert Gregorian dates to Thai solar dates,
1839
	 * Minguo dates or Minguo dates.
1840
	 *
1841
	 * Link: http://en.wikipedia.org/wiki/Thai_solar_calendar
1842
	 *       http://en.wikipedia.org/wiki/Minguo_calendar
1843
	 *       http://en.wikipedia.org/wiki/Japanese_era_name
1844
	 *
1845
	 * @param string $ts 14-character timestamp
1846
	 * @param string $cName Calender name
1847
	 * @return array Converted year, month, day
1848
	 */
1849
	private static function tsToYear( $ts, $cName ) {
1850
		$gy = substr( $ts, 0, 4 );
1851
		$gm = substr( $ts, 4, 2 );
1852
		$gd = substr( $ts, 6, 2 );
1853
1854
		if ( !strcmp( $cName, 'thai' ) ) {
1855
			# Thai solar dates
1856
			# Add 543 years to the Gregorian calendar
1857
			# Months and days are identical
1858
			$gy_offset = $gy + 543;
1859
		} elseif ( ( !strcmp( $cName, 'minguo' ) ) || !strcmp( $cName, 'juche' ) ) {
1860
			# Minguo dates
1861
			# Deduct 1911 years from the Gregorian calendar
1862
			# Months and days are identical
1863
			$gy_offset = $gy - 1911;
1864
		} elseif ( !strcmp( $cName, 'tenno' ) ) {
1865
			# Nengō dates up to Meiji period
1866
			# Deduct years from the Gregorian calendar
1867
			# depending on the nengo periods
1868
			# Months and days are identical
1869
			if ( ( $gy < 1912 )
1870
				|| ( ( $gy == 1912 ) && ( $gm < 7 ) )
1871
				|| ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd < 31 ) )
1872
			) {
1873
				# Meiji period
1874
				$gy_gannen = $gy - 1868 + 1;
1875
				$gy_offset = $gy_gannen;
1876
				if ( $gy_gannen == 1 ) {
1877
					$gy_offset = '元';
1878
				}
1879
				$gy_offset = '明治' . $gy_offset;
1880
			} elseif (
1881
				( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd == 31 ) ) ||
1882
				( ( $gy == 1912 ) && ( $gm >= 8 ) ) ||
1883
				( ( $gy > 1912 ) && ( $gy < 1926 ) ) ||
1884
				( ( $gy == 1926 ) && ( $gm < 12 ) ) ||
1885
				( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd < 26 ) )
1886
			) {
1887
				# Taishō period
1888
				$gy_gannen = $gy - 1912 + 1;
1889
				$gy_offset = $gy_gannen;
1890
				if ( $gy_gannen == 1 ) {
1891
					$gy_offset = '元';
1892
				}
1893
				$gy_offset = '大正' . $gy_offset;
1894
			} elseif (
1895
				( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd >= 26 ) ) ||
1896
				( ( $gy > 1926 ) && ( $gy < 1989 ) ) ||
1897
				( ( $gy == 1989 ) && ( $gm == 1 ) && ( $gd < 8 ) )
1898
			) {
1899
				# Shōwa period
1900
				$gy_gannen = $gy - 1926 + 1;
1901
				$gy_offset = $gy_gannen;
1902
				if ( $gy_gannen == 1 ) {
1903
					$gy_offset = '元';
1904
				}
1905
				$gy_offset = '昭和' . $gy_offset;
1906
			} else {
1907
				# Heisei period
1908
				$gy_gannen = $gy - 1989 + 1;
1909
				$gy_offset = $gy_gannen;
1910
				if ( $gy_gannen == 1 ) {
1911
					$gy_offset = '元';
1912
				}
1913
				$gy_offset = '平成' . $gy_offset;
1914
			}
1915
		} else {
1916
			$gy_offset = $gy;
1917
		}
1918
1919
		return [ $gy_offset, $gm, $gd ];
1920
	}
1921
1922
	/**
1923
	 * Gets directionality of the first strongly directional codepoint, for embedBidi()
1924
	 *
1925
	 * This is the rule the BIDI algorithm uses to determine the directionality of
1926
	 * paragraphs ( http://unicode.org/reports/tr9/#The_Paragraph_Level ) and
1927
	 * FSI isolates ( http://unicode.org/reports/tr9/#Explicit_Directional_Isolates ).
1928
	 *
1929
	 * TODO: Does not handle BIDI control characters inside the text.
1930
	 * TODO: Does not handle unallocated characters.
1931
	 *
1932
	 * @param string $text Text to test
1933
	 * @return null|string Directionality ('ltr' or 'rtl') or null
1934
	 */
1935
	private static function strongDirFromContent( $text = '' ) {
1936
		if ( !preg_match( self::$strongDirRegex, $text, $matches ) ) {
1937
			return null;
1938
		}
1939
		if ( $matches[1] === '' ) {
1940
			return 'rtl';
1941
		}
1942
		return 'ltr';
1943
	}
1944
1945
	/**
1946
	 * Roman number formatting up to 10000
1947
	 *
1948
	 * @param int $num
1949
	 *
1950
	 * @return string
1951
	 */
1952
	static function romanNumeral( $num ) {
1953
		static $table = [
1954
			[ '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ],
1955
			[ '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ],
1956
			[ '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ],
1957
			[ '', 'M', 'MM', 'MMM', 'MMMM', 'MMMMM', 'MMMMMM', 'MMMMMMM',
1958
				'MMMMMMMM', 'MMMMMMMMM', 'MMMMMMMMMM' ]
1959
		];
1960
1961
		$num = intval( $num );
1962
		if ( $num > 10000 || $num <= 0 ) {
1963
			return $num;
1964
		}
1965
1966
		$s = '';
1967
		for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1968
			if ( $num >= $pow10 ) {
1969
				$s .= $table[$i][(int)floor( $num / $pow10 )];
1970
			}
1971
			$num = $num % $pow10;
1972
		}
1973
		return $s;
1974
	}
1975
1976
	/**
1977
	 * Hebrew Gematria number formatting up to 9999
1978
	 *
1979
	 * @param int $num
1980
	 *
1981
	 * @return string
1982
	 */
1983
	static function hebrewNumeral( $num ) {
1984
		static $table = [
1985
			[ '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ],
1986
			[ '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ],
1987
			[ '',
1988
				[ 'ק' ],
1989
				[ 'ר' ],
1990
				[ 'ש' ],
1991
				[ 'ת' ],
1992
				[ 'ת', 'ק' ],
1993
				[ 'ת', 'ר' ],
1994
				[ 'ת', 'ש' ],
1995
				[ 'ת', 'ת' ],
1996
				[ 'ת', 'ת', 'ק' ],
1997
				[ 'ת', 'ת', 'ר' ],
1998
			],
1999
			[ '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ]
2000
		];
2001
2002
		$num = intval( $num );
2003
		if ( $num > 9999 || $num <= 0 ) {
2004
			return $num;
2005
		}
2006
2007
		// Round thousands have special notations
2008
		if ( $num === 1000 ) {
2009
			return "א' אלף";
2010
		} elseif ( $num % 1000 === 0 ) {
2011
			return $table[0][$num / 1000] . "' אלפים";
2012
		}
2013
2014
		$letters = [];
2015
2016
		for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
2017
			if ( $num >= $pow10 ) {
2018
				if ( $num === 15 || $num === 16 ) {
2019
					$letters[] = $table[0][9];
2020
					$letters[] = $table[0][$num - 9];
2021
					$num = 0;
2022
				} else {
2023
					$letters = array_merge(
2024
						$letters,
2025
						(array)$table[$i][intval( $num / $pow10 )]
2026
					);
2027
2028
					if ( $pow10 === 1000 ) {
2029
						$letters[] = "'";
2030
					}
2031
				}
2032
			}
2033
2034
			$num = $num % $pow10;
2035
		}
2036
2037
		$preTransformLength = count( $letters );
2038
		if ( $preTransformLength === 1 ) {
2039
			// Add geresh (single quote) to one-letter numbers
2040
			$letters[] = "'";
2041
		} else {
2042
			$lastIndex = $preTransformLength - 1;
2043
			$letters[$lastIndex] = str_replace(
2044
				[ 'כ', 'מ', 'נ', 'פ', 'צ' ],
2045
				[ 'ך', 'ם', 'ן', 'ף', 'ץ' ],
2046
				$letters[$lastIndex]
2047
			);
2048
2049
			// Add gershayim (double quote) to multiple-letter numbers,
2050
			// but exclude numbers with only one letter after the thousands
2051
			// (1001-1009, 1020, 1030, 2001-2009, etc.)
2052
			if ( $letters[1] === "'" && $preTransformLength === 3 ) {
2053
				$letters[] = "'";
2054
			} else {
2055
				array_splice( $letters, -1, 0, '"' );
2056
			}
2057
		}
2058
2059
		return implode( $letters );
2060
	}
2061
2062
	/**
2063
	 * Used by date() and time() to adjust the time output.
2064
	 *
2065
	 * @param string $ts The time in date('YmdHis') format
2066
	 * @param mixed $tz Adjust the time by this amount (default false, mean we
2067
	 *   get user timecorrection setting)
2068
	 * @return int
2069
	 */
2070
	public function userAdjust( $ts, $tz = false ) {
2071
		global $wgUser, $wgLocalTZoffset;
2072
2073
		if ( $tz === false ) {
2074
			$tz = $wgUser->getOption( 'timecorrection' );
2075
		}
2076
2077
		$data = explode( '|', $tz, 3 );
2078
2079
		if ( $data[0] == 'ZoneInfo' ) {
2080
			MediaWiki\suppressWarnings();
2081
			$userTZ = timezone_open( $data[2] );
2082
			MediaWiki\restoreWarnings();
2083
			if ( $userTZ !== false ) {
2084
				$date = date_create( $ts, timezone_open( 'UTC' ) );
2085
				date_timezone_set( $date, $userTZ );
2086
				$date = date_format( $date, 'YmdHis' );
2087
				return $date;
2088
			}
2089
			# Unrecognized timezone, default to 'Offset' with the stored offset.
2090
			$data[0] = 'Offset';
2091
		}
2092
2093
		if ( $data[0] == 'System' || $tz == '' ) {
2094
			# Global offset in minutes.
2095
			$minDiff = $wgLocalTZoffset;
2096
		} elseif ( $data[0] == 'Offset' ) {
2097
			$minDiff = intval( $data[1] );
2098 View Code Duplication
		} else {
2099
			$data = explode( ':', $tz );
2100
			if ( count( $data ) == 2 ) {
2101
				$data[0] = intval( $data[0] );
2102
				$data[1] = intval( $data[1] );
2103
				$minDiff = abs( $data[0] ) * 60 + $data[1];
2104
				if ( $data[0] < 0 ) {
2105
					$minDiff = -$minDiff;
2106
				}
2107
			} else {
2108
				$minDiff = intval( $data[0] ) * 60;
2109
			}
2110
		}
2111
2112
		# No difference ? Return time unchanged
2113
		if ( 0 == $minDiff ) {
2114
			return $ts;
2115
		}
2116
2117
		MediaWiki\suppressWarnings(); // E_STRICT system time bitching
2118
		# Generate an adjusted date; take advantage of the fact that mktime
2119
		# will normalize out-of-range values so we don't have to split $minDiff
2120
		# into hours and minutes.
2121
		$t = mktime( (
2122
			(int)substr( $ts, 8, 2 ) ), # Hours
2123
			(int)substr( $ts, 10, 2 ) + $minDiff, # Minutes
2124
			(int)substr( $ts, 12, 2 ), # Seconds
2125
			(int)substr( $ts, 4, 2 ), # Month
2126
			(int)substr( $ts, 6, 2 ), # Day
2127
			(int)substr( $ts, 0, 4 ) ); # Year
2128
2129
		$date = date( 'YmdHis', $t );
2130
		MediaWiki\restoreWarnings();
2131
2132
		return $date;
2133
	}
2134
2135
	/**
2136
	 * This is meant to be used by time(), date(), and timeanddate() to get
2137
	 * the date preference they're supposed to use, it should be used in
2138
	 * all children.
2139
	 *
2140
	 *<code>
2141
	 * function timeanddate([...], $format = true) {
2142
	 * 	$datePreference = $this->dateFormat($format);
2143
	 * [...]
2144
	 * }
2145
	 *</code>
2146
	 *
2147
	 * @param int|string|bool $usePrefs If true, the user's preference is used
2148
	 *   if false, the site/language default is used
2149
	 *   if int/string, assumed to be a format.
2150
	 * @return string
2151
	 */
2152
	function dateFormat( $usePrefs = true ) {
2153
		global $wgUser;
2154
2155
		if ( is_bool( $usePrefs ) ) {
2156
			if ( $usePrefs ) {
2157
				$datePreference = $wgUser->getDatePreference();
2158
			} else {
2159
				$datePreference = (string)User::getDefaultOption( 'date' );
2160
			}
2161
		} else {
2162
			$datePreference = (string)$usePrefs;
2163
		}
2164
2165
		// return int
2166
		if ( $datePreference == '' ) {
2167
			return 'default';
2168
		}
2169
2170
		return $datePreference;
2171
	}
2172
2173
	/**
2174
	 * Get a format string for a given type and preference
2175
	 * @param string $type May be 'date', 'time', 'both', or 'pretty'.
2176
	 * @param string $pref The format name as it appears in Messages*.php under
2177
	 *  $datePreferences.
2178
	 *
2179
	 * @since 1.22 New type 'pretty' that provides a more readable timestamp format
2180
	 *
2181
	 * @return string
2182
	 */
2183
	function getDateFormatString( $type, $pref ) {
2184
		$wasDefault = false;
2185
		if ( $pref == 'default' ) {
2186
			$wasDefault = true;
2187
			$pref = $this->getDefaultDateFormat();
2188
		}
2189
2190
		if ( !isset( $this->dateFormatStrings[$type][$pref] ) ) {
2191
			$df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2192
2193
			if ( $type === 'pretty' && $df === null ) {
2194
				$df = $this->getDateFormatString( 'date', $pref );
0 ignored issues
show
Bug introduced by
It seems like $pref defined by $this->getDefaultDateFormat() on line 2187 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...
2195
			}
2196
2197
			if ( !$wasDefault && $df === null ) {
2198
				$pref = $this->getDefaultDateFormat();
2199
				$df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2200
			}
2201
2202
			$this->dateFormatStrings[$type][$pref] = $df;
2203
		}
2204
		return $this->dateFormatStrings[$type][$pref];
2205
	}
2206
2207
	/**
2208
	 * @param string $ts The time format which needs to be turned into a
2209
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2210
	 * @param bool $adj Whether to adjust the time output according to the
2211
	 *   user configured offset ($timecorrection)
2212
	 * @param mixed $format True to use user's date format preference
2213
	 * @param string|bool $timecorrection The time offset as returned by
2214
	 *   validateTimeZone() in Special:Preferences
2215
	 * @return string
2216
	 */
2217 View Code Duplication
	public function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
2218
		$ts = wfTimestamp( TS_MW, $ts );
2219
		if ( $adj ) {
2220
			$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 2220 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...
2221
		}
2222
		$df = $this->getDateFormatString( 'date', $this->dateFormat( $format ) );
2223
		return $this->sprintfDate( $df, $ts );
2224
	}
2225
2226
	/**
2227
	 * @param string $ts The time format which needs to be turned into a
2228
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2229
	 * @param bool $adj Whether to adjust the time output according to the
2230
	 *   user configured offset ($timecorrection)
2231
	 * @param mixed $format True to use user's date format preference
2232
	 * @param string|bool $timecorrection The time offset as returned by
2233
	 *   validateTimeZone() in Special:Preferences
2234
	 * @return string
2235
	 */
2236 View Code Duplication
	public function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
2237
		$ts = wfTimestamp( TS_MW, $ts );
2238
		if ( $adj ) {
2239
			$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 2239 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...
2240
		}
2241
		$df = $this->getDateFormatString( 'time', $this->dateFormat( $format ) );
2242
		return $this->sprintfDate( $df, $ts );
2243
	}
2244
2245
	/**
2246
	 * @param string $ts The time format which needs to be turned into a
2247
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2248
	 * @param bool $adj Whether to adjust the time output according to the
2249
	 *   user configured offset ($timecorrection)
2250
	 * @param mixed $format What format to return, if it's false output the
2251
	 *   default one (default true)
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 timeanddate( $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( 'both', $this->dateFormat( $format ) );
2262
		return $this->sprintfDate( $df, $ts );
2263
	}
2264
2265
	/**
2266
	 * Takes a number of seconds and turns it into a text using values such as hours and minutes.
2267
	 *
2268
	 * @since 1.20
2269
	 *
2270
	 * @param int $seconds The amount of seconds.
2271
	 * @param array $chosenIntervals The intervals to enable.
2272
	 *
2273
	 * @return string
2274
	 */
2275
	public function formatDuration( $seconds, array $chosenIntervals = [] ) {
2276
		$intervals = $this->getDurationIntervals( $seconds, $chosenIntervals );
2277
2278
		$segments = [];
2279
2280 View Code Duplication
		foreach ( $intervals as $intervalName => $intervalValue ) {
2281
			// Messages: duration-seconds, duration-minutes, duration-hours, duration-days, duration-weeks,
2282
			// duration-years, duration-decades, duration-centuries, duration-millennia
2283
			$message = wfMessage( 'duration-' . $intervalName )->numParams( $intervalValue );
2284
			$segments[] = $message->inLanguage( $this )->escaped();
2285
		}
2286
2287
		return $this->listToText( $segments );
2288
	}
2289
2290
	/**
2291
	 * Takes a number of seconds and returns an array with a set of corresponding intervals.
2292
	 * For example 65 will be turned into array( minutes => 1, seconds => 5 ).
2293
	 *
2294
	 * @since 1.20
2295
	 *
2296
	 * @param int $seconds The amount of seconds.
2297
	 * @param array $chosenIntervals The intervals to enable.
2298
	 *
2299
	 * @return array
2300
	 */
2301
	public function getDurationIntervals( $seconds, array $chosenIntervals = [] ) {
2302 View Code Duplication
		if ( empty( $chosenIntervals ) ) {
2303
			$chosenIntervals = [
2304
				'millennia',
2305
				'centuries',
2306
				'decades',
2307
				'years',
2308
				'days',
2309
				'hours',
2310
				'minutes',
2311
				'seconds'
2312
			];
2313
		}
2314
2315
		$intervals = array_intersect_key( self::$durationIntervals, array_flip( $chosenIntervals ) );
2316
		$sortedNames = array_keys( $intervals );
2317
		$smallestInterval = array_pop( $sortedNames );
2318
2319
		$segments = [];
2320
2321
		foreach ( $intervals as $name => $length ) {
2322
			$value = floor( $seconds / $length );
2323
2324
			if ( $value > 0 || ( $name == $smallestInterval && empty( $segments ) ) ) {
2325
				$seconds -= $value * $length;
2326
				$segments[$name] = $value;
2327
			}
2328
		}
2329
2330
		return $segments;
2331
	}
2332
2333
	/**
2334
	 * Internal helper function for userDate(), userTime() and userTimeAndDate()
2335
	 *
2336
	 * @param string $type Can be 'date', 'time' or 'both'
2337
	 * @param string $ts The time format which needs to be turned into a
2338
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2339
	 * @param User $user User object used to get preferences for timezone and format
2340
	 * @param array $options Array, can contain the following keys:
2341
	 *   - 'timecorrection': time correction, can have the following values:
2342
	 *     - true: use user's preference
2343
	 *     - false: don't use time correction
2344
	 *     - int: value of time correction in minutes
2345
	 *   - 'format': format to use, can have the following values:
2346
	 *     - true: use user's preference
2347
	 *     - false: use default preference
2348
	 *     - string: format to use
2349
	 * @since 1.19
2350
	 * @return string
2351
	 */
2352
	private function internalUserTimeAndDate( $type, $ts, User $user, array $options ) {
2353
		$ts = wfTimestamp( TS_MW, $ts );
2354
		$options += [ 'timecorrection' => true, 'format' => true ];
2355
		if ( $options['timecorrection'] !== false ) {
2356
			if ( $options['timecorrection'] === true ) {
2357
				$offset = $user->getOption( 'timecorrection' );
2358
			} else {
2359
				$offset = $options['timecorrection'];
2360
			}
2361
			$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 2361 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...
2362
		}
2363
		if ( $options['format'] === true ) {
2364
			$format = $user->getDatePreference();
2365
		} else {
2366
			$format = $options['format'];
2367
		}
2368
		$df = $this->getDateFormatString( $type, $this->dateFormat( $format ) );
2369
		return $this->sprintfDate( $df, $ts );
2370
	}
2371
2372
	/**
2373
	 * Get the formatted date for the given timestamp and formatted for
2374
	 * the given user.
2375
	 *
2376
	 * @param mixed $ts Mixed: the time format which needs to be turned into a
2377
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2378
	 * @param User $user User object used to get preferences for timezone and format
2379
	 * @param array $options Array, can contain the following keys:
2380
	 *   - 'timecorrection': time correction, can have the following values:
2381
	 *     - true: use user's preference
2382
	 *     - false: don't use time correction
2383
	 *     - int: value of time correction in minutes
2384
	 *   - 'format': format to use, can have the following values:
2385
	 *     - true: use user's preference
2386
	 *     - false: use default preference
2387
	 *     - string: format to use
2388
	 * @since 1.19
2389
	 * @return string
2390
	 */
2391
	public function userDate( $ts, User $user, array $options = [] ) {
2392
		return $this->internalUserTimeAndDate( 'date', $ts, $user, $options );
2393
	}
2394
2395
	/**
2396
	 * Get the formatted time for the given timestamp and formatted for
2397
	 * the given user.
2398
	 *
2399
	 * @param mixed $ts The time format which needs to be turned into a
2400
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2401
	 * @param User $user User object used to get preferences for timezone and format
2402
	 * @param array $options Array, can contain the following keys:
2403
	 *   - 'timecorrection': time correction, can have the following values:
2404
	 *     - true: use user's preference
2405
	 *     - false: don't use time correction
2406
	 *     - int: value of time correction in minutes
2407
	 *   - 'format': format to use, can have the following values:
2408
	 *     - true: use user's preference
2409
	 *     - false: use default preference
2410
	 *     - string: format to use
2411
	 * @since 1.19
2412
	 * @return string
2413
	 */
2414
	public function userTime( $ts, User $user, array $options = [] ) {
2415
		return $this->internalUserTimeAndDate( 'time', $ts, $user, $options );
2416
	}
2417
2418
	/**
2419
	 * Get the formatted date and time for the given timestamp and formatted for
2420
	 * the given user.
2421
	 *
2422
	 * @param mixed $ts The time format which needs to be turned into a
2423
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2424
	 * @param User $user User object used to get preferences for timezone and format
2425
	 * @param array $options Array, can contain the following keys:
2426
	 *   - 'timecorrection': time correction, can have the following values:
2427
	 *     - true: use user's preference
2428
	 *     - false: don't use time correction
2429
	 *     - int: value of time correction in minutes
2430
	 *   - 'format': format to use, can have the following values:
2431
	 *     - true: use user's preference
2432
	 *     - false: use default preference
2433
	 *     - string: format to use
2434
	 * @since 1.19
2435
	 * @return string
2436
	 */
2437
	public function userTimeAndDate( $ts, User $user, array $options = [] ) {
2438
		return $this->internalUserTimeAndDate( 'both', $ts, $user, $options );
2439
	}
2440
2441
	/**
2442
	 * Get the timestamp in a human-friendly relative format, e.g., "3 days ago".
2443
	 *
2444
	 * Determine the difference between the timestamp and the current time, and
2445
	 * generate a readable timestamp by returning "<N> <units> ago", where the
2446
	 * largest possible unit is used.
2447
	 *
2448
	 * @since 1.26 (Prior to 1.26 method existed but was not meant to be used directly)
2449
	 *
2450
	 * @param MWTimestamp $time
2451
	 * @param MWTimestamp|null $relativeTo The base timestamp to compare to (defaults to now)
2452
	 * @param User|null $user User the timestamp is being generated for
2453
	 *  (or null to use main context's user)
2454
	 * @return string Formatted timestamp
2455
	 */
2456
	public function getHumanTimestamp(
2457
		MWTimestamp $time, MWTimestamp $relativeTo = null, User $user = null
2458
	) {
2459
		if ( $relativeTo === null ) {
2460
			$relativeTo = new MWTimestamp();
2461
		}
2462
		if ( $user === null ) {
2463
			$user = RequestContext::getMain()->getUser();
2464
		}
2465
2466
		// Adjust for the user's timezone.
2467
		$offsetThis = $time->offsetForUser( $user );
2468
		$offsetRel = $relativeTo->offsetForUser( $user );
2469
2470
		$ts = '';
2471
		if ( Hooks::run( 'GetHumanTimestamp', [ &$ts, $time, $relativeTo, $user, $this ] ) ) {
2472
			$ts = $this->getHumanTimestampInternal( $time, $relativeTo, $user );
2473
		}
2474
2475
		// Reset the timezone on the objects.
2476
		$time->timestamp->sub( $offsetThis );
2477
		$relativeTo->timestamp->sub( $offsetRel );
2478
2479
		return $ts;
2480
	}
2481
2482
	/**
2483
	 * Convert an MWTimestamp into a pretty human-readable timestamp using
2484
	 * the given user preferences and relative base time.
2485
	 *
2486
	 * @see Language::getHumanTimestamp
2487
	 * @param MWTimestamp $ts Timestamp to prettify
2488
	 * @param MWTimestamp $relativeTo Base timestamp
2489
	 * @param User $user User preferences to use
2490
	 * @return string Human timestamp
2491
	 * @since 1.26
2492
	 */
2493
	private function getHumanTimestampInternal(
2494
		MWTimestamp $ts, MWTimestamp $relativeTo, User $user
2495
	) {
2496
		$diff = $ts->diff( $relativeTo );
2497
		$diffDay = (bool)( (int)$ts->timestamp->format( 'w' ) -
2498
			(int)$relativeTo->timestamp->format( 'w' ) );
2499
		$days = $diff->days ?: (int)$diffDay;
2500
		if ( $diff->invert || $days > 5
2501
			&& $ts->timestamp->format( 'Y' ) !== $relativeTo->timestamp->format( 'Y' )
2502
		) {
2503
			// Timestamps are in different years: use full timestamp
2504
			// Also do full timestamp for future dates
2505
			/**
2506
			 * @todo FIXME: Add better handling of future timestamps.
2507
			 */
2508
			$format = $this->getDateFormatString( 'both', $user->getDatePreference() ?: 'default' );
2509
			$ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
2510
		} elseif ( $days > 5 ) {
2511
			// Timestamps are in same year,  but more than 5 days ago: show day and month only.
2512
			$format = $this->getDateFormatString( 'pretty', $user->getDatePreference() ?: 'default' );
2513
			$ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
2514
		} elseif ( $days > 1 ) {
2515
			// Timestamp within the past week: show the day of the week and time
2516
			$format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2517
			$weekday = self::$mWeekdayMsgs[$ts->timestamp->format( 'w' )];
2518
			// Messages:
2519
			// sunday-at, monday-at, tuesday-at, wednesday-at, thursday-at, friday-at, saturday-at
2520
			$ts = wfMessage( "$weekday-at" )
2521
				->inLanguage( $this )
2522
				->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2523
				->text();
2524
		} elseif ( $days == 1 ) {
2525
			// Timestamp was yesterday: say 'yesterday' and the time.
2526
			$format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2527
			$ts = wfMessage( 'yesterday-at' )
2528
				->inLanguage( $this )
2529
				->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2530
				->text();
2531
		} elseif ( $diff->h > 1 || $diff->h == 1 && $diff->i > 30 ) {
2532
			// Timestamp was today, but more than 90 minutes ago: say 'today' and the time.
2533
			$format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2534
			$ts = wfMessage( 'today-at' )
2535
				->inLanguage( $this )
2536
				->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2537
				->text();
2538
2539
		// From here on in, the timestamp was soon enough ago so that we can simply say
2540
		// XX units ago, e.g., "2 hours ago" or "5 minutes ago"
2541
		} elseif ( $diff->h == 1 ) {
2542
			// Less than 90 minutes, but more than an hour ago.
2543
			$ts = wfMessage( 'hours-ago' )->inLanguage( $this )->numParams( 1 )->text();
2544
		} elseif ( $diff->i >= 1 ) {
2545
			// A few minutes ago.
2546
			$ts = wfMessage( 'minutes-ago' )->inLanguage( $this )->numParams( $diff->i )->text();
2547
		} elseif ( $diff->s >= 30 ) {
2548
			// Less than a minute, but more than 30 sec ago.
2549
			$ts = wfMessage( 'seconds-ago' )->inLanguage( $this )->numParams( $diff->s )->text();
2550
		} else {
2551
			// Less than 30 seconds ago.
2552
			$ts = wfMessage( 'just-now' )->text();
2553
		}
2554
2555
		return $ts;
2556
	}
2557
2558
	/**
2559
	 * @param string $key
2560
	 * @return array|null
2561
	 */
2562
	public function getMessage( $key ) {
2563
		return self::$dataCache->getSubitem( $this->mCode, 'messages', $key );
2564
	}
2565
2566
	/**
2567
	 * @return array
2568
	 */
2569
	function getAllMessages() {
2570
		return self::$dataCache->getItem( $this->mCode, 'messages' );
2571
	}
2572
2573
	/**
2574
	 * @param string $in
2575
	 * @param string $out
2576
	 * @param string $string
2577
	 * @return string
2578
	 */
2579
	public function iconv( $in, $out, $string ) {
2580
		# This is a wrapper for iconv in all languages except esperanto,
2581
		# which does some nasty x-conversions beforehand
2582
2583
		# Even with //IGNORE iconv can whine about illegal characters in
2584
		# *input* string. We just ignore those too.
2585
		# REF: http://bugs.php.net/bug.php?id=37166
2586
		# REF: https://phabricator.wikimedia.org/T18885
2587
		MediaWiki\suppressWarnings();
2588
		$text = iconv( $in, $out . '//IGNORE', $string );
2589
		MediaWiki\restoreWarnings();
2590
		return $text;
2591
	}
2592
2593
	// callback functions for ucwords(), ucwordbreaks()
2594
2595
	/**
2596
	 * @param array $matches
2597
	 * @return mixed|string
2598
	 */
2599
	function ucwordbreaksCallbackAscii( $matches ) {
2600
		return $this->ucfirst( $matches[1] );
2601
	}
2602
2603
	/**
2604
	 * @param array $matches
2605
	 * @return string
2606
	 */
2607
	function ucwordbreaksCallbackMB( $matches ) {
2608
		return mb_strtoupper( $matches[0] );
2609
	}
2610
2611
	/**
2612
	 * @param array $matches
2613
	 * @return string
2614
	 */
2615
	function ucwordsCallbackMB( $matches ) {
2616
		return mb_strtoupper( $matches[0] );
2617
	}
2618
2619
	/**
2620
	 * Make a string's first character uppercase
2621
	 *
2622
	 * @param string $str
2623
	 *
2624
	 * @return string
2625
	 */
2626
	public function ucfirst( $str ) {
2627
		$o = ord( $str );
2628
		if ( $o < 96 ) { // if already uppercase...
2629
			return $str;
2630
		} elseif ( $o < 128 ) {
2631
			return ucfirst( $str ); // use PHP's ucfirst()
2632
		} else {
2633
			// fall back to more complex logic in case of multibyte strings
2634
			return $this->uc( $str, true );
2635
		}
2636
	}
2637
2638
	/**
2639
	 * Convert a string to uppercase
2640
	 *
2641
	 * @param string $str
2642
	 * @param bool $first
2643
	 *
2644
	 * @return string
2645
	 */
2646
	public function uc( $str, $first = false ) {
2647
		if ( $first ) {
2648
			if ( $this->isMultibyte( $str ) ) {
2649
				return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2650
			} else {
2651
				return ucfirst( $str );
2652
			}
2653
		} else {
2654
			return $this->isMultibyte( $str ) ? mb_strtoupper( $str ) : strtoupper( $str );
2655
		}
2656
	}
2657
2658
	/**
2659
	 * @param string $str
2660
	 * @return mixed|string
2661
	 */
2662
	function lcfirst( $str ) {
2663
		$o = ord( $str );
2664
		if ( !$o ) {
2665
			return strval( $str );
2666
		} elseif ( $o >= 128 ) {
2667
			return $this->lc( $str, true );
2668
		} elseif ( $o > 96 ) {
2669
			return $str;
2670
		} else {
2671
			$str[0] = strtolower( $str[0] );
2672
			return $str;
2673
		}
2674
	}
2675
2676
	/**
2677
	 * @param string $str
2678
	 * @param bool $first
2679
	 * @return mixed|string
2680
	 */
2681
	function lc( $str, $first = false ) {
2682
		if ( $first ) {
2683
			if ( $this->isMultibyte( $str ) ) {
2684
				return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2685
			} else {
2686
				return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 );
2687
			}
2688
		} else {
2689
			return $this->isMultibyte( $str ) ? mb_strtolower( $str ) : strtolower( $str );
2690
		}
2691
	}
2692
2693
	/**
2694
	 * @param string $str
2695
	 * @return bool
2696
	 */
2697
	function isMultibyte( $str ) {
2698
		return strlen( $str ) !== mb_strlen( $str );
2699
	}
2700
2701
	/**
2702
	 * @param string $str
2703
	 * @return mixed|string
2704
	 */
2705
	function ucwords( $str ) {
2706
		if ( $this->isMultibyte( $str ) ) {
2707
			$str = $this->lc( $str );
2708
2709
			// regexp to find first letter in each word (i.e. after each space)
2710
			$replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2711
2712
			// function to use to capitalize a single char
2713
			return preg_replace_callback(
2714
				$replaceRegexp,
2715
				[ $this, 'ucwordsCallbackMB' ],
2716
				$str
2717
			);
2718
		} else {
2719
			return ucwords( strtolower( $str ) );
2720
		}
2721
	}
2722
2723
	/**
2724
	 * capitalize words at word breaks
2725
	 *
2726
	 * @param string $str
2727
	 * @return mixed
2728
	 */
2729
	function ucwordbreaks( $str ) {
2730
		if ( $this->isMultibyte( $str ) ) {
2731
			$str = $this->lc( $str );
2732
2733
			// since \b doesn't work for UTF-8, we explicitely define word break chars
2734
			$breaks = "[ \-\(\)\}\{\.,\?!]";
2735
2736
			// find first letter after word break
2737
			$replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|" .
2738
				"$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2739
2740
			return preg_replace_callback(
2741
				$replaceRegexp,
2742
				[ $this, 'ucwordbreaksCallbackMB' ],
2743
				$str
2744
			);
2745
		} else {
2746
			return preg_replace_callback(
2747
				'/\b([\w\x80-\xff]+)\b/',
2748
				[ $this, 'ucwordbreaksCallbackAscii' ],
2749
				$str
2750
			);
2751
		}
2752
	}
2753
2754
	/**
2755
	 * Return a case-folded representation of $s
2756
	 *
2757
	 * This is a representation such that caseFold($s1)==caseFold($s2) if $s1
2758
	 * and $s2 are the same except for the case of their characters. It is not
2759
	 * necessary for the value returned to make sense when displayed.
2760
	 *
2761
	 * Do *not* perform any other normalisation in this function. If a caller
2762
	 * uses this function when it should be using a more general normalisation
2763
	 * function, then fix the caller.
2764
	 *
2765
	 * @param string $s
2766
	 *
2767
	 * @return string
2768
	 */
2769
	function caseFold( $s ) {
2770
		return $this->uc( $s );
2771
	}
2772
2773
	/**
2774
	 * @param string $s
2775
	 * @return string
2776
	 * @throws MWException
2777
	 */
2778
	function checkTitleEncoding( $s ) {
2779
		if ( is_array( $s ) ) {
2780
			throw new MWException( 'Given array to checkTitleEncoding.' );
2781
		}
2782
		if ( StringUtils::isUtf8( $s ) ) {
2783
			return $s;
2784
		}
2785
2786
		return $this->iconv( $this->fallback8bitEncoding(), 'utf-8', $s );
2787
	}
2788
2789
	/**
2790
	 * @return array
2791
	 */
2792
	function fallback8bitEncoding() {
2793
		return self::$dataCache->getItem( $this->mCode, 'fallback8bitEncoding' );
2794
	}
2795
2796
	/**
2797
	 * Most writing systems use whitespace to break up words.
2798
	 * Some languages such as Chinese don't conventionally do this,
2799
	 * which requires special handling when breaking up words for
2800
	 * searching etc.
2801
	 *
2802
	 * @return bool
2803
	 */
2804
	function hasWordBreaks() {
2805
		return true;
2806
	}
2807
2808
	/**
2809
	 * Some languages such as Chinese require word segmentation,
2810
	 * Specify such segmentation when overridden in derived class.
2811
	 *
2812
	 * @param string $string
2813
	 * @return string
2814
	 */
2815
	function segmentByWord( $string ) {
2816
		return $string;
2817
	}
2818
2819
	/**
2820
	 * Some languages have special punctuation need to be normalized.
2821
	 * Make such changes here.
2822
	 *
2823
	 * @param string $string
2824
	 * @return string
2825
	 */
2826
	function normalizeForSearch( $string ) {
2827
		return self::convertDoubleWidth( $string );
2828
	}
2829
2830
	/**
2831
	 * convert double-width roman characters to single-width.
2832
	 * range: ff00-ff5f ~= 0020-007f
2833
	 *
2834
	 * @param string $string
2835
	 *
2836
	 * @return string
2837
	 */
2838
	protected static function convertDoubleWidth( $string ) {
2839
		static $full = null;
2840
		static $half = null;
2841
2842
		if ( $full === null ) {
2843
			$fullWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2844
			$halfWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2845
			$full = str_split( $fullWidth, 3 );
2846
			$half = str_split( $halfWidth );
2847
		}
2848
2849
		$string = str_replace( $full, $half, $string );
2850
		return $string;
2851
	}
2852
2853
	/**
2854
	 * @param string $string
2855
	 * @param string $pattern
2856
	 * @return string
2857
	 */
2858
	protected static function insertSpace( $string, $pattern ) {
2859
		$string = preg_replace( $pattern, " $1 ", $string );
2860
		$string = preg_replace( '/ +/', ' ', $string );
2861
		return $string;
2862
	}
2863
2864
	/**
2865
	 * @param array $termsArray
2866
	 * @return array
2867
	 */
2868
	function convertForSearchResult( $termsArray ) {
2869
		# some languages, e.g. Chinese, need to do a conversion
2870
		# in order for search results to be displayed correctly
2871
		return $termsArray;
2872
	}
2873
2874
	/**
2875
	 * Get the first character of a string.
2876
	 *
2877
	 * @param string $s
2878
	 * @return string
2879
	 */
2880
	function firstChar( $s ) {
2881
		$matches = [];
2882
		preg_match(
2883
			'/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
2884
				'[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/',
2885
			$s,
2886
			$matches
2887
		);
2888
2889
		if ( isset( $matches[1] ) ) {
2890
			if ( strlen( $matches[1] ) != 3 ) {
2891
				return $matches[1];
2892
			}
2893
2894
			// Break down Hangul syllables to grab the first jamo
2895
			$code = UtfNormal\Utils::utf8ToCodepoint( $matches[1] );
2896
			if ( $code < 0xac00 || 0xd7a4 <= $code ) {
2897
				return $matches[1];
2898
			} elseif ( $code < 0xb098 ) {
2899
				return "\xe3\x84\xb1";
2900
			} elseif ( $code < 0xb2e4 ) {
2901
				return "\xe3\x84\xb4";
2902
			} elseif ( $code < 0xb77c ) {
2903
				return "\xe3\x84\xb7";
2904
			} elseif ( $code < 0xb9c8 ) {
2905
				return "\xe3\x84\xb9";
2906
			} elseif ( $code < 0xbc14 ) {
2907
				return "\xe3\x85\x81";
2908
			} elseif ( $code < 0xc0ac ) {
2909
				return "\xe3\x85\x82";
2910
			} elseif ( $code < 0xc544 ) {
2911
				return "\xe3\x85\x85";
2912
			} elseif ( $code < 0xc790 ) {
2913
				return "\xe3\x85\x87";
2914
			} elseif ( $code < 0xcc28 ) {
2915
				return "\xe3\x85\x88";
2916
			} elseif ( $code < 0xce74 ) {
2917
				return "\xe3\x85\x8a";
2918
			} elseif ( $code < 0xd0c0 ) {
2919
				return "\xe3\x85\x8b";
2920
			} elseif ( $code < 0xd30c ) {
2921
				return "\xe3\x85\x8c";
2922
			} elseif ( $code < 0xd558 ) {
2923
				return "\xe3\x85\x8d";
2924
			} else {
2925
				return "\xe3\x85\x8e";
2926
			}
2927
		} else {
2928
			return '';
2929
		}
2930
	}
2931
2932
	function initEncoding() {
2933
		# Some languages may have an alternate char encoding option
2934
		# (Esperanto X-coding, Japanese furigana conversion, etc)
2935
		# If this language is used as the primary content language,
2936
		# an override to the defaults can be set here on startup.
2937
	}
2938
2939
	/**
2940
	 * @param string $s
2941
	 * @return string
2942
	 */
2943
	function recodeForEdit( $s ) {
2944
		# For some languages we'll want to explicitly specify
2945
		# which characters make it into the edit box raw
2946
		# or are converted in some way or another.
2947
		global $wgEditEncoding;
2948
		if ( $wgEditEncoding == '' || $wgEditEncoding == 'UTF-8' ) {
2949
			return $s;
2950
		} else {
2951
			return $this->iconv( 'UTF-8', $wgEditEncoding, $s );
2952
		}
2953
	}
2954
2955
	/**
2956
	 * @param string $s
2957
	 * @return string
2958
	 */
2959
	function recodeInput( $s ) {
2960
		# Take the previous into account.
2961
		global $wgEditEncoding;
2962
		if ( $wgEditEncoding != '' ) {
2963
			$enc = $wgEditEncoding;
2964
		} else {
2965
			$enc = 'UTF-8';
2966
		}
2967
		if ( $enc == 'UTF-8' ) {
2968
			return $s;
2969
		} else {
2970
			return $this->iconv( $enc, 'UTF-8', $s );
2971
		}
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
	 * Get the grammar forms for the content language
3741
	 * @return array Array of grammar forms
3742
	 * @since 1.20
3743
	 */
3744
	function getGrammarForms() {
3745
		global $wgGrammarForms;
3746
		if ( isset( $wgGrammarForms[$this->getCode()] )
3747
			&& is_array( $wgGrammarForms[$this->getCode()] )
3748
		) {
3749
			return $wgGrammarForms[$this->getCode()];
3750
		}
3751
3752
		return [];
3753
	}
3754
	/**
3755
	 * Provides an alternative text depending on specified gender.
3756
	 * Usage {{gender:username|masculine|feminine|unknown}}.
3757
	 * username is optional, in which case the gender of current user is used,
3758
	 * but only in (some) interface messages; otherwise default gender is used.
3759
	 *
3760
	 * If no forms are given, an empty string is returned. If only one form is
3761
	 * given, it will be returned unconditionally. These details are implied by
3762
	 * the caller and cannot be overridden in subclasses.
3763
	 *
3764
	 * If three forms are given, the default is to use the third (unknown) form.
3765
	 * If fewer than three forms are given, the default is to use the first (masculine) form.
3766
	 * These details can be overridden in subclasses.
3767
	 *
3768
	 * @param string $gender
3769
	 * @param array $forms
3770
	 *
3771
	 * @return string
3772
	 */
3773
	function gender( $gender, $forms ) {
3774
		if ( !count( $forms ) ) {
3775
			return '';
3776
		}
3777
		$forms = $this->preConvertPlural( $forms, 2 );
3778
		if ( $gender === 'male' ) {
3779
			return $forms[0];
3780
		}
3781
		if ( $gender === 'female' ) {
3782
			return $forms[1];
3783
		}
3784
		return isset( $forms[2] ) ? $forms[2] : $forms[0];
3785
	}
3786
3787
	/**
3788
	 * Plural form transformations, needed for some languages.
3789
	 * For example, there are 3 form of plural in Russian and Polish,
3790
	 * depending on "count mod 10". See [[w:Plural]]
3791
	 * For English it is pretty simple.
3792
	 *
3793
	 * Invoked by putting {{plural:count|wordform1|wordform2}}
3794
	 * or {{plural:count|wordform1|wordform2|wordform3}}
3795
	 *
3796
	 * Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}}
3797
	 *
3798
	 * @param int $count Non-localized number
3799
	 * @param array $forms Different plural forms
3800
	 * @return string Correct form of plural for $count in this language
3801
	 */
3802
	function convertPlural( $count, $forms ) {
3803
		// Handle explicit n=pluralform cases
3804
		$forms = $this->handleExplicitPluralForms( $count, $forms );
3805
		if ( is_string( $forms ) ) {
3806
			return $forms;
3807
		}
3808
		if ( !count( $forms ) ) {
3809
			return '';
3810
		}
3811
3812
		$pluralForm = $this->getPluralRuleIndexNumber( $count );
3813
		$pluralForm = min( $pluralForm, count( $forms ) - 1 );
3814
		return $forms[$pluralForm];
3815
	}
3816
3817
	/**
3818
	 * Handles explicit plural forms for Language::convertPlural()
3819
	 *
3820
	 * In {{PLURAL:$1|0=nothing|one|many}}, 0=nothing will be returned if $1 equals zero.
3821
	 * If an explicitly defined plural form matches the $count, then
3822
	 * string value returned, otherwise array returned for further consideration
3823
	 * by CLDR rules or overridden convertPlural().
3824
	 *
3825
	 * @since 1.23
3826
	 *
3827
	 * @param int $count Non-localized number
3828
	 * @param array $forms Different plural forms
3829
	 *
3830
	 * @return array|string
3831
	 */
3832
	protected function handleExplicitPluralForms( $count, array $forms ) {
3833
		foreach ( $forms as $index => $form ) {
3834
			if ( preg_match( '/\d+=/i', $form ) ) {
3835
				$pos = strpos( $form, '=' );
3836
				if ( substr( $form, 0, $pos ) === (string)$count ) {
3837
					return substr( $form, $pos + 1 );
3838
				}
3839
				unset( $forms[$index] );
3840
			}
3841
		}
3842
		return array_values( $forms );
3843
	}
3844
3845
	/**
3846
	 * Checks that convertPlural was given an array and pads it to requested
3847
	 * amount of forms by copying the last one.
3848
	 *
3849
	 * @param array $forms Array of forms given to convertPlural
3850
	 * @param int $count How many forms should there be at least
3851
	 * @return array Padded array of forms or an exception if not an array
3852
	 */
3853
	protected function preConvertPlural( /* Array */ $forms, $count ) {
3854
		while ( count( $forms ) < $count ) {
3855
			$forms[] = $forms[count( $forms ) - 1];
3856
		}
3857
		return $forms;
3858
	}
3859
3860
	/**
3861
	 * Wraps argument with unicode control characters for directionality safety
3862
	 *
3863
	 * This solves the problem where directionality-neutral characters at the edge of
3864
	 * the argument string get interpreted with the wrong directionality from the
3865
	 * enclosing context, giving renderings that look corrupted like "(Ben_(WMF".
3866
	 *
3867
	 * The wrapping is LRE...PDF or RLE...PDF, depending on the detected
3868
	 * directionality of the argument string, using the BIDI algorithm's own "First
3869
	 * strong directional codepoint" rule. Essentially, this works round the fact that
3870
	 * there is no embedding equivalent of U+2068 FSI (isolation with heuristic
3871
	 * direction inference). The latter is cleaner but still not widely supported.
3872
	 *
3873
	 * @param string $text Text to wrap
3874
	 * @return string Text, wrapped in LRE...PDF or RLE...PDF or nothing
3875
	 */
3876
	public function embedBidi( $text = '' ) {
3877
		$dir = Language::strongDirFromContent( $text );
3878
		if ( $dir === 'ltr' ) {
3879
			// Wrap in LEFT-TO-RIGHT EMBEDDING ... POP DIRECTIONAL FORMATTING
3880
			return self::$lre . $text . self::$pdf;
3881
		}
3882
		if ( $dir === 'rtl' ) {
3883
			// Wrap in RIGHT-TO-LEFT EMBEDDING ... POP DIRECTIONAL FORMATTING
3884
			return self::$rle . $text . self::$pdf;
3885
		}
3886
		// No strong directionality: do not wrap
3887
		return $text;
3888
	}
3889
3890
	/**
3891
	 * @todo Maybe translate block durations.  Note that this function is somewhat misnamed: it
3892
	 * deals with translating the *duration* ("1 week", "4 days", etc), not the expiry time
3893
	 * (which is an absolute timestamp). Please note: do NOT add this blindly, as it is used
3894
	 * on old expiry lengths recorded in log entries. You'd need to provide the start date to
3895
	 * match up with it.
3896
	 *
3897
	 * @param string $str The validated block duration in English
3898
	 * @param User $user User object to use timezone from or null for $wgUser
3899
	 * @return string Somehow translated block duration
3900
	 * @see LanguageFi.php for example implementation
3901
	 */
3902
	function translateBlockExpiry( $str, User $user = null ) {
3903
		$duration = SpecialBlock::getSuggestedDurations( $this );
3904
		foreach ( $duration as $show => $value ) {
3905
			if ( strcmp( $str, $value ) == 0 ) {
3906
				return htmlspecialchars( trim( $show ) );
3907
			}
3908
		}
3909
3910
		if ( wfIsInfinity( $str ) ) {
3911
			foreach ( $duration as $show => $value ) {
3912
				if ( wfIsInfinity( $value ) ) {
3913
					return htmlspecialchars( trim( $show ) );
3914
				}
3915
			}
3916
		}
3917
3918
		// If all else fails, return a standard duration or timestamp description.
3919
		$time = strtotime( $str, 0 );
3920
		if ( $time === false ) { // Unknown format. Return it as-is in case.
3921
			return $str;
3922
		} elseif ( $time !== strtotime( $str, 1 ) ) { // It's a relative timestamp.
3923
			// $time is relative to 0 so it's a duration length.
3924
			return $this->formatDuration( $time );
3925
		} else { // It's an absolute timestamp.
3926
			if ( $time === 0 ) {
3927
				// wfTimestamp() handles 0 as current time instead of epoch.
3928
				$time = '19700101000000';
3929
			}
3930
			if ( $user ) {
3931
				return $this->userTimeAndDate( $time, $user );
3932
			}
3933
			return $this->timeanddate( $time );
3934
		}
3935
	}
3936
3937
	/**
3938
	 * languages like Chinese need to be segmented in order for the diff
3939
	 * to be of any use
3940
	 *
3941
	 * @param string $text
3942
	 * @return string
3943
	 */
3944
	public function segmentForDiff( $text ) {
3945
		return $text;
3946
	}
3947
3948
	/**
3949
	 * and unsegment to show the result
3950
	 *
3951
	 * @param string $text
3952
	 * @return string
3953
	 */
3954
	public function unsegmentForDiff( $text ) {
3955
		return $text;
3956
	}
3957
3958
	/**
3959
	 * Return the LanguageConverter used in the Language
3960
	 *
3961
	 * @since 1.19
3962
	 * @return LanguageConverter
3963
	 */
3964
	public function getConverter() {
3965
		return $this->mConverter;
3966
	}
3967
3968
	/**
3969
	 * convert text to all supported variants
3970
	 *
3971
	 * @param string $text
3972
	 * @return array
3973
	 */
3974
	public function autoConvertToAllVariants( $text ) {
3975
		return $this->mConverter->autoConvertToAllVariants( $text );
3976
	}
3977
3978
	/**
3979
	 * convert text to different variants of a language.
3980
	 *
3981
	 * @param string $text
3982
	 * @return string
3983
	 */
3984
	public function convert( $text ) {
3985
		return $this->mConverter->convert( $text );
3986
	}
3987
3988
	/**
3989
	 * Convert a Title object to a string in the preferred variant
3990
	 *
3991
	 * @param Title $title
3992
	 * @return string
3993
	 */
3994
	public function convertTitle( $title ) {
3995
		return $this->mConverter->convertTitle( $title );
3996
	}
3997
3998
	/**
3999
	 * Convert a namespace index to a string in the preferred variant
4000
	 *
4001
	 * @param int $ns
4002
	 * @return string
4003
	 */
4004
	public function convertNamespace( $ns ) {
4005
		return $this->mConverter->convertNamespace( $ns );
4006
	}
4007
4008
	/**
4009
	 * Check if this is a language with variants
4010
	 *
4011
	 * @return bool
4012
	 */
4013
	public function hasVariants() {
4014
		return count( $this->getVariants() ) > 1;
4015
	}
4016
4017
	/**
4018
	 * Check if the language has the specific variant
4019
	 *
4020
	 * @since 1.19
4021
	 * @param string $variant
4022
	 * @return bool
4023
	 */
4024
	public function hasVariant( $variant ) {
4025
		return (bool)$this->mConverter->validateVariant( $variant );
4026
	}
4027
4028
	/**
4029
	 * Perform output conversion on a string, and encode for safe HTML output.
4030
	 * @param string $text Text to be converted
4031
	 * @param bool $isTitle Whether this conversion is for the article title
4032
	 * @return string
4033
	 * @todo this should get integrated somewhere sane
4034
	 */
4035
	public function convertHtml( $text, $isTitle = false ) {
4036
		return htmlspecialchars( $this->convert( $text, $isTitle ) );
4037
	}
4038
4039
	/**
4040
	 * @param string $key
4041
	 * @return string
4042
	 */
4043
	public function convertCategoryKey( $key ) {
4044
		return $this->mConverter->convertCategoryKey( $key );
4045
	}
4046
4047
	/**
4048
	 * Get the list of variants supported by this language
4049
	 * see sample implementation in LanguageZh.php
4050
	 *
4051
	 * @return array An array of language codes
4052
	 */
4053
	public function getVariants() {
4054
		return $this->mConverter->getVariants();
4055
	}
4056
4057
	/**
4058
	 * @return string
4059
	 */
4060
	public function getPreferredVariant() {
4061
		return $this->mConverter->getPreferredVariant();
4062
	}
4063
4064
	/**
4065
	 * @return string
4066
	 */
4067
	public function getDefaultVariant() {
4068
		return $this->mConverter->getDefaultVariant();
4069
	}
4070
4071
	/**
4072
	 * @return string
4073
	 */
4074
	public function getURLVariant() {
4075
		return $this->mConverter->getURLVariant();
4076
	}
4077
4078
	/**
4079
	 * If a language supports multiple variants, it is
4080
	 * possible that non-existing link in one variant
4081
	 * actually exists in another variant. this function
4082
	 * tries to find it. See e.g. LanguageZh.php
4083
	 * The input parameters may be modified upon return
4084
	 *
4085
	 * @param string &$link The name of the link
4086
	 * @param Title &$nt The title object of the link
4087
	 * @param bool $ignoreOtherCond To disable other conditions when
4088
	 *   we need to transclude a template or update a category's link
4089
	 */
4090
	public function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
4091
		$this->mConverter->findVariantLink( $link, $nt, $ignoreOtherCond );
4092
	}
4093
4094
	/**
4095
	 * returns language specific options used by User::getPageRenderHash()
4096
	 * for example, the preferred language variant
4097
	 *
4098
	 * @return string
4099
	 */
4100
	function getExtraHashOptions() {
4101
		return $this->mConverter->getExtraHashOptions();
4102
	}
4103
4104
	/**
4105
	 * For languages that support multiple variants, the title of an
4106
	 * article may be displayed differently in different variants. this
4107
	 * function returns the apporiate title defined in the body of the article.
4108
	 *
4109
	 * @return string
4110
	 */
4111
	public function getParsedTitle() {
4112
		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...
4113
	}
4114
4115
	/**
4116
	 * Refresh the cache of conversion tables when
4117
	 * MediaWiki:Conversiontable* is updated.
4118
	 *
4119
	 * @param Title $title The Title of the page being updated
4120
	 */
4121
	public function updateConversionTable( Title $title ) {
4122
		$this->mConverter->updateConversionTable( $title );
4123
	}
4124
4125
	/**
4126
	 * Prepare external link text for conversion. When the text is
4127
	 * a URL, it shouldn't be converted, and it'll be wrapped in
4128
	 * the "raw" tag (-{R| }-) to prevent conversion.
4129
	 *
4130
	 * This function is called "markNoConversion" for historical
4131
	 * reasons.
4132
	 *
4133
	 * @param string $text Text to be used for external link
4134
	 * @param bool $noParse Wrap it without confirming it's a real URL first
4135
	 * @return string The tagged text
4136
	 */
4137
	public function markNoConversion( $text, $noParse = false ) {
4138
		// Excluding protocal-relative URLs may avoid many false positives.
4139
		if ( $noParse || preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
4140
			return $this->mConverter->markNoConversion( $text );
4141
		} else {
4142
			return $text;
4143
		}
4144
	}
4145
4146
	/**
4147
	 * A regular expression to match legal word-trailing characters
4148
	 * which should be merged onto a link of the form [[foo]]bar.
4149
	 *
4150
	 * @return string
4151
	 */
4152
	public function linkTrail() {
4153
		return self::$dataCache->getItem( $this->mCode, 'linkTrail' );
4154
	}
4155
4156
	/**
4157
	 * A regular expression character set to match legal word-prefixing
4158
	 * characters which should be merged onto a link of the form foo[[bar]].
4159
	 *
4160
	 * @return string
4161
	 */
4162
	public function linkPrefixCharset() {
4163
		return self::$dataCache->getItem( $this->mCode, 'linkPrefixCharset' );
4164
	}
4165
4166
	/**
4167
	 * Get the "parent" language which has a converter to convert a "compatible" language
4168
	 * (in another variant) to this language (eg. zh for zh-cn, but not en for en-gb).
4169
	 *
4170
	 * @return Language|null
4171
	 * @since 1.22
4172
	 */
4173
	public function getParentLanguage() {
4174
		if ( $this->mParentLanguage !== false ) {
4175
			return $this->mParentLanguage;
4176
		}
4177
4178
		$code = explode( '-', $this->getCode() )[0];
4179
		if ( !in_array( $code, LanguageConverter::$languagesWithVariants ) ) {
4180
			$this->mParentLanguage = null;
4181
			return null;
4182
		}
4183
		$lang = Language::factory( $code );
4184
		if ( !$lang->hasVariant( $this->getCode() ) ) {
4185
			$this->mParentLanguage = null;
4186
			return null;
4187
		}
4188
4189
		$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...
4190
		return $lang;
4191
	}
4192
4193
	/**
4194
	 * Compare with an other language object
4195
	 *
4196
	 * @since 1.28
4197
	 * @param Language $lang
4198
	 * @return boolean
4199
	 */
4200
	public function equals( Language $lang ) {
4201
		return $lang->getCode() === $this->mCode;
4202
	}
4203
4204
	/**
4205
	 * Get the internal language code for this language object
4206
	 *
4207
	 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
4208
	 * htmlspecialchars() or similar
4209
	 *
4210
	 * @return string
4211
	 */
4212
	public function getCode() {
4213
		return $this->mCode;
4214
	}
4215
4216
	/**
4217
	 * Get the code in BCP 47 format which we can use
4218
	 * inside of html lang="" tags.
4219
	 *
4220
	 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
4221
	 * htmlspecialchars() or similar.
4222
	 *
4223
	 * @since 1.19
4224
	 * @return string
4225
	 */
4226
	public function getHtmlCode() {
4227
		if ( is_null( $this->mHtmlCode ) ) {
4228
			$this->mHtmlCode = wfBCP47( $this->getCode() );
4229
		}
4230
		return $this->mHtmlCode;
4231
	}
4232
4233
	/**
4234
	 * @param string $code
4235
	 */
4236
	public function setCode( $code ) {
4237
		$this->mCode = $code;
4238
		// Ensure we don't leave incorrect cached data lying around
4239
		$this->mHtmlCode = null;
4240
		$this->mParentLanguage = false;
4241
	}
4242
4243
	/**
4244
	 * Get the language code from a file name. Inverse of getFileName()
4245
	 * @param string $filename $prefix . $languageCode . $suffix
4246
	 * @param string $prefix Prefix before the language code
4247
	 * @param string $suffix Suffix after the language code
4248
	 * @return string Language code, or false if $prefix or $suffix isn't found
4249
	 */
4250
	public static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
4251
		$m = null;
4252
		preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
4253
			preg_quote( $suffix, '/' ) . '/', $filename, $m );
4254
		if ( !count( $m ) ) {
4255
			return false;
4256
		}
4257
		return str_replace( '_', '-', strtolower( $m[1] ) );
4258
	}
4259
4260
	/**
4261
	 * @param string $code
4262
	 * @return string Name of the language class
4263
	 */
4264
	public static function classFromCode( $code ) {
4265
		if ( $code == 'en' ) {
4266
			return 'Language';
4267
		} else {
4268
			return 'Language' . str_replace( '-', '_', ucfirst( $code ) );
4269
		}
4270
	}
4271
4272
	/**
4273
	 * Get the name of a file for a certain language code
4274
	 * @param string $prefix Prepend this to the filename
4275
	 * @param string $code Language code
4276
	 * @param string $suffix Append this to the filename
4277
	 * @throws MWException
4278
	 * @return string $prefix . $mangledCode . $suffix
4279
	 */
4280
	public static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) {
4281
		if ( !self::isValidBuiltInCode( $code ) ) {
4282
			throw new MWException( "Invalid language code \"$code\"" );
4283
		}
4284
4285
		return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
4286
	}
4287
4288
	/**
4289
	 * @param string $code
4290
	 * @return string
4291
	 */
4292
	public static function getMessagesFileName( $code ) {
4293
		global $IP;
4294
		$file = self::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
4295
		Hooks::run( 'Language::getMessagesFileName', [ $code, &$file ] );
4296
		return $file;
4297
	}
4298
4299
	/**
4300
	 * @param string $code
4301
	 * @return string
4302
	 * @throws MWException
4303
	 * @since 1.23
4304
	 */
4305
	public static function getJsonMessagesFileName( $code ) {
4306
		global $IP;
4307
4308
		if ( !self::isValidBuiltInCode( $code ) ) {
4309
			throw new MWException( "Invalid language code \"$code\"" );
4310
		}
4311
4312
		return "$IP/languages/i18n/$code.json";
4313
	}
4314
4315
	/**
4316
	 * Get the first fallback for a given language.
4317
	 *
4318
	 * @param string $code
4319
	 *
4320
	 * @return bool|string
4321
	 */
4322
	public static function getFallbackFor( $code ) {
4323
		$fallbacks = self::getFallbacksFor( $code );
4324
		if ( $fallbacks ) {
4325
			return $fallbacks[0];
4326
		}
4327
		return false;
4328
	}
4329
4330
	/**
4331
	 * Get the ordered list of fallback languages.
4332
	 *
4333
	 * @since 1.19
4334
	 * @param string $code Language code
4335
	 * @return array Non-empty array, ending in "en"
4336
	 */
4337
	public static function getFallbacksFor( $code ) {
4338
		if ( $code === 'en' || !Language::isValidBuiltInCode( $code ) ) {
4339
			return [];
4340
		}
4341
		// For unknown languages, fallbackSequence returns an empty array,
4342
		// hardcode fallback to 'en' in that case.
4343
		return self::getLocalisationCache()->getItem( $code, 'fallbackSequence' ) ?: [ 'en' ];
4344
	}
4345
4346
	/**
4347
	 * Get the ordered list of fallback languages, ending with the fallback
4348
	 * language chain for the site language.
4349
	 *
4350
	 * @since 1.22
4351
	 * @param string $code Language code
4352
	 * @return array Array( fallbacks, site fallbacks )
4353
	 */
4354
	public static function getFallbacksIncludingSiteLanguage( $code ) {
4355
		global $wgLanguageCode;
4356
4357
		// Usually, we will only store a tiny number of fallback chains, so we
4358
		// keep them in static memory.
4359
		$cacheKey = "{$code}-{$wgLanguageCode}";
4360
4361
		if ( !array_key_exists( $cacheKey, self::$fallbackLanguageCache ) ) {
4362
			$fallbacks = self::getFallbacksFor( $code );
4363
4364
			// Append the site's fallback chain, including the site language itself
4365
			$siteFallbacks = self::getFallbacksFor( $wgLanguageCode );
4366
			array_unshift( $siteFallbacks, $wgLanguageCode );
4367
4368
			// Eliminate any languages already included in the chain
4369
			$siteFallbacks = array_diff( $siteFallbacks, $fallbacks );
4370
4371
			self::$fallbackLanguageCache[$cacheKey] = [ $fallbacks, $siteFallbacks ];
4372
		}
4373
		return self::$fallbackLanguageCache[$cacheKey];
4374
	}
4375
4376
	/**
4377
	 * Get all messages for a given language
4378
	 * WARNING: this may take a long time. If you just need all message *keys*
4379
	 * but need the *contents* of only a few messages, consider using getMessageKeysFor().
4380
	 *
4381
	 * @param string $code
4382
	 *
4383
	 * @return array
4384
	 */
4385
	public static function getMessagesFor( $code ) {
4386
		return self::getLocalisationCache()->getItem( $code, 'messages' );
4387
	}
4388
4389
	/**
4390
	 * Get a message for a given language
4391
	 *
4392
	 * @param string $key
4393
	 * @param string $code
4394
	 *
4395
	 * @return string
4396
	 */
4397
	public static function getMessageFor( $key, $code ) {
4398
		return self::getLocalisationCache()->getSubitem( $code, 'messages', $key );
4399
	}
4400
4401
	/**
4402
	 * Get all message keys for a given language. This is a faster alternative to
4403
	 * array_keys( Language::getMessagesFor( $code ) )
4404
	 *
4405
	 * @since 1.19
4406
	 * @param string $code Language code
4407
	 * @return array Array of message keys (strings)
4408
	 */
4409
	public static function getMessageKeysFor( $code ) {
4410
		return self::getLocalisationCache()->getSubitemList( $code, 'messages' );
4411
	}
4412
4413
	/**
4414
	 * @param string $talk
4415
	 * @return mixed
4416
	 */
4417
	function fixVariableInNamespace( $talk ) {
4418
		if ( strpos( $talk, '$1' ) === false ) {
4419
			return $talk;
4420
		}
4421
4422
		global $wgMetaNamespace;
4423
		$talk = str_replace( '$1', $wgMetaNamespace, $talk );
4424
4425
		# Allow grammar transformations
4426
		# Allowing full message-style parsing would make simple requests
4427
		# such as action=raw much more expensive than they need to be.
4428
		# This will hopefully cover most cases.
4429
		$talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
4430
			[ &$this, 'replaceGrammarInNamespace' ], $talk );
4431
		return str_replace( ' ', '_', $talk );
4432
	}
4433
4434
	/**
4435
	 * @param string $m
4436
	 * @return string
4437
	 */
4438
	function replaceGrammarInNamespace( $m ) {
4439
		return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
4440
	}
4441
4442
	/**
4443
	 * Decode an expiry (block, protection, etc) which has come from the DB
4444
	 *
4445
	 * @param string $expiry Database expiry String
4446
	 * @param bool|int $format True to process using language functions, or TS_ constant
4447
	 *     to return the expiry in a given timestamp
4448
	 * @param string $infinity If $format is not true, use this string for infinite expiry
4449
	 * @return string
4450
	 * @since 1.18
4451
	 */
4452
	public function formatExpiry( $expiry, $format = true, $infinity = 'infinity' ) {
4453
		static $dbInfinity;
4454
		if ( $dbInfinity === null ) {
4455
			$dbInfinity = wfGetDB( DB_SLAVE )->getInfinity();
4456
		}
4457
4458
		if ( $expiry == '' || $expiry === 'infinity' || $expiry == $dbInfinity ) {
4459
			return $format === true
4460
				? $this->getMessageFromDB( 'infiniteblock' )
4461
				: $infinity;
4462
		} else {
4463
			return $format === true
4464
				? $this->timeanddate( $expiry, /* User preference timezone */ true )
4465
				: wfTimestamp( $format, $expiry );
4466
		}
4467
	}
4468
4469
	/**
4470
	 * @todo Document
4471
	 * @param int|float $seconds
4472
	 * @param array $format Optional
4473
	 *   If $format['avoid'] === 'avoidseconds': don't mention seconds if $seconds >= 1 hour.
4474
	 *   If $format['avoid'] === 'avoidminutes': don't mention seconds/minutes if $seconds > 48 hours.
4475
	 *   If $format['noabbrevs'] is true: use 'seconds' and friends instead of 'seconds-abbrev'
4476
	 *     and friends.
4477
	 *   For backwards compatibility, $format may also be one of the strings 'avoidseconds'
4478
	 *     or 'avoidminutes'.
4479
	 * @return string
4480
	 */
4481
	function formatTimePeriod( $seconds, $format = [] ) {
4482
		if ( !is_array( $format ) ) {
4483
			$format = [ 'avoid' => $format ]; // For backwards compatibility
4484
		}
4485
		if ( !isset( $format['avoid'] ) ) {
4486
			$format['avoid'] = false;
4487
		}
4488
		if ( !isset( $format['noabbrevs'] ) ) {
4489
			$format['noabbrevs'] = false;
4490
		}
4491
		$secondsMsg = wfMessage(
4492
			$format['noabbrevs'] ? 'seconds' : 'seconds-abbrev' )->inLanguage( $this );
4493
		$minutesMsg = wfMessage(
4494
			$format['noabbrevs'] ? 'minutes' : 'minutes-abbrev' )->inLanguage( $this );
4495
		$hoursMsg = wfMessage(
4496
			$format['noabbrevs'] ? 'hours' : 'hours-abbrev' )->inLanguage( $this );
4497
		$daysMsg = wfMessage(
4498
			$format['noabbrevs'] ? 'days' : 'days-abbrev' )->inLanguage( $this );
4499
4500
		if ( round( $seconds * 10 ) < 100 ) {
4501
			$s = $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) );
4502
			$s = $secondsMsg->params( $s )->text();
4503
		} elseif ( round( $seconds ) < 60 ) {
4504
			$s = $this->formatNum( round( $seconds ) );
4505
			$s = $secondsMsg->params( $s )->text();
4506
		} elseif ( round( $seconds ) < 3600 ) {
4507
			$minutes = floor( $seconds / 60 );
4508
			$secondsPart = round( fmod( $seconds, 60 ) );
4509
			if ( $secondsPart == 60 ) {
4510
				$secondsPart = 0;
4511
				$minutes++;
4512
			}
4513
			$s = $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4514
			$s .= ' ';
4515
			$s .= $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4516
		} elseif ( round( $seconds ) <= 2 * 86400 ) {
4517
			$hours = floor( $seconds / 3600 );
4518
			$minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
4519
			$secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
4520
			if ( $secondsPart == 60 ) {
4521
				$secondsPart = 0;
4522
				$minutes++;
4523
			}
4524
			if ( $minutes == 60 ) {
4525
				$minutes = 0;
4526
				$hours++;
4527
			}
4528
			$s = $hoursMsg->params( $this->formatNum( $hours ) )->text();
4529
			$s .= ' ';
4530
			$s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4531
			if ( !in_array( $format['avoid'], [ 'avoidseconds', 'avoidminutes' ] ) ) {
4532
				$s .= ' ' . $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4533
			}
4534
		} else {
4535
			$days = floor( $seconds / 86400 );
4536
			if ( $format['avoid'] === 'avoidminutes' ) {
4537
				$hours = round( ( $seconds - $days * 86400 ) / 3600 );
4538
				if ( $hours == 24 ) {
4539
					$hours = 0;
4540
					$days++;
4541
				}
4542
				$s = $daysMsg->params( $this->formatNum( $days ) )->text();
4543
				$s .= ' ';
4544
				$s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4545
			} elseif ( $format['avoid'] === 'avoidseconds' ) {
4546
				$hours = floor( ( $seconds - $days * 86400 ) / 3600 );
4547
				$minutes = round( ( $seconds - $days * 86400 - $hours * 3600 ) / 60 );
4548
				if ( $minutes == 60 ) {
4549
					$minutes = 0;
4550
					$hours++;
4551
				}
4552
				if ( $hours == 24 ) {
4553
					$hours = 0;
4554
					$days++;
4555
				}
4556
				$s = $daysMsg->params( $this->formatNum( $days ) )->text();
4557
				$s .= ' ';
4558
				$s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4559
				$s .= ' ';
4560
				$s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4561
			} else {
4562
				$s = $daysMsg->params( $this->formatNum( $days ) )->text();
4563
				$s .= ' ';
4564
				$s .= $this->formatTimePeriod( $seconds - $days * 86400, $format );
4565
			}
4566
		}
4567
		return $s;
4568
	}
4569
4570
	/**
4571
	 * Format a bitrate for output, using an appropriate
4572
	 * unit (bps, kbps, Mbps, Gbps, Tbps, Pbps, Ebps, Zbps or Ybps) according to
4573
	 *   the magnitude in question.
4574
	 *
4575
	 * This use base 1000. For base 1024 use formatSize(), for another base
4576
	 * see formatComputingNumbers().
4577
	 *
4578
	 * @param int $bps
4579
	 * @return string
4580
	 */
4581
	function formatBitrate( $bps ) {
4582
		return $this->formatComputingNumbers( $bps, 1000, "bitrate-$1bits" );
4583
	}
4584
4585
	/**
4586
	 * @param int $size Size of the unit
4587
	 * @param int $boundary Size boundary (1000, or 1024 in most cases)
4588
	 * @param string $messageKey Message key to be uesd
4589
	 * @return string
4590
	 */
4591
	function formatComputingNumbers( $size, $boundary, $messageKey ) {
4592
		if ( $size <= 0 ) {
4593
			return str_replace( '$1', $this->formatNum( $size ),
4594
				$this->getMessageFromDB( str_replace( '$1', '', $messageKey ) )
4595
			);
4596
		}
4597
		$sizes = [ '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zeta', 'yotta' ];
4598
		$index = 0;
4599
4600
		$maxIndex = count( $sizes ) - 1;
4601
		while ( $size >= $boundary && $index < $maxIndex ) {
4602
			$index++;
4603
			$size /= $boundary;
4604
		}
4605
4606
		// For small sizes no decimal places necessary
4607
		$round = 0;
4608
		if ( $index > 1 ) {
4609
			// For MB and bigger two decimal places are smarter
4610
			$round = 2;
4611
		}
4612
		$msg = str_replace( '$1', $sizes[$index], $messageKey );
4613
4614
		$size = round( $size, $round );
4615
		$text = $this->getMessageFromDB( $msg );
4616
		return str_replace( '$1', $this->formatNum( $size ), $text );
4617
	}
4618
4619
	/**
4620
	 * Format a size in bytes for output, using an appropriate
4621
	 * unit (B, KB, MB, GB, TB, PB, EB, ZB or YB) according to the magnitude in question
4622
	 *
4623
	 * This method use base 1024. For base 1000 use formatBitrate(), for
4624
	 * another base see formatComputingNumbers()
4625
	 *
4626
	 * @param int $size Size to format
4627
	 * @return string Plain text (not HTML)
4628
	 */
4629
	function formatSize( $size ) {
4630
		return $this->formatComputingNumbers( $size, 1024, "size-$1bytes" );
4631
	}
4632
4633
	/**
4634
	 * Make a list item, used by various special pages
4635
	 *
4636
	 * @param string $page Page link
4637
	 * @param string $details HTML safe text between brackets
4638
	 * @param bool $oppositedm Add the direction mark opposite to your
4639
	 *   language, to display text properly
4640
	 * @return HTML escaped string
4641
	 */
4642
	function specialList( $page, $details, $oppositedm = true ) {
4643
		if ( !$details ) {
4644
			return $page;
4645
		}
4646
4647
		$dirmark = ( $oppositedm ? $this->getDirMark( true ) : '' ) . $this->getDirMark();
4648
		return
4649
			$page .
4650
			$dirmark .
4651
			$this->msg( 'word-separator' )->escaped() .
4652
			$this->msg( 'parentheses' )->rawParams( $details )->escaped();
4653
	}
4654
4655
	/**
4656
	 * Generate (prev x| next x) (20|50|100...) type links for paging
4657
	 *
4658
	 * @param Title $title Title object to link
4659
	 * @param int $offset
4660
	 * @param int $limit
4661
	 * @param array $query Optional URL query parameter string
4662
	 * @param bool $atend Optional param for specified if this is the last page
4663
	 * @return string
4664
	 */
4665
	public function viewPrevNext( Title $title, $offset, $limit,
4666
		array $query = [], $atend = false
4667
	) {
4668
		// @todo FIXME: Why on earth this needs one message for the text and another one for tooltip?
4669
4670
		# Make 'previous' link
4671
		$prev = wfMessage( 'prevn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4672 View Code Duplication
		if ( $offset > 0 ) {
4673
			$plink = $this->numLink( $title, max( $offset - $limit, 0 ), $limit,
4674
				$query, $prev, 'prevn-title', 'mw-prevlink' );
4675
		} else {
4676
			$plink = htmlspecialchars( $prev );
4677
		}
4678
4679
		# Make 'next' link
4680
		$next = wfMessage( 'nextn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4681 View Code Duplication
		if ( $atend ) {
4682
			$nlink = htmlspecialchars( $next );
4683
		} else {
4684
			$nlink = $this->numLink( $title, $offset + $limit, $limit,
4685
				$query, $next, 'nextn-title', 'mw-nextlink' );
4686
		}
4687
4688
		# Make links to set number of items per page
4689
		$numLinks = [];
4690
		foreach ( [ 20, 50, 100, 250, 500 ] as $num ) {
4691
			$numLinks[] = $this->numLink( $title, $offset, $num,
4692
				$query, $this->formatNum( $num ), 'shown-title', 'mw-numlink' );
4693
		}
4694
4695
		return wfMessage( 'viewprevnext' )->inLanguage( $this )->title( $title
4696
			)->rawParams( $plink, $nlink, $this->pipeList( $numLinks ) )->escaped();
4697
	}
4698
4699
	/**
4700
	 * Helper function for viewPrevNext() that generates links
4701
	 *
4702
	 * @param Title $title Title object to link
4703
	 * @param int $offset
4704
	 * @param int $limit
4705
	 * @param array $query Extra query parameters
4706
	 * @param string $link Text to use for the link; will be escaped
4707
	 * @param string $tooltipMsg Name of the message to use as tooltip
4708
	 * @param string $class Value of the "class" attribute of the link
4709
	 * @return string HTML fragment
4710
	 */
4711
	private function numLink( Title $title, $offset, $limit, array $query, $link,
4712
		$tooltipMsg, $class
4713
	) {
4714
		$query = [ 'limit' => $limit, 'offset' => $offset ] + $query;
4715
		$tooltip = wfMessage( $tooltipMsg )->inLanguage( $this )->title( $title )
4716
			->numParams( $limit )->text();
4717
4718
		return Html::element( 'a', [ 'href' => $title->getLocalURL( $query ),
4719
			'title' => $tooltip, 'class' => $class ], $link );
4720
	}
4721
4722
	/**
4723
	 * Get the conversion rule title, if any.
4724
	 *
4725
	 * @return string
4726
	 */
4727
	public function getConvRuleTitle() {
4728
		return $this->mConverter->getConvRuleTitle();
4729
	}
4730
4731
	/**
4732
	 * Get the compiled plural rules for the language
4733
	 * @since 1.20
4734
	 * @return array Associative array with plural form, and plural rule as key-value pairs
4735
	 */
4736 View Code Duplication
	public function getCompiledPluralRules() {
4737
		$pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'compiledPluralRules' );
4738
		$fallbacks = Language::getFallbacksFor( $this->mCode );
4739
		if ( !$pluralRules ) {
4740
			foreach ( $fallbacks as $fallbackCode ) {
4741
				$pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' );
4742
				if ( $pluralRules ) {
4743
					break;
4744
				}
4745
			}
4746
		}
4747
		return $pluralRules;
4748
	}
4749
4750
	/**
4751
	 * Get the plural rules for the language
4752
	 * @since 1.20
4753
	 * @return array Associative array with plural form number and plural rule as key-value pairs
4754
	 */
4755 View Code Duplication
	public function getPluralRules() {
4756
		$pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRules' );
4757
		$fallbacks = Language::getFallbacksFor( $this->mCode );
4758
		if ( !$pluralRules ) {
4759
			foreach ( $fallbacks as $fallbackCode ) {
4760
				$pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRules' );
4761
				if ( $pluralRules ) {
4762
					break;
4763
				}
4764
			}
4765
		}
4766
		return $pluralRules;
4767
	}
4768
4769
	/**
4770
	 * Get the plural rule types for the language
4771
	 * @since 1.22
4772
	 * @return array Associative array with plural form number and plural rule type as key-value pairs
4773
	 */
4774 View Code Duplication
	public function getPluralRuleTypes() {
4775
		$pluralRuleTypes = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRuleTypes' );
4776
		$fallbacks = Language::getFallbacksFor( $this->mCode );
4777
		if ( !$pluralRuleTypes ) {
4778
			foreach ( $fallbacks as $fallbackCode ) {
4779
				$pluralRuleTypes = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRuleTypes' );
4780
				if ( $pluralRuleTypes ) {
4781
					break;
4782
				}
4783
			}
4784
		}
4785
		return $pluralRuleTypes;
4786
	}
4787
4788
	/**
4789
	 * Find the index number of the plural rule appropriate for the given number
4790
	 * @param int $number
4791
	 * @return int The index number of the plural rule
4792
	 */
4793
	public function getPluralRuleIndexNumber( $number ) {
4794
		$pluralRules = $this->getCompiledPluralRules();
4795
		$form = Evaluator::evaluateCompiled( $number, $pluralRules );
4796
		return $form;
4797
	}
4798
4799
	/**
4800
	 * Find the plural rule type appropriate for the given number
4801
	 * For example, if the language is set to Arabic, getPluralType(5) should
4802
	 * return 'few'.
4803
	 * @since 1.22
4804
	 * @param int $number
4805
	 * @return string The name of the plural rule type, e.g. one, two, few, many
4806
	 */
4807
	public function getPluralRuleType( $number ) {
4808
		$index = $this->getPluralRuleIndexNumber( $number );
4809
		$pluralRuleTypes = $this->getPluralRuleTypes();
4810
		if ( isset( $pluralRuleTypes[$index] ) ) {
4811
			return $pluralRuleTypes[$index];
4812
		} else {
4813
			return 'other';
4814
		}
4815
	}
4816
}
4817