MediaWiki master
Language.php
Go to the documentation of this file.
1<?php
18namespace MediaWiki\Language;
19
20use CLDRPluralRuleParser\Evaluator;
21use DateTime;
22use DateTimeImmutable;
23use DateTimeInterface;
24use DateTimeZone;
25use InvalidArgumentException;
26use Locale;
46use NumberFormatter;
47use RuntimeException;
48use UtfNormal\Validator as UtfNormalValidator;
49use Wikimedia\Bcp47Code\Bcp47Code;
50use Wikimedia\DebugInfo\DebugInfoTrait;
55use Wikimedia\Timestamp\ConvertibleTimestamp;
56use Wikimedia\Timestamp\TimestampFormat as TS;
57
65class Language implements Bcp47Code {
66 use DebugInfoTrait;
67
68 public string $mCode;
69
73 public $mMagicExtensions = [];
74
76 private $mHtmlCode = null;
77
83 public $dateFormatStrings = [];
84
91
93 protected $namespaceNames;
95 protected $mNamespaceIds;
98
103 private $transformData = [];
104
109 private $grammarTransformCache;
110
112 private HookRunner $hookRunner;
113
117 private $overrideUcfirstCharacters;
118
123 private $numberFormatter = null;
124
128 public const WEEKDAY_MESSAGES = [
129 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
130 'friday', 'saturday'
131 ];
132
137 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
138 ];
139
143 public const MONTH_MESSAGES = [
144 'january', 'february', 'march', 'april', 'may_long', 'june',
145 'july', 'august', 'september', 'october', 'november',
146 'december'
147 ];
148
153
157 public const MONTH_GENITIVE_MESSAGES = [
158 'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
159 'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
160 'december-gen'
161 ];
162
167 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
168 'sep', 'oct', 'nov', 'dec'
169 ];
170
175
180 'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
181 'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
182 'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
183 'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
184 ];
185
190 'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
191 'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
192 'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
193 'hebrew-calendar-m10', 'hebrew-calendar-m11', 'hebrew-calendar-m12',
194 'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
195 ];
196
201 'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
202 'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
203 'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
204 'hebrew-calendar-m10-gen', 'hebrew-calendar-m11-gen', 'hebrew-calendar-m12-gen',
205 'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
206 ];
207
212 'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
213 'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
214 'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
215 'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12'
216 ];
217
221 protected const DURATION_INTERVALS = [
222 'millennia' => 1000 * 31_556_952,
223 'centuries' => 100 * 31_556_952,
224 'decades' => 10 * 31_556_952,
225 // The average year is 365.2425 days (365 + (24 * 3 + 25) / 400)
226 'years' => 31_556_952, // 365.2425 * 24 * 3600
227 // To simplify, we consider a month to be 1/12 of a year
228 'months' => 365.2425 * 24 * 3600 / 12,
229 'days' => 24 * 3600,
230 'hours' => 3600,
231 'minutes' => 60,
232 'seconds' => 1,
233 ];
234
241
243 private const DEADLINE_DATE_SPEC_BY_UNIT = [
244 'Y' => 'first day of January next year midnight',
245 'M' => 'first day of next month midnight',
246 'W' => 'monday next week midnight',
247 'D' => 'next day midnight',
248 // Note that this may be before the current time, in which case
249 // we advance by a calendar year; the ISOY is actually 1 week past
250 // this, since dec 28 is always in "the last week of the ISO year".
251 'ISOY' => 'december 28 midnight',
252 // Note that this may be before the current time, in which case 'D'
253 // is used.
254 'AM' => 'today noon',
255 // Note that this relative datetime specifier does not zero out
256 // minutes/seconds, but we will do so manually in
257 // ::computeUnitTimestampDeadline() when given the unit 'H'/'m'/'s'
258 'H' => 'next hour',
259 'm' => 'next minute',
260 's' => 'next second',
261 ];
262
266 private const LRM = "\u{200E}"; // U+200E LEFT-TO-RIGHT MARK
267 private const RLM = "\u{200F}"; // U+200F RIGHT-TO-LEFT MARK
268 private const LRE = "\u{202A}"; // U+202A LEFT-TO-RIGHT EMBEDDING
269 private const RLE = "\u{202B}"; // U+202B RIGHT-TO-LEFT EMBEDDING
270 private const PDF = "\u{202C}"; // U+202C POP DIRECTIONAL FORMATTING
271 // https://en.wikipedia.org/wiki/Arabic_letter_mark (Unicode 6.3.0)
272 private const ALM = "\u{061C}"; // U+061C ARABIC LETTER MARK
273
286 // @codeCoverageIgnoreStart
287 // phpcs:ignore Generic.Files.LineLength,MediaWiki.Commenting.PropertyDocumentation.MissingDocumentationPrivate
288 private static $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';
289 // @codeCoverageIgnoreEnd
290
294 public function __construct(
295 string $code,
297 private readonly NamespaceInfo $namespaceInfo,
299 private LocalisationCache $localisationCache,
301 private readonly LanguageNameUtils $langNameUtils,
303 private readonly LanguageFallback $langFallback,
305 private readonly LanguageConverterFactory $converterFactory,
307 private readonly HookContainer $hookContainer,
309 private readonly Config $config,
311 private readonly LeximorphFactory $leximorphFactory,
312 ) {
313 $this->mCode = $code;
314 $this->hookRunner = new HookRunner( $hookContainer );
315 }
316
321 public function getFallbackLanguages() {
322 return $this->langFallback->getAll( $this->mCode );
323 }
324
329 public function getBookstoreList() {
330 return $this->localisationCache->getItem( $this->mCode, 'bookstoreList' );
331 }
332
340 public function getNamespaces() {
341 if ( $this->namespaceNames === null ) {
342 $metaNamespace = $this->config->get( MainConfigNames::MetaNamespace );
343 $metaNamespaceTalk = $this->config->get( MainConfigNames::MetaNamespaceTalk );
344 $extraNamespaces = $this->config->get( MainConfigNames::ExtraNamespaces );
345 $validNamespaces = $this->namespaceInfo->getCanonicalNamespaces();
346
347 // @phan-suppress-next-line PhanTypeMismatchProperty
348 $this->namespaceNames = $extraNamespaces +
349 $this->localisationCache->getItem( $this->mCode, 'namespaceNames' );
350 // @phan-suppress-next-line PhanTypeInvalidLeftOperand
351 $this->namespaceNames += $validNamespaces;
352
353 $this->namespaceNames[NS_PROJECT] = $metaNamespace;
354 if ( $metaNamespaceTalk ) {
355 $this->namespaceNames[NS_PROJECT_TALK] = $metaNamespaceTalk;
356 } else {
357 $talk = $this->namespaceNames[NS_PROJECT_TALK];
358 $this->namespaceNames[NS_PROJECT_TALK] =
359 $this->fixVariableInNamespace( $talk );
360 }
361
362 # Sometimes a language will be localised but not actually exist on this wiki.
363 foreach ( $this->namespaceNames as $key => $text ) {
364 if ( !isset( $validNamespaces[$key] ) ) {
365 unset( $this->namespaceNames[$key] );
366 }
367 }
368
369 # The above mixing may leave namespaces out of canonical order.
370 # Re-order by namespace ID number...
371 ksort( $this->namespaceNames );
372
373 $this->getHookRunner()->onLanguageGetNamespaces( $this->namespaceNames );
374 }
375
377 }
378
383 public function setNamespaces( array $namespaces ) {
384 $this->namespaceNames = $namespaces;
385 $this->mNamespaceIds = null;
386 }
387
392 public function resetNamespaces() {
393 $this->namespaceNames = null;
394 $this->mNamespaceIds = null;
395 $this->namespaceAliases = null;
396 }
397
407 public function getFormattedNamespaces() {
408 $ns = $this->getNamespaces();
409 foreach ( $ns as $k => $v ) {
410 $ns[$k] = strtr( $v, '_', ' ' );
411 }
412 return $ns;
413 }
414
428 public function getNsText( $index ) {
429 $ns = $this->getNamespaces();
430 return $ns[$index] ?? false;
431 }
432
446 public function getFormattedNsText( $index ) {
447 $ns = $this->getNsText( $index );
448 return $ns === false ? '' : strtr( $ns, '_', ' ' );
449 }
450
459 public function getGenderNsText( $index, $gender ) {
460 $extraGenderNamespaces = $this->config->get( MainConfigNames::ExtraGenderNamespaces );
461
462 $ns = $extraGenderNamespaces +
463 (array)$this->localisationCache->getItem( $this->mCode, 'namespaceGenderAliases' );
464
465 return $ns[$index][$gender] ?? $this->getNsText( $index );
466 }
467
474 public function needsGenderDistinction() {
475 $extraGenderNamespaces = $this->config->get( MainConfigNames::ExtraGenderNamespaces );
476 $extraNamespaces = $this->config->get( MainConfigNames::ExtraNamespaces );
477 if ( count( $extraGenderNamespaces ) > 0 ) {
478 // $wgExtraGenderNamespaces overrides everything
479 return true;
480 } elseif ( isset( $extraNamespaces[NS_USER] ) && isset( $extraNamespaces[NS_USER_TALK] ) ) {
481 // @todo There may be other gender namespace than NS_USER & NS_USER_TALK in the future
482 // $wgExtraNamespaces overrides any gender aliases specified in i18n files
483 return false;
484 } else {
485 // Check what is in i18n files
486 $aliases = (array)$this->localisationCache->getItem( $this->mCode, 'namespaceGenderAliases' );
487 return count( $aliases ) > 0;
488 }
489 }
490
499 public function getLocalNsIndex( $text ) {
500 $lctext = $this->lc( $text );
501 $ids = $this->getNamespaceIds();
502 return $ids[$lctext] ?? false;
503 }
504
509 public function getNamespaceAliases() {
510 if ( $this->namespaceAliases === null ) {
511 $aliases = $this->localisationCache->getItem( $this->mCode, 'namespaceAliases' );
512 if ( !$aliases ) {
513 $aliases = [];
514 } else {
515 foreach ( $aliases as $name => $index ) {
516 if ( $index === NS_PROJECT_TALK ) {
517 unset( $aliases[$name] );
518 $name = $this->fixVariableInNamespace( $name );
519 $aliases[$name] = $index;
520 }
521 }
522 }
523
524 $extraGenderNamespaces = $this->config->get( MainConfigNames::ExtraGenderNamespaces );
525 $genders = $extraGenderNamespaces + (array)$this->localisationCache
526 ->getItem( $this->mCode, 'namespaceGenderAliases' );
527 foreach ( $genders as $index => $forms ) {
528 foreach ( $forms as $alias ) {
529 $aliases[$alias] = $index;
530 }
531 }
532
533 $langConverter = $this->getConverterInternal();
534 # Also add converted namespace names as aliases, to avoid confusion.
535 $convertedNames = [];
536 foreach ( $langConverter->getVariants() as $variant ) {
537 if ( $variant === $this->mCode ) {
538 continue;
539 }
540 foreach ( $this->getNamespaces() as $ns => $_ ) {
541 $convertedNames[$langConverter->convertNamespace( $ns, $variant )] = $ns;
542 }
543 }
544
545 $this->namespaceAliases = $aliases + $convertedNames;
546
547 // In the case of conflicts between $wgNamespaceAliases and other sources
548 // of aliasing, $wgNamespaceAliases wins.
549 $this->namespaceAliases = $this->config->get( MainConfigNames::NamespaceAliases ) +
551
552 # Filter out aliases to namespaces that don't exist, e.g. from extensions
553 # that aren't loaded here but are included in the l10n cache.
554 # (array_intersect preserves keys from its first argument)
555 $this->namespaceAliases = array_intersect(
556 $this->namespaceAliases,
557 array_keys( $this->getNamespaces() )
558 );
559 }
560
562 }
563
567 public function getNamespaceIds() {
568 if ( $this->mNamespaceIds === null ) {
569 # Put namespace names and aliases into a hashtable.
570 # If this is too slow, then we should arrange it so that it is done
571 # before caching. The catch is that at pre-cache time, the above
572 # class-specific fixup hasn't been done.
573 $this->mNamespaceIds = [];
574 foreach ( $this->getNamespaces() as $index => $name ) {
575 $this->mNamespaceIds[$this->lc( $name )] = $index;
576 }
577 foreach ( $this->getNamespaceAliases() as $name => $index ) {
578 $this->mNamespaceIds[$this->lc( $name )] = $index;
579 }
580 }
582 }
583
591 public function getNsIndex( $text ) {
592 $lctext = $this->lc( $text );
593 $ns = $this->namespaceInfo->getCanonicalIndex( $lctext );
594 if ( $ns !== null ) {
595 return $ns;
596 }
597 $ids = $this->getNamespaceIds();
598 return $ids[$lctext] ?? false;
599 }
600
608 public function getVariantname( $code, $usemsg = true ) {
609 if ( $usemsg ) {
610 $msg = $this->msg( "variantname-$code" );
611 if ( $msg->exists() ) {
612 return $msg->text();
613 }
614 }
615 $name = $this->langNameUtils->getLanguageName( $code );
616 if ( $name ) {
617 return $name; # if it's defined as a language name, show that
618 } else {
619 # otherwise, output the language code
620 return $code;
621 }
622 }
623
627 public function getDatePreferences() {
628 return $this->localisationCache->getItem( $this->mCode, 'datePreferences' );
629 }
630
634 public function getDateFormats() {
635 return $this->localisationCache->getItem( $this->mCode, 'dateFormats' );
636 }
637
641 public function getDefaultDateFormat() {
642 $df = $this->localisationCache->getItem( $this->mCode, 'defaultDateFormat' );
643 if ( $df === 'dmy or mdy' ) {
644 return $this->config->get( MainConfigNames::AmericanDates ) ? 'mdy' : 'dmy';
645 } else {
646 return $df;
647 }
648 }
649
653 public function getDatePreferenceMigrationMap() {
654 return $this->localisationCache->getItem( $this->mCode, 'datePreferenceMigrationMap' );
655 }
656
663 public function getMessageFromDB( $msg ) {
664 return $this->msg( $msg )->text();
665 }
666
676 protected function msg( $msg, ...$params ) {
677 return wfMessage( $msg, ...$params )->inLanguage( $this );
678 }
679
684 public function getMonthName( $key ) {
685 return $this->getMessageFromDB( self::MONTH_MESSAGES[$key - 1] );
686 }
687
691 public function getMonthNamesArray() {
692 $monthNames = [ '' ];
693 for ( $i = 1; $i <= 12; $i++ ) {
694 $monthNames[] = $this->getMonthName( $i );
695 }
696 return $monthNames;
697 }
698
703 public function getMonthNameGen( $key ) {
704 return $this->getMessageFromDB( self::MONTH_GENITIVE_MESSAGES[$key - 1] );
705 }
706
711 public function getMonthAbbreviation( $key ) {
712 return $this->getMessageFromDB( self::MONTH_ABBREVIATED_MESSAGES[$key - 1] );
713 }
714
718 public function getMonthAbbreviationsArray() {
719 $monthNames = [ '' ];
720 for ( $i = 1; $i <= 12; $i++ ) {
721 $monthNames[] = $this->getMonthAbbreviation( $i );
722 }
723 return $monthNames;
724 }
725
730 public function getWeekdayName( $key ) {
731 return $this->getMessageFromDB( self::WEEKDAY_MESSAGES[$key - 1] );
732 }
733
738 public function getWeekdayAbbreviation( $key ) {
739 return $this->getMessageFromDB( self::WEEKDAY_ABBREVIATED_MESSAGES[$key - 1] );
740 }
741
750 private static function dateTimeObj( &$dateTimeObj, $ts, $zone ): DateTime {
751 if ( !$dateTimeObj ) {
752 $dateTimeObj = DateTime::createFromFormat(
753 'YmdHis', $ts, $zone ?: new DateTimeZone( 'UTC' )
754 );
755 }
756 return $dateTimeObj;
757 }
758
768 private static function dateTimeObjFormat( &$dateTimeObj, $ts, $zone, $code ) {
769 return self::dateTimeObj( $dateTimeObj, $ts, $zone )->format( $code );
770 }
771
841 public function sprintfDate( $format, $ts, ?DateTimeZone $zone = null, &$ttl = 'unused' ) {
842 // @phan-suppress-previous-line PhanTypeMismatchDefault Type mismatch on pass-by-ref args
843 $s = '';
844 $raw = false;
845 $roman = false;
846 $hebrewNum = false;
847 $dateTimeObj = false;
848 $rawToggle = false;
849 $iranian = false;
850 $hebrew = false;
851 $hijri = false;
852 $thai = false;
853 $minguo = false;
854 $tenno = false;
855
856 $usedSecond = false;
857 $usedMinute = false;
858 $usedHour = false;
859 $usedAMPM = false;
860 $usedDay = false;
861 $usedWeek = false;
862 $usedMonth = false;
863 $usedYear = false;
864 $usedISOYear = false;
865 $usedIsLeapYear = false;
866
867 $usedHebrewMonth = false;
868 $usedIranianMonth = false;
869 $usedHijriMonth = false;
870 $usedHebrewYear = false;
871 $usedIranianYear = false;
872 $usedHijriYear = false;
873 $usedTennoYear = false;
874
875 if ( strlen( $ts ) !== 14 ) {
876 throw new InvalidArgumentException( __METHOD__ . ": The timestamp $ts should have 14 characters" );
877 }
878
879 if ( !ctype_digit( $ts ) ) {
880 throw new InvalidArgumentException( __METHOD__ . ": The timestamp $ts should be a number" );
881 }
882
883 $formatLength = strlen( $format );
884 for ( $p = 0; $p < $formatLength; $p++ ) {
885 $num = false;
886 $code = $format[$p];
887 if ( $code == 'x' && $p < $formatLength - 1 ) {
888 $code .= $format[++$p];
889 }
890
891 if ( ( $code === 'xi'
892 || $code === 'xj'
893 || $code === 'xk'
894 || $code === 'xm'
895 || $code === 'xo'
896 || $code === 'xt' )
897 && $p < $formatLength - 1
898 ) {
899 $code .= $format[++$p];
900 }
901
902 switch ( $code ) {
903 case 'xx':
904 $s .= 'x';
905 break;
906
907 case 'xn':
908 $raw = true;
909 break;
910
911 case 'xN':
912 $rawToggle = !$rawToggle;
913 break;
914
915 case 'xr':
916 $roman = true;
917 break;
918
919 case 'xh':
920 $hebrewNum = true;
921 break;
922
923 case 'xg':
924 $usedMonth = true;
925 $s .= $this->getMonthNameGen( (int)substr( $ts, 4, 2 ) );
926 break;
927
928 case 'xjx':
929 $usedHebrewMonth = true;
930 if ( !$hebrew ) {
931 $hebrew = self::tsToHebrew( $ts );
932 }
933 $s .= $this->getMessageFromDB( self::HEBREW_CALENDAR_MONTH_GENITIVE_MESSAGES[$hebrew[1] - 1] );
934 break;
935
936 case 'd':
937 $usedDay = true;
938 $num = substr( $ts, 6, 2 );
939 break;
940
941 case 'D':
942 $usedDay = true;
943 $s .= $this->getWeekdayAbbreviation(
944 (int)self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1
945 );
946 break;
947
948 case 'j':
949 $usedDay = true;
950 $num = intval( substr( $ts, 6, 2 ) );
951 break;
952
953 case 'xij':
954 $usedDay = true;
955 if ( !$iranian ) {
956 $iranian = self::tsToIranian( $ts );
957 }
958 $num = $iranian[2];
959 break;
960
961 case 'xmj':
962 $usedDay = true;
963 if ( !$hijri ) {
964 $hijri = self::tsToHijri( $ts );
965 }
966 $num = $hijri[2];
967 break;
968
969 case 'xjj':
970 $usedDay = true;
971 if ( !$hebrew ) {
972 $hebrew = self::tsToHebrew( $ts );
973 }
974 $num = $hebrew[2];
975 break;
976
977 case 'l':
978 $usedDay = true;
979 $s .= $this->getWeekdayName(
980 (int)self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1
981 );
982 break;
983
984 case 'F':
985 $usedMonth = true;
986 $s .= $this->getMonthName( (int)substr( $ts, 4, 2 ) );
987 break;
988
989 case 'xiF':
990 $usedIranianMonth = true;
991 if ( !$iranian ) {
992 $iranian = self::tsToIranian( $ts );
993 }
994 $s .= $this->getMessageFromDB( self::IRANIAN_CALENDAR_MONTHS_MESSAGES[$iranian[1] - 1] );
995 break;
996
997 case 'xmF':
998 $usedHijriMonth = true;
999 if ( !$hijri ) {
1000 $hijri = self::tsToHijri( $ts );
1001 }
1002 $s .= $this->getMessageFromDB( self::HIJRI_CALENDAR_MONTH_MESSAGES[$hijri[1] - 1] );
1003 break;
1004
1005 case 'xjF':
1006 $usedHebrewMonth = true;
1007 if ( !$hebrew ) {
1008 $hebrew = self::tsToHebrew( $ts );
1009 }
1010 $s .= $this->getMessageFromDB( self::HEBREW_CALENDAR_MONTHS_MESSAGES[$hebrew[1] - 1] );
1011 break;
1012
1013 case 'm':
1014 $usedMonth = true;
1015 $num = substr( $ts, 4, 2 );
1016 break;
1017
1018 case 'M':
1019 $usedMonth = true;
1020 $s .= $this->getMonthAbbreviation( (int)substr( $ts, 4, 2 ) );
1021 break;
1022
1023 case 'n':
1024 $usedMonth = true;
1025 $num = intval( substr( $ts, 4, 2 ) );
1026 break;
1027
1028 case 'xin':
1029 $usedIranianMonth = true;
1030 if ( !$iranian ) {
1031 $iranian = self::tsToIranian( $ts );
1032 }
1033 $num = $iranian[1];
1034 break;
1035
1036 case 'xmn':
1037 $usedHijriMonth = true;
1038 if ( !$hijri ) {
1039 $hijri = self::tsToHijri( $ts );
1040 }
1041 $num = $hijri[1];
1042 break;
1043
1044 case 'xjn':
1045 $usedHebrewMonth = true;
1046 if ( !$hebrew ) {
1047 $hebrew = self::tsToHebrew( $ts );
1048 }
1049 $num = $hebrew[1];
1050 break;
1051
1052 case 'xjt':
1053 $usedHebrewMonth = true;
1054 if ( !$hebrew ) {
1055 $hebrew = self::tsToHebrew( $ts );
1056 }
1057 $num = $hebrew[3];
1058 break;
1059
1060 case 'Y':
1061 $usedYear = true;
1062 $num = substr( $ts, 0, 4 );
1063 break;
1064
1065 case 'xiY':
1066 $usedIranianYear = true;
1067 if ( !$iranian ) {
1068 $iranian = self::tsToIranian( $ts );
1069 }
1070 $num = $iranian[0];
1071 break;
1072
1073 case 'xmY':
1074 $usedHijriYear = true;
1075 if ( !$hijri ) {
1076 $hijri = self::tsToHijri( $ts );
1077 }
1078 $num = $hijri[0];
1079 break;
1080
1081 case 'xjY':
1082 $usedHebrewYear = true;
1083 if ( !$hebrew ) {
1084 $hebrew = self::tsToHebrew( $ts );
1085 }
1086 $num = $hebrew[0];
1087 break;
1088
1089 case 'xkY':
1090 $usedYear = true;
1091 if ( !$thai ) {
1092 $thai = self::tsToYear( $ts, 'thai' );
1093 }
1094 $num = $thai[0];
1095 break;
1096
1097 case 'xoY':
1098 $usedYear = true;
1099 if ( !$minguo ) {
1100 $minguo = self::tsToYear( $ts, 'minguo' );
1101 }
1102 $num = $minguo[0];
1103 break;
1104
1105 case 'xtY':
1106 $usedTennoYear = true;
1107 if ( !$tenno ) {
1108 $tenno = self::tsToJapaneseGengo( $ts );
1109 }
1110 $num = $tenno;
1111 break;
1112
1113 case 'y':
1114 $usedYear = true;
1115 $num = substr( $ts, 2, 2 );
1116 break;
1117
1118 case 'xiy':
1119 $usedIranianYear = true;
1120 if ( !$iranian ) {
1121 $iranian = self::tsToIranian( $ts );
1122 }
1123 $num = substr( (string)$iranian[0], -2 );
1124 break;
1125
1126 case 'xit':
1127 $usedIranianYear = true;
1128 if ( !$iranian ) {
1129 $iranian = self::tsToIranian( $ts );
1130 }
1131 $num = self::IRANIAN_DAYS[$iranian[1] - 1];
1132 break;
1133
1134 case 'xiz':
1135 $usedIranianYear = true;
1136 if ( !$iranian ) {
1137 $iranian = self::tsToIranian( $ts );
1138 }
1139 $num = $iranian[3];
1140 break;
1141
1142 case 'a':
1143 $usedAMPM = true;
1144 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'am' : 'pm';
1145 break;
1146
1147 case 'A':
1148 $usedAMPM = true;
1149 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'AM' : 'PM';
1150 break;
1151
1152 case 'g':
1153 $usedHour = true;
1154 $h = (int)substr( $ts, 8, 2 );
1155 $num = $h % 12 ?: 12;
1156 break;
1157
1158 case 'G':
1159 $usedHour = true;
1160 $num = intval( substr( $ts, 8, 2 ) );
1161 break;
1162
1163 case 'h':
1164 $usedHour = true;
1165 $h = (int)substr( $ts, 8, 2 );
1166 $num = sprintf( '%02d', $h % 12 ?: 12 );
1167 break;
1168
1169 case 'H':
1170 $usedHour = true;
1171 $num = substr( $ts, 8, 2 );
1172 break;
1173
1174 case 'i':
1175 $usedMinute = true;
1176 $num = substr( $ts, 10, 2 );
1177 break;
1178
1179 case 's':
1180 $usedSecond = true;
1181 $num = substr( $ts, 12, 2 );
1182 break;
1183
1184 case 'c':
1185 case 'r':
1186 $usedSecond = true;
1187 // fall through
1188 case 'e':
1189 case 'O':
1190 case 'P':
1191 case 'T':
1192 $s .= self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1193 break;
1194
1195 case 'w':
1196 case 'N':
1197 case 'z':
1198 $usedDay = true;
1199 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1200 break;
1201
1202 case 'W':
1203 $usedWeek = true;
1204 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1205 break;
1206
1207 case 't':
1208 $usedMonth = true;
1209 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1210 break;
1211
1212 case 'L':
1213 $usedIsLeapYear = true;
1214 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1215 break;
1216
1217 case 'o':
1218 $usedISOYear = true;
1219 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1220 break;
1221
1222 case 'U':
1223 $usedSecond = true;
1224 // fall through
1225 case 'I':
1226 case 'Z':
1227 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1228 break;
1229
1230 case '\\':
1231 # Backslash escaping
1232 if ( $p < $formatLength - 1 ) {
1233 $s .= $format[++$p];
1234 } else {
1235 $s .= '\\';
1236 }
1237 break;
1238
1239 case '"':
1240 # Quoted literal
1241 if ( $p < $formatLength - 1 ) {
1242 $endQuote = strpos( $format, '"', $p + 1 );
1243 if ( $endQuote === false ) {
1244 # No terminating quote, assume literal "
1245 $s .= '"';
1246 } else {
1247 $s .= substr( $format, $p + 1, $endQuote - $p - 1 );
1248 $p = $endQuote;
1249 }
1250 } else {
1251 # Quote at the end of the string, assume literal "
1252 $s .= '"';
1253 }
1254 break;
1255
1256 default:
1257 $s .= $format[$p];
1258 }
1259 if ( $num !== false ) {
1260 if ( $rawToggle || $raw ) {
1261 $s .= $num;
1262 $raw = false;
1263 } elseif ( $roman ) {
1264 $s .= self::romanNumeral( $num );
1265 $roman = false;
1266 } elseif ( $hebrewNum ) {
1267 $s .= self::hebrewNumeral( $num );
1268 $hebrewNum = false;
1269 } elseif ( preg_match( '/^[\d.]+$/', $num ) ) {
1270 $s .= $this->formatNumNoSeparators( $num );
1271 } else {
1272 $s .= $num;
1273 }
1274 }
1275 }
1276
1277 if ( $ttl !== 'unused' ) {
1278 // Ensure $dateTimeObj is set.
1279 self::dateTimeObj( $dateTimeObj, $ts, $zone );
1280 }
1281 '@phan-var DateTime $dateTimeObj';
1282 if ( $ttl === 'unused' ) {
1283 // No need to calculate the TTL, the caller won't use it anyway.
1284 } elseif ( $usedSecond ) {
1285 $ttl = self::computeUnitTimestampDeadline( $dateTimeObj, 's' );
1286 } elseif ( $usedMinute ) {
1287 $ttl = self::computeUnitTimestampDeadline( $dateTimeObj, 'm' );
1288 } elseif ( $usedHour ) {
1289 $ttl = self::computeUnitTimestampDeadline( $dateTimeObj, 'H' );
1290 } elseif ( $usedAMPM ) {
1291 $ttl = self::computeUnitTimestampDeadline( $dateTimeObj, 'AM' );
1292 } elseif (
1293 $usedDay ||
1294 $usedHebrewMonth ||
1295 $usedIranianMonth ||
1296 $usedHijriMonth ||
1297 $usedHebrewYear ||
1298 $usedIranianYear ||
1299 $usedHijriYear ||
1300 $usedTennoYear
1301 ) {
1302 // @todo Someone who understands the non-Gregorian calendars
1303 // should write proper logic for them so that they don't need purged every day.
1304 $ttl = self::computeUnitTimestampDeadline( $dateTimeObj, 'D' );
1305 } else {
1306 $possibleTtls = [];
1307 if ( $usedWeek ) {
1308 $possibleTtls[] =
1309 self::computeUnitTimestampDeadline( $dateTimeObj, 'W' );
1310 } elseif ( $usedISOYear ) {
1311 // December 28th falls on the last ISO week of the year, every year.
1312 // The last ISO week of a year can be 52 or 53.
1313 $possibleTtls[] =
1314 self::computeUnitTimestampDeadline( $dateTimeObj, 'ISOY' );
1315 }
1316
1317 if ( $usedMonth ) {
1318 $possibleTtls[] =
1319 self::computeUnitTimestampDeadline( $dateTimeObj, 'M' );
1320 } elseif ( $usedYear ) {
1321 $possibleTtls[] =
1322 self::computeUnitTimestampDeadline( $dateTimeObj, 'Y' );
1323 } elseif ( $usedIsLeapYear ) {
1324 $year = (int)substr( $ts, 0, 4 );
1325 $mod = $year % 4;
1326 $timeRemainingInYear =
1327 self::computeUnitTimestampDeadline( $dateTimeObj, 'Y' );
1328 if ( $mod || ( !( $year % 100 ) && $year % 400 ) ) {
1329 // this isn't a leap year. see when the next one starts
1330 $nextCandidate = $year - $mod + 4;
1331 if ( $nextCandidate % 100 || !( $nextCandidate % 400 ) ) {
1332 $possibleTtls[] = ( $nextCandidate - $year - 1 ) * 365 * 86400 +
1333 $timeRemainingInYear;
1334 } else {
1335 $possibleTtls[] = ( $nextCandidate - $year + 3 ) * 365 * 86400 +
1336 $timeRemainingInYear;
1337 }
1338 } else {
1339 // this is a leap year, so the next year isn't
1340 $possibleTtls[] = $timeRemainingInYear;
1341 }
1342 }
1343
1344 if ( $possibleTtls ) {
1345 $ttl = min( $possibleTtls );
1346 }
1347 }
1348
1349 return $s;
1350 }
1351
1361 public static function computeUnitTimestampDeadline(
1362 DateTimeInterface $date,
1363 string $unit
1364 ): int {
1365 // Ensure we don't mutate our argument
1366 $date = DateTimeImmutable::createFromInterface( $date );
1367 $tsUnix = $date->getTimestamp();
1368
1369 $date = $date->modify( self::DEADLINE_DATE_SPEC_BY_UNIT[$unit] );
1370 if ( $unit === 'AM' ) {
1371 if ( $date->getTimestamp() <= $tsUnix ) {
1372 $date = $date->modify( self::DEADLINE_DATE_SPEC_BY_UNIT['D'] );
1373 }
1374 } elseif ( $unit === 'ISOY' ) {
1375 // December 28th falls on the last ISO week of the year, every year.
1376 // but we could be in the same, next, or previous ISO year as
1377 // December 28 of this calendar year.
1378 // Compute all three and use the first which is in the future.
1379 $lastYear = $date->modify(
1380 'december 28 midnight last year'
1381 )->modify( 'monday next week midnight' );
1382 $nextYear = $date->modify(
1383 'december 28 midnight next year'
1384 )->modify( 'monday next week midnight' );
1385 // Advance to first week of next ISO year
1386 $date = $date->modify( 'monday next week midnight' );
1387 if ( $lastYear->getTimestamp() > $tsUnix ) {
1388 $date = $lastYear;
1389 } elseif ( $date->getTimestamp() <= $tsUnix ) {
1390 $date = $nextYear;
1391 }
1392 } elseif ( $unit === 'H' ) {
1393 // Zero out the minutes/seconds
1394 $date = $date->setTime( intval( $date->format( 'H' ), 10 ), 0, 0 );
1395 } elseif ( $unit === 'm' ) {
1396 // Zero out the seconds
1397 $date = $date->setTime( intval( $date->format( 'H' ), 10 ),
1398 intval( $date->format( 'i' ), 10 ), 0 );
1399 } elseif ( $unit === 's' ) {
1400 // Zero out the fractional seconds
1401 $date = $date->setTime( intval( $date->format( 'H' ), 10 ),
1402 intval( $date->format( 'i' ), 10 ),
1403 intval( $date->format( 's' ), 10 ) );
1404 } else {
1405 $date = $date->setTime( 0, 0, 0 );
1406 }
1407 $deadlineUnix = (int)$date->format( 'U' );
1408 return $deadlineUnix - $tsUnix;
1409 }
1410
1414 private const GREG_DAYS = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
1415
1419 private const IRANIAN_DAYS = [ 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 ];
1420
1433 private static function tsToIranian( $ts ) {
1434 $gy = (int)substr( $ts, 0, 4 ) - 1600;
1435 $gm = (int)substr( $ts, 4, 2 ) - 1;
1436 $gd = (int)substr( $ts, 6, 2 ) - 1;
1437
1438 # Days passed from the beginning (including leap years)
1439 $gDayNo = 365 * $gy
1440 + floor( ( $gy + 3 ) / 4 )
1441 - floor( ( $gy + 99 ) / 100 )
1442 + floor( ( $gy + 399 ) / 400 );
1443
1444 // Add the number of days for the past months of this year
1445 for ( $i = 0; $i < $gm; $i++ ) {
1446 $gDayNo += self::GREG_DAYS[$i];
1447 }
1448
1449 // Leap years
1450 if ( $gm > 1 && ( ( $gy % 4 === 0 && $gy % 100 !== 0 ) || $gy % 400 == 0 ) ) {
1451 $gDayNo++;
1452 }
1453
1454 // Days passed in the current month
1455 $gDayNo += $gd;
1456
1457 $jDayNo = $gDayNo - 79;
1458
1459 $jNp = (int)floor( $jDayNo / 12053 );
1460 $jDayNo %= 12053;
1461
1462 $jy = 979 + 33 * $jNp + 4 * (int)floor( $jDayNo / 1461 );
1463 $jDayNo %= 1461;
1464
1465 if ( $jDayNo >= 366 ) {
1466 $jy += (int)floor( ( $jDayNo - 1 ) / 365 );
1467 $jDayNo = (int)floor( ( $jDayNo - 1 ) % 365 );
1468 }
1469
1470 $jz = $jDayNo;
1471
1472 for ( $i = 0; $i < 11 && $jDayNo >= self::IRANIAN_DAYS[$i]; $i++ ) {
1473 $jDayNo -= self::IRANIAN_DAYS[$i];
1474 }
1475
1476 $jm = $i + 1;
1477 $jd = $jDayNo + 1;
1478
1479 return [ $jy, $jm, $jd, $jz ];
1480 }
1481
1493 private static function tsToHijri( $ts ) {
1494 $year = (int)substr( $ts, 0, 4 );
1495 $month = (int)substr( $ts, 4, 2 );
1496 $day = (int)substr( $ts, 6, 2 );
1497
1498 $zyr = $year;
1499 $zd = $day;
1500 $zm = $month;
1501 $zy = $zyr;
1502
1503 if (
1504 ( $zy > 1582 ) || ( ( $zy == 1582 ) && ( $zm > 10 ) ) ||
1505 ( ( $zy == 1582 ) && ( $zm == 10 ) && ( $zd > 14 ) )
1506 ) {
1507 $zjd = (int)( ( 1461 * ( $zy + 4800 + (int)( ( $zm - 14 ) / 12 ) ) ) / 4 ) +
1508 (int)( ( 367 * ( $zm - 2 - 12 * ( (int)( ( $zm - 14 ) / 12 ) ) ) ) / 12 ) -
1509 (int)( ( 3 * (int)( ( $zy + 4900 + (int)( ( $zm - 14 ) / 12 ) ) / 100 ) ) / 4 ) +
1510 $zd - 32075;
1511 } else {
1512 $zjd = 367 * $zy - (int)( ( 7 * ( $zy + 5001 + (int)( ( $zm - 9 ) / 7 ) ) ) / 4 ) +
1513 (int)( ( 275 * $zm ) / 9 ) + $zd + 1729777;
1514 }
1515
1516 $zl = $zjd - 1948440 + 10632;
1517 $zn = (int)( ( $zl - 1 ) / 10631 );
1518 $zl = $zl - 10631 * $zn + 354;
1519 $zj = ( (int)( ( 10985 - $zl ) / 5316 ) ) * ( (int)( ( 50 * $zl ) / 17719 ) ) +
1520 ( (int)( $zl / 5670 ) ) * ( (int)( ( 43 * $zl ) / 15238 ) );
1521 $zl = $zl - ( (int)( ( 30 - $zj ) / 15 ) ) * ( (int)( ( 17719 * $zj ) / 50 ) ) -
1522 ( (int)( $zj / 16 ) ) * ( (int)( ( 15238 * $zj ) / 43 ) ) + 29;
1523 $zm = (int)( ( 24 * $zl ) / 709 );
1524 $zd = $zl - (int)( ( 709 * $zm ) / 24 );
1525 $zy = 30 * $zn + $zj - 30;
1526
1527 return [ $zy, $zm, $zd ];
1528 }
1529
1545 private static function tsToHebrew( $ts ) {
1546 # Parse date
1547 $year = (int)substr( $ts, 0, 4 );
1548 $month = (int)substr( $ts, 4, 2 );
1549 $day = (int)substr( $ts, 6, 2 );
1550
1551 # Calculate Hebrew year
1552 $hebrewYear = $year + 3760;
1553
1554 # Month number when September = 1, August = 12
1555 $month += 4;
1556 if ( $month > 12 ) {
1557 # Next year
1558 $month -= 12;
1559 $year++;
1560 $hebrewYear++;
1561 }
1562
1563 # Calculate day of year from 1 September
1564 $dayOfYear = $day;
1565 for ( $i = 1; $i < $month; $i++ ) {
1566 if ( $i == 6 ) {
1567 # February
1568 $dayOfYear += 28;
1569 # Check if the year is a leap year
1570 if ( $year % 400 == 0 || ( $year % 4 == 0 && $year % 100 > 0 ) ) {
1571 $dayOfYear++;
1572 }
1573 } elseif ( $i == 8 || $i == 10 || $i == 1 || $i == 3 ) {
1574 $dayOfYear += 30;
1575 } else {
1576 $dayOfYear += 31;
1577 }
1578 }
1579
1580 # Calculate the start of the Hebrew year
1581 $start = self::hebrewYearStart( $hebrewYear );
1582
1583 # Calculate next year's start
1584 if ( $dayOfYear <= $start ) {
1585 # Day is before the start of the year - it is the previous year
1586 # Next year's start
1587 $nextStart = $start;
1588 # Previous year
1589 $year--;
1590 $hebrewYear--;
1591 # Add days since the previous year's 1 September
1592 $dayOfYear += 365;
1593 if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1594 # Leap year
1595 $dayOfYear++;
1596 }
1597 # Start of the new (previous) year
1598 $start = self::hebrewYearStart( $hebrewYear );
1599 } else {
1600 # Next year's start
1601 $nextStart = self::hebrewYearStart( $hebrewYear + 1 );
1602 }
1603
1604 # Calculate Hebrew day of year
1605 $hebrewDayOfYear = $dayOfYear - $start;
1606
1607 # Difference between year's days
1608 $diff = $nextStart - $start;
1609 # Add 12 (or 13 for leap years) days to ignore the difference between
1610 # Hebrew and Gregorian year (353 at least vs. 365/6) - now the
1611 # difference is only about the year type
1612 if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1613 $diff += 13;
1614 } else {
1615 $diff += 12;
1616 }
1617
1618 # Check the year pattern, and is leap year
1619 # 0 means an incomplete year, 1 means a regular year, 2 means a complete year
1620 # This is mod 30, to work on both leap years (which add 30 days of Adar I)
1621 # and non-leap years
1622 $yearPattern = $diff % 30;
1623 # Check if leap year
1624 $isLeap = $diff >= 30;
1625
1626 # Calculate day in the month from number of day in the Hebrew year
1627 # Don't check Adar - if the day is not in Adar, we will stop before;
1628 # if it is in Adar, we will use it to check if it is Adar I or Adar II
1629 $hebrewDay = $hebrewDayOfYear;
1630 $hebrewMonth = 1;
1631 $days = 0;
1632 while ( $hebrewMonth <= 12 ) {
1633 # Calculate days in this month
1634 if ( $isLeap && $hebrewMonth == 6 ) {
1635 # Leap year - has Adar I, with 30 days, and Adar II, with 29 days
1636 $days = 30;
1637 if ( $hebrewDay <= $days ) {
1638 # Day in Adar I
1639 $hebrewMonth = 13;
1640 } else {
1641 # Subtract the days of Adar I
1642 $hebrewDay -= $days;
1643 # Try Adar II
1644 $days = 29;
1645 if ( $hebrewDay <= $days ) {
1646 # Day in Adar II
1647 $hebrewMonth = 14;
1648 }
1649 }
1650 } elseif ( $hebrewMonth == 2 && $yearPattern == 2 ) {
1651 # Cheshvan in a complete year (otherwise as the rule below)
1652 $days = 30;
1653 } elseif ( $hebrewMonth == 3 && $yearPattern == 0 ) {
1654 # Kislev in an incomplete year (otherwise as the rule below)
1655 $days = 29;
1656 } else {
1657 # Odd months have 30 days, even have 29
1658 $days = 30 - ( $hebrewMonth - 1 ) % 2;
1659 }
1660 if ( $hebrewDay <= $days ) {
1661 # In the current month
1662 break;
1663 } else {
1664 # Subtract the days of the current month
1665 $hebrewDay -= $days;
1666 # Try in the next month
1667 $hebrewMonth++;
1668 }
1669 }
1670
1671 return [ $hebrewYear, $hebrewMonth, $hebrewDay, $days ];
1672 }
1673
1683 private static function hebrewYearStart( $year ) {
1684 $a = ( 12 * ( $year - 1 ) + 17 ) % 19;
1685 $b = ( $year - 1 ) % 4;
1686 $m = 32.044093161144 + 1.5542417966212 * $a + $b / 4.0 - 0.0031777940220923 * ( $year - 1 );
1687 if ( $m < 0 ) {
1688 $m--;
1689 }
1690 $Mar = intval( $m );
1691 if ( $m < 0 ) {
1692 $m++;
1693 }
1694 $m -= $Mar;
1695
1696 $c = ( $Mar + 3 * ( $year - 1 ) + 5 * $b + 5 ) % 7;
1697 if ( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
1698 $Mar++;
1699 } elseif ( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
1700 $Mar += 2;
1701 } elseif ( $c == 2 || $c == 4 || $c == 6 ) {
1702 $Mar++;
1703 }
1704
1705 $Mar += intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
1706 return $Mar;
1707 }
1708
1720 private static function tsToYear( $ts, $cName ) {
1721 $gy = (int)substr( $ts, 0, 4 );
1722 $gm = (int)substr( $ts, 4, 2 );
1723 $gd = (int)substr( $ts, 6, 2 );
1724
1725 if ( $cName === 'thai' ) {
1726 # Thai solar dates
1727 # Add 543 years to the Gregorian calendar
1728 # Months and days are identical
1729 $gy_offset = $gy + 543;
1730 # fix for dates between 1912 and 1941
1731 # https://en.wikipedia.org/?oldid=836596673#New_year
1732 if ( $gy >= 1912 && $gy <= 1940 ) {
1733 if ( $gm <= 3 ) {
1734 $gy_offset--;
1735 }
1736 $gm = ( $gm - 3 ) % 12;
1737 }
1738 } elseif ( $cName === 'minguo' || $cName === 'juche' ) {
1739 # Minguo dates
1740 # Deduct 1911 years from the Gregorian calendar
1741 # Months and days are identical
1742 $gy_offset = $gy - 1911;
1743 } else {
1744 $gy_offset = $gy;
1745 }
1746
1747 return [ $gy_offset, $gm, $gd ];
1748 }
1749
1758 private static function tsToJapaneseGengo( $ts ) {
1759 # Nengō dates up to Meiji period.
1760 # Deduct years from the Gregorian calendar
1761 # depending on the nengo periods
1762 # The months and days are identical
1763 $gy = (int)substr( $ts, 0, 4 );
1764 $ts = (int)$ts;
1765 if ( $ts >= 18730101000000 && $ts < 19120730000000 ) {
1766 # Meiji period; start from meiji 6 (1873) it starts using gregorian year
1767 return self::tsToJapaneseGengoCalculate( $gy, 1868, '明治' );
1768 } elseif ( $ts >= 19120730000000 && $ts < 19261225000000 ) {
1769 # Taishō period
1770 return self::tsToJapaneseGengoCalculate( $gy, 1912, '大正' );
1771 } elseif ( $ts >= 19261225000000 && $ts < 19890108000000 ) {
1772 # Shōwa period
1773 return self::tsToJapaneseGengoCalculate( $gy, 1926, '昭和' );
1774 } elseif ( $ts >= 19890108000000 && $ts < 20190501000000 ) {
1775 # Heisei period
1776 return self::tsToJapaneseGengoCalculate( $gy, 1989, '平成' );
1777 } elseif ( $ts >= 20190501000000 ) {
1778 # Reiwa period
1779 return self::tsToJapaneseGengoCalculate( $gy, 2019, '令和' );
1780 }
1781 return "西暦$gy";
1782 }
1783
1794 private static function tsToJapaneseGengoCalculate( $gy, $startYear, $gengo ) {
1795 $gy_offset = $gy - $startYear + 1;
1796 if ( $gy_offset == 1 ) {
1797 $gy_offset = '元';
1798 }
1799 return "$gengo$gy_offset";
1800 }
1801
1815 private static function strongDirFromContent( $text = '' ) {
1816 if ( !preg_match( self::$strongDirRegex, $text, $matches ) ) {
1817 return null;
1818 }
1819 if ( $matches[1] === '' ) {
1820 return 'rtl';
1821 }
1822 return 'ltr';
1823 }
1824
1832 public static function romanNumeral( $num ) {
1833 static $table = [
1834 [ '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ],
1835 [ '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ],
1836 [ '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ],
1837 [ '', 'M', 'MM', 'MMM', 'MMMM', 'MMMMM', 'MMMMMM', 'MMMMMMM',
1838 'MMMMMMMM', 'MMMMMMMMM', 'MMMMMMMMMM' ]
1839 ];
1840
1841 $num = intval( $num );
1842 if ( $num > 10000 || $num <= 0 ) {
1843 return (string)$num;
1844 }
1845
1846 $s = '';
1847 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1848 if ( $num >= $pow10 ) {
1849 $s .= $table[$i][(int)floor( $num / $pow10 )];
1850 }
1851 $num %= $pow10;
1852 }
1853 return $s;
1854 }
1855
1863 public static function hebrewNumeral( $num ) {
1864 static $table = [
1865 [ '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ],
1866 [ '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ],
1867 [ '',
1868 [ 'ק' ],
1869 [ 'ר' ],
1870 [ 'ש' ],
1871 [ 'ת' ],
1872 [ 'ת', 'ק' ],
1873 [ 'ת', 'ר' ],
1874 [ 'ת', 'ש' ],
1875 [ 'ת', 'ת' ],
1876 [ 'ת', 'ת', 'ק' ],
1877 [ 'ת', 'ת', 'ר' ],
1878 ],
1879 [ '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ]
1880 ];
1881
1882 $num = intval( $num );
1883 if ( $num > 9999 || $num <= 0 ) {
1884 return (string)$num;
1885 }
1886
1887 // Round thousands have special notations
1888 if ( $num === 1000 ) {
1889 return "א' אלף";
1890 } elseif ( $num % 1000 === 0 ) {
1891 return $table[0][$num / 1000] . "' אלפים";
1892 }
1893
1894 $letters = [];
1895
1896 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1897 if ( $num >= $pow10 ) {
1898 if ( $num === 15 || $num === 16 ) {
1899 $letters[] = $table[0][9];
1900 $letters[] = $table[0][$num - 9];
1901 $num = 0;
1902 } else {
1903 $letters = array_merge(
1904 $letters,
1905 (array)$table[$i][intval( $num / $pow10 )]
1906 );
1907
1908 if ( $pow10 === 1000 ) {
1909 $letters[] = "'";
1910 }
1911 }
1912 }
1913
1914 $num %= $pow10;
1915 }
1916
1917 $preTransformLength = count( $letters );
1918 if ( $preTransformLength === 1 ) {
1919 // Add geresh (single quote) to one-letter numbers
1920 $letters[] = "'";
1921 } else {
1922 $lastIndex = $preTransformLength - 1;
1923 $letters[$lastIndex] = strtr(
1924 $letters[$lastIndex],
1925 [ 'כ' => 'ך', 'מ' => 'ם', 'נ' => 'ן', 'פ' => 'ף', 'צ' => 'ץ' ]
1926 );
1927
1928 // Add gershayim (double quote) to multiple-letter numbers,
1929 // but exclude numbers with only one letter after the thousands
1930 // (1001-1009, 1020, 1030, 2001-2009, etc.)
1931 if ( $letters[1] === "'" && $preTransformLength === 3 ) {
1932 $letters[] = "'";
1933 } else {
1934 array_splice( $letters, -1, 0, '"' );
1935 }
1936 }
1937
1938 return implode( $letters );
1939 }
1940
1949 public function userAdjust( $ts, $tz = false ) {
1950 $localTZoffset = $this->config->get( MainConfigNames::LocalTZoffset );
1951 if ( $tz === false ) {
1952 $optionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
1953 $tz = $optionsLookup->getOption(
1954 RequestContext::getMain()->getUser(),
1955 'timecorrection'
1956 );
1957 }
1958
1959 $timeCorrection = new UserTimeCorrection( (string)$tz, null, $localTZoffset );
1960
1961 $tzObj = $timeCorrection->getTimeZone();
1962 if ( $tzObj ) {
1963 $date = new DateTime( $ts, new DateTimeZone( 'UTC' ) );
1964 $date->setTimezone( $tzObj );
1965 return self::makeMediaWikiTimestamp( $ts, $date );
1966 }
1967 $minDiff = $timeCorrection->getTimeOffset();
1968
1969 # No difference? Return the time unchanged
1970 if ( $minDiff === 0 ) {
1971 return $ts;
1972 }
1973
1974 $date = new DateTime( $ts );
1975 $date->modify( "{$minDiff} minutes" );
1976 return self::makeMediaWikiTimestamp( $ts, $date );
1977 }
1978
1997 private function convertDateFormatToJs( $format, $options = [] ) {
1998 // CLDR calendar IDs by format code
1999 $calendars = [
2000 'j' => 'hebrew',
2001 'i' => 'persian', // Iranian
2002 'm' => 'islamic', // Hijri
2003 'k' => 'buddhist', // Thai
2004 'o' => 'roc', // Minguo
2005 't' => 'japanese' // Tenno
2006 ];
2007
2008 $s = '';
2009 $mwOpts = [];
2010 $intlOpts = [
2011 'numberingSystem' => $this->localisationCache
2012 ->getItem( $this->mCode, 'numberingSystem' ) ?? 'latn'
2013 ];
2014 $unsupported = [];
2015 $formatLength = strlen( $format );
2016
2017 for ( $p = 0; $p < $formatLength; $p++ ) {
2018 $code = $format[$p];
2019 if ( $code == 'x' && $p < $formatLength - 1 ) {
2020 $code .= $format[++$p];
2021 }
2022
2023 if ( ( $code === 'xi'
2024 || $code === 'xj'
2025 || $code === 'xk'
2026 || $code === 'xm'
2027 || $code === 'xo'
2028 || $code === 'xt' )
2029 && $p < $formatLength - 1
2030 ) {
2031 $code .= $format[++$p];
2032 }
2033
2034 switch ( $code ) {
2035 case 'xx':
2036 $s .= 'x';
2037 break;
2038
2039 case 'xn':
2040 case 'xN':
2041 // Raw number -- usually safe enough in ISO 8601 styles
2042 // although we don't support switching
2043 $mwOpts['locale'] = 'en';
2044 unset( $intlOpts['numberingSystem'] );
2045 break;
2046
2047 case 'xr':
2048 // Roman numerals
2049 // Multiple numbering systems are usually used in the one format,
2050 // which is unsupported. Also, browsers do not implement it.
2051 $unsupported[] = $code;
2052 break;
2053
2054 case 'xh':
2055 // Hebrew numerals. Browsers do not implement it as of 2025,
2056 // and usually multiple numbering systems are desired in a
2057 // single format.
2058 $unsupported[] = $code;
2059 $intlOpts['numberingSystem'] = 'hebr';
2060 break;
2061
2062 case 'xg':
2063 // Genitive month name
2064 $intlOpts['month'] = 'long';
2065 $s .= '{mwMonthGen}';
2066 break;
2067
2068 case 'xjx':
2069 // Genitive month name in Hebrew calendar
2070 $intlOpts['calendar'] = 'hebrew';
2071 $intlOpts['month'] = 'long';
2072 $s .= '{mwMonthGen}';
2073 break;
2074
2075 case 'd':
2076 $intlOpts['day'] = '2-digit';
2077 $s .= '{day}';
2078 break;
2079
2080 case 'D':
2081 $intlOpts['weekday'] = 'short';
2082 $s .= '{weekday}';
2083 break;
2084
2085 case 'j':
2086 $intlOpts['day'] = 'numeric';
2087 $s .= '{day}';
2088 break;
2089
2090 case 'xij':
2091 case 'xmj':
2092 case 'xjj':
2093 // Day number in special calendar
2094 $intlOpts['day'] = 'numeric';
2095 $intlOpts['calendar'] = $calendars[$code[1]];
2096 $s .= '{day}';
2097 break;
2098
2099 case 'l':
2100 $intlOpts['weekday'] = 'long';
2101 $s .= '{weekday}';
2102 break;
2103
2104 case 'F':
2105 $intlOpts['month'] = 'long';
2106 $s .= '{mwMonth}';
2107 break;
2108
2109 case 'xiF':
2110 case 'xmF':
2111 case 'xjF':
2112 // Full month name in special calendar
2113 $intlOpts['month'] = 'long';
2114 $intlOpts['calendar'] = $calendars[$code[1]];
2115 $s .= '{month}';
2116 break;
2117
2118 case 'm':
2119 $intlOpts['month'] = '2-digit';
2120 $s .= '{month}';
2121 break;
2122
2123 case 'M':
2124 $intlOpts['month'] = 'short';
2125 $s .= '{mwMonthAbbrev}';
2126 break;
2127
2128 case 'n':
2129 $intlOpts['month'] = 'numeric';
2130 $s .= '{month}';
2131 break;
2132
2133 case 'xin':
2134 case 'xmn':
2135 case 'xjn':
2136 // Numeric month in special calendar
2137 $intlOpts['month'] = 'numeric';
2138 $intlOpts['calendar'] = $calendars[$code[1]];
2139 $s .= '{month}';
2140 break;
2141
2142 case 'xjt':
2143 // Number of days in the given Hebrew month -- not supported
2144 $unsupported[] = $code;
2145 break;
2146
2147 case 'Y':
2148 $intlOpts['year'] = 'numeric';
2149 $s .= '{year}';
2150 break;
2151
2152 case 'xiY':
2153 case 'xmY':
2154 case 'xjY':
2155 case 'xkY':
2156 case 'xoY':
2157 // Year number in special calendar
2158 $intlOpts['year'] = 'numeric';
2159 $intlOpts['calendar'] = $calendars[$code[1]];
2160 $s .= '{year}';
2161 break;
2162
2163 case 'xtY':
2164 // Japanese year needs to be prefixed with the era name to
2165 // be consistent with tsToJapaneseGengo()
2166 $intlOpts['era'] = 'short';
2167 $intlOpts['year'] = 'numeric';
2168 $intlOpts['calendar'] = $calendars[$code[1]];
2169 $s .= '{era}{year}';
2170 break;
2171
2172 case 'y':
2173 $intlOpts['year'] = '2-digit';
2174 $s .= '{year}';
2175 break;
2176
2177 case 'xiy':
2178 // Iranian 2-digit year
2179 $intlOpts['calendar'] = 'persian';
2180 $intlOpts['year'] = '2-digit';
2181 $s .= '{year}';
2182 break;
2183
2184 case 'xit':
2185 // Number of days in Iranian month -- not supported
2186 $unsupported[] = $code;
2187 break;
2188
2189 case 'xiz':
2190 // Day of the year -- not supported
2191 $unsupported[] = $code;
2192 break;
2193
2194 case 'a':
2195 case 'A':
2196 // AM/PM
2197 $intlOpts['hour12'] = true;
2198 $s .= '{dayPeriod}';
2199 break;
2200
2201 case 'g':
2202 // Hour in 12-hour clock, without leading zero
2203 $intlOpts['hour'] = 'numeric';
2204 $intlOpts['hour12'] = true;
2205 $s .= '{hour}';
2206 break;
2207
2208 case 'G':
2209 // Hour in 24-hour clock, without leading zero
2210 $intlOpts['hour'] = 'numeric';
2211 $s .= '{hour}';
2212 break;
2213
2214 case 'h':
2215 // Hour in 12-hour clock, with leading zero
2216 $intlOpts['hour'] = '2-digit';
2217 $intlOpts['hour12'] = true;
2218 $s .= '{hour}';
2219 break;
2220
2221 case 'H':
2222 // Hour in 24-hour clock, without leading zero
2223 $intlOpts['hour'] = '2-digit';
2224 $intlOpts['hour12'] = false;
2225 $s .= '{hour}';
2226 break;
2227
2228 case 'i':
2229 $intlOpts['minute'] = '2-digit';
2230 $s .= '{minute}';
2231 break;
2232
2233 case 's':
2234 $intlOpts['second'] = '2-digit';
2235 $s .= '{second}';
2236 break;
2237
2238 case 'c':
2239 // ISO 8601
2240 $mwOpts['locale'] = 'en';
2241 $intlOpts += [
2242 'year' => 'numeric', 'month' => '2-digit', 'day' => '2-digit',
2243 'hour' => '2-digit', 'minute' => '2-digit', 'second' => '2-digit',
2244 'hour12' => false, 'timeZoneName' => 'longOffset'
2245 ];
2246 $s .= '{year}-{month}-{day}T{hour}:{minute}:{second}{timeZoneName}';
2247 break;
2248
2249 case 'r':
2250 // RFC 2822
2251 $mwOpts['locale'] = 'en';
2252 $intlOpts += [
2253 'year' => 'numeric', 'month' => 'short', 'day' => 'numeric',
2254 'hour' => '2-digit', 'minute' => '2-digit', 'second' => '2-digit',
2255 'hour12' => false, 'weekday' => 'short', 'timeZoneName' => 'longOffset',
2256 ];
2257 $s .= '{weekday}, {day} {month} {year} {hour}:{minute}:{second} {timeZoneName}';
2258 break;
2259
2260 case 'e':
2261 // Time zone identifier -- not supported, fall through to offset
2262 case 'O':
2263 // Offset without colon not supported
2264 $unsupported[] = $code;
2265 // Fall through to with colon
2266 case 'P':
2267 $intlOpts['timeZoneName'] = 'longOffset';
2268 $s .= '{timeZoneName}';
2269 break;
2270
2271 case 'T':
2272 // Time zone abbreviation
2273 $intlOpts['timeZoneName'] = 'short';
2274 $s .= '{timeZoneName}';
2275 break;
2276
2277 case 'w':
2278 case 'N':
2279 // Numeric day of week -- not supported
2280 $unsupported[] = $code;
2281 $intlOpts['weekday'] = 'short';
2282 $s .= '{weekday}';
2283 break;
2284
2285 case 'z':
2286 // Day of year
2287 case 'W':
2288 // Week number
2289 case 't':
2290 // Number of days in month
2291 case 'L':
2292 // Leap year flag
2293 case 'o':
2294 // ISO week numbering year
2295 case 'U':
2296 // Seconds since UNIX epoch
2297 case 'I':
2298 // DST flag
2299 case 'Z':
2300 // Timezone offset in seconds
2301 $unsupported[] = $code;
2302 break;
2303
2304 case '\\':
2305 # Backslash escaping
2306 if ( $p < $formatLength - 1 ) {
2307 $s .= $format[++$p];
2308 } else {
2309 $s .= '\\';
2310 }
2311 break;
2312
2313 case '"':
2314 # Quoted literal
2315 if ( $p < $formatLength - 1 ) {
2316 $endQuote = strpos( $format, '"', $p + 1 );
2317 if ( $endQuote === false ) {
2318 # No terminating quote, assume literal "
2319 $s .= '"';
2320 } else {
2321 $s .= substr( $format, $p + 1, $endQuote - $p - 1 );
2322 $p = $endQuote;
2323 }
2324 } else {
2325 # Quote at the end of the string, assume literal "
2326 $s .= '"';
2327 }
2328 break;
2329
2330 default:
2331 $s .= $format[$p];
2332 }
2333 }
2334 $mwOpts['options'] = $intlOpts;
2335 $mwOpts['pattern'] = $s;
2336 if ( isset( $options['includeRange'] ) && $options['includeRange'] ) {
2337 $mwOpts['rangePattern'] = $this->convertJsRangePattern( $s );
2338 }
2339 if ( $unsupported ) {
2340 $mwOpts['error'] = 'Unsupported format code(s): ' .
2341 implode( ', ', $unsupported );
2342 }
2343 return $mwOpts;
2344 }
2345
2353 private function convertJsRangePattern( $pattern ) {
2354 if ( $pattern === '' ) {
2355 return '';
2356 }
2357
2358 $start = preg_replace_callback(
2359 '/\{(\w+)}/',
2360 static fn ( $matches ) => '{' . $matches[1] . '1}',
2361 $pattern
2362 );
2363 $end = preg_replace_callback(
2364 '/\{(\w+)}/',
2365 static fn ( $matches ) => '{' . $matches[1] . '2}',
2366 $pattern
2367 );
2368
2369 return "{$start} – {$end}";
2370 }
2371
2381 private static function makeMediaWikiTimestamp( $fallback, $date ) {
2382 $ts = $date->format( 'YmdHis' );
2383 return strlen( $ts ) === 14 ? $ts : $fallback;
2384 }
2385
2401 public function dateFormat( $usePrefs = true ) {
2402 if ( is_bool( $usePrefs ) ) {
2403 if ( $usePrefs ) {
2404 $datePreference = RequestContext::getMain()
2405 ->getUser()
2406 ->getDatePreference();
2407 } else {
2408 $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
2409 $datePreference = (string)$userOptionsLookup->getDefaultOption( 'date' );
2410 }
2411 } else {
2412 $datePreference = (string)$usePrefs;
2413 }
2414
2415 // return int
2416 if ( $datePreference == '' ) {
2417 return 'default';
2418 }
2419
2420 return $datePreference;
2421 }
2422
2433 public function getDateFormatString( $type, $pref ) {
2434 $wasDefault = false;
2435 if ( $pref == 'default' ) {
2436 $wasDefault = true;
2437 $pref = $this->getDefaultDateFormat();
2438 }
2439
2440 if ( !isset( $this->dateFormatStrings[$type][$pref] ) ) {
2441 $df = $this->localisationCache
2442 ->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2443
2444 if ( $type === 'pretty' && $df === null ) {
2445 $df = $this->getDateFormatString( 'date', $pref );
2446 }
2447
2448 if ( !$wasDefault && $df === null ) {
2449 $pref = $this->getDefaultDateFormat();
2450 $df = $this->localisationCache
2451 ->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2452 }
2453
2454 $this->dateFormatStrings[$type][$pref] = $df;
2455 }
2456 return $this->dateFormatStrings[$type][$pref];
2457 }
2458
2469 public function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
2470 $ts = wfTimestamp( TS::MW, $ts );
2471 if ( $adj ) {
2472 $ts = $this->userAdjust( $ts, $timecorrection );
2473 }
2474 $df = $this->getDateFormatString( 'date', $this->dateFormat( $format ) );
2475 return $this->sprintfDate( $df, $ts );
2476 }
2477
2488 public function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
2489 $ts = wfTimestamp( TS::MW, $ts );
2490 if ( $adj ) {
2491 $ts = $this->userAdjust( $ts, $timecorrection );
2492 }
2493 $df = $this->getDateFormatString( 'time', $this->dateFormat( $format ) );
2494 return $this->sprintfDate( $df, $ts );
2495 }
2496
2508 public function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false ) {
2509 $ts = wfTimestamp( TS::MW, $ts );
2510 if ( $adj ) {
2511 $ts = $this->userAdjust( $ts, $timecorrection );
2512 }
2513 $df = $this->getDateFormatString( 'both', $this->dateFormat( $format ) );
2514 return $this->sprintfDate( $df, $ts );
2515 }
2516
2527 public function formatDuration( $seconds, array $chosenIntervals = [] ) {
2528 $intervals = $this->getDurationIntervals( $seconds, $chosenIntervals );
2529
2530 $segments = [];
2531
2532 foreach ( $intervals as $intervalName => $intervalValue ) {
2533 // Messages: duration-seconds, duration-minutes, duration-hours, duration-days, duration-weeks,
2534 // duration-years, duration-decades, duration-centuries, duration-millennia
2535 $message = $this->msg( 'duration-' . $intervalName )->numParams( $intervalValue );
2536 $segments[] = $message->escaped();
2537 }
2538
2539 return $this->listToText( $segments );
2540 }
2541
2552 int $timestamp1,
2553 int $timestamp2,
2554 ?int $precision = null
2555 ): string {
2556 $precision ??= count( self::DURATION_INTERVALS );
2557
2558 $sortedTimestamps = [ $timestamp1, $timestamp2 ];
2559 sort( $sortedTimestamps );
2560
2561 $tz = new DateTimeZone( 'UTC' );
2562 $date1 = ( new DateTimeImmutable() )
2563 ->setTimezone( $tz )
2564 ->setTimestamp( $sortedTimestamps[0] );
2565 $date2 = ( new DateTimeImmutable() )
2566 ->setTimezone( $tz )
2567 ->setTimestamp( $sortedTimestamps[1] );
2568
2569 $interval = $date1->diff( $date2 );
2570
2571 $format = [];
2572 if ( $interval->y >= 1000 ) {
2573 $millennia = floor( $interval->y / 1000 );
2574 $format[] = $this->msg( 'duration-millennia' )->numParams( $millennia )->text();
2575 $interval->y -= $millennia * 1000;
2576 }
2577 if ( $interval->y >= 100 ) {
2578 $centuries = floor( $interval->y / 100 );
2579 $format[] = $this->msg( 'duration-centuries' )->numParams( $centuries )->text();
2580 $interval->y -= $centuries * 100;
2581 }
2582 if ( $interval->y >= 10 ) {
2583 $decades = floor( $interval->y / 10 );
2584 $format[] = $this->msg( 'duration-decades' )->numParams( $decades )->text();
2585 $interval->y -= $decades * 10;
2586 }
2587 if ( $interval->y !== 0 ) {
2588 $format[] = $this->msg( 'duration-years' )->numParams( $interval->y )->text();
2589 }
2590 if ( $interval->m !== 0 ) {
2591 $format[] = $this->msg( 'duration-months' )->numParams( $interval->m )->text();
2592 }
2593 if ( $interval->d !== 0 ) {
2594 $format[] = $this->msg( 'duration-days' )->numParams( $interval->d )->text();
2595 }
2596 if ( $interval->h !== 0 ) {
2597 $format[] = $this->msg( 'duration-hours' )->numParams( $interval->h )->text();
2598 }
2599 if ( $interval->i !== 0 ) {
2600 $format[] = $this->msg( 'duration-minutes' )->numParams( $interval->i )->text();
2601 }
2602 if ( $interval->s !== 0 ) {
2603 $format[] = $this->msg( 'duration-seconds' )->numParams( $interval->s )->text();
2604 }
2605
2606 // slice the array to the provided precision
2607 $format = array_slice( $format, 0, $precision );
2608 // build the string from the array
2609 $format = $this->listToText( $format );
2610
2611 return $format ?: $this->msg( 'duration-seconds' )->numParams( 0 )->text();
2612 }
2613
2625 public function getDurationIntervals( $seconds, array $chosenIntervals = [] ) {
2626 if ( !$chosenIntervals ) {
2627 // Default intervals. Do not include `months` as they were not part of the original default implementation
2628 $chosenIntervals = [
2629 'millennia',
2630 'centuries',
2631 'decades',
2632 'years',
2633 'days',
2634 'hours',
2635 'minutes',
2636 'seconds'
2637 ];
2638 }
2639
2640 $intervals = array_intersect_key( self::DURATION_INTERVALS,
2641 array_fill_keys( $chosenIntervals, true ) );
2642 $smallestInterval = array_key_last( $intervals );
2643
2644 $segments = [];
2645
2646 foreach ( $intervals as $name => $length ) {
2647 $value = (int)( $seconds / $length );
2648
2649 if ( $value > 0 || ( $name == $smallestInterval && !$segments ) ) {
2650 $seconds -= $value * $length;
2651 $segments[$name] = $value;
2652 }
2653 }
2654
2655 return $segments;
2656 }
2657
2677 private function internalUserTimeAndDate( $type, $ts, UserIdentity $user, array $options ) {
2678 $ts = wfTimestamp( TS::MW, $ts );
2679 $options += [ 'timecorrection' => true, 'format' => true ];
2680 if ( $options['timecorrection'] !== false ) {
2681 if ( $options['timecorrection'] === true ) {
2682 $offset = MediaWikiServices::getInstance()
2683 ->getUserOptionsLookup()
2684 ->getOption( $user, 'timecorrection' );
2685 } else {
2686 $offset = $options['timecorrection'];
2687 }
2688 $ts = $this->userAdjust( $ts, $offset );
2689 }
2690 if ( $options['format'] === true ) {
2691 $format = MediaWikiServices::getInstance()
2692 ->getUserFactory()
2693 ->newFromUserIdentity( $user )
2694 ->getDatePreference();
2695 } else {
2696 $format = $options['format'];
2697 }
2698 $df = $this->getDateFormatString( $type, $this->dateFormat( $format ) );
2699 return $this->sprintfDate( $df, $ts );
2700 }
2701
2721 public function userDate( $ts, UserIdentity $user, array $options = [] ) {
2722 return $this->internalUserTimeAndDate( 'date', $ts, $user, $options );
2723 }
2724
2744 public function userTime( $ts, UserIdentity $user, array $options = [] ) {
2745 return $this->internalUserTimeAndDate( 'time', $ts, $user, $options );
2746 }
2747
2767 public function userTimeAndDate( $ts, UserIdentity $user, array $options = [] ) {
2768 return $this->internalUserTimeAndDate( 'both', $ts, $user, $options );
2769 }
2770
2786 public function getHumanTimestamp(
2787 MWTimestamp $time, ?MWTimestamp $relativeTo = null, ?UserIdentity $user = null
2788 ) {
2789 $relativeTo ??= new MWTimestamp();
2790 if ( $user === null ) {
2791 $user = RequestContext::getMain()->getUser();
2792 } else {
2793 // For compatibility with the hook signature and self::getHumanTimestampInternal
2794 $user = MediaWikiServices::getInstance()
2795 ->getUserFactory()
2796 ->newFromUserIdentity( $user );
2797 }
2798
2799 // Adjust for the user's timezone.
2800 $offsetThis = $time->offsetForUser( $user );
2801 $offsetRel = $relativeTo->offsetForUser( $user );
2802
2803 $ts = '';
2804 if ( $this->getHookRunner()->onGetHumanTimestamp( $ts, $time, $relativeTo, $user, $this ) ) {
2805 $ts = $this->getHumanTimestampInternal( $time, $relativeTo, $user );
2806 }
2807
2808 // Reset the timezone on the objects.
2809 $time->timestamp->sub( $offsetThis );
2810 $relativeTo->timestamp->sub( $offsetRel );
2811
2812 return $ts;
2813 }
2814
2826 private function getHumanTimestampInternal(
2827 MWTimestamp $ts, MWTimestamp $relativeTo, User $user
2828 ) {
2829 $diff = $ts->diff( $relativeTo );
2830 $diffDay = (bool)( (int)$ts->timestamp->format( 'w' ) -
2831 (int)$relativeTo->timestamp->format( 'w' ) );
2832 $days = $diff->days ?: (int)$diffDay;
2833
2834 if ( $diff->invert ) {
2835 // Future dates: Use full timestamp
2839 $format = $this->getDateFormatString( 'both', $user->getDatePreference() ?: 'default' );
2840 $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS::MW ) );
2841 } elseif (
2842 $days > 5 &&
2843 $ts->timestamp->format( 'Y' ) !== $relativeTo->timestamp->format( 'Y' )
2844 ) {
2845 // Timestamps are in different years and more than 5 days apart: use full date
2846 $format = $this->getDateFormatString( 'date', $user->getDatePreference() ?: 'default' );
2847 $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS::MW ) );
2848 } elseif ( $days > 5 ) {
2849 // Timestamps are in same year and more than 5 days ago: show day and month only.
2850 $format = $this->getDateFormatString( 'pretty', $user->getDatePreference() ?: 'default' );
2851 $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS::MW ) );
2852 } elseif ( $days > 1 ) {
2853 // Timestamp within the past 5 days: show the day of the week and time
2854 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2855 $weekday = self::WEEKDAY_MESSAGES[(int)$ts->timestamp->format( 'w' )];
2856 // The following messages are used here:
2857 // * sunday-at, monday-at, tuesday-at, wednesday-at, thursday-at, friday-at, saturday-at
2858 $ts = $this->msg( "$weekday-at" )
2859 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS::MW ) ) )
2860 ->text();
2861 } elseif ( $days == 1 ) {
2862 // Timestamp was yesterday: say 'yesterday' and the time.
2863 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2864 $ts = $this->msg( 'yesterday-at' )
2865 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS::MW ) ) )
2866 ->text();
2867 } elseif ( $diff->h > 1 || ( $diff->h == 1 && $diff->i > 30 ) ) {
2868 // Timestamp was today, but more than 90 minutes ago: say 'today' and the time.
2869 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2870 $ts = $this->msg( 'today-at' )
2871 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS::MW ) ) )
2872 ->text();
2873
2874 // From here on in, the timestamp was soon enough ago so that we can simply say
2875 // XX units ago, e.g., "2 hours ago" or "5 minutes ago"
2876 } elseif ( $diff->h == 1 ) {
2877 // Less than 90 minutes, but more than an hour ago.
2878 $ts = $this->msg( 'hours-ago' )->numParams( 1 )->text();
2879 } elseif ( $diff->i >= 1 ) {
2880 // A few minutes ago.
2881 $ts = $this->msg( 'minutes-ago' )->numParams( $diff->i )->text();
2882 } elseif ( $diff->s >= 30 ) {
2883 // Less than a minute, but more than 30 sec ago.
2884 $ts = $this->msg( 'seconds-ago' )->numParams( $diff->s )->text();
2885 } else {
2886 // Less than 30 seconds ago.
2887 $ts = $this->msg( 'just-now' )->text();
2888 }
2889
2890 return $ts;
2891 }
2892
2901 public function getGroupName( $group ) {
2902 $msg = $this->msg( "group-$group" );
2903 return $msg->isBlank() ? $group : $msg->text();
2904 }
2905
2915 public function getGroupMemberName( string $group, $member ) {
2916 if ( $member instanceof UserIdentity ) {
2917 $member = $member->getName();
2918 }
2919 $msg = $this->msg( "group-$group-member", $member );
2920 return $msg->isBlank() ? $group : $msg->text();
2921 }
2922
2928 public function getMessage( $key ) {
2929 return $this->localisationCache->getSubitem( $this->mCode, 'messages', $key );
2930 }
2931
2936 public function getAllMessages() {
2937 return $this->localisationCache->getItem( $this->mCode, 'messages' );
2938 }
2939
2946 public function iconv( $in, $out, $string ) {
2947 # Even with //IGNORE iconv can whine about illegal characters in
2948 # *input* string. We just ignore those too.
2949 # REF: https://bugs.php.net/bug.php?id=37166
2950 # REF: https://phabricator.wikimedia.org/T18885
2951 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
2952 return @iconv( $in, $out . '//IGNORE', $string );
2953 }
2954
2959 public function ucfirst( $str ) {
2960 // T410920: ord() doesn't like an empty string, so just return early
2961 if ( $str === '' ) {
2962 return '';
2963 }
2964
2965 $octetCode = ord( $str[0] );
2966 // See https://en.wikipedia.org/wiki/ASCII#Printable_characters
2967 if ( $octetCode < 96 ) {
2968 // Assume this is an uppercase/uncased ASCII character
2969 return (string)$str;
2970 } elseif ( $octetCode < 128 ) {
2971 // Assume this is a lowercase/uncased ASCII character
2972 return ucfirst( $str );
2973 }
2974 $first = mb_substr( $str, 0, 1 );
2975 if ( strlen( $first ) === 1 ) {
2976 // Broken UTF-8?
2977 return ucfirst( $str );
2978 }
2979
2980 // Memoize the config table
2981 $overrides = $this->overrideUcfirstCharacters
2982 ??= $this->config->get( MainConfigNames::OverrideUcfirstCharacters );
2983
2984 // Use the config table and fall back to MB_CASE_TITLE
2985 $ucFirst = $overrides[$first] ?? mb_convert_case( $first, MB_CASE_TITLE );
2986 if ( $ucFirst !== $first ) {
2987 return $ucFirst . mb_substr( $str, 1 );
2988 } else {
2989 return $str;
2990 }
2991 }
2992
2998 public function uc( $str, $first = false ) {
2999 if ( $first ) {
3000 return $this->ucfirst( $str );
3001 } else {
3002 return $this->isMultibyte( $str ) ? mb_strtoupper( $str ) : strtoupper( $str );
3003 }
3004 }
3005
3010 public function lcfirst( $str ) {
3011 // T410920: ord() doesn't like an empty string, so just return early
3012 if ( $str === '' ) {
3013 return '';
3014 }
3015
3016 $octetCode = ord( $str[0] );
3017 // See https://en.wikipedia.org/wiki/ASCII#Printable_characters
3018 if ( $octetCode < 96 ) {
3019 // Assume this is an uppercase/uncased ASCII character
3020 return lcfirst( $str );
3021 } elseif ( $octetCode < 128 ) {
3022 // Assume this is a lowercase/uncased ASCII character
3023 return (string)$str;
3024 }
3025
3026 return $this->isMultibyte( $str )
3027 // Assume this is a multibyte character and mb_internal_encoding() is appropriate
3028 ? mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 )
3029 // Assume this is a non-multibyte character and LC_CASE is appropriate
3030 : lcfirst( $str );
3031 }
3032
3038 public function lc( $str, $first = false ) {
3039 if ( $first ) {
3040 return $this->lcfirst( $str );
3041 } else {
3042 return $this->isMultibyte( $str ) ? mb_strtolower( $str ) : strtolower( $str );
3043 }
3044 }
3045
3050 private function isMultibyte( $str ) {
3051 return strlen( $str ) !== mb_strlen( $str );
3052 }
3053
3058 public function ucwords( $str ) {
3059 if ( $this->isMultibyte( $str ) ) {
3060 $str = $this->lc( $str );
3061
3062 // regexp to find the first letter in each word (i.e., after each space)
3063 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
3064
3065 // function to use to capitalize a single char
3066 return preg_replace_callback(
3067 $replaceRegexp,
3068 static function ( $matches ) {
3069 return mb_strtoupper( $matches[0] );
3070 },
3071 $str
3072 );
3073 } else {
3074 return ucwords( strtolower( $str ) );
3075 }
3076 }
3077
3084 public function ucwordbreaks( $str ) {
3085 if ( $this->isMultibyte( $str ) ) {
3086 $str = $this->lc( $str );
3087
3088 // since \b doesn't work for UTF-8, we explicitly define word break chars
3089 $breaks = "[ \-\‍(\‍)\}\{\.,\?!]";
3090
3091 // find the first letter after word break
3092 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|" .
3093 "$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
3094
3095 return preg_replace_callback(
3096 $replaceRegexp,
3097 static function ( $matches ) {
3098 return mb_strtoupper( $matches[0] );
3099 },
3100 $str
3101 );
3102 } else {
3103 return preg_replace_callback(
3104 '/\b([\w\x80-\xff]+)\b/',
3105 function ( $matches ) {
3106 return $this->ucfirst( $matches[1] );
3107 },
3108 $str
3109 );
3110 }
3111 }
3112
3128 public function caseFold( $s ) {
3129 return $this->uc( $s );
3130 }
3131
3136 public function checkTitleEncoding( string $s ) {
3137 if ( StringUtils::isUtf8( $s ) ) {
3138 return $s;
3139 }
3140
3141 return $this->iconv( $this->fallback8bitEncoding(), 'utf-8', $s );
3142 }
3143
3147 public function fallback8bitEncoding() {
3148 return $this->localisationCache->getItem( $this->mCode, 'fallback8bitEncoding' );
3149 }
3150
3159 public function hasWordBreaks() {
3160 return true;
3161 }
3162
3170 public function segmentByWord( $string ) {
3171 return $string;
3172 }
3173
3179 protected function getSearchIndexVariant() {
3180 return null;
3181 }
3182
3194 public function normalizeForSearch( $text ) {
3195 $text = self::convertDoubleWidth( $text );
3196 if ( $this->getSearchIndexVariant() ) {
3197 return $this->getConverterInternal()->autoConvert( $text, $this->getSearchIndexVariant() );
3198 }
3199 return $text;
3200 }
3201
3209 protected static function convertDoubleWidth( $string ) {
3210 static $transTable = null;
3211 $transTable ??= array_combine(
3212 mb_str_split( '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' ),
3213 str_split( '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' )
3214 );
3215
3216 return strtr( $string, $transTable );
3217 }
3218
3224 protected static function insertSpace( $string, $pattern ) {
3225 $string = preg_replace( $pattern, " $1 ", $string );
3226 return preg_replace( '/ +/', ' ', $string );
3227 }
3228
3233 public function convertForSearchResult( $termsArray ) {
3234 # some languages, e.g., Chinese, need to do a conversion
3235 # in order for search results to be displayed correctly
3236 return $termsArray;
3237 }
3238
3245 public function firstChar( $s ) {
3246 $firstChar = mb_substr( $s, 0, 1 );
3247
3248 if ( $firstChar === '' || strlen( $firstChar ) != 3 ) {
3249 return $firstChar;
3250 }
3251
3252 // Break down Hangul syllables to grab the first jamo
3253 $code = mb_ord( $firstChar );
3254 if ( $code < 0xac00 || $code >= 0xd7a4 ) {
3255 return $firstChar;
3256 } elseif ( $code < 0xb098 ) {
3257 return "\u{3131}";
3258 } elseif ( $code < 0xb2e4 ) {
3259 return "\u{3134}";
3260 } elseif ( $code < 0xb77c ) {
3261 return "\u{3137}";
3262 } elseif ( $code < 0xb9c8 ) {
3263 return "\u{3139}";
3264 } elseif ( $code < 0xbc14 ) {
3265 return "\u{3141}";
3266 } elseif ( $code < 0xc0ac ) {
3267 return "\u{3142}";
3268 } elseif ( $code < 0xc544 ) {
3269 return "\u{3145}";
3270 } elseif ( $code < 0xc790 ) {
3271 return "\u{3147}";
3272 } elseif ( $code < 0xcc28 ) {
3273 return "\u{3148}";
3274 } elseif ( $code < 0xce74 ) {
3275 return "\u{314A}";
3276 } elseif ( $code < 0xd0c0 ) {
3277 return "\u{314B}";
3278 } elseif ( $code < 0xd30c ) {
3279 return "\u{314C}";
3280 } elseif ( $code < 0xd558 ) {
3281 return "\u{314D}";
3282 } else {
3283 return "\u{314E}";
3284 }
3285 }
3286
3296 public function normalize( $s ) {
3297 $allUnicodeFixes = $this->config->get( MainConfigNames::AllUnicodeFixes );
3298
3299 $s = UtfNormalValidator::cleanUp( $s );
3300 // Optimization: This is disabled by default to avoid negative performance impact.
3301 if ( $allUnicodeFixes ) {
3302 $s = $this->transformUsingPairFile( NormalizeAr::class, $s );
3303 $s = $this->transformUsingPairFile( NormalizeMl::class, $s );
3304 }
3305
3306 return $s;
3307 }
3308
3320 protected function transformUsingPairFile( string $dataClass, string $input ): string {
3321 if ( !isset( $this->transformData[$dataClass] ) ) {
3322 $this->transformData[$dataClass] = new ReplacementArray( $dataClass::PAIRS );
3323 }
3324
3325 return $this->transformData[$dataClass]->replace( $input );
3326 }
3327
3333 public function isRTL() {
3334 return $this->localisationCache->getItem( $this->mCode, 'rtl' );
3335 }
3336
3341 public function getDir() {
3342 return $this->isRTL() ? 'rtl' : 'ltr';
3343 }
3344
3353 public function alignStart() {
3354 return $this->isRTL() ? 'right' : 'left';
3355 }
3356
3365 public function alignEnd() {
3366 return $this->isRTL() ? 'left' : 'right';
3367 }
3368
3387 public function getDirMarkEntity( $opposite = false ) {
3388 wfDeprecated( __METHOD__, '1.43' );
3389
3390 if ( $opposite ) {
3391 return $this->isRTL() ? '&lrm;' : '&rlm;';
3392 }
3393 return $this->isRTL() ? '&rlm;' : '&lrm;';
3394 }
3395
3411 public function getDirMark( $opposite = false ) {
3412 if ( $opposite ) {
3413 return $this->isRTL() ? self::LRM : self::RLM;
3414 }
3415 return $this->isRTL() ? self::RLM : self::LRM;
3416 }
3417
3425 public function getArrow( $direction = 'forwards' ) {
3426 switch ( $direction ) {
3427 case 'forwards':
3428 return $this->isRTL() ? '←' : '→';
3429 case 'backwards':
3430 return $this->isRTL() ? '→' : '←';
3431 case 'left':
3432 return '←';
3433 case 'right':
3434 return '→';
3435 case 'up':
3436 return '↑';
3437 case 'down':
3438 return '↓';
3439 }
3440 }
3441
3447 public function linkPrefixExtension() {
3448 return $this->localisationCache->getItem( $this->mCode, 'linkPrefixExtension' );
3449 }
3450
3456 public function getMagicWords() {
3457 return $this->localisationCache->getItem( $this->mCode, 'magicWords' );
3458 }
3459
3465 public function getMagic( $mw ) {
3466 $rawEntry = $this->mMagicExtensions[$mw->mId] ??
3467 $this->localisationCache->getSubitem( $this->mCode, 'magicWords', $mw->mId );
3468
3469 if ( !is_array( $rawEntry ) ) {
3470 wfWarn( "\"$rawEntry\" is not a valid magic word for \"$mw->mId\"" );
3471 } else {
3472 $mw->mCaseSensitive = $rawEntry[0];
3473 $mw->mSynonyms = array_slice( $rawEntry, 1 );
3474 }
3475 }
3476
3485 public function getSpecialPageAliases() {
3486 // Cache aliases because it may be slow to load them
3487 $this->mExtendedSpecialPageAliases ??=
3488 $this->localisationCache->getItem( $this->mCode, 'specialPageAliases' );
3489
3490 return $this->mExtendedSpecialPageAliases;
3491 }
3492
3500 public function emphasize( $text ) {
3501 wfDeprecated( __METHOD__, '1.46' );
3502 return "<em>$text</em>";
3503 }
3504
3525 public function formatNum( $number ) {
3526 return $this->formatNumInternal( $number, false );
3527 }
3528
3537 private function formatNumInternal( $number, bool $noSeparators ): string {
3538 // From PHP 8.5, we can't cast NAN to string, and since the method
3539 // accepts float, there's no way to exclude NAN, which is subtype.
3540 // So handle it explicitly.
3541 $number = is_float( $number ) && is_nan( $number ) ? 'NAN' : (string)$number;
3542
3543 if ( $number === '' ) {
3544 return $number;
3545 }
3546 if ( $number === 'NAN' ) {
3547 return $this->msg( 'formatnum-nan' )->text();
3548 }
3549 if ( $number === 'INF' ) {
3550 return "∞";
3551 }
3552 if ( $number === '-INF' ) {
3553 return "\u{2212}∞";
3554 }
3555 if ( !is_numeric( $number ) ) {
3556 # T267587: downgrade this to level:warn while we chase down the long
3557 # trail of callers.
3558 # wfDeprecated( 'Language::formatNum with a non-numeric string', '1.36' );
3559 LoggerFactory::getInstance( 'formatnum' )->warning(
3560 'Language::formatNum with non-numeric string',
3561 [ 'number' => $number ]
3562 );
3563 $validNumberRe = '(-(?=[\d\.]))?(\d+|(?=\.\d))(\.\d*)?([Ee][-+]?\d+)?';
3564 // For backwards-compat, apply formatNum piecewise on the valid
3565 // numbers in the string. Don't split on NAN/INF in this legacy
3566 // case as they are likely to be found embedded inside non-numeric
3567 // text.
3568 return preg_replace_callback( "/{$validNumberRe}/", function ( $m ) use ( $noSeparators ) {
3569 return $this->formatNumInternal( $m[0], $noSeparators );
3570 }, $number );
3571 }
3572
3573 if ( !$noSeparators ) {
3574 $separatorTransformTable = $this->separatorTransformTable();
3575 $fmt = $this->getNumberFormatter();
3576
3577 // minimumGroupingDigits can be used to suppress groupings below a certain value.
3578 // This is used for languages such as Polish, where one would only write the grouping
3579 // separator for values above 9999 - numbers with more than 4 digits.
3580 // NumberFormatter is yet to support minimumGroupingDigits, ICU has it as experimental feature.
3581 // The attribute value is used by adding it to the grouping separator value. If
3582 // the input number has fewer integer digits, the grouping separator is suppressed.
3583 $minimumGroupingDigits = $this->minimumGroupingDigits();
3584 // Minimum length of a number to do digit grouping on.
3585 // http://unicode.org/reports/tr35/tr35-numbers.html#Examples_of_minimumGroupingDigits
3586 $minimumLength = $minimumGroupingDigits + $fmt->getAttribute( NumberFormatter::GROUPING_SIZE );
3587 if ( $minimumGroupingDigits > 1
3588 && !preg_match( '/^\-?\d{' . $minimumLength . '}/', $number )
3589 ) {
3590 // This number does not need commas inserted (even if
3591 // NumberFormatter thinks it does) because it's not long
3592 // enough. We still need to do decimal separator
3593 // transformation, though. For example, 1234.56 becomes 1234,56
3594 // in pl with $minimumGroupingDigits = 2.
3595 $number = strtr( $number, $separatorTransformTable ?: [] );
3596 } elseif ( $number === '-0' ) {
3597 // Special case to ensure we don't lose the minus sign by
3598 // converting to an int.
3599 $number = strtr( $number, $separatorTransformTable ?: [] );
3600 } else {
3601 // NumberFormatter supports separator transformation,
3602 // but it does not know all languages MW
3603 // supports. Example: arq. Also, languages like pl have
3604 // customisation. So manually set it.
3605 $fmt = clone $fmt;
3606
3608 $fmt->setSymbol(
3609 NumberFormatter::DECIMAL_SEPARATOR_SYMBOL,
3610 $separatorTransformTable[ '.' ] ?? '.'
3611 );
3612 $fmt->setSymbol(
3613 NumberFormatter::GROUPING_SEPARATOR_SYMBOL,
3614 $separatorTransformTable[ ',' ] ?? ','
3615 );
3616 }
3617
3618 // Maintain # of digits before and after the decimal point
3619 // (and presence of decimal point)
3620 if ( preg_match( '/^-?(\d*)(\.(\d*))?$/', $number, $m ) ) {
3621 $fmt->setAttribute( NumberFormatter::MIN_INTEGER_DIGITS, strlen( $m[1] ) );
3622 if ( isset( $m[2] ) ) {
3623 $fmt->setAttribute( NumberFormatter::DECIMAL_ALWAYS_SHOWN, 1 );
3624 }
3625 $fmt->setAttribute( NumberFormatter::FRACTION_DIGITS, strlen( $m[3] ?? '' ) );
3626 }
3627 $number = $fmt->format( (float)$number );
3628 }
3629 }
3630
3631 if ( $this->config->get( MainConfigNames::TranslateNumerals ) ) {
3632 // This is often unnecessary: PHP's NumberFormatter will often
3633 // do the digit transform itself (T267614)
3634 $s = $this->digitTransformTable();
3635 if ( $s ) {
3636 $number = strtr( $number, $s );
3637 }
3638 }
3639 # T10327: Make our formatted numbers prettier by using a
3640 # proper Unicode 'minus' character.
3641 $number = strtr( $number, [ '-' => "\u{2212}" ] );
3642
3643 // Remove any LRM or RLM characters generated from NumberFormatter,
3644 // since directionality is handled outside of this context.
3645 // Similarly remove \u61C (ALM) which is added starting PHP 7.3+
3646 return strtr( $number, [
3647 self::LRM => '',
3648 self::RLM => '',
3649 self::ALM => '',
3650 ] );
3651 }
3652
3661 public function formatNumNoSeparators( $number ) {
3662 return $this->formatNumInternal( $number, true );
3663 }
3664
3669 public function parseFormattedNumber( $number ) {
3670 if ( $number === $this->msg( 'formatnum-nan' )->text() ) {
3671 return "NAN";
3672 }
3673 if ( $number === "∞" ) {
3674 return "INF";
3675 }
3676 // Accept either ASCII hyphen-minus or the unicode minus emitted by
3677 // ::formatNum()
3678 $number = strtr( $number, [ "\u{2212}" => '-' ] );
3679 if ( $number === "-∞" ) {
3680 return "-INF";
3681 }
3682 $s = $this->digitTransformTable();
3683 if ( $s ) {
3684 // Eliminate empty array values such as ''. (T66347)
3685 $s = array_filter( $s );
3686 $number = strtr( $number, array_flip( $s ) );
3687 }
3688
3689 $s = $this->separatorTransformTable();
3690 if ( $s ) {
3691 // Eliminate empty array values such as ''. (T66347)
3692 $s = array_filter( $s );
3693 $number = strtr( $number, array_flip( $s ) );
3694 }
3695
3696 return strtr( $number, [ ',' => '' ] );
3697 }
3698
3702 public function digitGroupingPattern() {
3703 return $this->localisationCache->getItem( $this->mCode, 'digitGroupingPattern' );
3704 }
3705
3709 public function digitTransformTable() {
3710 return $this->localisationCache->getItem( $this->mCode, 'digitTransformTable' );
3711 }
3712
3716 public function separatorTransformTable() {
3717 return $this->localisationCache->getItem( $this->mCode, 'separatorTransformTable' );
3718 }
3719
3728 public function minimumGroupingDigits(): int {
3729 return $this->localisationCache->getItem( $this->mCode, 'minimumGroupingDigits' ) ?? 1;
3730 }
3731
3743 public function listToText( array $list ) {
3744 $itemCount = count( $list );
3745 if ( $itemCount < 1 ) {
3746 return '';
3747 }
3748 $text = array_pop( $list );
3749 if ( $itemCount > 1 ) {
3750 $and = $this->msg( 'and' )->escaped();
3751 $space = $this->msg( 'word-separator' )->escaped();
3752 $comma = '';
3753 if ( $itemCount > 2 ) {
3754 $comma = $this->msg( 'comma-separator' )->escaped();
3755 }
3756 $text = implode( $comma, $list ) . $and . $space . $text;
3757 }
3758 // @phan-suppress-next-line PhanTypeMismatchReturnNullable False positive
3759 return $text;
3760 }
3761
3771 public function commaList( array $list ): string {
3772 return implode(
3773 $this->msg( 'comma-separator' )->escaped(),
3774 $list
3775 );
3776 }
3777
3787 public function semicolonList( array $list ): string {
3788 return implode(
3789 $this->msg( 'semicolon-separator' )->escaped(),
3790 $list
3791 );
3792 }
3793
3802 public function pipeList( array $list ): string {
3803 return implode(
3804 $this->msg( 'pipe-separator' )->escaped(),
3805 $list
3806 );
3807 }
3808
3825 public function truncateForDatabase( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3826 return $this->truncateInternal(
3827 $string, $length, $ellipsis, $adjustLength, 'strlen', 'mb_strcut'
3828 );
3829 }
3830
3856 public function truncateForVisual( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3857 // Passing encoding to mb_strlen and mb_substr is optional.
3858 // Encoding defaults to mb_internal_encoding(), which is set to UTF-8 in Setup.php, so
3859 // explicit specification of encoding is skipped.
3860 // Note: Both multibyte methods are callables invoked in truncateInternal.
3861 return $this->truncateInternal(
3862 $string, $length, $ellipsis, $adjustLength, 'mb_strlen', 'mb_substr'
3863 );
3864 }
3865
3882 private function truncateInternal(
3883 $string, $length, $ellipsis, $adjustLength, callable $measureLength, callable $getSubstring
3884 ) {
3885 # Check if there is no need to truncate
3886 if ( $measureLength( $string ) <= abs( $length ) ) {
3887 return $string; // no need to truncate
3888 }
3889
3890 # Use the localized ellipsis character
3891 if ( $ellipsis == '...' ) {
3892 $ellipsis = $this->msg( 'ellipsis' )->text();
3893 }
3894 if ( $length == 0 ) {
3895 return $ellipsis; // convention
3896 }
3897
3898 $stringOriginal = $string;
3899 # If ellipsis length is >= $length then we can't apply $adjustLength
3900 if ( $adjustLength && $measureLength( $ellipsis ) >= abs( $length ) ) {
3901 $string = $ellipsis; // this can be slightly unexpected
3902 # Otherwise, truncate and add ellipsis...
3903 } else {
3904 $ellipsisLength = $adjustLength ? $measureLength( $ellipsis ) : 0;
3905 if ( $length > 0 ) {
3906 $length -= $ellipsisLength;
3907 $string = $getSubstring( $string, 0, $length ); // xyz...
3908 $string = rtrim( $string ) . $ellipsis;
3909 } else {
3910 $length += $ellipsisLength;
3911 $string = $getSubstring( $string, $length ); // ...xyz
3912 $string = $ellipsis . ltrim( $string );
3913 }
3914 }
3915
3916 # Do not truncate if the ellipsis makes the string longer/equal (T24181).
3917 # This check is *not* redundant if $adjustLength, due to the single case where
3918 # LEN($ellipsis) > ABS($limit arg); $stringOriginal could be shorter than $string.
3919 if ( $measureLength( $string ) < $measureLength( $stringOriginal ) ) {
3920 return $string;
3921 } else {
3922 return $stringOriginal;
3923 }
3924 }
3925
3933 protected function removeBadCharLast( $string ) {
3934 if ( $string != '' ) {
3935 $char = ord( substr( $string, -1 ) );
3936 $m = [];
3937 if ( $char >= 0xc0 ) {
3938 # We got the first byte only of a multibyte char; remove it.
3939 $string = substr( $string, 0, -1 );
3940 } elseif ( $char >= 0x80 &&
3941 // Use the /s modifier (PCRE_DOTALL) so (.*) also matches newlines
3942 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
3943 '[\xf0-\xf7][\x80-\xbf]{1,2})$/s', $string, $m )
3944 ) {
3945 # We chopped in the middle of a character; remove it
3946 $string = $m[1];
3947 }
3948 }
3949 return $string;
3950 }
3951
3967 public function truncateHtml( $text, $length, $ellipsis = '...' ) {
3968 # Use the localized ellipsis character
3969 if ( $ellipsis == '...' ) {
3970 $ellipsis = $this->msg( 'ellipsis' )->escaped();
3971 }
3972 # Check if there is clearly no need to truncate
3973 if ( $length <= 0 ) {
3974 return $ellipsis; // no text shown, nothing to format (convention)
3975 } elseif ( strlen( $text ) <= $length ) {
3976 return $text; // string short enough even *with* HTML (short-circuit)
3977 }
3978
3979 $dispLen = 0; // innerHTML length so far
3980 $testingEllipsis = false; // check if ellipses will make the string longer/equal?
3981 $tagType = 0; // 0-open, 1-close
3982 $bracketState = 0; // 1-tag start, 2-tag name, 0-neither
3983 $entityState = 0; // 0-not entity, 1-entity
3984 $tag = $ret = ''; // accumulated tag name, accumulated result string
3985 $openTags = []; // open tag stack
3986 $maybeState = null; // possible truncation state
3987
3988 $textLen = strlen( $text );
3989 $neLength = max( 0, $length - strlen( $ellipsis ) ); // non-ellipsis len if truncated
3990 for ( $pos = 0; true; ++$pos ) {
3991 # Consider truncation once the display length has reached the maximum.
3992 # We check if $dispLen > 0 to grab tags for the $neLength = 0 case.
3993 # Check that we're not in the middle of a bracket/entity...
3994 if ( $dispLen && $dispLen >= $neLength && $bracketState == 0 && !$entityState ) {
3995 if ( !$testingEllipsis ) {
3996 $testingEllipsis = true;
3997 # Save where we are; we will truncate here unless there turn out to
3998 # be so few remaining characters that truncation is not necessary.
3999 if ( !$maybeState ) { // already saved? ($neLength = 0 case)
4000 $maybeState = [ $ret, $openTags ]; // save state
4001 }
4002 } elseif ( $dispLen > $length && $dispLen > strlen( $ellipsis ) ) {
4003 # The string in fact does need truncation, the truncation point was OK.
4004 // @phan-suppress-next-line PhanTypeInvalidExpressionArrayDestructuring
4005 [ $ret, $openTags ] = $maybeState; // reload state
4006 $ret = $this->removeBadCharLast( $ret ); // multi-byte char fix
4007 $ret .= $ellipsis; // add ellipsis
4008 break;
4009 }
4010 }
4011 if ( $pos >= $textLen ) {
4012 break; // extra iteration just for the checks above
4013 }
4014
4015 # Read the next char...
4016 $ch = $text[$pos];
4017 $lastCh = $pos ? $text[$pos - 1] : '';
4018 $ret .= $ch; // add to result string
4019 if ( $ch == '<' ) {
4020 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags ); // for bad HTML
4021 $entityState = 0; // for bad HTML
4022 $bracketState = 1; // tag started (checking for backslash)
4023 } elseif ( $ch == '>' ) {
4024 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags );
4025 $entityState = 0; // for bad HTML
4026 $bracketState = 0; // out of brackets
4027 } elseif ( $bracketState == 1 ) {
4028 if ( $ch == '/' ) {
4029 $tagType = 1; // close tag (e.g. "</span>")
4030 } else {
4031 $tagType = 0; // open tag (e.g. "<span>")
4032 $tag .= $ch;
4033 }
4034 $bracketState = 2; // building tag name
4035 } elseif ( $bracketState == 2 ) {
4036 if ( $ch != ' ' ) {
4037 $tag .= $ch;
4038 } else {
4039 // Name found (e.g. "<a href=..."), add on tag attributes...
4040 $pos += $this->truncate_skip( $ret, $text, "<>", $pos + 1 );
4041 }
4042 } elseif ( $bracketState == 0 ) {
4043 if ( $entityState ) {
4044 if ( $ch == ';' ) {
4045 $entityState = 0;
4046 $dispLen++; // entity is one displayed char
4047 }
4048 } else {
4049 if ( $neLength == 0 && !$maybeState ) {
4050 // Save the state without $ch. We want to *hit* the first
4051 // display char (to get tags) but not *use* it if truncating.
4052 $maybeState = [ substr( $ret, 0, -1 ), $openTags ];
4053 }
4054 if ( $ch == '&' ) {
4055 $entityState = 1; // entity found, (e.g. "&#160;")
4056 } else {
4057 $dispLen++; // this char is displayed
4058 // Add the next $max display text chars after this in one swoop...
4059 $max = ( $testingEllipsis ? $length : $neLength ) - $dispLen;
4060 $skipped = $this->truncate_skip( $ret, $text, "<>&", $pos + 1, $max );
4061 $dispLen += $skipped;
4062 $pos += $skipped;
4063 }
4064 }
4065 }
4066 }
4067 // Close the last tag if left unclosed by bad HTML
4068 $this->truncate_endBracket( $tag, $tagType, $text[$textLen - 1], $openTags );
4069 while ( count( $openTags ) > 0 ) {
4070 $ret .= '</' . array_pop( $openTags ) . '>'; // close open tags
4071 }
4072 return $ret;
4073 }
4074
4086 private function truncate_skip( &$ret, $text, $search, $start, $len = null ) {
4087 if ( $len === null ) {
4088 // -1 means "no limit" for strcspn
4089 $len = -1;
4090 } elseif ( $len < 0 ) {
4091 $len = 0;
4092 }
4093 $skipCount = 0;
4094 if ( $start < strlen( $text ) ) {
4095 $skipCount = strcspn( $text, $search, $start, $len );
4096 $ret .= substr( $text, $start, $skipCount );
4097 }
4098 return $skipCount;
4099 }
4100
4111 private function truncate_endBracket( &$tag, $tagType, $lastCh, &$openTags ) {
4112 $tag = ltrim( $tag );
4113 if ( $tag != '' ) {
4114 if ( $tagType == 0 && $lastCh != '/' ) {
4115 $openTags[] = $tag; // tag opened (didn't close itself)
4116 } elseif ( $tagType == 1 ) {
4117 if ( $openTags && $tag == $openTags[count( $openTags ) - 1] ) {
4118 array_pop( $openTags ); // tag closed
4119 }
4120 }
4121 $tag = '';
4122 }
4123 }
4124
4133 public function convertGrammar( $word, $case ) {
4134 $grammarForms = $this->config->get( MainConfigNames::GrammarForms );
4135 if ( isset( $grammarForms[$this->getCode()][$case][$word] ) ) {
4136 return $grammarForms[$this->getCode()][$case][$word];
4137 }
4138
4139 $manager = $this->leximorphFactory->getManager( $this );
4140 if ( $manager ) {
4141 return $manager->getGrammar()->process( $word, $case, $grammarForms );
4142 }
4143
4144 $grammarTransformations = $this->getGrammarTransformations();
4145
4146 if ( isset( $grammarTransformations[$case] ) ) {
4147 $forms = $grammarTransformations[$case];
4148
4149 // Some names of grammar rules are aliases for other rules.
4150 // In such cases the value is a string rather than object,
4151 // so load the actual rules.
4152 if ( is_string( $forms ) ) {
4153 $forms = $grammarTransformations[$forms];
4154 }
4155
4156 foreach ( $forms as $rule ) {
4157 $form = $rule[0];
4158
4159 if ( $form === '@metadata' ) {
4160 continue;
4161 }
4162
4163 $replacement = $rule[1];
4164
4165 $regex = '/' . addcslashes( $form, '/' ) . '/u';
4166 $patternMatches = preg_match( $regex, $word );
4167
4168 if ( $patternMatches === false ) {
4170 'An error occurred while processing grammar. ' .
4171 "Word: '$word'. Regex: /$form/."
4172 );
4173 } elseif ( $patternMatches === 1 ) {
4174 $word = preg_replace( $regex, $replacement, $word );
4175
4176 break;
4177 }
4178 }
4179 }
4180
4181 return $word;
4182 }
4183
4190 public function getGrammarForms() {
4191 $grammarForms = $this->config->get( MainConfigNames::GrammarForms );
4192 if ( isset( $grammarForms[$this->getCode()] )
4193 && is_array( $grammarForms[$this->getCode()] )
4194 ) {
4195 return $grammarForms[$this->getCode()];
4196 }
4197
4198 return [];
4199 }
4200
4209 public function getGrammarTransformations() {
4210 $provider = $this->leximorphFactory->getProvider( $this );
4211 if ( $provider ) {
4212 return $provider->getGrammarTransformationsProvider()->getTransformations();
4213 }
4214
4215 global $IP;
4216 if ( $this->grammarTransformCache !== null ) {
4217 return $this->grammarTransformCache;
4218 }
4219
4220 $grammarDataFile = $IP . "/languages/data/grammarTransformations/{$this->getCode()}.json";
4221 $this->grammarTransformCache = is_readable( $grammarDataFile )
4222 ? FormatJson::decode( file_get_contents( $grammarDataFile ), true )
4223 : [];
4224
4225 if ( $this->grammarTransformCache === null ) {
4226 throw new RuntimeException( "Invalid grammar data for \"{$this->getCode()}\"." );
4227 }
4228
4229 return $this->grammarTransformCache;
4230 }
4231
4251 public function gender( $gender, $forms ) {
4252 if ( !count( $forms ) ) {
4253 return '';
4254 }
4255
4256 $manager = $this->leximorphFactory->getManager( $this );
4257 if ( $manager ) {
4258 return $manager->getGender()->process( $gender, $forms );
4259 }
4260
4261 $forms = $this->preConvertPlural( $forms, 2 );
4262 if ( $gender === 'male' ) {
4263 return $forms[0];
4264 }
4265 if ( $gender === 'female' ) {
4266 return $forms[1];
4267 }
4268 return $forms[2] ?? $forms[0];
4269 }
4270
4286 public function convertPlural( $count, $forms ) {
4287 $manager = $this->leximorphFactory->getManager( $this );
4288 if ( $manager ) {
4289 return $manager->getPlural()->process( $count, $forms );
4290 }
4291
4292 // Handle explicit n=pluralform cases
4293 $forms = $this->handleExplicitPluralForms( $count, $forms );
4294 if ( is_string( $forms ) ) {
4295 return $forms;
4296 }
4297 if ( !count( $forms ) ) {
4298 return '';
4299 }
4300
4301 $pluralForm = $this->getPluralRuleIndexNumber( $count );
4302 $pluralForm = min( $pluralForm, count( $forms ) - 1 );
4303 return $forms[$pluralForm];
4304 }
4305
4321 protected function handleExplicitPluralForms( $count, array $forms ) {
4322 foreach ( $forms as $index => $form ) {
4323 if ( preg_match( '/\d+=/i', $form ) ) {
4324 $pos = strpos( $form, '=' );
4325 if ( substr( $form, 0, $pos ) === (string)$count ) {
4326 return substr( $form, $pos + 1 );
4327 }
4328 unset( $forms[$index] );
4329 }
4330 }
4331 return array_values( $forms );
4332 }
4333
4342 protected function preConvertPlural( /* Array */ $forms, $count ) {
4343 return array_pad( $forms, $count, end( $forms ) );
4344 }
4345
4356 public function getFormalityIndex(): int {
4357 $provider = $this->leximorphFactory->getProvider( $this );
4358 if ( $provider ) {
4359 return $provider->getFormalityIndexProvider()->getFormalityIndex();
4360 }
4361
4362 return $this->localisationCache->getItem( $this->mCode, 'formalityIndex' ) ?? 0;
4363 }
4364
4387 public function embedBidi( $text = '' ) {
4388 $manager = $this->leximorphFactory->getManager( $this );
4389 if ( $manager ) {
4390 return $manager->getBidi()->process( $text );
4391 }
4392
4393 $dir = self::strongDirFromContent( $text );
4394 if ( $dir === 'ltr' ) {
4395 // Wrap in LEFT-TO-RIGHT EMBEDDING ... POP DIRECTIONAL FORMATTING
4396 return self::LRE . $text . self::PDF;
4397 }
4398 if ( $dir === 'rtl' ) {
4399 // Wrap in RIGHT-TO-LEFT EMBEDDING ... POP DIRECTIONAL FORMATTING
4400 return self::RLE . $text . self::PDF;
4401 }
4402 // No strong directionality: do not wrap
4403 return $text;
4404 }
4405
4415 public function getBlockDurations( bool $includeOther = true ): array {
4416 $msg = $this->msg( 'ipboptions' );
4417 $a = $msg->isDisabled() ? [] : XmlSelect::parseOptionsMessage( $msg->text() );
4418
4419 if ( $a && $includeOther ) {
4420 // If options exist, add other to the end instead of the beginning (which
4421 // is what happens by default).
4422 $a[ $this->msg( 'ipbother' )->text() ] = 'other';
4423 }
4424
4425 return $a;
4426 }
4427
4442 public function translateBlockExpiry( $str, ?UserIdentity $user = null, $now = 0 ) {
4443 $duration = $this->getBlockDurations();
4444 $show = array_search( $str, $duration, true );
4445 if ( $show !== false ) {
4446 return trim( $show );
4447 }
4448
4449 if ( wfIsInfinity( $str ) ) {
4450 foreach ( $duration as $show => $value ) {
4451 if ( wfIsInfinity( $value ) ) {
4452 return trim( $show );
4453 }
4454 }
4455 }
4456
4457 // If all else fails, return a standard duration or timestamp description.
4458 $time = strtotime( $str, $now );
4459 if ( $time === false ) { // Unknown format. Return it as-is in case.
4460 return $str;
4461 } elseif ( $time !== strtotime( $str, $now + 1 ) ) { // It's a relative timestamp.
4462 // The result differs based on current time, so the difference
4463 // is a fixed duration length.
4464 return $this->formatDurationBetweenTimestamps( $time, $now );
4465 } else { // It's an absolute timestamp.
4466 if ( $time === 0 ) {
4467 // wfTimestamp() handles 0 as current time instead of epoch.
4468 $time = '19700101000000';
4469 }
4470
4471 // Return the timestamp as-is if it is in an unknown format to ConvertibleTimestamp (T354663)
4472 $time = ConvertibleTimestamp::convert( TS::MW, $time );
4473 if ( $time === false ) {
4474 return $str;
4475 }
4476
4477 if ( $user ) {
4478 return $this->userTimeAndDate( $time, $user );
4479 }
4480 return $this->timeanddate( $time );
4481 }
4482 }
4483
4491 public function segmentForDiff( $text ) {
4492 return $text;
4493 }
4494
4501 public function unsegmentForDiff( $text ) {
4502 return $text;
4503 }
4504
4511 public function linkTrail() {
4512 return $this->localisationCache->getItem( $this->mCode, 'linkTrail' );
4513 }
4514
4521 public function linkPrefixCharset() {
4522 return $this->localisationCache->getItem( $this->mCode, 'linkPrefixCharset' );
4523 }
4524
4532 public function equals( Language $lang ) {
4533 return $lang === $this || $lang->getCode() === $this->mCode;
4534 }
4535
4542 public function getCode(): string {
4543 return $this->mCode;
4544 }
4545
4556 public function getHtmlCode() {
4557 $this->mHtmlCode ??= LanguageCode::bcp47( $this->getCode() );
4558 return $this->mHtmlCode;
4559 }
4560
4568 public function toBcp47Code(): string {
4569 return $this->getHtmlCode();
4570 }
4571
4579 public function isSameCodeAs( Bcp47Code $other ): bool {
4580 if ( $this === $other ) {
4581 return true;
4582 }
4583 if ( $other instanceof Language ) {
4584 // Compare the mediawiki-internal code
4585 return $this->equals( $other );
4586 }
4587 // Bcp-47 codes are case insensitive.
4588 // See Bcp47CodeValue::isSameCode()
4589 return strcasecmp( $this->toBcp47Code(), $other->toBcp47Code() ) === 0;
4590 }
4591
4600 public static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
4601 $m = null;
4602 preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
4603 preg_quote( $suffix, '/' ) . '/', $filename, $m );
4604 if ( !count( $m ) ) {
4605 return false;
4606 }
4607 return str_replace( '_', '-', strtolower( $m[1] ) );
4608 }
4609
4614 private function fixVariableInNamespace( $talk ) {
4615 if ( !str_contains( $talk, '$1' ) ) {
4616 return $talk;
4617 }
4618
4619 $talk = str_replace( '$1', $this->config->get( MainConfigNames::MetaNamespace ), $talk );
4620
4621 # Allow grammar transformations
4622 # Allowing full message-style parsing would make simple requests
4623 # such as action=raw much more expensive than they need to be.
4624 # This will hopefully cover most cases.
4625 $talk = preg_replace_callback(
4626 '/{{grammar:(.*?)\|(.*?)}}/i',
4627 function ( $m ) {
4628 return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
4629 },
4630 $talk
4631 );
4632 return str_replace( ' ', '_', $talk );
4633 }
4634
4647 public function formatExpiry( $expiry, $format = true, $infinity = 'infinity', $user = null ) {
4648 static $dbInfinity;
4649 $dbInfinity ??= MediaWikiServices::getInstance()->getConnectionProvider()
4650 ->getReplicaDatabase()
4651 ->getInfinity();
4652
4653 if ( $expiry == '' || $expiry === 'infinity' || $expiry == $dbInfinity ) {
4654 return $format === true
4655 ? $this->getMessageFromDB( 'infiniteblock' )
4656 : $infinity;
4657 } else {
4658 if ( $format === true ) {
4659 return $user
4660 ? $this->userTimeAndDate( $expiry, $user )
4661 : $this->timeanddate( $expiry, /* User preference timezone */ true );
4662 }
4663 return wfTimestamp( $format, $expiry );
4664 }
4665 }
4666
4681 public function formatTimePeriod( $seconds, $format = [] ) {
4682 if ( !is_array( $format ) ) {
4683 $format = [ 'avoid' => $format ]; // For backwards compatibility
4684 }
4685 if ( !isset( $format['avoid'] ) ) {
4686 $format['avoid'] = false;
4687 }
4688 if ( !isset( $format['noabbrevs'] ) ) {
4689 $format['noabbrevs'] = false;
4690 }
4691 $secondsMsg = $this->msg( $format['noabbrevs'] ? 'seconds' : 'seconds-abbrev' );
4692 $minutesMsg = $this->msg( $format['noabbrevs'] ? 'minutes' : 'minutes-abbrev' );
4693 $hoursMsg = $this->msg( $format['noabbrevs'] ? 'hours' : 'hours-abbrev' );
4694 $daysMsg = $this->msg( $format['noabbrevs'] ? 'days' : 'days-abbrev' );
4695 $space = $this->msg( 'word-separator' )->text();
4696
4697 if ( round( $seconds * 10 ) < 100 ) {
4698 $s = $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) );
4699 $s = $secondsMsg->params( $s )->text();
4700 } elseif ( round( $seconds ) < 60 ) {
4701 $s = $this->formatNum( round( $seconds ) );
4702 $s = $secondsMsg->params( $s )->text();
4703 } elseif ( round( $seconds ) < 3600 ) {
4704 $minutes = floor( $seconds / 60 );
4705 $secondsPart = round( fmod( $seconds, 60 ) );
4706 if ( $secondsPart == 60 ) {
4707 $secondsPart = 0;
4708 $minutes++;
4709 }
4710 $s = $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4711 $s .= $space;
4712 $s .= $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4713 } elseif ( round( $seconds ) <= 2 * 86400 ) {
4714 $hours = floor( $seconds / 3600 );
4715 $minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
4716 $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
4717 if ( $secondsPart == 60 ) {
4718 $secondsPart = 0;
4719 $minutes++;
4720 }
4721 if ( $minutes == 60 ) {
4722 $minutes = 0;
4723 $hours++;
4724 }
4725 $s = $hoursMsg->params( $this->formatNum( $hours ) )->text();
4726 $s .= $space;
4727 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4728 if ( !in_array( $format['avoid'], [ 'avoidseconds', 'avoidminutes', 'avoidhours' ] ) ) {
4729 $s .= $space . $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4730 }
4731 } else {
4732 $days = floor( $seconds / 86400 );
4733 if ( $format['avoid'] === 'avoidhours' ) {
4734 $hours = round( ( $seconds - $days * 86400 ) / 3600 );
4735 if ( $hours == 24 ) {
4736 $days++;
4737 }
4738 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4739 } elseif ( $format['avoid'] === 'avoidminutes' ) {
4740 $hours = round( ( $seconds - $days * 86400 ) / 3600 );
4741 if ( $hours == 24 ) {
4742 $hours = 0;
4743 $days++;
4744 }
4745 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4746 $s .= $space;
4747 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4748 } elseif ( $format['avoid'] === 'avoidseconds' ) {
4749 $hours = floor( ( $seconds - $days * 86400 ) / 3600 );
4750 $minutes = round( ( $seconds - $days * 86400 - $hours * 3600 ) / 60 );
4751 if ( $minutes == 60 ) {
4752 $minutes = 0;
4753 $hours++;
4754 }
4755 if ( $hours == 24 ) {
4756 $hours = 0;
4757 $days++;
4758 }
4759 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4760 $s .= $space;
4761 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4762 $s .= $space;
4763 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4764 } else {
4765 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4766 $s .= $space;
4767 $s .= $this->formatTimePeriod( $seconds - $days * 86400, $format );
4768 }
4769 }
4770 return $s;
4771 }
4772
4784 public function formatBitrate( $bps ) {
4785 // messages used: bitrate-bits, bitrate-kilobits, bitrate-megabits, bitrate-gigabits, bitrate-terabits,
4786 // bitrate-petabits, bitrate-exabits, bitrate-zettabits, bitrate-yottabits, bitrate-ronnabits,
4787 // bitrate-quettabits
4788 return $this->formatComputingNumbers( $bps, 1000, "bitrate-$1bits" );
4789 }
4790
4797 public function formatComputingNumbers( $size, $boundary, $messageKey ) {
4798 if ( $size <= 0 ) {
4799 return str_replace( '$1', $this->formatNum( $size ),
4800 $this->getMessageFromDB( str_replace( '$1', '', $messageKey ) )
4801 );
4802 }
4803 $sizes = [ '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zetta', 'yotta', 'ronna', 'quetta' ];
4804 $index = 0;
4805
4806 $maxIndex = count( $sizes ) - 1;
4807 while ( $size >= $boundary && $index < $maxIndex ) {
4808 $index++;
4809 $size /= $boundary;
4810 }
4811
4812 // For small sizes no decimal places necessary
4813 $round = 0;
4814 if ( $index > 1 ) {
4815 // For MB and larger units, two decimal places are smarter
4816 $round = 2;
4817 }
4818 $msg = str_replace( '$1', $sizes[$index], $messageKey );
4819
4820 $size = round( $size, $round );
4821 $text = $this->getMessageFromDB( $msg );
4822 return str_replace( '$1', $this->formatNum( $size ), $text );
4823 }
4824
4835 public function formatSize( $size ) {
4836 // messages used: size-bytes, size-kilobytes, size-megabytes, size-gigabytes, size-terabytes,
4837 // size-petabytes, size-exabytes, size-zettabytes, size-yottabytes, size-ronnabytes, size-quettabytes
4838 return $this->formatComputingNumbers( $size, 1024, "size-$1bytes" );
4839 }
4840
4848 public function specialList( $page, $details ) {
4849 if ( !$details ) {
4850 return $page;
4851 }
4852
4853 return Html::rawElement( 'bdi', [ 'dir' => $this->getDir() ], $page ) .
4854 $this->msg( 'word-separator' )->escaped() .
4855 $this->msg( 'parentheses' )->rawParams( $details )->escaped();
4856 }
4857
4858 private function getNumberFormatter(): NumberFormatter {
4859 if ( $this->numberFormatter === null ) {
4860 $digitGroupingPattern = $this->digitGroupingPattern();
4861 $code = $this->getCode();
4862 if ( !( $this->config->get( MainConfigNames::TranslateNumerals )
4863 && $this->langNameUtils->isValidCode( $code ) )
4864 ) {
4865 $code = Locale::getDefault(); // POSIX system default locale
4866 }
4867
4868 $fmt = $this->createNumberFormatter( $code, $digitGroupingPattern );
4869 if ( !$fmt ) {
4870 // Hardcode fallback for invalid languages to 'en', as English is always defined.
4871 $fallbacks = $this->getFallbackLanguages() ?: [ 'en' ];
4872 foreach ( $fallbacks as $fallbackCode ) {
4873 $fmt = $this->createNumberFormatter( $fallbackCode, $digitGroupingPattern );
4874 if ( $fmt ) {
4875 break;
4876 }
4877 }
4878 if ( !$fmt ) {
4879 throw new RuntimeException(
4880 'Could not instance NumberFormatter for ' . $code . ' and all fallbacks'
4881 );
4882 }
4883 }
4884
4885 $this->numberFormatter = $fmt;
4886 }
4887 return $this->numberFormatter;
4888 }
4889
4890 private function createNumberFormatter( string $code, ?string $digitGroupingPattern ): ?NumberFormatter {
4891 try {
4893 return new NumberFormatter(
4894 $code, NumberFormatter::PATTERN_DECIMAL, $digitGroupingPattern
4895 );
4896 }
4897 // @suppress PhanParamTooFew Phan thinks this always requires 3 parameters, that's wrong
4898 return new NumberFormatter( $code, NumberFormatter::DECIMAL );
4899 } catch ( \ValueError ) {
4900 // Value Errors are thrown since php8.4 for invalid locales
4901 return null;
4902 }
4903 }
4904
4911 public function getCompiledPluralRules() {
4912 $provider = $this->leximorphFactory->getProvider( $this );
4913 if ( $provider ) {
4914 return $provider->getPluralProvider()->getCompiledPluralRules();
4915 }
4916
4917 $pluralRules =
4918 $this->localisationCache->getItem( strtolower( $this->mCode ), 'compiledPluralRules' );
4919 if ( !$pluralRules ) {
4920 $fallbacks = $this->getFallbackLanguages();
4921 foreach ( $fallbacks as $fallbackCode ) {
4922 $pluralRules = $this->localisationCache
4923 ->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' );
4924 if ( $pluralRules ) {
4925 break;
4926 }
4927 }
4928 }
4929 return $pluralRules;
4930 }
4931
4938 public function getPluralRules() {
4939 $pluralRules =
4940 $this->localisationCache->getItem( strtolower( $this->mCode ), 'pluralRules' );
4941 if ( !$pluralRules ) {
4942 $fallbacks = $this->getFallbackLanguages();
4943 foreach ( $fallbacks as $fallbackCode ) {
4944 $pluralRules = $this->localisationCache
4945 ->getItem( strtolower( $fallbackCode ), 'pluralRules' );
4946 if ( $pluralRules ) {
4947 break;
4948 }
4949 }
4950 }
4951 return $pluralRules;
4952 }
4953
4960 public function getPluralRuleTypes() {
4961 $provider = $this->leximorphFactory->getProvider( $this );
4962 if ( $provider ) {
4963 return $provider->getPluralProvider()->getPluralRuleTypes();
4964 }
4965
4966 $pluralRuleTypes =
4967 $this->localisationCache->getItem( strtolower( $this->mCode ), 'pluralRuleTypes' );
4968 if ( !$pluralRuleTypes ) {
4969 $fallbacks = $this->getFallbackLanguages();
4970 foreach ( $fallbacks as $fallbackCode ) {
4971 $pluralRuleTypes = $this->localisationCache
4972 ->getItem( strtolower( $fallbackCode ), 'pluralRuleTypes' );
4973 if ( $pluralRuleTypes ) {
4974 break;
4975 }
4976 }
4977 }
4978 return $pluralRuleTypes;
4979 }
4980
4987 public function getPluralRuleIndexNumber( $number ) {
4988 $provider = $this->leximorphFactory->getProvider( $this );
4989 if ( $provider ) {
4990 return $provider->getPluralProvider()->getPluralRuleIndexNumber( $number );
4991 }
4992
4993 $pluralRules = $this->getCompiledPluralRules();
4994 return Evaluator::evaluateCompiled( $number, $pluralRules );
4995 }
4996
5006 public function getPluralRuleType( $number ) {
5007 $provider = $this->leximorphFactory->getProvider( $this );
5008 if ( $provider ) {
5009 return $provider->getPluralProvider()->getPluralRuleType( $number );
5010 }
5011
5012 $index = $this->getPluralRuleIndexNumber( $number );
5013 $pluralRuleTypes = $this->getPluralRuleTypes();
5014 return $pluralRuleTypes[$index] ?? 'other';
5015 }
5016
5023 protected function getConverterInternal() {
5024 return $this->converterFactory->getLanguageConverter( $this );
5025 }
5026
5033 protected function getHookContainer() {
5034 return $this->hookContainer;
5035 }
5036
5045 protected function getHookRunner() {
5046 return $this->hookRunner;
5047 }
5048
5054 public function getJsData() {
5055 return [
5056 'digitTransformTable' => $this->digitTransformTable(),
5057 'separatorTransformTable' => $this->separatorTransformTable(),
5058 'minimumGroupingDigits' => $this->minimumGroupingDigits(),
5059 'formalityIndex' => $this->getFormalityIndex(),
5060 'grammarForms' => $this->getGrammarForms(),
5061 'grammarTransformations' => $this->getGrammarTransformations(),
5062 'pluralRules' => $this->getPluralRules(),
5063 'digitGroupingPattern' => $this->digitGroupingPattern(),
5064 'fallbackLanguages' => $this->getFallbackLanguages(),
5065 'bcp47Map' => LanguageCode::getNonstandardLanguageCodeMapping(),
5066 ];
5067 }
5068
5073 public function getJsDateFormats() {
5074 $jsLcFormats = $this->localisationCache->getItem( $this->mCode, 'jsDateFormats' );
5075 $phpLcFormats = $this->localisationCache->getItem( $this->mCode, 'dateFormats' );
5076
5077 $styles = $this->getDatePreferences() ?: [];
5078 $default = $this->getDefaultDateFormat();
5079 // Always include the default style
5080 if ( !in_array( $default, $styles, true ) ) {
5081 $styles[] = $default;
5082 }
5083 $results = [];
5084 foreach ( $styles as $style ) {
5085 if ( $style === 'default' ) {
5086 // Default is not a real style for our purposes
5087 continue;
5088 }
5089 foreach ( [ 'time', 'date', 'both', 'pretty' ] as $type ) {
5090 $key = "$style $type";
5091 $resolvedType = $type;
5092 $resolvedStyle = $style;
5093 $resolvedKey = $key;
5094
5095 // If the PHP localisation lacks the "pretty" type, fall back to "date"
5096 if ( !isset( $phpLcFormats[$key] ) && $type === 'pretty' ) {
5097 $resolvedType = 'date';
5098 $resolvedKey = "$resolvedStyle $resolvedType";
5099 }
5100
5101 // If $jsDateFormats has an alias, follow it.
5102 // This is used to gracefully remove formats that don't work in the browser.
5103 $alias = $jsLcFormats[$resolvedKey]['alias'] ?? '';
5104 if ( preg_match( '/^(.*) ([^ ]*)$/', $alias, $m ) ) {
5105 $resolvedType = $m[2];
5106 $resolvedStyle = $m[1];
5107 $resolvedKey = "$resolvedStyle $resolvedType";
5108 }
5109
5110 $jsFormat = $this->convertDateFormatToJs(
5111 $this->getDateFormatString( $resolvedType, $resolvedStyle ),
5112 [ 'includeRange' => true ]
5113 );
5114 if ( isset( $jsLcFormats[$resolvedKey] ) ) {
5115 $results[$key] = array_merge_recursive( $jsFormat, $jsLcFormats[$resolvedKey] );
5116 } else {
5117 $results[$key] = $jsFormat;
5118 }
5119 }
5120 }
5121 return $results;
5122 }
5123}
5124
5126class_alias( Language::class, 'Language' );
const NS_USER
Definition Defines.php:53
const NS_PROJECT_TALK
Definition Defines.php:56
const NS_USER_TALK
Definition Defines.php:54
const NS_PROJECT
Definition Defines.php:55
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
wfTimestamp( $outputtype=TS::UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfIsInfinity( $str)
Determine input string is represents as infinity.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
$fallback
$separatorTransformTable
$digitGroupingPattern
$minimumGroupingDigits
if(!defined('MEDIAWIKI')) if(!defined( 'MW_ENTRY_POINT')) $IP
Environment checks.
Definition Setup.php:102
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:71
Group all the pieces relevant to the context of a request into one instance.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
This class is a collection of static functions that serve two purposes:
Definition Html.php:44
JSON formatter wrapper class.
An interface for creating language converters.
A service that provides utilities to do with language names and codes.
Base class for language-specific code.
Definition Language.php:65
resetNamespaces()
Resets all the namespace caches.
Definition Language.php:392
formatDuration( $seconds, array $chosenIntervals=[])
Takes a number of seconds and turns it into a text using values such as hours and minutes.
hasWordBreaks()
Most writing systems use whitespace to break up words.
date( $ts, $adj=false, $format=true, $timecorrection=false)
formatDurationBetweenTimestamps(int $timestamp1, int $timestamp2, ?int $precision=null)
Takes two timestamps and turns the difference between them into a text using values such as hours and...
formatNumNoSeparators( $number)
Front-end for non-commafied formatNum.
static computeUnitTimestampDeadline(DateTimeInterface $date, string $unit)
Compute a cache expiry to account for a dynamic timestamp displayed in output.
array< string, int > null $mNamespaceIds
Indexed by localized lower-cased namespace name.
Definition Language.php:95
userTimeAndDate( $ts, UserIdentity $user, array $options=[])
Get the formatted date and time for the given timestamp and formatted for the given user.
convertGrammar( $word, $case)
Grammatical transformations, needed for inflected languages Invoked by putting {{grammar:case|word}} ...
formatTimePeriod( $seconds, $format=[])
Formats a time given in seconds into a string representation of that time.
formatExpiry( $expiry, $format=true, $infinity='infinity', $user=null)
Decode an expiry (block, protection, etc.) which has come from the DB.
truncateHtml( $text, $length, $ellipsis='...')
Truncate a string of valid HTML to a specified length in bytes, appending an optional string (e....
isRTL()
For right-to-left language support.
getPluralRuleIndexNumber( $number)
Find the index number of the plural rule appropriate for the given number.
formatBitrate( $bps)
Format a bitrate for output, using an appropriate unit (bps, kbps, Mbps, Gbps, Tbps,...
linkPrefixCharset()
A regular expression character set to match legal word-prefixing characters which should be merged on...
equals(Language $lang)
Compare with another language object.
getDir()
Return the correct HTML 'dir' attribute value for this language.
getGrammarForms()
Get the grammar forms for the content language.
alignStart()
Return 'left' or 'right' as appropriate alignment for line-start for this language's text direction.
timeanddate( $ts, $adj=false, $format=true, $timecorrection=false)
userAdjust( $ts, $tz=false)
Used by date() and time() to adjust the time output.
alignEnd()
Return 'right' or 'left' as appropriate alignment for line-end for this language's text direction.
formatNum( $number)
Normally we output all numbers in plain en_US style, that is 293,291.235 for two hundred ninety-three...
isSameCodeAs(Bcp47Code $other)
Compare this Language object to a Bcp47Code.
dateFormat( $usePrefs=true)
This is meant to be used by time(), date(), and timeanddate() to get the date preference they're supp...
getBlockDurations(bool $includeOther=true)
Get an array of suggested block durations from MediaWiki:Ipboptions.
getNsText( $index)
Get a namespace value by key.
Definition Language.php:428
getHumanTimestamp(MWTimestamp $time, ?MWTimestamp $relativeTo=null, ?UserIdentity $user=null)
Get the timestamp in a human-friendly relative format, e.g., "3 days ago".
firstChar( $s)
Get the first character of a string.
handleExplicitPluralForms( $count, array $forms)
Handles explicit plural forms for Language::convertPlural()
normalize( $s)
Convert a UTF-8 string to normal form C.
static int[] $durationIntervals
Definition Language.php:240
truncateForVisual( $string, $length, $ellipsis='...', $adjustLength=true)
Truncate a string to a specified number of characters, appending an optional string (e....
convertPlural( $count, $forms)
Plural form transformations, needed for some languages.
static hebrewNumeral( $num)
Hebrew Gematria number formatting up to 9999.
array< int, string > null $namespaceNames
Indexed by numeric namespace ID.
Definition Language.php:93
ucwordbreaks( $str)
capitalize words at word breaks
string[][] $dateFormatStrings
memoize
Definition Language.php:83
setNamespaces(array $namespaces)
Arbitrarily set all the namespace names at once.
Definition Language.php:383
getFormattedNamespaces()
A convenience function that returns {.
Definition Language.php:407
userDate( $ts, UserIdentity $user, array $options=[])
Get the formatted date for the given timestamp and formatted for the given user.
getConverterInternal()
Return the LanguageConverter for this language, convenience function for use in the language classes ...
__construct(string $code, private readonly NamespaceInfo $namespaceInfo, private LocalisationCache $localisationCache, private readonly LanguageNameUtils $langNameUtils, private readonly LanguageFallback $langFallback, private readonly LanguageConverterFactory $converterFactory, private readonly HookContainer $hookContainer, private readonly Config $config, private readonly LeximorphFactory $leximorphFactory,)
Definition Language.php:294
getCompiledPluralRules()
Get the compiled plural rules for the language.
getDirMark( $opposite=false)
A hidden direction mark (LRM or RLM), depending on the language direction.
static getCodeFromFileName( $filename, $prefix='Language', $suffix='.php')
Get the language code from a file name.
getSpecialPageAliases()
Get all aliases for all special pages.
linkTrail()
A regular expression to match legal word-trailing characters which should be merged onto a link of th...
getGroupName( $group)
Gets the localized friendly name for a group, if it exists.
userTime( $ts, UserIdentity $user, array $options=[])
Get the formatted time for the given timestamp and formatted for the given user.
translateBlockExpiry( $str, ?UserIdentity $user=null, $now=0)
getSearchIndexVariant()
Specify the language variant that should be used for search indexing.
removeBadCharLast( $string)
Remove bytes that represent an incomplete Unicode character at the end of string (e....
truncateForDatabase( $string, $length, $ellipsis='...', $adjustLength=true)
Truncate a string to a specified length in bytes, appending an optional string (e....
getPluralRuleTypes()
Get the plural rule types for the language.
getGroupMemberName(string $group, $member)
Gets the localized name for a member of a user group if it exists.
getCode()
Get the internal language code for this language object.
getPluralRuleType( $number)
Find the plural rule type appropriate for the given number.
const HEBREW_CALENDAR_MONTH_GENITIVE_MESSAGES
Definition Language.php:200
getBookstoreList()
Exports $wgBookstoreListEn.
Definition Language.php:329
getArrow( $direction='forwards')
An arrow, depending on the language direction.
emphasize( $text)
Italic is unsuitable for some languages.
needsGenderDistinction()
Whether this language uses gender-dependent namespace aliases.
Definition Language.php:474
getDirMarkEntity( $opposite=false)
A hidden direction mark (LRM or RLM), depending on the language direction.
semicolonList(array $list)
Take a list of strings and build a locale-friendly semicolon-separated list, using the local semicolo...
iconv( $in, $out, $string)
convertForSearchResult( $termsArray)
getNamespaces()
Returns an array of localised namespaces (with underscores, without considering language variants) in...
Definition Language.php:340
linkPrefixExtension()
To allow "foo[[bar]]" to extend the link over the whole word "foobar".
array< string, int > null $namespaceAliases
Map from alias to namespace ID.
Definition Language.php:97
getFormattedNsText( $index)
A convenience function that returns the same thing as getNsText() except with '_' changed to ' ',...
Definition Language.php:446
getMagic( $mw)
Fill a MagicWord object with data from this instance.
segmentForDiff( $text)
Languages like Chinese need to be segmented in order for the diff to be of any use.
formatSize( $size)
Format a size in bytes for output, using an appropriate unit (B, KB, MB, GB, TB, PB,...
commaList(array $list)
Take a list of strings and build a locale-friendly comma-separated list, using the local comma-separa...
pipeList(array $list)
Same as commaList, but separate it with the pipe instead.
static convertDoubleWidth( $string)
Convert double-width roman characters to single-width.
getGrammarTransformations()
Get the grammar transformations data for the language.
getMagicWords()
Get all the magic words from the localisation cache.
transformUsingPairFile(string $dataClass, string $input)
Transform a string using serialized data stored in the given file (which must be in the serialized su...
unsegmentForDiff( $text)
And unsegment to show the result.
specialList( $page, $details)
Make a list item, used by various special pages.
normalizeForSearch( $text)
Some languages have special punctuation need to be normalized.
getHtmlCode()
Get the code in BCP 47 format which we can use inside html lang="" tags.
getPluralRules()
Get the plural rules for the language.
getLocalNsIndex( $text)
Get a namespace key by case-insensitive value.
Definition Language.php:499
getHookContainer()
Get a HookContainer, for hook metadata and running extension hooks.
caseFold( $s)
Return a case-folded representation of $s.
preConvertPlural($forms, $count)
Checks that convertPlural was given an array and pads it to requested number of forms by copying the ...
formatComputingNumbers( $size, $boundary, $messageKey)
uc( $str, $first=false)
getVariantname( $code, $usemsg=true)
Short names for language variants used for language conversion links.
Definition Language.php:608
getDateFormatString( $type, $pref)
Get a format string for a given type and preference.
msg( $msg,... $params)
Gets the Message object from this language.
Definition Language.php:676
minimumGroupingDigits()
The minimum number of digits a number must have, in addition to the grouping size,...
toBcp47Code()
Implement the Bcp47Code interface.
string[][] null $mExtendedSpecialPageAliases
memoize
Definition Language.php:90
gender( $gender, $forms)
Provides an alternative text depending on specified gender.
getHookRunner()
Get a HookRunner, for running core hooks.
listToText(array $list)
Take a list of strings and build a locale-friendly comma-separated list, using the local comma-separa...
lc( $str, $first=false)
embedBidi( $text='')
Wraps argument with unicode control characters for directionality safety.
static romanNumeral( $num)
Roman number formatting up to 10000.
static insertSpace( $string, $pattern)
getDurationIntervals( $seconds, array $chosenIntervals=[])
Takes a number of seconds and returns an array with a set of corresponding intervals.
segmentByWord( $string)
Some languages such as Chinese require word segmentation, Specify such segmentation when overridden i...
getGenderNsText( $index, $gender)
Returns gender-dependent namespace alias if available.
Definition Language.php:459
getFormalityIndex()
Some languages provide translations in different levels of formality (or manner of address),...
time( $ts, $adj=false, $format=true, $timecorrection=false)
getNsIndex( $text)
Get a namespace key by case-insensitive value.
Definition Language.php:591
Create and cache Manager and Provider instances.
Caching for the contents of localisation files.
Create PSR-3 logger objects.
A class containing constants representing the names of configuration variables.
const ExtraGenderNamespaces
Name constant for the ExtraGenderNamespaces setting, for use with Config::get()
const NamespaceAliases
Name constant for the NamespaceAliases setting, for use with Config::get()
const MetaNamespace
Name constant for the MetaNamespace setting, for use with Config::get()
const ExtraNamespaces
Name constant for the ExtraNamespaces setting, for use with Config::get()
const MetaNamespaceTalk
Name constant for the MetaNamespaceTalk setting, for use with Config::get()
Service locator for MediaWiki core services.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:144
This class encapsulates "magic words" such as "#redirect", NOTOC, etc.
Definition MagicWord.php:51
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
getDefaultOption(string $opt, ?UserIdentity $userIdentity=null)
Get a given default option value.
Utility class to parse the TimeCorrection string value.
User class for the MediaWiki software.
Definition User.php:129
getDatePreference()
Get the user's preferred date format.
Definition User.php:2020
Library for creating and parsing MW-style timestamps.
offsetForUser(UserIdentity $user)
Adjust the timestamp depending on the given user's preferences.
Class for generating HTML <select> or <datalist> elements.
Definition XmlSelect.php:16
Value object representing a message parameter with one of the types from {.
Wrapper around strtr() that holds replacements.
A collection of static methods to play with strings.
return[ 'config-schema-inverse'=>['default'=>['ConfigRegistry'=>['main'=> 'MediaWiki\\Config\\GlobalVarConfig::newInstance',], 'Sitename'=> 'MediaWiki', 'Server'=> false, 'CanonicalServer'=> false, 'ServerName'=> false, 'AssumeProxiesUseDefaultProtocolPorts'=> true, 'HttpsPort'=> 443, 'ForceHTTPS'=> false, 'ScriptPath'=> '/wiki', 'UsePathInfo'=> null, 'Script'=> false, 'LoadScript'=> false, 'RestPath'=> false, 'StylePath'=> false, 'LocalStylePath'=> false, 'ExtensionAssetsPath'=> false, 'ExtensionDirectory'=> null, 'StyleDirectory'=> null, 'ArticlePath'=> false, 'UploadPath'=> false, 'ImgAuthPath'=> false, 'ThumbPath'=> false, 'UploadDirectory'=> false, 'FileCacheDirectory'=> false, 'Logo'=> false, 'Logos'=> false, 'Favicon'=> '/favicon.ico', 'AppleTouchIcon'=> false, 'ReferrerPolicy'=> false, 'TmpDirectory'=> false, 'UploadBaseUrl'=> '', 'UploadStashScalerBaseUrl'=> false, 'ActionPaths'=>[], 'MainPageIsDomainRoot'=> false, 'EnableUploads'=> false, 'UploadStashMaxAge'=> 21600, 'EnableAsyncUploads'=> false, 'EnableAsyncUploadsByURL'=> false, 'EnableChunkedUploads'=> false, 'UploadMaintenance'=> false, 'IllegalFileChars'=> ':\\/\\\\', 'DeletedDirectory'=> false, 'ImgAuthDetails'=> false, 'ImgAuthUrlPathMap'=>[], 'LocalFileRepo'=>['class'=> 'MediaWiki\\FileRepo\\LocalRepo', 'name'=> 'local', 'directory'=> null, 'scriptDirUrl'=> null, 'favicon'=> null, 'url'=> null, 'hashLevels'=> null, 'thumbScriptUrl'=> null, 'transformVia404'=> null, 'deletedDir'=> null, 'deletedHashLevels'=> null, 'updateCompatibleMetadata'=> null, 'reserializeMetadata'=> null,], 'ForeignFileRepos'=>[], 'UseInstantCommons'=> false, 'UseSharedUploads'=> false, 'SharedUploadDirectory'=> null, 'SharedUploadPath'=> null, 'HashedSharedUploadDirectory'=> true, 'RepositoryBaseUrl'=> 'https:'FetchCommonsDescriptions'=> false, 'SharedUploadDBname'=> false, 'SharedUploadDBprefix'=> '', 'SharedUploadDBschema'=> null, 'CacheSharedUploads'=> true, 'ForeignUploadTargets'=>['local',], 'UploadDialog'=>['fields'=>['description'=> true, 'date'=> false, 'categories'=> false,], 'licensemessages'=>['local'=> 'generic-local', 'foreign'=> 'generic-foreign',], 'comment'=>['local'=> '', 'foreign'=> '',], 'format'=>['filepage'=> ' $DESCRIPTION', 'description'=> ' $TEXT', 'ownwork'=> '', 'license'=> '', 'uncategorized'=> '',],], 'FileBackends'=>[], 'LockManagers'=>[], 'DefaultLockManager'=> null, 'ShowEXIF'=> null, 'UpdateCompatibleMetadata'=> false, 'AllowCopyUploads'=> false, 'CopyUploadsDomains'=>[], 'CopyUploadsFromSpecialUpload'=> false, 'CopyUploadProxy'=> false, 'CopyUploadTimeout'=> false, 'CopyUploadAllowOnWikiDomainConfig'=> false, 'MaxUploadSize'=> 104857600, 'MinUploadChunkSize'=> 1024, 'UploadNavigationUrl'=> false, 'UploadMissingFileUrl'=> false, 'ThumbnailScriptPath'=> false, 'SharedThumbnailScriptPath'=> false, 'HashedUploadDirectory'=> true, 'CSPUploadEntryPoint'=> true, 'FileExtensions'=>['png', 'gif', 'jpg', 'jpeg', 'webp',], 'ProhibitedFileExtensions'=>['html', 'htm', 'js', 'jsb', 'mhtml', 'mht', 'xhtml', 'xht', 'php', 'phtml', 'php3', 'php4', 'php5', 'phps', 'phar', 'shtml', 'jhtml', 'pl', 'py', 'cgi', 'exe', 'scr', 'dll', 'msi', 'vbs', 'bat', 'com', 'pif', 'cmd', 'vxd', 'cpl', 'xml',], 'MimeTypeExclusions'=>['text/html', 'application/javascript', 'text/javascript', 'text/x-javascript', 'application/x-shellscript', 'application/x-php', 'text/x-php', 'text/x-python', 'text/x-perl', 'text/x-bash', 'text/x-sh', 'text/x-csh', 'text/scriptlet', 'application/x-msdownload', 'application/x-msmetafile', 'application/java', 'application/xml', 'text/xml',], 'CheckFileExtensions'=> true, 'StrictFileExtensions'=> true, 'DisableUploadScriptChecks'=> false, 'UploadSizeWarning'=> false, 'TrustedMediaFormats'=>['BITMAP', 'AUDIO', 'VIDEO', 'image/svg+xml', 'application/pdf',], 'MediaHandlers'=>[], 'NativeImageLazyLoading'=> true, 'ParserTestMediaHandlers'=>['image/jpeg'=> 'MockBitmapHandler', 'image/png'=> 'MockBitmapHandler', 'image/gif'=> 'MockBitmapHandler', 'image/tiff'=> 'MockBitmapHandler', 'image/webp'=> 'MockBitmapHandler', 'image/x-ms-bmp'=> 'MockBitmapHandler', 'image/x-bmp'=> 'MockBitmapHandler', 'image/x-xcf'=> 'MockBitmapHandler', 'image/svg+xml'=> 'MockSvgHandler', 'image/vnd.djvu'=> 'MockDjVuHandler',], 'UseImageResize'=> true, 'UseImageMagick'=> false, 'ImageMagickConvertCommand'=> '/usr/bin/convert', 'MaxInterlacingAreas'=>[], 'SharpenParameter'=> '0x0.4', 'SharpenReductionThreshold'=> 0.85, 'ImageMagickTempDir'=> false, 'CustomConvertCommand'=> false, 'JpegTran'=> '/usr/bin/jpegtran', 'JpegPixelFormat'=> 'yuv420', 'JpegQuality'=> 80, 'Exiv2Command'=> '/usr/bin/exiv2', 'Exiftool'=> '/usr/bin/exiftool', 'SVGConverters'=>['ImageMagick'=> ' $path/convert -background "#ffffff00" -thumbnail $widthx$height\\! $input PNG:$output', 'inkscape'=> ' $path/inkscape -w $width -o $output $input', 'batik'=> 'java -Djava.awt.headless=true -jar $path/batik-rasterizer.jar -w $width -d $output $input', 'rsvg'=> ' $path/rsvg-convert -w $width -h $height -l $lang -o $output $input', 'ImagickExt'=>['SvgHandler::rasterizeImagickExt',],], 'SVGConverter'=> 'ImageMagick', 'SVGConverterPath'=> '', 'SVGMaxSize'=> 5120, 'SVGMetadataCutoff'=> 5242880, 'SVGNativeRendering'=> true, 'SVGNativeRenderingSizeLimit'=> 51200, 'MediaInTargetLanguage'=> true, 'MaxImageArea'=> 12500000, 'MaxAnimatedGifArea'=> 12500000, 'MaxAnimatedWebPArea'=> 12500000, 'WebPThumbnailType'=>['webp', 'image/webp',], 'TiffThumbnailType'=>[], 'ThumbnailEpoch'=> '20030516000000', 'AttemptFailureEpoch'=> 1, 'IgnoreImageErrors'=> false, 'GenerateThumbnailOnParse'=> true, 'ShowArchiveThumbnails'=> true, 'EnableAutoRotation'=> null, 'Antivirus'=> null, 'AntivirusSetup'=>['clamav'=>['command'=> 'clamscan --no-summary ', 'codemap'=>[0=> 0, 1=> 1, 52=> -1, ' *'=> false,], 'messagepattern'=> '/.*?:(.*)/sim',],], 'AntivirusRequired'=> true, 'VerifyMimeType'=> true, 'MimeTypeFile'=> 'internal', 'MimeInfoFile'=> 'internal', 'MimeDetectorCommand'=> null, 'TrivialMimeDetection'=> false, 'XMLMimeTypes'=>['http:'svg'=> 'image/svg+xml', 'http:'http:'html'=> 'text/html',], 'ImageLimits'=>[[320, 240,], [640, 480,], [800, 600,], [1024, 768,], [1280, 1024,], [2560, 2048,],], 'ThumbLimits'=>[120, 150, 180, 200, 220, 250, 300, 400,], 'ThumbnailNamespaces'=>[6,], 'ThumbnailSteps'=> null, 'ThumbnailBuckets'=> null, 'ThumbnailMinimumBucketDistance'=> 50, 'UploadThumbnailRenderMap'=>[], 'UploadThumbnailRenderMethod'=> 'jobqueue', 'UploadThumbnailRenderHttpCustomHost'=> false, 'UploadThumbnailRenderHttpCustomDomain'=> false, 'UseTinyRGBForJPGThumbnails'=> false, 'GalleryOptions'=>[], 'ThumbUpright'=> 0.75, 'DirectoryMode'=> 511, 'ResponsiveImages'=> true, 'ImagePreconnect'=> false, 'TrackMediaRequestProvenance'=> false, 'DjvuUseBoxedCommand'=> false, 'DjvuDump'=> null, 'DjvuRenderer'=> null, 'DjvuTxt'=> null, 'DjvuPostProcessor'=> 'pnmtojpeg', 'DjvuOutputExtension'=> 'jpg', 'EmergencyContact'=> false, 'PasswordSender'=> false, 'NoReplyAddress'=> false, 'EnableEmail'=> true, 'EnableUserEmail'=> true, 'UserEmailUseReplyTo'=> true, 'PasswordReminderResendTime'=> 24, 'NewPasswordExpiry'=> 604800, 'UserEmailConfirmationTokenExpiry'=> 604800, 'PasswordExpirationDays'=> false, 'PasswordExpireGrace'=> 604800, 'SMTP'=> false, 'AdditionalMailParams'=> null, 'AllowHTMLEmail'=> false, 'EnotifFromEditor'=> false, 'EmailAuthentication'=> true, 'EmailConfirmationBanner'=> false, 'EnotifWatchlist'=> false, 'EnotifUserTalk'=> false, 'EnotifRevealEditorAddress'=> false, 'EnotifMinorEdits'=> true, 'EnotifUseRealName'=> false, 'UsersNotifiedOnAllChanges'=>[], 'DBname'=> 'my_wiki', 'DBmwschema'=> null, 'DBprefix'=> '', 'DBserver'=> 'localhost', 'DBport'=> 5432, 'DBuser'=> 'wikiuser', 'DBpassword'=> '', 'DBtype'=> 'mysql', 'DBssl'=> false, 'DBcompress'=> false, 'DBStrictWarnings'=> false, 'DBadminuser'=> null, 'DBadminpassword'=> null, 'SearchType'=> null, 'SearchTypeAlternatives'=> null, 'DBTableOptions'=> 'ENGINE=InnoDB, DEFAULT CHARSET=binary', 'SQLMode'=> '', 'SQLiteDataDir'=> '', 'SharedDB'=> null, 'SharedPrefix'=> false, 'SharedTables'=>['user', 'user_properties', 'user_autocreate_serial',], 'SharedSchema'=> false, 'DBservers'=> false, 'LBFactoryConf'=>['class'=> 'Wikimedia\\Rdbms\\LBFactorySimple',], 'DataCenterUpdateStickTTL'=> 10, 'DBerrorLog'=> false, 'DBerrorLogTZ'=> false, 'LocalDatabases'=>[], 'DatabaseReplicaLagWarning'=> 10, 'DatabaseReplicaLagCritical'=> 30, 'MaxExecutionTimeForExpensiveQueries'=> 0, 'VirtualDomainsMapping'=>[], 'RemoteVirtualDomainsMapping'=>[], 'FileSchemaMigrationStage'=> 3, 'ExternalLinksDomainGaps'=>[], 'ContentHandlers'=>['wikitext'=>['class'=> 'MediaWiki\\Content\\WikitextContentHandler', 'services'=>['TitleFactory', 'ParserFactory', 'GlobalIdGenerator', 'LanguageNameUtils', 'LinkRenderer', 'MagicWordFactory', 'ParsoidParserFactory',],], 'javascript'=>['class'=> 'MediaWiki\\Content\\JavaScriptContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'UserOptionsLookup', 'CodeHighlighter',],], 'json'=>['class'=> 'MediaWiki\\Content\\JsonContentHandler', 'services'=>['ParsoidParserFactory', 'TitleFactory',],], 'css'=>['class'=> 'MediaWiki\\Content\\CssContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'UserOptionsLookup', 'CodeHighlighter',],], 'vue'=>['class'=> 'MediaWiki\\Content\\VueContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'CodeHighlighter',],], 'text'=> 'MediaWiki\\Content\\TextContentHandler', 'unknown'=> 'MediaWiki\\Content\\FallbackContentHandler',], 'NamespaceContentModels'=>[], 'TextModelsToParse'=>['wikitext', 'javascript', 'css',], 'CompressRevisions'=> false, 'ExternalStores'=>[], 'ExternalServers'=>[], 'DefaultExternalStore'=> false, 'RevisionCacheExpiry'=> 604800, 'PageLanguageUseDB'=> false, 'DiffEngine'=> null, 'ExternalDiffEngine'=> false, 'Wikidiff2Options'=>[], 'RequestTimeLimit'=> null, 'TransactionalTimeLimit'=> 120, 'CriticalSectionTimeLimit'=> 180.0, 'MiserMode'=> false, 'DisableQueryPages'=> false, 'QueryCacheLimit'=> 1000, 'WantedPagesThreshold'=> 1, 'AllowSlowParserFunctions'=> false, 'AllowSchemaUpdates'=> true, 'MaxArticleSize'=> 2048, 'MemoryLimit'=> '50M', 'PoolCounterConf'=> null, 'PoolCountClientConf'=>['servers'=>['127.0.0.1',], 'timeout'=> 0.1,], 'MaxUserDBWriteDuration'=> false, 'MaxJobDBWriteDuration'=> false, 'LinkHolderBatchSize'=> 1000, 'MaximumMovedPages'=> 100, 'ForceDeferredUpdatesPreSend'=> false, 'MultiShardSiteStats'=> false, 'CacheDirectory'=> false, 'MainCacheType'=> 0, 'MessageCacheType'=> -1, 'ParserCacheType'=> -1, 'SessionCacheType'=> -1, 'AnonSessionCacheType'=> false, 'LanguageConverterCacheType'=> -1, 'ObjectCaches'=>[0=>['class'=> 'Wikimedia\\ObjectCache\\EmptyBagOStuff', 'reportDupes'=> false,], 1=>['class'=> 'MediaWiki\\ObjectCache\\SqlBagOStuff', 'loggroup'=> 'SQLBagOStuff',], 'memcached-php'=>['class'=> 'Wikimedia\\ObjectCache\\MemcachedPhpBagOStuff', 'loggroup'=> 'memcached',], 'memcached-pecl'=>['class'=> 'Wikimedia\\ObjectCache\\MemcachedPeclBagOStuff', 'loggroup'=> 'memcached',], 'hash'=>['class'=> 'Wikimedia\\ObjectCache\\HashBagOStuff', 'reportDupes'=> false,], 'apc'=>['class'=> 'Wikimedia\\ObjectCache\\APCUBagOStuff', 'reportDupes'=> false,], 'apcu'=>['class'=> 'Wikimedia\\ObjectCache\\APCUBagOStuff', 'reportDupes'=> false,],], 'WANObjectCache'=>[], 'MicroStashType'=> -1, 'MainStash'=> 1, 'ParsoidCacheConfig'=>['StashType'=> null, 'StashDuration'=> 86400,], 'ParsoidSelectiveUpdateSampleRate'=> 0, 'SplitParsoidParserCache'=> false, 'ParserCacheFilterConfig'=>['pcache'=>['default'=>['minCpuTime'=> 0,],], 'postproc-pcache'=>['default'=>['minCpuTime'=> 9223372036854775807,],], 'parsoid-pcache'=>['default'=>['minCpuTime'=> 0,],], 'postproc-parsoid-pcache'=>['default'=>['minCpuTime'=> 0,],],], 'ChronologyProtectorSecret'=> '', 'ParserCacheExpireTime'=> 86400, 'ParserCacheAsyncExpireTime'=> 60, 'ParserCacheAsyncRefreshJobs'=> true, 'OldRevisionParserCacheExpireTime'=> 3600, 'ObjectCacheSessionExpiry'=> 3600, 'SuspiciousIpExpiry'=> false, 'SessionPbkdf2Iterations'=> 10001, 'UseSessionCookieJwt'=> false, 'JwtSessionCookieIssuer'=> null, 'MemCachedServers'=>['127.0.0.1:11211',], 'MemCachedPersistent'=> false, 'MemCachedTimeout'=> 500000, 'UseLocalMessageCache'=> false, 'AdaptiveMessageCache'=> false, 'LocalisationCacheConf'=>['class'=> 'MediaWiki\\Language\\LocalisationCache', 'store'=> 'detect', 'storeClass'=> false, 'storeDirectory'=> false, 'storeServer'=>[], 'forceRecache'=> false, 'manualRecache'=> false,], 'CachePages'=> true, 'CacheEpoch'=> '20030516000000', 'GitInfoCacheDirectory'=> false, 'UseFileCache'=> false, 'FileCacheDepth'=> 2, 'RenderHashAppend'=> '', 'EnableSidebarCache'=> false, 'SidebarCacheExpiry'=> 86400, 'UseGzip'=> false, 'InvalidateCacheOnLocalSettingsChange'=> true, 'ExtensionInfoMTime'=> false, 'EnableRemoteBagOStuffTests'=> false, 'UseCdn'=> false, 'VaryOnXFP'=> false, 'InternalServer'=> false, 'CdnMaxAge'=> 18000, 'CdnMaxageLagged'=> 30, 'CdnMaxageStale'=> 10, 'CdnReboundPurgeDelay'=> 0, 'CdnMaxageSubstitute'=> 60, 'ForcedRawSMaxage'=> 300, 'CdnServers'=>[], 'CdnServersNoPurge'=>[], 'HTCPRouting'=>[], 'HTCPMulticastTTL'=> 1, 'UsePrivateIPs'=> false, 'CdnMatchParameterOrder'=> true, 'LanguageCode'=> 'en', 'GrammarForms'=>[], 'InterwikiMagic'=> true, 'HideInterlanguageLinks'=> false, 'ExtraInterlanguageLinkPrefixes'=>[], 'InterlanguageLinkCodeMap'=>[], 'ExtraLanguageNames'=>[], 'ExtraLanguageCodes'=>['bh'=> 'bho', 'no'=> 'nb', 'simple'=> 'en',], 'DummyLanguageCodes'=>[], 'AllUnicodeFixes'=> false, 'LegacyEncoding'=> false, 'AmericanDates'=> false, 'TranslateNumerals'=> true, 'UseDatabaseMessages'=> true, 'MaxMsgCacheEntrySize'=> 10000, 'DisableLangConversion'=> false, 'DisableTitleConversion'=> false, 'DefaultLanguageVariant'=> false, 'UsePigLatinVariant'=> false, 'DisabledVariants'=>[], 'VariantArticlePath'=> false, 'UseXssLanguage'=> false, 'LoginLanguageSelector'=> false, 'ForceUIMsgAsContentMsg'=>[], 'RawHtmlMessages'=>[], 'Localtimezone'=> null, 'LocalTZoffset'=> null, 'OverrideUcfirstCharacters'=>[], 'MimeType'=> 'text/html', 'Html5Version'=> null, 'EditSubmitButtonLabelPublish'=> false, 'XhtmlNamespaces'=>[], 'SiteNotice'=> '', 'BrowserFormatDetection'=> 'telephone=no', 'SkinMetaTags'=>[], 'DefaultSkin'=> 'vector-2022', 'FallbackSkin'=> 'fallback', 'SkipSkins'=>[], 'DisableOutputCompression'=> false, 'FragmentMode'=>['html5', 'legacy',], 'ExternalInterwikiFragmentMode'=> 'legacy', 'FooterIcons'=>['copyright'=>['copyright'=>[],], 'poweredby'=>['mediawiki'=>['src'=> null, 'url'=> 'https:'alt'=> 'Powered by MediaWiki', 'lang'=> 'en',],],], 'EnableSectionShare'=> false, 'PageShareSkinsEnabled'=>[], 'UseCombinedLoginLink'=> false, 'Edititis'=> false, 'Send404Code'=> true, 'ShowRollbackEditCount'=> 10, 'EnableCanonicalServerLink'=> false, 'InterwikiLogoOverride'=>[], 'ResourceModules'=>[], 'ResourceModuleSkinStyles'=>[], 'ResourceLoaderSources'=>[], 'ResourceBasePath'=> null, 'ResourceLoaderMaxage'=>[], 'ResourceLoaderDebug'=> false, 'ResourceLoaderMaxQueryLength'=> false, 'ResourceLoaderValidateJS'=> true, 'ResourceLoaderEnableJSProfiler'=> false, 'ResourceLoaderStorageEnabled'=> true, 'ResourceLoaderStorageVersion'=> 1, 'ResourceLoaderEnableSourceMapLinks'=> true, 'AllowSiteCSSOnRestrictedPages'=> false, 'VueDevelopmentMode'=> false, 'CodexDevelopmentDir'=> null, 'MetaNamespace'=> false, 'MetaNamespaceTalk'=> false, 'CanonicalNamespaceNames'=>[-2=> 'Media', -1=> 'Special', 0=> '', 1=> 'Talk', 2=> 'User', 3=> 'User_talk', 4=> 'Project', 5=> 'Project_talk', 6=> 'File', 7=> 'File_talk', 8=> 'MediaWiki', 9=> 'MediaWiki_talk', 10=> 'Template', 11=> 'Template_talk', 12=> 'Help', 13=> 'Help_talk', 14=> 'Category', 15=> 'Category_talk',], 'ExtraNamespaces'=>[], 'ExtraGenderNamespaces'=>[], 'NamespaceAliases'=>[], 'LegalTitleChars'=> ' %!"$&\'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+', 'CapitalLinks' => true, 'CapitalLinkOverrides' => [ ], 'NamespacesWithSubpages' => [ 1 => true, 2 => true, 3 => true, 4 => true, 5 => true, 7 => true, 8 => true, 9 => true, 10 => true, 11 => true, 12 => true, 13 => true, 15 => true, ], 'NamespacesWithoutAutoSummaries' => [ ], 'ContentNamespaces' => [ 0, ], 'ShortPagesNamespaceExclusions' => [ ], 'ExtraSignatureNamespaces' => [ ], 'InvalidRedirectTargets' => [ 'Filepath', 'Mypage', 'Mytalk', 'Redirect', 'Mylog', ], 'DisableHardRedirects' => false, 'FixDoubleRedirects' => false, 'LocalInterwikis' => [ ], 'InterwikiExpiry' => 10800, 'InterwikiCache' => false, 'InterwikiScopes' => 3, 'InterwikiFallbackSite' => 'wiki', 'RedirectSources' => false, 'SiteTypes' => [ 'mediawiki' => 'MediaWiki\\Site\\MediaWikiSite', ], 'MaxTocLevel' => 999, 'MaxPPNodeCount' => 1000000, 'MaxTemplateDepth' => 100, 'MaxPPExpandDepth' => 100, 'UrlProtocols' => [ 'bitcoin:', 'ftp: 'ftps: 'geo:', 'git: 'gopher: 'http: 'https: 'irc: 'ircs: 'magnet:', 'mailto:', 'matrix:', 'mms: 'news:', 'nntp: 'redis: 'sftp: 'sip:', 'sips:', 'sms:', 'ssh: 'svn: 'tel:', 'telnet: 'urn:', 'wikipedia: 'worldwind: 'xmpp:', ' ], 'CleanSignatures' => true, 'AllowExternalImages' => false, 'AllowExternalImagesFrom' => '', 'EnableImageWhitelist' => false, 'TidyConfig' => [ ], 'ParsoidSettings' => [ 'useSelser' => true, ], 'ParsoidExperimentalParserFunctionOutput' => false, 'RawHtml' => false, 'ExternalLinkTarget' => false, 'NoFollowLinks' => true, 'NoFollowNsExceptions' => [ ], 'NoFollowDomainExceptions' => [ 'mediawiki.org', ], 'RegisterInternalExternals' => false, 'ExternalLinksIgnoreDomains' => [ ], 'AllowDisplayTitle' => true, 'RestrictDisplayTitle' => true, 'ExpensiveParserFunctionLimit' => 100, 'PreprocessorCacheThreshold' => 1000, 'EnableScaryTranscluding' => false, 'TranscludeCacheExpiry' => 3600, 'EnableMagicLinks' => [ 'ISBN' => false, 'PMID' => false, 'RFC' => false, ], 'ParserEnableUserLanguage' => false, 'ArticleCountMethod' => 'link', 'ActiveUserDays' => 30, 'LearnerEdits' => 10, 'LearnerMemberSince' => 4, 'ExperiencedUserEdits' => 500, 'ExperiencedUserMemberSince' => 30, 'ManualRevertSearchRadius' => 15, 'RevertedTagMaxDepth' => 15, 'CentralIdLookupProviders' => [ 'local' => [ 'class' => 'MediaWiki\\User\\CentralId\\LocalIdLookup', 'services' => [ 'MainConfig', 'DBLoadBalancerFactory', 'HideUserUtils', ], ], ], 'CentralIdLookupProvider' => 'local', 'UserRegistrationProviders' => [ 'local' => [ 'class' => 'MediaWiki\\User\\Registration\\LocalUserRegistrationProvider', 'services' => [ 'ConnectionProvider', ], ], ], 'PasswordPolicy' => [ 'policies' => [ 'bureaucrat' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'sysop' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'interface-admin' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'bot' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'default' => [ 'MinimalPasswordLength' => [ 'value' => 8, 'suggestChangeOnLogin' => true, ], 'PasswordCannotBeSubstringInUsername' => [ 'value' => true, 'suggestChangeOnLogin' => true, ], 'PasswordCannotMatchDefaults' => [ 'value' => true, 'suggestChangeOnLogin' => true, ], 'MaximalPasswordLength' => [ 'value' => 4096, 'suggestChangeOnLogin' => true, ], 'PasswordNotInCommonList' => [ 'value' => true, 'suggestChangeOnLogin' => true, ], ], ], 'checks' => [ 'MinimalPasswordLength' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkMinimalPasswordLength', ], 'MinimumPasswordLengthToLogin' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkMinimumPasswordLengthToLogin', ], 'PasswordCannotBeSubstringInUsername' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkPasswordCannotBeSubstringInUsername', ], 'PasswordCannotMatchDefaults' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkPasswordCannotMatchDefaults', ], 'MaximalPasswordLength' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkMaximalPasswordLength', ], 'PasswordNotInCommonList' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkPasswordNotInCommonList', ], ], ], 'AuthManagerConfig' => null, 'AuthManagerAutoConfig' => [ 'preauth' => [ 'MediaWiki\\Auth\\ThrottlePreAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\ThrottlePreAuthenticationProvider', 'sort' => 0, ], 'MediaWiki\\Auth\\PreviouslyRenamedAccountPreAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\PreviouslyRenamedAccountPreAuthenticationProvider', 'services' => [ 'ConnectionProvider', 'UserFactory', ], 'sort' => 0, ], ], 'primaryauth' => [ 'MediaWiki\\Auth\\TemporaryPasswordPrimaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\TemporaryPasswordPrimaryAuthenticationProvider', 'services' => [ 'DBLoadBalancerFactory', 'UserOptionsLookup', ], 'args' => [ [ 'authoritative' => false, ], ], 'sort' => 0, ], 'MediaWiki\\Auth\\LocalPasswordPrimaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\LocalPasswordPrimaryAuthenticationProvider', 'services' => [ 'DBLoadBalancerFactory', ], 'args' => [ [ 'authoritative' => true, ], ], 'sort' => 100, ], ], 'secondaryauth' => [ 'MediaWiki\\Auth\\CheckBlocksSecondaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\CheckBlocksSecondaryAuthenticationProvider', 'sort' => 0, ], 'MediaWiki\\Auth\\ResetPasswordSecondaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\ResetPasswordSecondaryAuthenticationProvider', 'sort' => 100, ], 'MediaWiki\\Auth\\EmailNotificationSecondaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\EmailNotificationSecondaryAuthenticationProvider', 'services' => [ 'DBLoadBalancerFactory', ], 'sort' => 200, ], ], ], 'RememberMe' => 'choose', 'ReauthenticateTime' => [ 'default' => 3600, ], 'ChangeCredentialsBlacklist' => [ 'MediaWiki\\Auth\\TemporaryPasswordAuthenticationRequest', ], 'RemoveCredentialsBlacklist' => [ 'MediaWiki\\Auth\\PasswordAuthenticationRequest', ], 'InvalidPasswordReset' => true, 'PasswordDefault' => 'pbkdf2', 'PasswordConfig' => [ 'A' => [ 'class' => 'MediaWiki\\Password\\MWOldPassword', ], 'B' => [ 'class' => 'MediaWiki\\Password\\MWSaltedPassword', ], 'pbkdf2-legacyA' => [ 'class' => 'MediaWiki\\Password\\LayeredParameterizedPassword', 'types' => [ 'A', 'pbkdf2', ], ], 'pbkdf2-legacyB' => [ 'class' => 'MediaWiki\\Password\\LayeredParameterizedPassword', 'types' => [ 'B', 'pbkdf2', ], ], 'bcrypt' => [ 'class' => 'MediaWiki\\Password\\BcryptPassword', 'cost' => 9, ], 'pbkdf2' => [ 'class' => 'MediaWiki\\Password\\Pbkdf2PasswordUsingOpenSSL', 'algo' => 'sha512', 'cost' => '30000', 'length' => '64', ], 'argon2' => [ 'class' => 'MediaWiki\\Password\\Argon2Password', 'algo' => 'auto', ], ], 'PasswordResetRoutes' => [ 'username' => true, 'email' => true, ], 'MaxSigChars' => 255, 'SignatureValidation' => 'warning', 'SignatureAllowedLintErrors' => [ 'obsolete-tag', ], 'MaxNameChars' => 255, 'ReservedUsernames' => [ 'MediaWiki default', 'Conversion script', 'Maintenance script', 'Template namespace initialisation script', 'ScriptImporter', 'Delete page script', 'Move page script', 'Command line script', 'Unknown user', 'msg:double-redirect-fixer', 'msg:usermessage-editor', 'msg:proxyblocker', 'msg:sorbs', 'msg:spambot_username', 'msg:autochange-username', ], 'DefaultUserOptions' => [ 'ccmeonemails' => 0, 'date' => 'default', 'diffonly' => 0, 'diff-type' => 'table', 'disablemail' => 0, 'editfont' => 'monospace', 'editondblclick' => 0, 'editrecovery' => 0, 'editsectiononrightclick' => 0, 'email-allow-new-users' => 1, 'enotifminoredits' => 0, 'enotifrevealaddr' => 0, 'enotifusertalkpages' => 1, 'enotifwatchlistpages' => 1, 'extendwatchlist' => 1, 'fancysig' => 0, 'forceeditsummary' => 0, 'forcesafemode' => 0, 'gender' => 'unknown', 'hidecategorization' => 1, 'hideminor' => 0, 'hidepatrolled' => 0, 'imagesize' => 2, 'minordefault' => 0, 'newpageshidepatrolled' => 0, 'nickname' => '', 'norollbackdiff' => 0, 'prefershttps' => 1, 'previewonfirst' => 0, 'previewontop' => 1, 'pst-cssjs' => 1, 'rcdays' => 7, 'rcenhancedfilters-disable' => 0, 'rclimit' => 50, 'requireemail' => 0, 'search-match-redirect' => true, 'search-special-page' => 'Search', 'search-thumbnail-extra-namespaces' => true, 'searchlimit' => 20, 'showhiddencats' => 0, 'shownumberswatching' => 1, 'showrollbackconfirmation' => 0, 'skin' => false, 'skin-responsive' => 1, 'thumbsize' => 5, 'underline' => 2, 'useeditwarning' => 1, 'uselivepreview' => 0, 'usenewrc' => 1, 'watchcreations' => 1, 'watchcreations-expiry' => 'infinite', 'watchdefault' => 1, 'watchdefault-expiry' => 'infinite', 'watchdeletion' => 0, 'watchlistdays' => 7, 'watchlisthideanons' => 0, 'watchlisthidebots' => 0, 'watchlisthidecategorization' => 1, 'watchlisthideliu' => 0, 'watchlisthideminor' => 0, 'watchlisthideown' => 0, 'watchlisthidepatrolled' => 0, 'watchlistreloadautomatically' => 0, 'watchlistunwatchlinks' => 0, 'watchmoves' => 0, 'watchrollback' => 0, 'watchuploads' => 1, 'watchrollback-expiry' => 'infinite', 'watchstar-expiry' => 'infinite', 'wlenhancedfilters-disable' => 0, 'wllimit' => 250, ], 'ConditionalUserOptions' => [ ], 'HiddenPrefs' => [ ], 'UserJsPrefLimit' => 100, 'InvalidUsernameCharacters' => '@:>=', 'UserrightsInterwikiDelimiter' => '@', 'SecureLogin' => false, 'AuthenticationTokenVersion' => null, 'SessionProviders' => [ 'MediaWiki\\Session\\CookieSessionProvider' => [ 'class' => 'MediaWiki\\Session\\CookieSessionProvider', 'args' => [ [ 'priority' => 30, ], ], 'services' => [ 'JwtCodec', 'UrlUtils', ], ], 'MediaWiki\\Session\\BotPasswordSessionProvider' => [ 'class' => 'MediaWiki\\Session\\BotPasswordSessionProvider', 'args' => [ [ 'priority' => 75, ], ], 'services' => [ 'GrantsInfo', ], ], ], 'AutoCreateTempUser' => [ 'known' => false, 'enabled' => false, 'actions' => [ 'edit', ], 'genPattern' => '~$1', 'matchPattern' => null, 'reservedPattern' => '~$1', 'serialProvider' => [ 'type' => 'local', 'useYear' => true, ], 'serialMapping' => [ 'type' => 'readable-numeric', ], 'expireAfterDays' => 90, 'notifyBeforeExpirationDays' => 10, ], 'AutoblockExemptions' => [ ], 'AutoblockExpiry' => 86400, 'BlockAllowsUTEdit' => true, 'BlockCIDRLimit' => [ 'IPv4' => 16, 'IPv6' => 19, ], 'BlockDisablesLogin' => false, 'EnableMultiBlocks' => false, 'WhitelistRead' => false, 'WhitelistReadRegexp' => false, 'EmailConfirmToEdit' => false, 'HideIdentifiableRedirects' => true, 'GroupPermissions' => [ '*' => [ 'createaccount' => true, 'autocreateaccount' => true, 'read' => true, 'edit' => true, 'createpage' => true, 'createtalk' => true, 'viewmyprivateinfo' => true, 'editmyprivateinfo' => true, 'editmyoptions' => true, ], 'user' => [ 'move' => true, 'move-subpages' => true, 'move-rootuserpages' => true, 'move-categorypages' => true, 'movefile' => true, 'read' => true, 'edit' => true, 'createpage' => true, 'createtalk' => true, 'upload' => true, 'reupload' => true, 'reupload-shared' => true, 'minoredit' => true, 'editmyusercss' => true, 'editmyuserjson' => true, 'editmyuserjs' => true, 'editmyuserjsredirect' => true, 'sendemail' => true, 'applychangetags' => true, 'changetags' => true, 'viewmywatchlist' => true, 'editmywatchlist' => true, 'createwithcontentmodel' => true, 'logout' => true, ], 'autoconfirmed' => [ 'autoconfirmed' => true, 'editsemiprotected' => true, ], 'bot' => [ 'bot' => true, 'autoconfirmed' => true, 'editsemiprotected' => true, 'nominornewtalk' => true, 'autopatrol' => true, 'suppressredirect' => true, 'apihighlimits' => true, ], 'sysop' => [ 'block' => true, 'createaccount' => true, 'createpreviouslyrenamedaccount' => true, 'delete' => true, 'bigdelete' => true, 'deletedhistory' => true, 'deletedtext' => true, 'undelete' => true, 'editcontentmodel' => true, 'editinterface' => true, 'editsitejson' => true, 'edituserjson' => true, 'import' => true, 'importupload' => true, 'move' => true, 'move-subpages' => true, 'move-rootuserpages' => true, 'move-categorypages' => true, 'patrol' => true, 'autopatrol' => true, 'protect' => true, 'editprotected' => true, 'rollback' => true, 'upload' => true, 'reupload' => true, 'reupload-shared' => true, 'unwatchedpages' => true, 'autoconfirmed' => true, 'editsemiprotected' => true, 'ipblock-exempt' => true, 'blockemail' => true, 'markbotedits' => true, 'apihighlimits' => true, 'browsearchive' => true, 'noratelimit' => true, 'movefile' => true, 'unblockself' => true, 'suppressredirect' => true, 'mergehistory' => true, 'managechangetags' => true, 'deletechangetags' => true, ], 'interface-admin' => [ 'editinterface' => true, 'editsitecss' => true, 'editsitejson' => true, 'editsitejs' => true, 'editusercss' => true, 'edituserjson' => true, 'edituserjs' => true, ], 'bureaucrat' => [ 'userrights' => true, 'noratelimit' => true, 'renameuser' => true, ], 'suppress' => [ 'hideuser' => true, 'suppressrevision' => true, 'viewsuppressed' => true, 'suppressionlog' => true, 'deleterevision' => true, 'deletelogentry' => true, ], ], 'PrivilegedGroups' => [ 'bureaucrat', 'interface-admin', 'suppress', 'sysop', ], 'RevokePermissions' => [ ], 'GroupInheritsPermissions' => [ ], 'ImplicitGroups' => [ '*', 'user', 'autoconfirmed', ], 'GroupsAddToSelf' => [ ], 'GroupsRemoveFromSelf' => [ ], 'RestrictedGroups' => [ ], 'UserRequirementsPrivateConditions' => [ ], 'RestrictionTypes' => [ 'create', 'edit', 'move', 'upload', ], 'RestrictionLevels' => [ '', 'autoconfirmed', 'sysop', ], 'CascadingRestrictionLevels' => [ 'sysop', ], 'SemiprotectedRestrictionLevels' => [ 'autoconfirmed', ], 'NamespaceProtection' => [ ], 'RestrictUserPageEditing' => false, 'NonincludableNamespaces' => [ ], 'AutoConfirmAge' => 0, 'AutoConfirmCount' => 0, 'Autopromote' => [ 'autoconfirmed' => [ '&', [ 1, null, ], [ 2, null, ], ], ], 'AutopromoteOnce' => [ 'onEdit' => [ ], ], 'AutopromoteOnceLogInRC' => true, 'AutopromoteOnceRCExcludedGroups' => [ ], 'AddGroups' => [ ], 'RemoveGroups' => [ ], 'AvailableRights' => [ ], 'ImplicitRights' => [ ], 'DeleteRevisionsLimit' => 0, 'DeleteRevisionsBatchSize' => 1000, 'HideUserContribLimit' => 1000, 'AccountCreationThrottle' => [ [ 'count' => 0, 'seconds' => 86400, ], ], 'TempAccountCreationThrottle' => [ [ 'count' => 1, 'seconds' => 600, ], [ 'count' => 6, 'seconds' => 86400, ], ], 'TempAccountNameAcquisitionThrottle' => [ [ 'count' => 60, 'seconds' => 86400, ], ], 'SpamRegex' => [ ], 'SummarySpamRegex' => [ ], 'EnableDnsBlacklist' => false, 'DnsBlacklistUrls' => [ ], 'ProxyList' => [ ], 'ProxyWhitelist' => [ ], 'SoftBlockRanges' => [ ], 'ApplyIpBlocksToXff' => false, 'RateLimits' => [ 'edit' => [ 'ip' => [ 8, 60, ], 'newbie' => [ 8, 60, ], 'user' => [ 90, 60, ], ], 'move' => [ 'newbie' => [ 2, 120, ], 'user' => [ 8, 60, ], ], 'upload' => [ 'ip' => [ 8, 60, ], 'newbie' => [ 8, 60, ], ], 'rollback' => [ 'user' => [ 10, 60, ], 'newbie' => [ 5, 120, ], ], 'mailpassword' => [ 'ip' => [ 5, 3600, ], ], 'sendemail' => [ 'ip' => [ 5, 86400, ], 'newbie' => [ 5, 86400, ], 'user' => [ 20, 86400, ], ], 'changeemail' => [ 'ip-all' => [ 10, 3600, ], 'user' => [ 4, 86400, ], ], 'confirmemail' => [ 'ip-all' => [ 10, 3600, ], 'user' => [ 4, 86400, ], ], 'purge' => [ 'ip' => [ 30, 60, ], 'user' => [ 30, 60, ], ], 'linkpurge' => [ 'ip' => [ 30, 60, ], 'user' => [ 30, 60, ], ], 'renderfile' => [ 'ip' => [ 700, 30, ], 'user' => [ 700, 30, ], ], 'renderfile-nonstandard' => [ 'ip' => [ 70, 30, ], 'user' => [ 70, 30, ], ], 'stashedit' => [ 'ip' => [ 30, 60, ], 'newbie' => [ 30, 60, ], ], 'stashbasehtml' => [ 'ip' => [ 5, 60, ], 'newbie' => [ 5, 60, ], ], 'changetags' => [ 'ip' => [ 8, 60, ], 'newbie' => [ 8, 60, ], ], 'editcontentmodel' => [ 'newbie' => [ 2, 120, ], 'user' => [ 8, 60, ], ], ], 'RateLimitsExcludedIPs' => [ ], 'PutIPinRC' => true, 'QueryPageDefaultLimit' => 50, 'ExternalQuerySources' => [ ], 'PasswordAttemptThrottle' => [ [ 'count' => 5, 'seconds' => 300, ], [ 'count' => 150, 'seconds' => 172800, ], ], 'GrantPermissions' => [ 'basic' => [ 'autocreateaccount' => true, 'autoconfirmed' => true, 'autopatrol' => true, 'editsemiprotected' => true, 'ipblock-exempt' => true, 'nominornewtalk' => true, 'patrolmarks' => true, 'read' => true, 'unwatchedpages' => true, ], 'highvolume' => [ 'bot' => true, 'apihighlimits' => true, 'noratelimit' => true, 'markbotedits' => true, ], 'import' => [ 'import' => true, 'importupload' => true, ], 'editpage' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'pagelang' => true, ], 'editprotected' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editprotected' => true, ], 'editmycssjs' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editmyusercss' => true, 'editmyuserjson' => true, 'editmyuserjs' => true, ], 'editmyoptions' => [ 'editmyoptions' => true, 'editmyuserjson' => true, ], 'editinterface' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editinterface' => true, 'edituserjson' => true, 'editsitejson' => true, ], 'editsiteconfig' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editinterface' => true, 'edituserjson' => true, 'editsitejson' => true, 'editusercss' => true, 'edituserjs' => true, 'editsitecss' => true, 'editsitejs' => true, ], 'createeditmovepage' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'createpage' => true, 'createtalk' => true, 'delete-redirect' => true, 'move' => true, 'move-rootuserpages' => true, 'move-subpages' => true, 'move-categorypages' => true, 'suppressredirect' => true, ], 'uploadfile' => [ 'upload' => true, 'reupload-own' => true, ], 'uploadeditmovefile' => [ 'upload' => true, 'reupload-own' => true, 'reupload' => true, 'reupload-shared' => true, 'upload_by_url' => true, 'movefile' => true, 'suppressredirect' => true, ], 'patrol' => [ 'patrol' => true, ], 'rollback' => [ 'rollback' => true, ], 'blockusers' => [ 'block' => true, 'blockemail' => true, ], 'viewdeleted' => [ 'browsearchive' => true, 'deletedhistory' => true, 'deletedtext' => true, ], 'viewrestrictedlogs' => [ 'suppressionlog' => true, ], 'delete' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'browsearchive' => true, 'deletedhistory' => true, 'deletedtext' => true, 'delete' => true, 'bigdelete' => true, 'deletelogentry' => true, 'deleterevision' => true, 'undelete' => true, ], 'oversight' => [ 'suppressrevision' => true, 'viewsuppressed' => true, ], 'protect' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editprotected' => true, 'protect' => true, ], 'viewmywatchlist' => [ 'viewmywatchlist' => true, ], 'editmywatchlist' => [ 'editmywatchlist' => true, ], 'sendemail' => [ 'sendemail' => true, ], 'createaccount' => [ 'createaccount' => true, ], 'privateinfo' => [ 'viewmyprivateinfo' => true, ], 'mergehistory' => [ 'mergehistory' => true, ], 'managesessions' => [ 'logout' => true, ], ], 'GrantPermissionGroups' => [ 'basic' => 'hidden', 'editpage' => 'page-interaction', 'createeditmovepage' => 'page-interaction', 'editprotected' => 'page-interaction', 'patrol' => 'page-interaction', 'uploadfile' => 'file-interaction', 'uploadeditmovefile' => 'file-interaction', 'sendemail' => 'email', 'viewmywatchlist' => 'watchlist-interaction', 'editmywatchlist' => 'watchlist-interaction', 'editmycssjs' => 'customization', 'editmyoptions' => 'customization', 'editinterface' => 'administration', 'editsiteconfig' => 'administration', 'rollback' => 'administration', 'blockusers' => 'administration', 'delete' => 'administration', 'viewdeleted' => 'administration', 'viewrestrictedlogs' => 'administration', 'protect' => 'administration', 'oversight' => 'administration', 'createaccount' => 'administration', 'mergehistory' => 'administration', 'import' => 'administration', 'highvolume' => 'high-volume', 'privateinfo' => 'private-information', 'managesessions' => 'private-information', ], 'GrantRiskGroups' => [ 'basic' => 'low', 'editpage' => 'low', 'createeditmovepage' => 'low', 'editprotected' => 'vandalism', 'patrol' => 'low', 'uploadfile' => 'low', 'uploadeditmovefile' => 'low', 'sendemail' => 'security', 'viewmywatchlist' => 'low', 'editmywatchlist' => 'low', 'editmycssjs' => 'security', 'editmyoptions' => 'security', 'editinterface' => 'vandalism', 'editsiteconfig' => 'security', 'rollback' => 'low', 'blockusers' => 'vandalism', 'delete' => 'vandalism', 'viewdeleted' => 'vandalism', 'viewrestrictedlogs' => 'security', 'protect' => 'vandalism', 'oversight' => 'security', 'createaccount' => 'low', 'mergehistory' => 'vandalism', 'import' => 'security', 'highvolume' => 'low', 'privateinfo' => 'low', 'managesessions' => 'low', ], 'EnableBotPasswords' => true, 'BotPasswordsCluster' => false, 'BotPasswordsDatabase' => false, 'BotPasswordsLimit' => 100, 'SecretKey' => false, 'JwtPrivateKey' => false, 'JwtPublicKey' => false, 'AllowUserJs' => false, 'ReauthenticateForActions' => [ 'edituserjs' => 'edituserjscss', 'editusercss' => 'edituserjscss', 'editsitejs' => 'editsitejscss', 'editsitecss' => 'editsitejscss', ], 'AllowUserCss' => false, 'AllowUserCssPrefs' => true, 'UseSiteJs' => true, 'UseSiteCss' => true, 'BreakFrames' => false, 'EditPageFrameOptions' => 'DENY', 'ApiFrameOptions' => 'DENY', 'CSPHeader' => false, 'CSPReportOnlyHeader' => false, 'CSPUseReportURIDirective' => false, 'CSPFalsePositiveUrls' => [ 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'chrome-extension' => true, ], 'AllowCrossOrigin' => false, 'RestAllowCrossOriginCookieAuth' => false, 'SessionSecret' => false, 'CookieExpiration' => 2592000, 'ExtendedLoginCookieExpiration' => 15552000, 'SessionCookieJwtExpiration' => 14400, 'CookieDomain' => '', 'CookiePath' => '/', 'CookieSecure' => 'detect', 'CookiePrefix' => false, 'CookieHttpOnly' => true, 'CookieSameSite' => null, 'CacheVaryCookies' => [ ], 'SessionName' => false, 'CookieSetOnAutoblock' => true, 'CookieSetOnIpBlock' => true, 'DebugLogFile' => '', 'DebugLogPrefix' => '', 'DebugRedirects' => false, 'DebugRawPage' => false, 'DebugComments' => false, 'DebugDumpSql' => false, 'TrxProfilerLimits' => [ 'GET' => [ 'masterConns' => 0, 'writes' => 0, 'readQueryTime' => 5, 'readQueryRows' => 10000, ], 'POST' => [ 'readQueryTime' => 5, 'writeQueryTime' => 1, 'readQueryRows' => 100000, 'maxAffected' => 1000, ], 'POST-nonwrite' => [ 'writes' => 0, 'readQueryTime' => 5, 'readQueryRows' => 10000, ], 'PostSend-GET' => [ 'readQueryTime' => 5, 'writeQueryTime' => 1, 'readQueryRows' => 10000, 'maxAffected' => 1000, 'masterConns' => 0, 'writes' => 0, ], 'PostSend-POST' => [ 'readQueryTime' => 5, 'writeQueryTime' => 1, 'readQueryRows' => 100000, 'maxAffected' => 1000, ], 'JobRunner' => [ 'readQueryTime' => 30, 'writeQueryTime' => 5, 'readQueryRows' => 100000, 'maxAffected' => 500, ], 'Maintenance' => [ 'writeQueryTime' => 5, 'maxAffected' => 1000, ], ], 'DebugLogGroups' => [ ], 'MWLoggerDefaultSpi' => [ 'class' => 'MediaWiki\\Logger\\LegacySpi', ], 'ShowDebug' => false, 'SpecialVersionShowHooks' => false, 'ShowExceptionDetails' => false, 'LogExceptionBacktrace' => true, 'PropagateErrors' => true, 'ShowHostnames' => false, 'OverrideHostname' => false, 'DevelopmentWarnings' => false, 'DeprecationReleaseLimit' => false, 'Profiler' => [ ], 'StatsdServer' => false, 'StatsdMetricPrefix' => 'MediaWiki', 'StatsTarget' => null, 'StatsFormat' => null, 'StatsPrefix' => 'mediawiki', 'OpenTelemetryConfig' => null, 'PageInfoTransclusionLimit' => 50, 'EnableJavaScriptTest' => false, 'DebugToolbar' => false, 'ApiClientErrorSampleRate' => 1.0, 'DisableTextSearch' => false, 'AdvancedSearchHighlighting' => false, 'SearchHighlightBoundaries' => '[\\p{Z}\\p{P}\\p{C}]', 'OpenSearchTemplates' => [ 'application/x-suggestions+json' => false, 'application/x-suggestions+xml' => false, ], 'OpenSearchDefaultLimit' => 10, 'OpenSearchDescriptionLength' => 100, 'SearchSuggestCacheExpiry' => 1200, 'DisableSearchUpdate' => false, 'NamespacesToBeSearchedDefault' => [ true, ], 'DisableInternalSearch' => false, 'SearchForwardUrl' => null, 'SitemapNamespaces' => false, 'SitemapNamespacesPriorities' => false, 'SitemapApiConfig' => [ ], 'SpecialSearchFormOptions' => [ ], 'SearchMatchRedirectPreference' => false, 'SearchRunSuggestedQuery' => true, 'Diff3' => '/usr/bin/diff3', 'Diff' => '/usr/bin/diff', 'PreviewOnOpenNamespaces' => [ 14 => true, ], 'UniversalEditButton' => true, 'UseAutomaticEditSummaries' => true, 'CommandLineDarkBg' => false, 'ReadOnly' => null, 'ReadOnlyWatchedItemStore' => false, 'ReadOnlyFile' => false, 'UpgradeKey' => false, 'GitBin' => '/usr/bin/git', 'GitRepositoryViewers' => [ 'https: 'ssh: 'https: 'git@github\\.com:(.*?)(\\.git)?' => 'https: ], 'InstallerInitialPages' => [ [ 'titlemsg' => 'mainpage', 'text' => '{{subst:int:mainpagetext}}{{subst:int:mainpagedocfooter}}', ], ], 'RCMaxAge' => 7776000, 'WatchersMaxAge' => 15552000, 'UnwatchedPageSecret' => 1, 'RCFilterByAge' => false, 'RCLinkLimits' => [ 50, 100, 250, 500, ], 'RCLinkDays' => [ 1, 3, 7, 14, 30, ], 'RCFeeds' => [ ], 'RCWatchCategoryMembership' => false, 'UseRCPatrol' => true, 'StructuredChangeFiltersLiveUpdatePollingRate' => 3, 'UseNPPatrol' => true, 'UseFilePatrol' => true, 'Feed' => true, 'FeedLimit' => 50, 'FeedCacheTimeout' => 60, 'FeedDiffCutoff' => 32768, 'OverrideSiteFeed' => [ ], 'FeedClasses' => [ 'rss' => 'MediaWiki\\Feed\\RSSFeed', 'atom' => 'MediaWiki\\Feed\\AtomFeed', ], 'AdvertisedFeedTypes' => [ 'atom', ], 'RCShowWatchingUsers' => false, 'RCShowChangedSize' => true, 'RCChangedSizeThreshold' => 500, 'ShowUpdatedMarker' => true, 'DisableAnonTalk' => false, 'UseTagFilter' => true, 'SoftwareTags' => [ 'mw-contentmodelchange' => true, 'mw-new-redirect' => true, 'mw-removed-redirect' => true, 'mw-changed-redirect-target' => true, 'mw-blank' => true, 'mw-replace' => true, 'mw-recreated' => true, 'mw-rollback' => true, 'mw-undo' => true, 'mw-manual-revert' => true, 'mw-reverted' => true, 'mw-server-side-upload' => true, 'mw-ipblock-appeal' => true, 'mw-edited-other-users-js' => true, 'mw-edited-other-users-css' => true, ], 'RestrictedTagViewRights' => [ ], 'UnwatchedPageThreshold' => false, 'RecentChangesFlags' => [ 'newpage' => [ 'letter' => 'newpageletter', 'title' => 'recentchanges-label-newpage', 'legend' => 'recentchanges-legend-newpage', 'grouping' => 'any', ], 'minor' => [ 'letter' => 'minoreditletter', 'title' => 'recentchanges-label-minor', 'legend' => 'recentchanges-legend-minor', 'class' => 'minoredit', 'grouping' => 'all', ], 'bot' => [ 'letter' => 'boteditletter', 'title' => 'recentchanges-label-bot', 'legend' => 'recentchanges-legend-bot', 'class' => 'botedit', 'grouping' => 'all', ], 'unpatrolled' => [ 'letter' => 'unpatrolledletter', 'title' => 'recentchanges-label-unpatrolled', 'legend' => 'recentchanges-legend-unpatrolled', 'grouping' => 'any', ], ], 'WatchlistExpiry' => false, 'EnableWatchstarPopover' => false, 'EnableWatchlistLabels' => false, 'WatchlistLabelsMaxPerUser' => 100, 'WatchlistPurgeRate' => 0.1, 'WatchlistExpiryMaxDuration' => '1 year', 'EnableChangesListQueryPartitioning' => false, 'RightsPage' => null, 'RightsUrl' => null, 'RightsText' => null, 'RightsIcon' => null, 'UseCopyrightUpload' => false, 'MaxCredits' => 0, 'ShowCreditsIfMax' => true, 'ImportSources' => [ ], 'ImportTargetNamespace' => null, 'ExportAllowHistory' => true, 'ExportMaxHistory' => 0, 'ExportAllowListContributors' => false, 'ExportMaxLinkDepth' => 0, 'ExportFromNamespaces' => false, 'ExportAllowAll' => false, 'ExportPagelistLimit' => 5000, 'XmlDumpSchemaVersion' => '0.11', 'WikiFarmSettingsDirectory' => null, 'WikiFarmSettingsExtension' => 'yaml', 'ExtensionFunctions' => [ ], 'ExtensionMessagesFiles' => [ ], 'MessagesDirs' => [ ], 'TranslationAliasesDirs' => [ ], 'ExtensionEntryPointListFiles' => [ ], 'EnableParserLimitReporting' => true, 'ValidSkinNames' => [ ], 'SpecialPages' => [ ], 'ExtensionCredits' => [ ], 'Hooks' => [ ], 'ServiceWiringFiles' => [ ], 'JobClasses' => [ 'deletePage' => 'MediaWiki\\Page\\DeletePageJob', 'refreshLinks' => 'MediaWiki\\JobQueue\\Jobs\\RefreshLinksJob', 'deleteLinks' => 'MediaWiki\\Page\\DeleteLinksJob', 'htmlCacheUpdate' => 'MediaWiki\\JobQueue\\Jobs\\HTMLCacheUpdateJob', 'sendMail' => [ 'class' => 'MediaWiki\\Mail\\EmaillingJob', 'services' => [ 'Emailer', ], ], 'enotifNotify' => [ 'class' => 'MediaWiki\\RecentChanges\\RecentChangeNotifyJob', 'services' => [ 'RecentChangeLookup', ], ], 'fixDoubleRedirect' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\DoubleRedirectJob', 'services' => [ 'RevisionLookup', 'MagicWordFactory', 'WikiPageFactory', ], 'needsPage' => true, ], 'AssembleUploadChunks' => 'MediaWiki\\JobQueue\\Jobs\\AssembleUploadChunksJob', 'PublishStashedFile' => 'MediaWiki\\JobQueue\\Jobs\\PublishStashedFileJob', 'ThumbnailRender' => 'MediaWiki\\JobQueue\\Jobs\\ThumbnailRenderJob', 'UploadFromUrl' => 'MediaWiki\\JobQueue\\Jobs\\UploadFromUrlJob', 'recentChangesUpdate' => 'MediaWiki\\RecentChanges\\RecentChangesUpdateJob', 'refreshLinksPrioritized' => 'MediaWiki\\JobQueue\\Jobs\\RefreshLinksJob', 'refreshLinksDynamic' => 'MediaWiki\\JobQueue\\Jobs\\RefreshLinksJob', 'activityUpdateJob' => 'MediaWiki\\Watchlist\\ActivityUpdateJob', 'categoryMembershipChange' => [ 'class' => 'MediaWiki\\RecentChanges\\CategoryMembershipChangeJob', 'services' => [ 'RecentChangeFactory', ], ], 'CategoryCountUpdateJob' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\CategoryCountUpdateJob', 'services' => [ 'ConnectionProvider', 'NamespaceInfo', ], ], 'clearUserWatchlist' => 'MediaWiki\\Watchlist\\ClearUserWatchlistJob', 'watchlistExpiry' => [ 'class' => 'MediaWiki\\Watchlist\\WatchlistExpiryJob', 'needsPage' => false, ], 'cdnPurge' => 'MediaWiki\\JobQueue\\Jobs\\CdnPurgeJob', 'userGroupExpiry' => 'MediaWiki\\User\\UserGroupExpiryJob', 'clearWatchlistNotifications' => 'MediaWiki\\Watchlist\\ClearWatchlistNotificationsJob', 'userOptionsUpdate' => 'MediaWiki\\User\\Options\\UserOptionsUpdateJob', 'revertedTagUpdate' => 'MediaWiki\\JobQueue\\Jobs\\RevertedTagUpdateJob', 'null' => 'MediaWiki\\JobQueue\\Jobs\\NullJob', 'userEditCountInit' => 'MediaWiki\\User\\UserEditCountInitJob', 'renameUserTable' => [ 'class' => 'MediaWiki\\RenameUser\\Job\\RenameUserTableJob', 'services' => [ 'MainConfig', 'DBLoadBalancerFactory', ], ], 'renameUserDerived' => [ 'class' => 'MediaWiki\\RenameUser\\Job\\RenameUserDerivedJob', 'services' => [ 'RenameUserFactory', 'UserFactory', ], ], 'renameUser' => [ 'class' => 'MediaWiki\\RenameUser\\Job\\RenameUserTableJob', 'services' => [ 'MainConfig', 'DBLoadBalancerFactory', ], ], ], 'JobTypesExcludedFromDefaultQueue' => [ 'AssembleUploadChunks', 'PublishStashedFile', 'UploadFromUrl', ], 'JobBackoffThrottling' => [ ], 'JobTypeConf' => [ 'default' => [ 'class' => 'MediaWiki\\JobQueue\\JobQueueDB', 'order' => 'random', 'claimTTL' => 3600, ], ], 'JobQueueIncludeInMaxLagFactor' => false, 'SpecialPageCacheUpdates' => [ 'Statistics' => [ 'MediaWiki\\Deferred\\SiteStatsUpdate', 'cacheUpdate', ], ], 'PagePropLinkInvalidations' => [ 'hiddencat' => 'categorylinks', ], 'CategoryMagicGallery' => true, 'CategoryPagingLimit' => 200, 'CategoryCollation' => 'uppercase', 'TempCategoryCollations' => [ ], 'SortedCategories' => false, 'TrackingCategories' => [ ], 'LogTypes' => [ '', 'block', 'protect', 'rights', 'delete', 'upload', 'move', 'import', 'interwiki', 'patrol', 'merge', 'suppress', 'tag', 'managetags', 'contentmodel', 'renameuser', ], 'LogRestrictions' => [ 'suppress' => 'suppressionlog', ], 'FilterLogTypes' => [ 'patrol' => true, 'tag' => true, 'newusers' => false, ], 'LogNames' => [ '' => 'all-logs-page', 'block' => 'blocklogpage', 'protect' => 'protectlogpage', 'rights' => 'rightslog', 'delete' => 'dellogpage', 'upload' => 'uploadlogpage', 'move' => 'movelogpage', 'import' => 'importlogpage', 'patrol' => 'patrol-log-page', 'merge' => 'mergelog', 'suppress' => 'suppressionlog', ], 'LogHeaders' => [ '' => 'alllogstext', 'block' => 'blocklogtext', 'delete' => 'dellogpagetext', 'import' => 'importlogpagetext', 'merge' => 'mergelogpagetext', 'move' => 'movelogpagetext', 'patrol' => 'patrol-log-header', 'protect' => 'protectlogtext', 'rights' => 'rightslogtext', 'suppress' => 'suppressionlogtext', 'upload' => 'uploadlogpagetext', ], 'LogActions' => [ ], 'LogActionsHandlers' => [ 'block/block' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'block/reblock' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'block/unblock' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'contentmodel/change' => 'MediaWiki\\Logging\\ContentModelLogFormatter', 'contentmodel/new' => 'MediaWiki\\Logging\\ContentModelLogFormatter', 'delete/delete' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/delete_redir' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/delete_redir2' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/event' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/restore' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'delete/revision' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'import/interwiki' => 'MediaWiki\\Logging\\ImportLogFormatter', 'import/upload' => 'MediaWiki\\Logging\\ImportLogFormatter', 'interwiki/iw_add' => 'MediaWiki\\Logging\\InterwikiLogFormatter', 'interwiki/iw_delete' => 'MediaWiki\\Logging\\InterwikiLogFormatter', 'interwiki/iw_edit' => 'MediaWiki\\Logging\\InterwikiLogFormatter', 'managetags/activate' => 'MediaWiki\\Logging\\LogFormatter', 'managetags/create' => 'MediaWiki\\Logging\\LogFormatter', 'managetags/deactivate' => 'MediaWiki\\Logging\\LogFormatter', 'managetags/delete' => 'MediaWiki\\Logging\\LogFormatter', 'merge/merge' => [ 'class' => 'MediaWiki\\Logging\\MergeLogFormatter', 'services' => [ 'TitleParser', ], ], 'merge/merge-into' => [ 'class' => 'MediaWiki\\Logging\\MergeLogFormatter', 'services' => [ 'TitleParser', ], ], 'move/move' => [ 'class' => 'MediaWiki\\Logging\\MoveLogFormatter', 'services' => [ 'TitleParser', ], ], 'move/move_redir' => [ 'class' => 'MediaWiki\\Logging\\MoveLogFormatter', 'services' => [ 'TitleParser', ], ], 'patrol/patrol' => 'MediaWiki\\Logging\\PatrolLogFormatter', 'patrol/autopatrol' => 'MediaWiki\\Logging\\PatrolLogFormatter', 'protect/modify' => [ 'class' => 'MediaWiki\\Logging\\ProtectLogFormatter', 'services' => [ 'TitleParser', ], ], 'protect/move_prot' => [ 'class' => 'MediaWiki\\Logging\\ProtectLogFormatter', 'services' => [ 'TitleParser', ], ], 'protect/protect' => [ 'class' => 'MediaWiki\\Logging\\ProtectLogFormatter', 'services' => [ 'TitleParser', ], ], 'protect/unprotect' => [ 'class' => 'MediaWiki\\Logging\\ProtectLogFormatter', 'services' => [ 'TitleParser', ], ], 'renameuser/renameuser' => [ 'class' => 'MediaWiki\\Logging\\RenameuserLogFormatter', 'services' => [ 'TitleParser', ], ], 'rights/autopromote' => 'MediaWiki\\Logging\\RightsLogFormatter', 'rights/rights' => 'MediaWiki\\Logging\\RightsLogFormatter', 'suppress/block' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'suppress/delete' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'suppress/event' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'suppress/reblock' => [ 'class' => 'MediaWiki\\Logging\\BlockLogFormatter', 'services' => [ 'TitleParser', 'NamespaceInfo', ], ], 'suppress/revision' => 'MediaWiki\\Logging\\DeleteLogFormatter', 'tag/update' => 'MediaWiki\\Logging\\TagLogFormatter', 'upload/overwrite' => 'MediaWiki\\Logging\\UploadLogFormatter', 'upload/revert' => 'MediaWiki\\Logging\\UploadLogFormatter', 'upload/upload' => 'MediaWiki\\Logging\\UploadLogFormatter', ], 'ActionFilteredLogs' => [ 'block' => [ 'block' => [ 'block', ], 'reblock' => [ 'reblock', ], 'unblock' => [ 'unblock', ], ], 'contentmodel' => [ 'change' => [ 'change', ], 'new' => [ 'new', ], ], 'delete' => [ 'delete' => [ 'delete', ], 'delete_redir' => [ 'delete_redir', 'delete_redir2', ], 'restore' => [ 'restore', ], 'event' => [ 'event', ], 'revision' => [ 'revision', ], ], 'import' => [ 'interwiki' => [ 'interwiki', ], 'upload' => [ 'upload', ], ], 'managetags' => [ 'create' => [ 'create', ], 'delete' => [ 'delete', ], 'activate' => [ 'activate', ], 'deactivate' => [ 'deactivate', ], ], 'move' => [ 'move' => [ 'move', ], 'move_redir' => [ 'move_redir', ], ], 'newusers' => [ 'create' => [ 'create', 'newusers', ], 'create2' => [ 'create2', ], 'autocreate' => [ 'autocreate', ], 'byemail' => [ 'byemail', ], ], 'protect' => [ 'protect' => [ 'protect', ], 'modify' => [ 'modify', ], 'unprotect' => [ 'unprotect', ], 'move_prot' => [ 'move_prot', ], ], 'rights' => [ 'rights' => [ 'rights', ], 'autopromote' => [ 'autopromote', ], ], 'suppress' => [ 'event' => [ 'event', ], 'revision' => [ 'revision', ], 'delete' => [ 'delete', ], 'block' => [ 'block', ], 'reblock' => [ 'reblock', ], ], 'upload' => [ 'upload' => [ 'upload', ], 'overwrite' => [ 'overwrite', ], 'revert' => [ 'revert', ], ], ], 'NewUserLog' => true, 'PageCreationLog' => true, 'AllowSpecialInclusion' => true, 'DisableQueryPageUpdate' => false, 'CountCategorizedImagesAsUsed' => false, 'MaxRedirectLinksRetrieved' => 500, 'RangeContributionsCIDRLimit' => [ 'IPv4' => 16, 'IPv6' => 32, ], 'Actions' => [ ], 'DefaultRobotPolicy' => 'index,follow', 'NamespaceRobotPolicies' => [ ], 'ArticleRobotPolicies' => [ ], 'ExemptFromUserRobotsControl' => null, 'DebugAPI' => false, 'APIModules' => [ ], 'APIFormatModules' => [ ], 'APIMetaModules' => [ ], 'APIPropModules' => [ ], 'APIListModules' => [ ], 'APIMaxDBRows' => 5000, 'APIMaxResultSize' => 8388608, 'APIMaxUncachedDiffs' => 1, 'APIMaxLagThreshold' => 7, 'APICacheHelpTimeout' => 3600, 'APIUselessQueryPages' => [ 'MIMEsearch', 'LinkSearch', ], 'AjaxLicensePreview' => true, 'CrossSiteAJAXdomains' => [ ], 'CrossSiteAJAXdomainExceptions' => [ ], 'AllowedCorsHeaders' => [ 'Accept', 'Accept-Language', 'Content-Language', 'Content-Type', 'Accept-Encoding', 'DNT', 'Origin', 'User-Agent', 'Api-User-Agent', 'Promise-Non-Write-API-Action', 'Access-Control-Max-Age', 'Authorization', ], 'RestAPIAdditionalRouteFiles' => [ ], 'RestLocalModuleTestBaseUrl' => null, 'RestModuleOverrides' => [ ], 'RestExternalModules' => [ ], 'RestTermsOfServiceUrl' => null, 'MaxShellMemory' => 307200, 'MaxShellFileSize' => 102400, 'MaxShellTime' => 180, 'MaxShellWallClockTime' => 180, 'ShellCgroup' => false, 'PhpCli' => '/usr/bin/php', 'ShellRestrictionMethod' => 'autodetect', 'ShellboxUrls' => [ 'default' => null, ], 'ShellboxSecretKey' => null, 'ShellboxShell' => '/bin/sh', 'HTTPTimeout' => 25, 'HTTPConnectTimeout' => 5.0, 'HTTPMaxTimeout' => 0, 'HTTPMaxConnectTimeout' => 0, 'HTTPImportTimeout' => 25, 'HTTPUserAgentContact' => false, 'AsyncHTTPTimeout' => 25, 'HTTPProxy' => '', 'LocalVirtualHosts' => [ ], 'LocalHTTPProxy' => false, 'AllowExternalReqID' => false, 'GenerateReqIDFormat' => 'rand24', 'JobRunRate' => 1, 'RunJobsAsync' => false, 'UpdateRowsPerJob' => 300, 'UpdateRowsPerQuery' => 100, 'RedirectOnLogin' => null, 'EventRelayerConfig' => [ 'default' => [ 'class' => 'Wikimedia\\EventRelayer\\EventRelayerNull', ], ], 'Pingback' => false, 'OriginTrials' => [ ], 'ReportToExpiry' => 86400, 'ReportToEndpoints' => [ ], 'FeaturePolicyReportOnly' => [ ], 'SkinsPreferred' => [ 'vector-2022', 'vector', ], 'SpecialContributeSkinsEnabled' => [ ], 'SpecialContributeNewPageTarget' => null, 'EnableEditRecovery' => false, 'EditRecoveryExpiry' => 2592000, 'UseCodexSpecialBlock' => false, 'ShowLogoutConfirmation' => false, 'EnableProtectionIndicators' => true, 'OutputPipelineStages' => [ ], 'FeatureShutdown' => [ ], 'UseLeximorph' => false, 'UsePostprocCacheLegacy' => false, 'UsePostprocCacheParsoid' => true, 'ParserOptionsLogUnsafeSampleRate' => 0, 'ReturnExperimentalPFragmentTypes' => [ ], 'UseParsoidLinksUpdate' => true, 'UseParsoidMessages' => false, 'UseParsoidParser' => false, 'SiteLookup' => [ ], ], 'type' => [ 'ConfigRegistry' => 'object', 'AssumeProxiesUseDefaultProtocolPorts' => 'boolean', 'ForceHTTPS' => 'boolean', 'ExtensionDirectory' => [ 'string', 'null', ], 'StyleDirectory' => [ 'string', 'null', ], 'UploadDirectory' => [ 'string', 'boolean', 'null', ], 'Logos' => [ 'object', 'boolean', ], 'ReferrerPolicy' => [ 'array', 'string', 'boolean', ], 'ActionPaths' => 'object', 'MainPageIsDomainRoot' => 'boolean', 'ImgAuthUrlPathMap' => 'object', 'LocalFileRepo' => 'object', 'ForeignFileRepos' => 'array', 'UseSharedUploads' => 'boolean', 'SharedUploadDirectory' => [ 'string', 'null', ], 'SharedUploadPath' => [ 'string', 'null', ], 'HashedSharedUploadDirectory' => 'boolean', 'FetchCommonsDescriptions' => 'boolean', 'SharedUploadDBname' => [ 'boolean', 'string', ], 'SharedUploadDBprefix' => 'string', 'SharedUploadDBschema' => [ 'string', 'null', ], 'CacheSharedUploads' => 'boolean', 'ForeignUploadTargets' => 'array', 'UploadDialog' => 'object', 'FileBackends' => 'object', 'LockManagers' => 'array', 'DefaultLockManager' => [ 'string', 'null', ], 'CopyUploadsDomains' => 'array', 'CopyUploadTimeout' => [ 'boolean', 'integer', ], 'SharedThumbnailScriptPath' => [ 'string', 'boolean', ], 'HashedUploadDirectory' => 'boolean', 'CSPUploadEntryPoint' => 'boolean', 'FileExtensions' => 'array', 'ProhibitedFileExtensions' => 'array', 'MimeTypeExclusions' => 'array', 'TrustedMediaFormats' => 'array', 'MediaHandlers' => 'object', 'NativeImageLazyLoading' => 'boolean', 'ParserTestMediaHandlers' => 'object', 'MaxInterlacingAreas' => 'object', 'SVGConverters' => 'object', 'SVGNativeRendering' => [ 'string', 'boolean', ], 'MaxImageArea' => [ 'string', 'integer', 'boolean', ], 'WebPThumbnailType' => 'array', 'TiffThumbnailType' => 'array', 'GenerateThumbnailOnParse' => 'boolean', 'EnableAutoRotation' => [ 'boolean', 'null', ], 'Antivirus' => [ 'string', 'null', ], 'AntivirusSetup' => 'object', 'MimeDetectorCommand' => [ 'string', 'null', ], 'XMLMimeTypes' => 'object', 'ImageLimits' => 'array', 'ThumbLimits' => 'array', 'ThumbnailNamespaces' => 'array', 'ThumbnailSteps' => [ 'array', 'null', ], 'ThumbnailBuckets' => [ 'array', 'null', ], 'UploadThumbnailRenderMap' => 'object', 'GalleryOptions' => 'object', 'DjvuDump' => [ 'string', 'null', ], 'DjvuRenderer' => [ 'string', 'null', ], 'DjvuTxt' => [ 'string', 'null', ], 'DjvuPostProcessor' => [ 'string', 'null', ], 'SMTP' => [ 'boolean', 'object', ], 'EnotifFromEditor' => 'boolean', 'EmailConfirmationBanner' => 'boolean', 'EnotifRevealEditorAddress' => 'boolean', 'UsersNotifiedOnAllChanges' => 'object', 'DBmwschema' => [ 'string', 'null', ], 'SharedTables' => 'array', 'DBservers' => [ 'boolean', 'array', ], 'LBFactoryConf' => 'object', 'LocalDatabases' => 'array', 'VirtualDomainsMapping' => 'object', 'RemoteVirtualDomainsMapping' => 'object', 'FileSchemaMigrationStage' => 'integer', 'ExternalLinksDomainGaps' => 'object', 'ContentHandlers' => 'object', 'NamespaceContentModels' => 'object', 'TextModelsToParse' => 'array', 'ExternalStores' => 'array', 'ExternalServers' => 'object', 'DefaultExternalStore' => [ 'array', 'boolean', ], 'RevisionCacheExpiry' => 'integer', 'PageLanguageUseDB' => 'boolean', 'DiffEngine' => [ 'string', 'null', ], 'ExternalDiffEngine' => [ 'string', 'boolean', ], 'Wikidiff2Options' => 'object', 'RequestTimeLimit' => [ 'integer', 'null', ], 'CriticalSectionTimeLimit' => 'number', 'PoolCounterConf' => [ 'object', 'null', ], 'PoolCountClientConf' => 'object', 'MaxUserDBWriteDuration' => [ 'integer', 'boolean', ], 'MaxJobDBWriteDuration' => [ 'integer', 'boolean', ], 'MultiShardSiteStats' => 'boolean', 'ObjectCaches' => 'object', 'WANObjectCache' => 'object', 'MicroStashType' => [ 'string', 'integer', ], 'ParsoidCacheConfig' => 'object', 'ParsoidSelectiveUpdateSampleRate' => 'integer', 'SplitParsoidParserCache' => 'boolean', 'ParserCacheFilterConfig' => 'object', 'ChronologyProtectorSecret' => 'string', 'SuspiciousIpExpiry' => [ 'integer', 'boolean', ], 'MemCachedServers' => 'array', 'LocalisationCacheConf' => 'object', 'ExtensionInfoMTime' => [ 'integer', 'boolean', ], 'CdnServers' => 'object', 'CdnServersNoPurge' => 'object', 'HTCPRouting' => 'object', 'GrammarForms' => 'object', 'ExtraInterlanguageLinkPrefixes' => 'array', 'InterlanguageLinkCodeMap' => 'object', 'ExtraLanguageNames' => 'object', 'ExtraLanguageCodes' => 'object', 'DummyLanguageCodes' => 'object', 'DisabledVariants' => 'object', 'ForceUIMsgAsContentMsg' => 'object', 'RawHtmlMessages' => 'array', 'OverrideUcfirstCharacters' => 'object', 'XhtmlNamespaces' => 'object', 'BrowserFormatDetection' => 'string', 'SkinMetaTags' => 'object', 'SkipSkins' => 'object', 'FragmentMode' => 'array', 'FooterIcons' => 'object', 'PageShareSkinsEnabled' => 'array', 'InterwikiLogoOverride' => 'array', 'ResourceModules' => 'object', 'ResourceModuleSkinStyles' => 'object', 'ResourceLoaderSources' => 'object', 'ResourceLoaderMaxage' => 'object', 'ResourceLoaderMaxQueryLength' => [ 'integer', 'boolean', ], 'CanonicalNamespaceNames' => 'object', 'ExtraNamespaces' => 'object', 'ExtraGenderNamespaces' => 'object', 'NamespaceAliases' => 'object', 'CapitalLinkOverrides' => 'object', 'NamespacesWithSubpages' => 'object', 'NamespacesWithoutAutoSummaries' => 'array', 'ContentNamespaces' => 'array', 'ShortPagesNamespaceExclusions' => 'array', 'ExtraSignatureNamespaces' => 'array', 'InvalidRedirectTargets' => 'array', 'LocalInterwikis' => 'array', 'InterwikiCache' => [ 'boolean', 'object', ], 'SiteTypes' => 'object', 'UrlProtocols' => 'array', 'TidyConfig' => 'object', 'ParsoidSettings' => 'object', 'ParsoidExperimentalParserFunctionOutput' => 'boolean', 'NoFollowNsExceptions' => 'array', 'NoFollowDomainExceptions' => 'array', 'ExternalLinksIgnoreDomains' => 'array', 'EnableMagicLinks' => 'object', 'ManualRevertSearchRadius' => 'integer', 'RevertedTagMaxDepth' => 'integer', 'CentralIdLookupProviders' => 'object', 'CentralIdLookupProvider' => 'string', 'UserRegistrationProviders' => 'object', 'PasswordPolicy' => 'object', 'AuthManagerConfig' => [ 'object', 'null', ], 'AuthManagerAutoConfig' => 'object', 'RememberMe' => 'string', 'ReauthenticateTime' => 'object', 'ChangeCredentialsBlacklist' => 'array', 'RemoveCredentialsBlacklist' => 'array', 'PasswordConfig' => 'object', 'PasswordResetRoutes' => 'object', 'SignatureAllowedLintErrors' => 'array', 'ReservedUsernames' => 'array', 'DefaultUserOptions' => 'object', 'ConditionalUserOptions' => 'object', 'HiddenPrefs' => 'array', 'UserJsPrefLimit' => 'integer', 'AuthenticationTokenVersion' => [ 'string', 'null', ], 'SessionProviders' => 'object', 'AutoCreateTempUser' => 'object', 'AutoblockExemptions' => 'array', 'BlockCIDRLimit' => 'object', 'EnableMultiBlocks' => 'boolean', 'GroupPermissions' => 'object', 'PrivilegedGroups' => 'array', 'RevokePermissions' => 'object', 'GroupInheritsPermissions' => 'object', 'ImplicitGroups' => 'array', 'GroupsAddToSelf' => 'object', 'GroupsRemoveFromSelf' => 'object', 'RestrictedGroups' => 'object', 'UserRequirementsPrivateConditions' => 'array', 'RestrictionTypes' => 'array', 'RestrictionLevels' => 'array', 'CascadingRestrictionLevels' => 'array', 'SemiprotectedRestrictionLevels' => 'array', 'NamespaceProtection' => 'object', 'RestrictUserPageEditing' => 'boolean', 'NonincludableNamespaces' => 'object', 'Autopromote' => 'object', 'AutopromoteOnce' => 'object', 'AutopromoteOnceRCExcludedGroups' => 'array', 'AddGroups' => 'object', 'RemoveGroups' => 'object', 'AvailableRights' => 'array', 'ImplicitRights' => 'array', 'AccountCreationThrottle' => [ 'integer', 'array', ], 'TempAccountCreationThrottle' => 'array', 'TempAccountNameAcquisitionThrottle' => 'array', 'SpamRegex' => 'array', 'SummarySpamRegex' => 'array', 'DnsBlacklistUrls' => 'array', 'ProxyList' => [ 'string', 'array', ], 'ProxyWhitelist' => 'array', 'SoftBlockRanges' => 'array', 'RateLimits' => 'object', 'RateLimitsExcludedIPs' => 'array', 'ExternalQuerySources' => 'object', 'PasswordAttemptThrottle' => 'array', 'GrantPermissions' => 'object', 'GrantPermissionGroups' => 'object', 'GrantRiskGroups' => 'object', 'EnableBotPasswords' => 'boolean', 'BotPasswordsCluster' => [ 'string', 'boolean', ], 'BotPasswordsDatabase' => [ 'string', 'boolean', ], 'BotPasswordsLimit' => 'integer', 'ReauthenticateForActions' => 'object', 'CSPHeader' => [ 'boolean', 'object', ], 'CSPReportOnlyHeader' => [ 'boolean', 'object', ], 'CSPUseReportURIDirective' => [ 'boolean', 'object', ], 'CSPFalsePositiveUrls' => 'object', 'AllowCrossOrigin' => 'boolean', 'RestAllowCrossOriginCookieAuth' => 'boolean', 'CookieSameSite' => [ 'string', 'null', ], 'CacheVaryCookies' => 'array', 'TrxProfilerLimits' => 'object', 'DebugLogGroups' => 'object', 'MWLoggerDefaultSpi' => 'object', 'Profiler' => 'object', 'StatsTarget' => [ 'string', 'null', ], 'StatsFormat' => [ 'string', 'null', ], 'StatsPrefix' => 'string', 'OpenTelemetryConfig' => [ 'object', 'null', ], 'OpenSearchTemplates' => 'object', 'NamespacesToBeSearchedDefault' => 'object', 'SitemapNamespaces' => [ 'boolean', 'array', ], 'SitemapNamespacesPriorities' => [ 'boolean', 'object', ], 'SitemapApiConfig' => 'object', 'SpecialSearchFormOptions' => 'object', 'SearchMatchRedirectPreference' => 'boolean', 'SearchRunSuggestedQuery' => 'boolean', 'PreviewOnOpenNamespaces' => 'object', 'ReadOnlyWatchedItemStore' => 'boolean', 'GitRepositoryViewers' => 'object', 'InstallerInitialPages' => 'array', 'RCLinkLimits' => 'array', 'RCLinkDays' => 'array', 'RCFeeds' => 'object', 'OverrideSiteFeed' => 'object', 'FeedClasses' => 'object', 'AdvertisedFeedTypes' => 'array', 'SoftwareTags' => 'object', 'RestrictedTagViewRights' => 'object', 'RecentChangesFlags' => 'object', 'WatchlistExpiry' => 'boolean', 'EnableWatchstarPopover' => 'boolean', 'EnableWatchlistLabels' => 'boolean', 'WatchlistLabelsMaxPerUser' => 'integer', 'WatchlistPurgeRate' => 'number', 'WatchlistExpiryMaxDuration' => [ 'string', 'null', ], 'EnableChangesListQueryPartitioning' => 'boolean', 'ImportSources' => 'object', 'ExtensionFunctions' => 'array', 'ExtensionMessagesFiles' => 'object', 'MessagesDirs' => 'object', 'TranslationAliasesDirs' => 'object', 'ExtensionEntryPointListFiles' => 'object', 'ValidSkinNames' => 'object', 'SpecialPages' => 'object', 'ExtensionCredits' => 'object', 'Hooks' => 'object', 'ServiceWiringFiles' => 'array', 'JobClasses' => 'object', 'JobTypesExcludedFromDefaultQueue' => 'array', 'JobBackoffThrottling' => 'object', 'JobTypeConf' => 'object', 'SpecialPageCacheUpdates' => 'object', 'PagePropLinkInvalidations' => 'object', 'TempCategoryCollations' => 'array', 'SortedCategories' => 'boolean', 'TrackingCategories' => 'array', 'LogTypes' => 'array', 'LogRestrictions' => 'object', 'FilterLogTypes' => 'object', 'LogNames' => 'object', 'LogHeaders' => 'object', 'LogActions' => 'object', 'LogActionsHandlers' => 'object', 'ActionFilteredLogs' => 'object', 'RangeContributionsCIDRLimit' => 'object', 'Actions' => 'object', 'NamespaceRobotPolicies' => 'object', 'ArticleRobotPolicies' => 'object', 'ExemptFromUserRobotsControl' => [ 'array', 'null', ], 'APIModules' => 'object', 'APIFormatModules' => 'object', 'APIMetaModules' => 'object', 'APIPropModules' => 'object', 'APIListModules' => 'object', 'APIUselessQueryPages' => 'array', 'CrossSiteAJAXdomains' => 'object', 'CrossSiteAJAXdomainExceptions' => 'object', 'AllowedCorsHeaders' => 'array', 'RestAPIAdditionalRouteFiles' => 'array', 'RestLocalModuleTestBaseUrl' => [ 'string', 'null', ], 'RestModuleOverrides' => 'object', 'RestExternalModules' => 'object', 'RestTermsOfServiceUrl' => [ 'string', 'null', ], 'ShellRestrictionMethod' => [ 'string', 'boolean', ], 'ShellboxUrls' => 'object', 'ShellboxSecretKey' => [ 'string', 'null', ], 'ShellboxShell' => [ 'string', 'null', ], 'HTTPTimeout' => 'number', 'HTTPConnectTimeout' => 'number', 'HTTPMaxTimeout' => 'number', 'HTTPMaxConnectTimeout' => 'number', 'HTTPUserAgentContact' => [ 'string', 'boolean', ], 'LocalVirtualHosts' => 'object', 'LocalHTTPProxy' => [ 'string', 'boolean', ], 'GenerateReqIDFormat' => 'string', 'EventRelayerConfig' => 'object', 'Pingback' => 'boolean', 'OriginTrials' => 'array', 'ReportToExpiry' => 'integer', 'ReportToEndpoints' => 'array', 'FeaturePolicyReportOnly' => 'array', 'SkinsPreferred' => 'array', 'SpecialContributeSkinsEnabled' => 'array', 'SpecialContributeNewPageTarget' => [ 'string', 'null', ], 'EnableEditRecovery' => 'boolean', 'EditRecoveryExpiry' => 'integer', 'UseCodexSpecialBlock' => 'boolean', 'ShowLogoutConfirmation' => 'boolean', 'EnableProtectionIndicators' => 'boolean', 'OutputPipelineStages' => 'object', 'FeatureShutdown' => 'array', 'UseLeximorph' => 'boolean', 'UsePostprocCacheLegacy' => 'boolean', 'UsePostprocCacheParsoid' => 'boolean', 'ParserOptionsLogUnsafeSampleRate' => 'integer', 'ReturnExperimentalPFragmentTypes' => 'array', 'UseParsoidLinksUpdate' => [ 'boolean', 'null', ], 'UseParsoidMessages' => [ 'boolean', 'null', ], 'UseParsoidParser' => 'boolean', 'SiteLookup' => 'object', ], 'mergeStrategy' => [ 'WebPThumbnailType' => 'replace', 'TiffThumbnailType' => 'replace', 'LBFactoryConf' => 'replace', 'InterwikiCache' => 'replace', 'PasswordPolicy' => 'array_replace_recursive', 'AuthManagerAutoConfig' => 'array_plus_2d', 'GroupPermissions' => 'array_plus_2d', 'RevokePermissions' => 'array_plus_2d', 'AddGroups' => 'array_merge_recursive', 'RemoveGroups' => 'array_merge_recursive', 'RateLimits' => 'array_plus_2d', 'GrantPermissions' => 'array_plus_2d', 'MWLoggerDefaultSpi' => 'replace', 'Profiler' => 'replace', 'Hooks' => 'array_merge_recursive', 'RestModuleOverrides' => 'array_replace_recursive', 'RestExternalModules' => 'array_replace_recursive', ], 'dynamicDefault' => [ 'UsePathInfo' => [ 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultUsePathInfo', ], ], 'Script' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultScript', ], ], 'LoadScript' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLoadScript', ], ], 'RestPath' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultRestPath', ], ], 'StylePath' => [ 'use' => [ 'ResourceBasePath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultStylePath', ], ], 'LocalStylePath' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLocalStylePath', ], ], 'ExtensionAssetsPath' => [ 'use' => [ 'ResourceBasePath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultExtensionAssetsPath', ], ], 'ArticlePath' => [ 'use' => [ 'Script', 'UsePathInfo', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultArticlePath', ], ], 'UploadPath' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultUploadPath', ], ], 'FileCacheDirectory' => [ 'use' => [ 'UploadDirectory', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultFileCacheDirectory', ], ], 'Logo' => [ 'use' => [ 'ResourceBasePath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLogo', ], ], 'DeletedDirectory' => [ 'use' => [ 'UploadDirectory', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultDeletedDirectory', ], ], 'ShowEXIF' => [ 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultShowEXIF', ], ], 'SharedPrefix' => [ 'use' => [ 'DBprefix', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultSharedPrefix', ], ], 'SharedSchema' => [ 'use' => [ 'DBmwschema', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultSharedSchema', ], ], 'DBerrorLogTZ' => [ 'use' => [ 'Localtimezone', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultDBerrorLogTZ', ], ], 'Localtimezone' => [ 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLocaltimezone', ], ], 'LocalTZoffset' => [ 'use' => [ 'Localtimezone', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultLocalTZoffset', ], ], 'ResourceBasePath' => [ 'use' => [ 'ScriptPath', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultResourceBasePath', ], ], 'MetaNamespace' => [ 'use' => [ 'Sitename', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultMetaNamespace', ], ], 'CookieSecure' => [ 'use' => [ 'ForceHTTPS', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultCookieSecure', ], ], 'CookiePrefix' => [ 'use' => [ 'SharedDB', 'SharedPrefix', 'SharedTables', 'DBname', 'DBprefix', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultCookiePrefix', ], ], 'ReadOnlyFile' => [ 'use' => [ 'UploadDirectory', ], 'callback' => [ 'MediaWiki\\MainConfigSchema', 'getDefaultReadOnlyFile', ], ], ], ], 'config-schema' => [ 'UploadStashScalerBaseUrl' => [ 'deprecated' => 'since 1.36 Use thumbProxyUrl in $wgLocalFileRepo', ], 'IllegalFileChars' => [ 'deprecated' => 'since 1.41; no longer customizable', ], 'ThumbnailNamespaces' => [ 'items' => [ 'type' => 'integer', ], ], 'LocalDatabases' => [ 'items' => [ 'type' => 'string', ], ], 'ParserCacheFilterConfig' => [ 'additionalProperties' => [ 'type' => 'object', 'description' => 'A map of namespace IDs to filter definitions.', 'additionalProperties' => [ 'type' => 'object', 'description' => 'A map of filter names to values.', 'properties' => [ 'minCpuTime' => [ 'type' => 'number', ], ], ], ], ], 'RawHtmlMessages' => [ 'items' => [ 'type' => 'string', ], ], 'InterwikiLogoOverride' => [ 'items' => [ 'type' => 'string', ], ], 'LegalTitleChars' => [ 'deprecated' => 'since 1.41; use Extension:TitleBlacklist to customize', ], 'ReauthenticateTime' => [ 'additionalProperties' => [ 'type' => [ 'integer', 'array', ], ], ], 'ChangeCredentialsBlacklist' => [ 'items' => [ 'type' => 'string', ], ], 'RemoveCredentialsBlacklist' => [ 'items' => [ 'type' => 'string', ], ], 'GroupPermissions' => [ 'additionalProperties' => [ 'type' => 'object', 'additionalProperties' => [ 'type' => 'boolean', ], ], ], 'GroupInheritsPermissions' => [ 'additionalProperties' => [ 'type' => 'string', ], ], 'AvailableRights' => [ 'items' => [ 'type' => 'string', ], ], 'ImplicitRights' => [ 'items' => [ 'type' => 'string', ], ], 'SoftBlockRanges' => [ 'items' => [ 'type' => 'string', ], ], 'ExternalQuerySources' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'enabled' => [ 'type' => 'boolean', 'default' => false, ], 'url' => [ 'type' => 'string', 'format' => 'uri', ], 'timeout' => [ 'type' => 'integer', 'default' => 10, ], ], 'required' => [ 'enabled', 'url', ], 'additionalProperties' => false, ], ], 'GrantPermissions' => [ 'additionalProperties' => [ 'type' => 'object', 'additionalProperties' => [ 'type' => 'boolean', ], ], ], 'GrantPermissionGroups' => [ 'additionalProperties' => [ 'type' => 'string', ], ], 'SitemapNamespacesPriorities' => [ 'deprecated' => 'since 1.45 and ignored', ], 'SitemapApiConfig' => [ 'additionalProperties' => [ 'enabled' => [ 'type' => 'bool', ], 'sitemapsPerIndex' => [ 'type' => 'int', ], 'pagesPerSitemap' => [ 'type' => 'int', ], 'expiry' => [ 'type' => 'int', ], 'skipRedirects' => [ 'type' => 'bool', ], ], ], 'SoftwareTags' => [ 'additionalProperties' => [ 'type' => 'boolean', ], ], 'UseCopyrightUpload' => [ 'deprecated' => 'since 1.47 This feature is being removed.', ], 'JobBackoffThrottling' => [ 'additionalProperties' => [ 'type' => 'number', ], ], 'JobTypeConf' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'class' => [ 'type' => 'string', ], 'order' => [ 'type' => 'string', ], 'claimTTL' => [ 'type' => 'integer', ], ], ], ], 'TrackingCategories' => [ 'deprecated' => 'since 1.25 Extensions should now register tracking categories using the new extension registration system.', ], 'RangeContributionsCIDRLimit' => [ 'additionalProperties' => [ 'type' => 'integer', ], ], 'RestModuleOverrides' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'availability' => [ 'type' => 'string', ], 'groups' => [ 'type' => 'array', ], ], ], ], 'RestExternalModules' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'info' => [ 'type' => 'object', 'properties' => [ 'version' => [ 'type' => 'string', ], 'title' => [ 'type' => 'string', ], 'x-i18n-title' => [ 'type' => 'string', ], 'description' => [ 'type' => 'string', ], 'x-i18n-description' => [ 'type' => 'string', ], ], 'required' => [ 'version', ], ], 'base' => [ 'type' => 'string', 'format' => 'uri', ], 'spec' => [ 'type' => 'string', 'format' => 'uri', ], ], 'required' => [ 'info', 'base', 'spec', ], ], ], 'ShellboxUrls' => [ 'additionalProperties' => [ 'type' => [ 'string', 'boolean', 'null', ], ], ], ], 'obsolete-config' => [ 'MangleFlashPolicy' => 'Since 1.39; no longer has any effect.', 'EnableOpenSearchSuggest' => 'Since 1.35, no longer used', 'AutoloadAttemptLowercase' => 'Since 1.40; no longer has any effect.', ],]
Interface for configuration instances.
Definition Config.php:18
Interface for objects representing user identity.
msg( $key,... $params)