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;
35use MediaWiki\Languages\LanguageConverterFactory;
36use MediaWiki\Languages\LanguageFallback;
37use MediaWiki\Languages\LanguageNameUtils;
49use NumberFormatter;
50use RuntimeException;
51use UtfNormal\Validator as UtfNormalValidator;
52use Wikimedia\Bcp47Code\Bcp47Code;
53use Wikimedia\DebugInfo\DebugInfoTrait;
58use Wikimedia\Timestamp\ConvertibleTimestamp;
59use Wikimedia\Timestamp\TimestampFormat as TS;
60
68class Language implements Bcp47Code {
69 use DebugInfoTrait;
70
72 public $mCode;
73
77 public $mMagicExtensions = [];
78
80 private $mHtmlCode = null;
81
87 public $dateFormatStrings = [];
88
95
97 protected $namespaceNames;
99 protected $mNamespaceIds;
102
107 private $transformData = [];
108
113 private $namespaceInfo;
114
119 private $localisationCache;
120
125 private $langNameUtils;
126
131 private $langFallback;
132
137 private $grammarTransformCache;
138
143 private $converterFactory;
144
149 private $hookContainer;
150
155 private $hookRunner;
156
161 private $config;
162
166 private $overrideUcfirstCharacters;
167
172 private $numberFormatter = null;
173
177 public const WEEKDAY_MESSAGES = [
178 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
179 'friday', 'saturday'
180 ];
181
186 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
187 ];
188
192 public const MONTH_MESSAGES = [
193 'january', 'february', 'march', 'april', 'may_long', 'june',
194 'july', 'august', 'september', 'october', 'november',
195 'december'
196 ];
197
202
206 public const MONTH_GENITIVE_MESSAGES = [
207 'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
208 'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
209 'december-gen'
210 ];
211
216 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
217 'sep', 'oct', 'nov', 'dec'
218 ];
219
224
229 'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
230 'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
231 'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
232 'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
233 ];
234
239 'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
240 'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
241 'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
242 'hebrew-calendar-m10', 'hebrew-calendar-m11', 'hebrew-calendar-m12',
243 'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
244 ];
245
250 'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
251 'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
252 'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
253 'hebrew-calendar-m10-gen', 'hebrew-calendar-m11-gen', 'hebrew-calendar-m12-gen',
254 'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
255 ];
256
261 'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
262 'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
263 'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
264 'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12'
265 ];
266
270 protected const DURATION_INTERVALS = [
271 'millennia' => 1000 * 31_556_952,
272 'centuries' => 100 * 31_556_952,
273 'decades' => 10 * 31_556_952,
274 // The average year is 365.2425 days (365 + (24 * 3 + 25) / 400)
275 'years' => 31_556_952, // 365.2425 * 24 * 3600
276 // To simplify, we consider a month to be 1/12 of a year
277 'months' => 365.2425 * 24 * 3600 / 12,
278 'days' => 24 * 3600,
279 'hours' => 3600,
280 'minutes' => 60,
281 'seconds' => 1,
282 ];
283
290
292 private const DEADLINE_DATE_SPEC_BY_UNIT = [
293 'Y' => 'first day of January next year midnight',
294 'M' => 'first day of next month midnight',
295 'W' => 'monday next week midnight',
296 'D' => 'next day midnight',
297 // Note that this may be before the current time, in which case
298 // we advance by a calendar year; the ISOY is actually 1 week past
299 // this, since dec 28 is always in "the last week of the ISO year".
300 'ISOY' => 'december 28 midnight',
301 // Note that this may be before the current time, in which case 'D'
302 // is used.
303 'AM' => 'today noon',
304 // Note that this relative datetime specifier does not zero out
305 // minutes/seconds, but we will do so manually in
306 // ::computeUnitTimestampDeadline() when given the unit 'H'/'m'/'s'
307 'H' => 'next hour',
308 'm' => 'next minute',
309 's' => 'next second',
310 ];
311
315 private const LRM = "\u{200E}"; // U+200E LEFT-TO-RIGHT MARK
316 private const RLM = "\u{200F}"; // U+200F RIGHT-TO-LEFT MARK
317 private const LRE = "\u{202A}"; // U+202A LEFT-TO-RIGHT EMBEDDING
318 private const RLE = "\u{202B}"; // U+202B RIGHT-TO-LEFT EMBEDDING
319 private const PDF = "\u{202C}"; // U+202C POP DIRECTIONAL FORMATTING
320 // https://en.wikipedia.org/wiki/Arabic_letter_mark (Unicode 6.3.0)
321 private const ALM = "\u{061C}"; // U+061C ARABIC LETTER MARK
322
335 // @codeCoverageIgnoreStart
336 // phpcs:ignore Generic.Files.LineLength,MediaWiki.Commenting.PropertyDocumentation.MissingDocumentationPrivate
337 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';
338 // @codeCoverageIgnoreEnd
339
343 public function __construct(
344 string $code,
345 NamespaceInfo $namespaceInfo,
346 LocalisationCache $localisationCache,
347 LanguageNameUtils $langNameUtils,
348 LanguageFallback $langFallback,
349 LanguageConverterFactory $converterFactory,
350 HookContainer $hookContainer,
351 Config $config
352 ) {
353 $this->mCode = $code;
354 $this->namespaceInfo = $namespaceInfo;
355 $this->localisationCache = $localisationCache;
356 $this->langNameUtils = $langNameUtils;
357 $this->langFallback = $langFallback;
358 $this->converterFactory = $converterFactory;
359 $this->hookContainer = $hookContainer;
360 $this->hookRunner = new HookRunner( $hookContainer );
361 $this->config = $config;
362 }
363
368 public function getFallbackLanguages() {
369 return $this->langFallback->getAll( $this->mCode );
370 }
371
376 public function getBookstoreList() {
377 return $this->localisationCache->getItem( $this->mCode, 'bookstoreList' );
378 }
379
387 public function getNamespaces() {
388 if ( $this->namespaceNames === null ) {
389 $metaNamespace = $this->config->get( MainConfigNames::MetaNamespace );
390 $metaNamespaceTalk = $this->config->get( MainConfigNames::MetaNamespaceTalk );
391 $extraNamespaces = $this->config->get( MainConfigNames::ExtraNamespaces );
392 $validNamespaces = $this->namespaceInfo->getCanonicalNamespaces();
393
394 // @phan-suppress-next-line PhanTypeMismatchProperty
395 $this->namespaceNames = $extraNamespaces +
396 $this->localisationCache->getItem( $this->mCode, 'namespaceNames' );
397 // @phan-suppress-next-line PhanTypeInvalidLeftOperand
398 $this->namespaceNames += $validNamespaces;
399
400 $this->namespaceNames[NS_PROJECT] = $metaNamespace;
401 if ( $metaNamespaceTalk ) {
402 $this->namespaceNames[NS_PROJECT_TALK] = $metaNamespaceTalk;
403 } else {
404 $talk = $this->namespaceNames[NS_PROJECT_TALK];
405 $this->namespaceNames[NS_PROJECT_TALK] =
406 $this->fixVariableInNamespace( $talk );
407 }
408
409 # Sometimes a language will be localised but not actually exist on this wiki.
410 foreach ( $this->namespaceNames as $key => $text ) {
411 if ( !isset( $validNamespaces[$key] ) ) {
412 unset( $this->namespaceNames[$key] );
413 }
414 }
415
416 # The above mixing may leave namespaces out of canonical order.
417 # Re-order by namespace ID number...
418 ksort( $this->namespaceNames );
419
420 $this->getHookRunner()->onLanguageGetNamespaces( $this->namespaceNames );
421 }
422
424 }
425
430 public function setNamespaces( array $namespaces ) {
431 $this->namespaceNames = $namespaces;
432 $this->mNamespaceIds = null;
433 }
434
439 public function resetNamespaces() {
440 $this->namespaceNames = null;
441 $this->mNamespaceIds = null;
442 $this->namespaceAliases = null;
443 }
444
454 public function getFormattedNamespaces() {
455 $ns = $this->getNamespaces();
456 foreach ( $ns as $k => $v ) {
457 $ns[$k] = strtr( $v, '_', ' ' );
458 }
459 return $ns;
460 }
461
475 public function getNsText( $index ) {
476 $ns = $this->getNamespaces();
477 return $ns[$index] ?? false;
478 }
479
493 public function getFormattedNsText( $index ) {
494 $ns = $this->getNsText( $index );
495 return $ns === false ? '' : strtr( $ns, '_', ' ' );
496 }
497
506 public function getGenderNsText( $index, $gender ) {
507 $extraGenderNamespaces = $this->config->get( MainConfigNames::ExtraGenderNamespaces );
508
509 $ns = $extraGenderNamespaces +
510 (array)$this->localisationCache->getItem( $this->mCode, 'namespaceGenderAliases' );
511
512 return $ns[$index][$gender] ?? $this->getNsText( $index );
513 }
514
521 public function needsGenderDistinction() {
522 $extraGenderNamespaces = $this->config->get( MainConfigNames::ExtraGenderNamespaces );
523 $extraNamespaces = $this->config->get( MainConfigNames::ExtraNamespaces );
524 if ( count( $extraGenderNamespaces ) > 0 ) {
525 // $wgExtraGenderNamespaces overrides everything
526 return true;
527 } elseif ( isset( $extraNamespaces[NS_USER] ) && isset( $extraNamespaces[NS_USER_TALK] ) ) {
528 // @todo There may be other gender namespace than NS_USER & NS_USER_TALK in the future
529 // $wgExtraNamespaces overrides any gender aliases specified in i18n files
530 return false;
531 } else {
532 // Check what is in i18n files
533 $aliases = (array)$this->localisationCache->getItem( $this->mCode, 'namespaceGenderAliases' );
534 return count( $aliases ) > 0;
535 }
536 }
537
546 public function getLocalNsIndex( $text ) {
547 $lctext = $this->lc( $text );
548 $ids = $this->getNamespaceIds();
549 return $ids[$lctext] ?? false;
550 }
551
556 public function getNamespaceAliases() {
557 if ( $this->namespaceAliases === null ) {
558 $aliases = $this->localisationCache->getItem( $this->mCode, 'namespaceAliases' );
559 if ( !$aliases ) {
560 $aliases = [];
561 } else {
562 foreach ( $aliases as $name => $index ) {
563 if ( $index === NS_PROJECT_TALK ) {
564 unset( $aliases[$name] );
565 $name = $this->fixVariableInNamespace( $name );
566 $aliases[$name] = $index;
567 }
568 }
569 }
570
571 $extraGenderNamespaces = $this->config->get( MainConfigNames::ExtraGenderNamespaces );
572 $genders = $extraGenderNamespaces + (array)$this->localisationCache
573 ->getItem( $this->mCode, 'namespaceGenderAliases' );
574 foreach ( $genders as $index => $forms ) {
575 foreach ( $forms as $alias ) {
576 $aliases[$alias] = $index;
577 }
578 }
579
580 $langConverter = $this->getConverterInternal();
581 # Also add converted namespace names as aliases, to avoid confusion.
582 $convertedNames = [];
583 foreach ( $langConverter->getVariants() as $variant ) {
584 if ( $variant === $this->mCode ) {
585 continue;
586 }
587 foreach ( $this->getNamespaces() as $ns => $_ ) {
588 $convertedNames[$langConverter->convertNamespace( $ns, $variant )] = $ns;
589 }
590 }
591
592 $this->namespaceAliases = $aliases + $convertedNames;
593
594 // In the case of conflicts between $wgNamespaceAliases and other sources
595 // of aliasing, $wgNamespaceAliases wins.
596 $this->namespaceAliases = $this->config->get( MainConfigNames::NamespaceAliases ) +
598
599 # Filter out aliases to namespaces that don't exist, e.g. from extensions
600 # that aren't loaded here but are included in the l10n cache.
601 # (array_intersect preserves keys from its first argument)
602 $this->namespaceAliases = array_intersect(
603 $this->namespaceAliases,
604 array_keys( $this->getNamespaces() )
605 );
606 }
607
609 }
610
614 public function getNamespaceIds() {
615 if ( $this->mNamespaceIds === null ) {
616 # Put namespace names and aliases into a hashtable.
617 # If this is too slow, then we should arrange it so that it is done
618 # before caching. The catch is that at pre-cache time, the above
619 # class-specific fixup hasn't been done.
620 $this->mNamespaceIds = [];
621 foreach ( $this->getNamespaces() as $index => $name ) {
622 $this->mNamespaceIds[$this->lc( $name )] = $index;
623 }
624 foreach ( $this->getNamespaceAliases() as $name => $index ) {
625 $this->mNamespaceIds[$this->lc( $name )] = $index;
626 }
627 }
629 }
630
638 public function getNsIndex( $text ) {
639 $lctext = $this->lc( $text );
640 $ns = $this->namespaceInfo->getCanonicalIndex( $lctext );
641 if ( $ns !== null ) {
642 return $ns;
643 }
644 $ids = $this->getNamespaceIds();
645 return $ids[$lctext] ?? false;
646 }
647
655 public function getVariantname( $code, $usemsg = true ) {
656 if ( $usemsg ) {
657 $msg = $this->msg( "variantname-$code" );
658 if ( $msg->exists() ) {
659 return $msg->text();
660 }
661 }
662 $name = $this->langNameUtils->getLanguageName( $code );
663 if ( $name ) {
664 return $name; # if it's defined as a language name, show that
665 } else {
666 # otherwise, output the language code
667 return $code;
668 }
669 }
670
674 public function getDatePreferences() {
675 return $this->localisationCache->getItem( $this->mCode, 'datePreferences' );
676 }
677
681 public function getDateFormats() {
682 return $this->localisationCache->getItem( $this->mCode, 'dateFormats' );
683 }
684
688 public function getDefaultDateFormat() {
689 $df = $this->localisationCache->getItem( $this->mCode, 'defaultDateFormat' );
690 if ( $df === 'dmy or mdy' ) {
691 return $this->config->get( MainConfigNames::AmericanDates ) ? 'mdy' : 'dmy';
692 } else {
693 return $df;
694 }
695 }
696
700 public function getDatePreferenceMigrationMap() {
701 return $this->localisationCache->getItem( $this->mCode, 'datePreferenceMigrationMap' );
702 }
703
710 public function getMessageFromDB( $msg ) {
711 return $this->msg( $msg )->text();
712 }
713
723 protected function msg( $msg, ...$params ) {
724 return wfMessage( $msg, ...$params )->inLanguage( $this );
725 }
726
731 public function getMonthName( $key ) {
732 return $this->getMessageFromDB( self::MONTH_MESSAGES[$key - 1] );
733 }
734
738 public function getMonthNamesArray() {
739 $monthNames = [ '' ];
740 for ( $i = 1; $i <= 12; $i++ ) {
741 $monthNames[] = $this->getMonthName( $i );
742 }
743 return $monthNames;
744 }
745
750 public function getMonthNameGen( $key ) {
751 return $this->getMessageFromDB( self::MONTH_GENITIVE_MESSAGES[$key - 1] );
752 }
753
758 public function getMonthAbbreviation( $key ) {
759 return $this->getMessageFromDB( self::MONTH_ABBREVIATED_MESSAGES[$key - 1] );
760 }
761
765 public function getMonthAbbreviationsArray() {
766 $monthNames = [ '' ];
767 for ( $i = 1; $i <= 12; $i++ ) {
768 $monthNames[] = $this->getMonthAbbreviation( $i );
769 }
770 return $monthNames;
771 }
772
777 public function getWeekdayName( $key ) {
778 return $this->getMessageFromDB( self::WEEKDAY_MESSAGES[$key - 1] );
779 }
780
785 public function getWeekdayAbbreviation( $key ) {
786 return $this->getMessageFromDB( self::WEEKDAY_ABBREVIATED_MESSAGES[$key - 1] );
787 }
788
797 private static function dateTimeObj( &$dateTimeObj, $ts, $zone ): DateTime {
798 if ( !$dateTimeObj ) {
799 $dateTimeObj = DateTime::createFromFormat(
800 'YmdHis', $ts, $zone ?: new DateTimeZone( 'UTC' )
801 );
802 }
803 return $dateTimeObj;
804 }
805
815 private static function dateTimeObjFormat( &$dateTimeObj, $ts, $zone, $code ) {
816 return self::dateTimeObj( $dateTimeObj, $ts, $zone )->format( $code );
817 }
818
888 public function sprintfDate( $format, $ts, ?DateTimeZone $zone = null, &$ttl = 'unused' ) {
889 // @phan-suppress-previous-line PhanTypeMismatchDefault Type mismatch on pass-by-ref args
890 $s = '';
891 $raw = false;
892 $roman = false;
893 $hebrewNum = false;
894 $dateTimeObj = false;
895 $rawToggle = false;
896 $iranian = false;
897 $hebrew = false;
898 $hijri = false;
899 $thai = false;
900 $minguo = false;
901 $tenno = false;
902
903 $usedSecond = false;
904 $usedMinute = false;
905 $usedHour = false;
906 $usedAMPM = false;
907 $usedDay = false;
908 $usedWeek = false;
909 $usedMonth = false;
910 $usedYear = false;
911 $usedISOYear = false;
912 $usedIsLeapYear = false;
913
914 $usedHebrewMonth = false;
915 $usedIranianMonth = false;
916 $usedHijriMonth = false;
917 $usedHebrewYear = false;
918 $usedIranianYear = false;
919 $usedHijriYear = false;
920 $usedTennoYear = false;
921
922 if ( strlen( $ts ) !== 14 ) {
923 throw new InvalidArgumentException( __METHOD__ . ": The timestamp $ts should have 14 characters" );
924 }
925
926 if ( !ctype_digit( $ts ) ) {
927 throw new InvalidArgumentException( __METHOD__ . ": The timestamp $ts should be a number" );
928 }
929
930 $formatLength = strlen( $format );
931 for ( $p = 0; $p < $formatLength; $p++ ) {
932 $num = false;
933 $code = $format[$p];
934 if ( $code == 'x' && $p < $formatLength - 1 ) {
935 $code .= $format[++$p];
936 }
937
938 if ( ( $code === 'xi'
939 || $code === 'xj'
940 || $code === 'xk'
941 || $code === 'xm'
942 || $code === 'xo'
943 || $code === 'xt' )
944 && $p < $formatLength - 1
945 ) {
946 $code .= $format[++$p];
947 }
948
949 switch ( $code ) {
950 case 'xx':
951 $s .= 'x';
952 break;
953
954 case 'xn':
955 $raw = true;
956 break;
957
958 case 'xN':
959 $rawToggle = !$rawToggle;
960 break;
961
962 case 'xr':
963 $roman = true;
964 break;
965
966 case 'xh':
967 $hebrewNum = true;
968 break;
969
970 case 'xg':
971 $usedMonth = true;
972 $s .= $this->getMonthNameGen( (int)substr( $ts, 4, 2 ) );
973 break;
974
975 case 'xjx':
976 $usedHebrewMonth = true;
977 if ( !$hebrew ) {
978 $hebrew = self::tsToHebrew( $ts );
979 }
980 $s .= $this->getMessageFromDB( self::HEBREW_CALENDAR_MONTH_GENITIVE_MESSAGES[$hebrew[1] - 1] );
981 break;
982
983 case 'd':
984 $usedDay = true;
985 $num = substr( $ts, 6, 2 );
986 break;
987
988 case 'D':
989 $usedDay = true;
990 $s .= $this->getWeekdayAbbreviation(
991 (int)self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1
992 );
993 break;
994
995 case 'j':
996 $usedDay = true;
997 $num = intval( substr( $ts, 6, 2 ) );
998 break;
999
1000 case 'xij':
1001 $usedDay = true;
1002 if ( !$iranian ) {
1003 $iranian = self::tsToIranian( $ts );
1004 }
1005 $num = $iranian[2];
1006 break;
1007
1008 case 'xmj':
1009 $usedDay = true;
1010 if ( !$hijri ) {
1011 $hijri = self::tsToHijri( $ts );
1012 }
1013 $num = $hijri[2];
1014 break;
1015
1016 case 'xjj':
1017 $usedDay = true;
1018 if ( !$hebrew ) {
1019 $hebrew = self::tsToHebrew( $ts );
1020 }
1021 $num = $hebrew[2];
1022 break;
1023
1024 case 'l':
1025 $usedDay = true;
1026 $s .= $this->getWeekdayName(
1027 (int)self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1
1028 );
1029 break;
1030
1031 case 'F':
1032 $usedMonth = true;
1033 $s .= $this->getMonthName( (int)substr( $ts, 4, 2 ) );
1034 break;
1035
1036 case 'xiF':
1037 $usedIranianMonth = true;
1038 if ( !$iranian ) {
1039 $iranian = self::tsToIranian( $ts );
1040 }
1041 $s .= $this->getMessageFromDB( self::IRANIAN_CALENDAR_MONTHS_MESSAGES[$iranian[1] - 1] );
1042 break;
1043
1044 case 'xmF':
1045 $usedHijriMonth = true;
1046 if ( !$hijri ) {
1047 $hijri = self::tsToHijri( $ts );
1048 }
1049 $s .= $this->getMessageFromDB( self::HIJRI_CALENDAR_MONTH_MESSAGES[$hijri[1] - 1] );
1050 break;
1051
1052 case 'xjF':
1053 $usedHebrewMonth = true;
1054 if ( !$hebrew ) {
1055 $hebrew = self::tsToHebrew( $ts );
1056 }
1057 $s .= $this->getMessageFromDB( self::HEBREW_CALENDAR_MONTHS_MESSAGES[$hebrew[1] - 1] );
1058 break;
1059
1060 case 'm':
1061 $usedMonth = true;
1062 $num = substr( $ts, 4, 2 );
1063 break;
1064
1065 case 'M':
1066 $usedMonth = true;
1067 $s .= $this->getMonthAbbreviation( (int)substr( $ts, 4, 2 ) );
1068 break;
1069
1070 case 'n':
1071 $usedMonth = true;
1072 $num = intval( substr( $ts, 4, 2 ) );
1073 break;
1074
1075 case 'xin':
1076 $usedIranianMonth = true;
1077 if ( !$iranian ) {
1078 $iranian = self::tsToIranian( $ts );
1079 }
1080 $num = $iranian[1];
1081 break;
1082
1083 case 'xmn':
1084 $usedHijriMonth = true;
1085 if ( !$hijri ) {
1086 $hijri = self::tsToHijri( $ts );
1087 }
1088 $num = $hijri[1];
1089 break;
1090
1091 case 'xjn':
1092 $usedHebrewMonth = true;
1093 if ( !$hebrew ) {
1094 $hebrew = self::tsToHebrew( $ts );
1095 }
1096 $num = $hebrew[1];
1097 break;
1098
1099 case 'xjt':
1100 $usedHebrewMonth = true;
1101 if ( !$hebrew ) {
1102 $hebrew = self::tsToHebrew( $ts );
1103 }
1104 $num = $hebrew[3];
1105 break;
1106
1107 case 'Y':
1108 $usedYear = true;
1109 $num = substr( $ts, 0, 4 );
1110 break;
1111
1112 case 'xiY':
1113 $usedIranianYear = true;
1114 if ( !$iranian ) {
1115 $iranian = self::tsToIranian( $ts );
1116 }
1117 $num = $iranian[0];
1118 break;
1119
1120 case 'xmY':
1121 $usedHijriYear = true;
1122 if ( !$hijri ) {
1123 $hijri = self::tsToHijri( $ts );
1124 }
1125 $num = $hijri[0];
1126 break;
1127
1128 case 'xjY':
1129 $usedHebrewYear = true;
1130 if ( !$hebrew ) {
1131 $hebrew = self::tsToHebrew( $ts );
1132 }
1133 $num = $hebrew[0];
1134 break;
1135
1136 case 'xkY':
1137 $usedYear = true;
1138 if ( !$thai ) {
1139 $thai = self::tsToYear( $ts, 'thai' );
1140 }
1141 $num = $thai[0];
1142 break;
1143
1144 case 'xoY':
1145 $usedYear = true;
1146 if ( !$minguo ) {
1147 $minguo = self::tsToYear( $ts, 'minguo' );
1148 }
1149 $num = $minguo[0];
1150 break;
1151
1152 case 'xtY':
1153 $usedTennoYear = true;
1154 if ( !$tenno ) {
1155 $tenno = self::tsToJapaneseGengo( $ts );
1156 }
1157 $num = $tenno;
1158 break;
1159
1160 case 'y':
1161 $usedYear = true;
1162 $num = substr( $ts, 2, 2 );
1163 break;
1164
1165 case 'xiy':
1166 $usedIranianYear = true;
1167 if ( !$iranian ) {
1168 $iranian = self::tsToIranian( $ts );
1169 }
1170 $num = substr( (string)$iranian[0], -2 );
1171 break;
1172
1173 case 'xit':
1174 $usedIranianYear = true;
1175 if ( !$iranian ) {
1176 $iranian = self::tsToIranian( $ts );
1177 }
1178 $num = self::IRANIAN_DAYS[$iranian[1] - 1];
1179 break;
1180
1181 case 'xiz':
1182 $usedIranianYear = true;
1183 if ( !$iranian ) {
1184 $iranian = self::tsToIranian( $ts );
1185 }
1186 $num = $iranian[3];
1187 break;
1188
1189 case 'a':
1190 $usedAMPM = true;
1191 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'am' : 'pm';
1192 break;
1193
1194 case 'A':
1195 $usedAMPM = true;
1196 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'AM' : 'PM';
1197 break;
1198
1199 case 'g':
1200 $usedHour = true;
1201 $h = (int)substr( $ts, 8, 2 );
1202 $num = $h % 12 ?: 12;
1203 break;
1204
1205 case 'G':
1206 $usedHour = true;
1207 $num = intval( substr( $ts, 8, 2 ) );
1208 break;
1209
1210 case 'h':
1211 $usedHour = true;
1212 $h = (int)substr( $ts, 8, 2 );
1213 $num = sprintf( '%02d', $h % 12 ?: 12 );
1214 break;
1215
1216 case 'H':
1217 $usedHour = true;
1218 $num = substr( $ts, 8, 2 );
1219 break;
1220
1221 case 'i':
1222 $usedMinute = true;
1223 $num = substr( $ts, 10, 2 );
1224 break;
1225
1226 case 's':
1227 $usedSecond = true;
1228 $num = substr( $ts, 12, 2 );
1229 break;
1230
1231 case 'c':
1232 case 'r':
1233 $usedSecond = true;
1234 // fall through
1235 case 'e':
1236 case 'O':
1237 case 'P':
1238 case 'T':
1239 $s .= self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1240 break;
1241
1242 case 'w':
1243 case 'N':
1244 case 'z':
1245 $usedDay = true;
1246 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1247 break;
1248
1249 case 'W':
1250 $usedWeek = true;
1251 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1252 break;
1253
1254 case 't':
1255 $usedMonth = true;
1256 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1257 break;
1258
1259 case 'L':
1260 $usedIsLeapYear = true;
1261 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1262 break;
1263
1264 case 'o':
1265 $usedISOYear = true;
1266 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1267 break;
1268
1269 case 'U':
1270 $usedSecond = true;
1271 // fall through
1272 case 'I':
1273 case 'Z':
1274 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1275 break;
1276
1277 case '\\':
1278 # Backslash escaping
1279 if ( $p < $formatLength - 1 ) {
1280 $s .= $format[++$p];
1281 } else {
1282 $s .= '\\';
1283 }
1284 break;
1285
1286 case '"':
1287 # Quoted literal
1288 if ( $p < $formatLength - 1 ) {
1289 $endQuote = strpos( $format, '"', $p + 1 );
1290 if ( $endQuote === false ) {
1291 # No terminating quote, assume literal "
1292 $s .= '"';
1293 } else {
1294 $s .= substr( $format, $p + 1, $endQuote - $p - 1 );
1295 $p = $endQuote;
1296 }
1297 } else {
1298 # Quote at the end of the string, assume literal "
1299 $s .= '"';
1300 }
1301 break;
1302
1303 default:
1304 $s .= $format[$p];
1305 }
1306 if ( $num !== false ) {
1307 if ( $rawToggle || $raw ) {
1308 $s .= $num;
1309 $raw = false;
1310 } elseif ( $roman ) {
1311 $s .= self::romanNumeral( $num );
1312 $roman = false;
1313 } elseif ( $hebrewNum ) {
1314 $s .= self::hebrewNumeral( $num );
1315 $hebrewNum = false;
1316 } elseif ( preg_match( '/^[\d.]+$/', $num ) ) {
1317 $s .= $this->formatNumNoSeparators( $num );
1318 } else {
1319 $s .= $num;
1320 }
1321 }
1322 }
1323
1324 if ( $ttl !== 'unused' ) {
1325 // Ensure $dateTimeObj is set.
1326 self::dateTimeObj( $dateTimeObj, $ts, $zone );
1327 }
1328 '@phan-var DateTime $dateTimeObj';
1329 if ( $ttl === 'unused' ) {
1330 // No need to calculate the TTL, the caller won't use it anyway.
1331 } elseif ( $usedSecond ) {
1332 $ttl = self::computeUnitTimestampDeadline( $dateTimeObj, 's' );
1333 } elseif ( $usedMinute ) {
1334 $ttl = self::computeUnitTimestampDeadline( $dateTimeObj, 'm' );
1335 } elseif ( $usedHour ) {
1336 $ttl = self::computeUnitTimestampDeadline( $dateTimeObj, 'H' );
1337 } elseif ( $usedAMPM ) {
1338 $ttl = self::computeUnitTimestampDeadline( $dateTimeObj, 'AM' );
1339 } elseif (
1340 $usedDay ||
1341 $usedHebrewMonth ||
1342 $usedIranianMonth ||
1343 $usedHijriMonth ||
1344 $usedHebrewYear ||
1345 $usedIranianYear ||
1346 $usedHijriYear ||
1347 $usedTennoYear
1348 ) {
1349 // @todo Someone who understands the non-Gregorian calendars
1350 // should write proper logic for them so that they don't need purged every day.
1351 $ttl = self::computeUnitTimestampDeadline( $dateTimeObj, 'D' );
1352 } else {
1353 $possibleTtls = [];
1354 if ( $usedWeek ) {
1355 $possibleTtls[] =
1356 self::computeUnitTimestampDeadline( $dateTimeObj, 'W' );
1357 } elseif ( $usedISOYear ) {
1358 // December 28th falls on the last ISO week of the year, every year.
1359 // The last ISO week of a year can be 52 or 53.
1360 $possibleTtls[] =
1361 self::computeUnitTimestampDeadline( $dateTimeObj, 'ISOY' );
1362 }
1363
1364 if ( $usedMonth ) {
1365 $possibleTtls[] =
1366 self::computeUnitTimestampDeadline( $dateTimeObj, 'M' );
1367 } elseif ( $usedYear ) {
1368 $possibleTtls[] =
1369 self::computeUnitTimestampDeadline( $dateTimeObj, 'Y' );
1370 } elseif ( $usedIsLeapYear ) {
1371 $year = (int)substr( $ts, 0, 4 );
1372 $mod = $year % 4;
1373 $timeRemainingInYear =
1374 self::computeUnitTimestampDeadline( $dateTimeObj, 'Y' );
1375 if ( $mod || ( !( $year % 100 ) && $year % 400 ) ) {
1376 // this isn't a leap year. see when the next one starts
1377 $nextCandidate = $year - $mod + 4;
1378 if ( $nextCandidate % 100 || !( $nextCandidate % 400 ) ) {
1379 $possibleTtls[] = ( $nextCandidate - $year - 1 ) * 365 * 86400 +
1380 $timeRemainingInYear;
1381 } else {
1382 $possibleTtls[] = ( $nextCandidate - $year + 3 ) * 365 * 86400 +
1383 $timeRemainingInYear;
1384 }
1385 } else {
1386 // this is a leap year, so the next year isn't
1387 $possibleTtls[] = $timeRemainingInYear;
1388 }
1389 }
1390
1391 if ( $possibleTtls ) {
1392 $ttl = min( $possibleTtls );
1393 }
1394 }
1395
1396 return $s;
1397 }
1398
1408 public static function computeUnitTimestampDeadline(
1409 DateTimeInterface $date,
1410 string $unit
1411 ): int {
1412 // Ensure we don't mutate our argument
1413 $date = DateTimeImmutable::createFromInterface( $date );
1414 $tsUnix = $date->getTimestamp();
1415
1416 $date = $date->modify( self::DEADLINE_DATE_SPEC_BY_UNIT[$unit] );
1417 if ( $unit === 'AM' ) {
1418 if ( $date->getTimestamp() <= $tsUnix ) {
1419 $date = $date->modify( self::DEADLINE_DATE_SPEC_BY_UNIT['D'] );
1420 }
1421 } elseif ( $unit === 'ISOY' ) {
1422 // December 28th falls on the last ISO week of the year, every year.
1423 // but we could be in the same, next, or previous ISO year as
1424 // December 28 of this calendar year.
1425 // Compute all three and use the first which is in the future.
1426 $lastYear = $date->modify(
1427 'december 28 midnight last year'
1428 )->modify( 'monday next week midnight' );
1429 $nextYear = $date->modify(
1430 'december 28 midnight next year'
1431 )->modify( 'monday next week midnight' );
1432 // Advance to first week of next ISO year
1433 $date = $date->modify( 'monday next week midnight' );
1434 if ( $lastYear->getTimestamp() > $tsUnix ) {
1435 $date = $lastYear;
1436 } elseif ( $date->getTimestamp() <= $tsUnix ) {
1437 $date = $nextYear;
1438 }
1439 } elseif ( $unit === 'H' ) {
1440 // Zero out the minutes/seconds
1441 $date = $date->setTime( intval( $date->format( 'H' ), 10 ), 0, 0 );
1442 } elseif ( $unit === 'm' ) {
1443 // Zero out the seconds
1444 $date = $date->setTime( intval( $date->format( 'H' ), 10 ),
1445 intval( $date->format( 'i' ), 10 ), 0 );
1446 } elseif ( $unit === 's' ) {
1447 // Zero out the fractional seconds
1448 $date = $date->setTime( intval( $date->format( 'H' ), 10 ),
1449 intval( $date->format( 'i' ), 10 ),
1450 intval( $date->format( 's' ), 10 ) );
1451 } else {
1452 $date = $date->setTime( 0, 0, 0 );
1453 }
1454 $deadlineUnix = (int)$date->format( 'U' );
1455 return $deadlineUnix - $tsUnix;
1456 }
1457
1461 private const GREG_DAYS = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
1462
1466 private const IRANIAN_DAYS = [ 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 ];
1467
1480 private static function tsToIranian( $ts ) {
1481 $gy = (int)substr( $ts, 0, 4 ) - 1600;
1482 $gm = (int)substr( $ts, 4, 2 ) - 1;
1483 $gd = (int)substr( $ts, 6, 2 ) - 1;
1484
1485 # Days passed from the beginning (including leap years)
1486 $gDayNo = 365 * $gy
1487 + floor( ( $gy + 3 ) / 4 )
1488 - floor( ( $gy + 99 ) / 100 )
1489 + floor( ( $gy + 399 ) / 400 );
1490
1491 // Add the number of days for the past months of this year
1492 for ( $i = 0; $i < $gm; $i++ ) {
1493 $gDayNo += self::GREG_DAYS[$i];
1494 }
1495
1496 // Leap years
1497 if ( $gm > 1 && ( ( $gy % 4 === 0 && $gy % 100 !== 0 ) || $gy % 400 == 0 ) ) {
1498 $gDayNo++;
1499 }
1500
1501 // Days passed in the current month
1502 $gDayNo += $gd;
1503
1504 $jDayNo = $gDayNo - 79;
1505
1506 $jNp = (int)floor( $jDayNo / 12053 );
1507 $jDayNo %= 12053;
1508
1509 $jy = 979 + 33 * $jNp + 4 * (int)floor( $jDayNo / 1461 );
1510 $jDayNo %= 1461;
1511
1512 if ( $jDayNo >= 366 ) {
1513 $jy += (int)floor( ( $jDayNo - 1 ) / 365 );
1514 $jDayNo = (int)floor( ( $jDayNo - 1 ) % 365 );
1515 }
1516
1517 $jz = $jDayNo;
1518
1519 for ( $i = 0; $i < 11 && $jDayNo >= self::IRANIAN_DAYS[$i]; $i++ ) {
1520 $jDayNo -= self::IRANIAN_DAYS[$i];
1521 }
1522
1523 $jm = $i + 1;
1524 $jd = $jDayNo + 1;
1525
1526 return [ $jy, $jm, $jd, $jz ];
1527 }
1528
1540 private static function tsToHijri( $ts ) {
1541 $year = (int)substr( $ts, 0, 4 );
1542 $month = (int)substr( $ts, 4, 2 );
1543 $day = (int)substr( $ts, 6, 2 );
1544
1545 $zyr = $year;
1546 $zd = $day;
1547 $zm = $month;
1548 $zy = $zyr;
1549
1550 if (
1551 ( $zy > 1582 ) || ( ( $zy == 1582 ) && ( $zm > 10 ) ) ||
1552 ( ( $zy == 1582 ) && ( $zm == 10 ) && ( $zd > 14 ) )
1553 ) {
1554 $zjd = (int)( ( 1461 * ( $zy + 4800 + (int)( ( $zm - 14 ) / 12 ) ) ) / 4 ) +
1555 (int)( ( 367 * ( $zm - 2 - 12 * ( (int)( ( $zm - 14 ) / 12 ) ) ) ) / 12 ) -
1556 (int)( ( 3 * (int)( ( $zy + 4900 + (int)( ( $zm - 14 ) / 12 ) ) / 100 ) ) / 4 ) +
1557 $zd - 32075;
1558 } else {
1559 $zjd = 367 * $zy - (int)( ( 7 * ( $zy + 5001 + (int)( ( $zm - 9 ) / 7 ) ) ) / 4 ) +
1560 (int)( ( 275 * $zm ) / 9 ) + $zd + 1729777;
1561 }
1562
1563 $zl = $zjd - 1948440 + 10632;
1564 $zn = (int)( ( $zl - 1 ) / 10631 );
1565 $zl = $zl - 10631 * $zn + 354;
1566 $zj = ( (int)( ( 10985 - $zl ) / 5316 ) ) * ( (int)( ( 50 * $zl ) / 17719 ) ) +
1567 ( (int)( $zl / 5670 ) ) * ( (int)( ( 43 * $zl ) / 15238 ) );
1568 $zl = $zl - ( (int)( ( 30 - $zj ) / 15 ) ) * ( (int)( ( 17719 * $zj ) / 50 ) ) -
1569 ( (int)( $zj / 16 ) ) * ( (int)( ( 15238 * $zj ) / 43 ) ) + 29;
1570 $zm = (int)( ( 24 * $zl ) / 709 );
1571 $zd = $zl - (int)( ( 709 * $zm ) / 24 );
1572 $zy = 30 * $zn + $zj - 30;
1573
1574 return [ $zy, $zm, $zd ];
1575 }
1576
1592 private static function tsToHebrew( $ts ) {
1593 # Parse date
1594 $year = (int)substr( $ts, 0, 4 );
1595 $month = (int)substr( $ts, 4, 2 );
1596 $day = (int)substr( $ts, 6, 2 );
1597
1598 # Calculate Hebrew year
1599 $hebrewYear = $year + 3760;
1600
1601 # Month number when September = 1, August = 12
1602 $month += 4;
1603 if ( $month > 12 ) {
1604 # Next year
1605 $month -= 12;
1606 $year++;
1607 $hebrewYear++;
1608 }
1609
1610 # Calculate day of year from 1 September
1611 $dayOfYear = $day;
1612 for ( $i = 1; $i < $month; $i++ ) {
1613 if ( $i == 6 ) {
1614 # February
1615 $dayOfYear += 28;
1616 # Check if the year is a leap year
1617 if ( $year % 400 == 0 || ( $year % 4 == 0 && $year % 100 > 0 ) ) {
1618 $dayOfYear++;
1619 }
1620 } elseif ( $i == 8 || $i == 10 || $i == 1 || $i == 3 ) {
1621 $dayOfYear += 30;
1622 } else {
1623 $dayOfYear += 31;
1624 }
1625 }
1626
1627 # Calculate the start of the Hebrew year
1628 $start = self::hebrewYearStart( $hebrewYear );
1629
1630 # Calculate next year's start
1631 if ( $dayOfYear <= $start ) {
1632 # Day is before the start of the year - it is the previous year
1633 # Next year's start
1634 $nextStart = $start;
1635 # Previous year
1636 $year--;
1637 $hebrewYear--;
1638 # Add days since the previous year's 1 September
1639 $dayOfYear += 365;
1640 if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1641 # Leap year
1642 $dayOfYear++;
1643 }
1644 # Start of the new (previous) year
1645 $start = self::hebrewYearStart( $hebrewYear );
1646 } else {
1647 # Next year's start
1648 $nextStart = self::hebrewYearStart( $hebrewYear + 1 );
1649 }
1650
1651 # Calculate Hebrew day of year
1652 $hebrewDayOfYear = $dayOfYear - $start;
1653
1654 # Difference between year's days
1655 $diff = $nextStart - $start;
1656 # Add 12 (or 13 for leap years) days to ignore the difference between
1657 # Hebrew and Gregorian year (353 at least vs. 365/6) - now the
1658 # difference is only about the year type
1659 if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1660 $diff += 13;
1661 } else {
1662 $diff += 12;
1663 }
1664
1665 # Check the year pattern, and is leap year
1666 # 0 means an incomplete year, 1 means a regular year, 2 means a complete year
1667 # This is mod 30, to work on both leap years (which add 30 days of Adar I)
1668 # and non-leap years
1669 $yearPattern = $diff % 30;
1670 # Check if leap year
1671 $isLeap = $diff >= 30;
1672
1673 # Calculate day in the month from number of day in the Hebrew year
1674 # Don't check Adar - if the day is not in Adar, we will stop before;
1675 # if it is in Adar, we will use it to check if it is Adar I or Adar II
1676 $hebrewDay = $hebrewDayOfYear;
1677 $hebrewMonth = 1;
1678 $days = 0;
1679 while ( $hebrewMonth <= 12 ) {
1680 # Calculate days in this month
1681 if ( $isLeap && $hebrewMonth == 6 ) {
1682 # Leap year - has Adar I, with 30 days, and Adar II, with 29 days
1683 $days = 30;
1684 if ( $hebrewDay <= $days ) {
1685 # Day in Adar I
1686 $hebrewMonth = 13;
1687 } else {
1688 # Subtract the days of Adar I
1689 $hebrewDay -= $days;
1690 # Try Adar II
1691 $days = 29;
1692 if ( $hebrewDay <= $days ) {
1693 # Day in Adar II
1694 $hebrewMonth = 14;
1695 }
1696 }
1697 } elseif ( $hebrewMonth == 2 && $yearPattern == 2 ) {
1698 # Cheshvan in a complete year (otherwise as the rule below)
1699 $days = 30;
1700 } elseif ( $hebrewMonth == 3 && $yearPattern == 0 ) {
1701 # Kislev in an incomplete year (otherwise as the rule below)
1702 $days = 29;
1703 } else {
1704 # Odd months have 30 days, even have 29
1705 $days = 30 - ( $hebrewMonth - 1 ) % 2;
1706 }
1707 if ( $hebrewDay <= $days ) {
1708 # In the current month
1709 break;
1710 } else {
1711 # Subtract the days of the current month
1712 $hebrewDay -= $days;
1713 # Try in the next month
1714 $hebrewMonth++;
1715 }
1716 }
1717
1718 return [ $hebrewYear, $hebrewMonth, $hebrewDay, $days ];
1719 }
1720
1730 private static function hebrewYearStart( $year ) {
1731 $a = ( 12 * ( $year - 1 ) + 17 ) % 19;
1732 $b = ( $year - 1 ) % 4;
1733 $m = 32.044093161144 + 1.5542417966212 * $a + $b / 4.0 - 0.0031777940220923 * ( $year - 1 );
1734 if ( $m < 0 ) {
1735 $m--;
1736 }
1737 $Mar = intval( $m );
1738 if ( $m < 0 ) {
1739 $m++;
1740 }
1741 $m -= $Mar;
1742
1743 $c = ( $Mar + 3 * ( $year - 1 ) + 5 * $b + 5 ) % 7;
1744 if ( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
1745 $Mar++;
1746 } elseif ( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
1747 $Mar += 2;
1748 } elseif ( $c == 2 || $c == 4 || $c == 6 ) {
1749 $Mar++;
1750 }
1751
1752 $Mar += intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
1753 return $Mar;
1754 }
1755
1767 private static function tsToYear( $ts, $cName ) {
1768 $gy = (int)substr( $ts, 0, 4 );
1769 $gm = (int)substr( $ts, 4, 2 );
1770 $gd = (int)substr( $ts, 6, 2 );
1771
1772 if ( $cName === 'thai' ) {
1773 # Thai solar dates
1774 # Add 543 years to the Gregorian calendar
1775 # Months and days are identical
1776 $gy_offset = $gy + 543;
1777 # fix for dates between 1912 and 1941
1778 # https://en.wikipedia.org/?oldid=836596673#New_year
1779 if ( $gy >= 1912 && $gy <= 1940 ) {
1780 if ( $gm <= 3 ) {
1781 $gy_offset--;
1782 }
1783 $gm = ( $gm - 3 ) % 12;
1784 }
1785 } elseif ( $cName === 'minguo' || $cName === 'juche' ) {
1786 # Minguo dates
1787 # Deduct 1911 years from the Gregorian calendar
1788 # Months and days are identical
1789 $gy_offset = $gy - 1911;
1790 } else {
1791 $gy_offset = $gy;
1792 }
1793
1794 return [ $gy_offset, $gm, $gd ];
1795 }
1796
1805 private static function tsToJapaneseGengo( $ts ) {
1806 # Nengō dates up to Meiji period.
1807 # Deduct years from the Gregorian calendar
1808 # depending on the nengo periods
1809 # The months and days are identical
1810 $gy = (int)substr( $ts, 0, 4 );
1811 $ts = (int)$ts;
1812 if ( $ts >= 18730101000000 && $ts < 19120730000000 ) {
1813 # Meiji period; start from meiji 6 (1873) it starts using gregorian year
1814 return self::tsToJapaneseGengoCalculate( $gy, 1868, '明治' );
1815 } elseif ( $ts >= 19120730000000 && $ts < 19261225000000 ) {
1816 # Taishō period
1817 return self::tsToJapaneseGengoCalculate( $gy, 1912, '大正' );
1818 } elseif ( $ts >= 19261225000000 && $ts < 19890108000000 ) {
1819 # Shōwa period
1820 return self::tsToJapaneseGengoCalculate( $gy, 1926, '昭和' );
1821 } elseif ( $ts >= 19890108000000 && $ts < 20190501000000 ) {
1822 # Heisei period
1823 return self::tsToJapaneseGengoCalculate( $gy, 1989, '平成' );
1824 } elseif ( $ts >= 20190501000000 ) {
1825 # Reiwa period
1826 return self::tsToJapaneseGengoCalculate( $gy, 2019, '令和' );
1827 }
1828 return "西暦$gy";
1829 }
1830
1841 private static function tsToJapaneseGengoCalculate( $gy, $startYear, $gengo ) {
1842 $gy_offset = $gy - $startYear + 1;
1843 if ( $gy_offset == 1 ) {
1844 $gy_offset = '元';
1845 }
1846 return "$gengo$gy_offset";
1847 }
1848
1862 private static function strongDirFromContent( $text = '' ) {
1863 if ( !preg_match( self::$strongDirRegex, $text, $matches ) ) {
1864 return null;
1865 }
1866 if ( $matches[1] === '' ) {
1867 return 'rtl';
1868 }
1869 return 'ltr';
1870 }
1871
1879 public static function romanNumeral( $num ) {
1880 static $table = [
1881 [ '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ],
1882 [ '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ],
1883 [ '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ],
1884 [ '', 'M', 'MM', 'MMM', 'MMMM', 'MMMMM', 'MMMMMM', 'MMMMMMM',
1885 'MMMMMMMM', 'MMMMMMMMM', 'MMMMMMMMMM' ]
1886 ];
1887
1888 $num = intval( $num );
1889 if ( $num > 10000 || $num <= 0 ) {
1890 return (string)$num;
1891 }
1892
1893 $s = '';
1894 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1895 if ( $num >= $pow10 ) {
1896 $s .= $table[$i][(int)floor( $num / $pow10 )];
1897 }
1898 $num %= $pow10;
1899 }
1900 return $s;
1901 }
1902
1910 public static function hebrewNumeral( $num ) {
1911 static $table = [
1912 [ '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ],
1913 [ '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ],
1914 [ '',
1915 [ 'ק' ],
1916 [ 'ר' ],
1917 [ 'ש' ],
1918 [ 'ת' ],
1919 [ 'ת', 'ק' ],
1920 [ 'ת', 'ר' ],
1921 [ 'ת', 'ש' ],
1922 [ 'ת', 'ת' ],
1923 [ 'ת', 'ת', 'ק' ],
1924 [ 'ת', 'ת', 'ר' ],
1925 ],
1926 [ '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ]
1927 ];
1928
1929 $num = intval( $num );
1930 if ( $num > 9999 || $num <= 0 ) {
1931 return (string)$num;
1932 }
1933
1934 // Round thousands have special notations
1935 if ( $num === 1000 ) {
1936 return "א' אלף";
1937 } elseif ( $num % 1000 === 0 ) {
1938 return $table[0][$num / 1000] . "' אלפים";
1939 }
1940
1941 $letters = [];
1942
1943 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1944 if ( $num >= $pow10 ) {
1945 if ( $num === 15 || $num === 16 ) {
1946 $letters[] = $table[0][9];
1947 $letters[] = $table[0][$num - 9];
1948 $num = 0;
1949 } else {
1950 $letters = array_merge(
1951 $letters,
1952 (array)$table[$i][intval( $num / $pow10 )]
1953 );
1954
1955 if ( $pow10 === 1000 ) {
1956 $letters[] = "'";
1957 }
1958 }
1959 }
1960
1961 $num %= $pow10;
1962 }
1963
1964 $preTransformLength = count( $letters );
1965 if ( $preTransformLength === 1 ) {
1966 // Add geresh (single quote) to one-letter numbers
1967 $letters[] = "'";
1968 } else {
1969 $lastIndex = $preTransformLength - 1;
1970 $letters[$lastIndex] = strtr(
1971 $letters[$lastIndex],
1972 [ 'כ' => 'ך', 'מ' => 'ם', 'נ' => 'ן', 'פ' => 'ף', 'צ' => 'ץ' ]
1973 );
1974
1975 // Add gershayim (double quote) to multiple-letter numbers,
1976 // but exclude numbers with only one letter after the thousands
1977 // (1001-1009, 1020, 1030, 2001-2009, etc.)
1978 if ( $letters[1] === "'" && $preTransformLength === 3 ) {
1979 $letters[] = "'";
1980 } else {
1981 array_splice( $letters, -1, 0, '"' );
1982 }
1983 }
1984
1985 return implode( $letters );
1986 }
1987
1996 public function userAdjust( $ts, $tz = false ) {
1997 $localTZoffset = $this->config->get( MainConfigNames::LocalTZoffset );
1998 if ( $tz === false ) {
1999 $optionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
2000 $tz = $optionsLookup->getOption(
2001 RequestContext::getMain()->getUser(),
2002 'timecorrection'
2003 );
2004 }
2005
2006 $timeCorrection = new UserTimeCorrection( (string)$tz, null, $localTZoffset );
2007
2008 $tzObj = $timeCorrection->getTimeZone();
2009 if ( $tzObj ) {
2010 $date = new DateTime( $ts, new DateTimeZone( 'UTC' ) );
2011 $date->setTimezone( $tzObj );
2012 return self::makeMediaWikiTimestamp( $ts, $date );
2013 }
2014 $minDiff = $timeCorrection->getTimeOffset();
2015
2016 # No difference? Return the time unchanged
2017 if ( $minDiff === 0 ) {
2018 return $ts;
2019 }
2020
2021 $date = new DateTime( $ts );
2022 $date->modify( "{$minDiff} minutes" );
2023 return self::makeMediaWikiTimestamp( $ts, $date );
2024 }
2025
2041 private function convertDateFormatToJs( $format ) {
2042 // CLDR calendar IDs by format code
2043 $calendars = [
2044 'j' => 'hebrew',
2045 'i' => 'persian', // Iranian
2046 'm' => 'islamic', // Hijri
2047 'k' => 'buddhist', // Thai
2048 'o' => 'roc', // Minguo
2049 't' => 'japanese' // Tenno
2050 ];
2051
2052 $s = '';
2053 $mwOpts = [];
2054 $intlOpts = [
2055 'numberingSystem' => $this->localisationCache
2056 ->getItem( $this->mCode, 'numberingSystem' ) ?? 'latn'
2057 ];
2058 $unsupported = [];
2059 $formatLength = strlen( $format );
2060
2061 for ( $p = 0; $p < $formatLength; $p++ ) {
2062 $code = $format[$p];
2063 if ( $code == 'x' && $p < $formatLength - 1 ) {
2064 $code .= $format[++$p];
2065 }
2066
2067 if ( ( $code === 'xi'
2068 || $code === 'xj'
2069 || $code === 'xk'
2070 || $code === 'xm'
2071 || $code === 'xo'
2072 || $code === 'xt' )
2073 && $p < $formatLength - 1
2074 ) {
2075 $code .= $format[++$p];
2076 }
2077
2078 switch ( $code ) {
2079 case 'xx':
2080 $s .= 'x';
2081 break;
2082
2083 case 'xn':
2084 case 'xN':
2085 // Raw number -- usually safe enough in ISO 8601 styles
2086 // although we don't support switching
2087 $mwOpts['locale'] = 'en';
2088 unset( $intlOpts['numberingSystem'] );
2089 break;
2090
2091 case 'xr':
2092 // Roman numerals
2093 // Multiple numbering systems are usually used in the one format,
2094 // which is unsupported. Also, browsers do not implement it.
2095 $unsupported[] = $code;
2096 break;
2097
2098 case 'xh':
2099 // Hebrew numerals. Browsers do not implement it as of 2025,
2100 // and usually multiple numbering systems are desired in a
2101 // single format.
2102 $unsupported[] = $code;
2103 $intlOpts['numberingSystem'] = 'hebr';
2104 break;
2105
2106 case 'xg':
2107 // Genitive month name
2108 $intlOpts['month'] = 'long';
2109 $s .= '{mwMonthGen}';
2110 break;
2111
2112 case 'xjx':
2113 // Genitive month name in Hebrew calendar
2114 $intlOpts['calendar'] = 'hebrew';
2115 $intlOpts['month'] = 'long';
2116 $s .= '{mwMonthGen}';
2117 break;
2118
2119 case 'd':
2120 $intlOpts['day'] = '2-digit';
2121 $s .= '{day}';
2122 break;
2123
2124 case 'D':
2125 $intlOpts['weekday'] = 'short';
2126 $s .= '{weekday}';
2127 break;
2128
2129 case 'j':
2130 $intlOpts['day'] = 'numeric';
2131 $s .= '{day}';
2132 break;
2133
2134 case 'xij':
2135 case 'xmj':
2136 case 'xjj':
2137 // Day number in special calendar
2138 $intlOpts['day'] = 'numeric';
2139 $intlOpts['calendar'] = $calendars[$code[1]];
2140 $s .= '{day}';
2141 break;
2142
2143 case 'l':
2144 $intlOpts['weekday'] = 'long';
2145 $s .= '{weekday}';
2146 break;
2147
2148 case 'F':
2149 $intlOpts['month'] = 'long';
2150 $s .= '{mwMonth}';
2151 break;
2152
2153 case 'xiF':
2154 case 'xmF':
2155 case 'xjF':
2156 // Full month name in special calendar
2157 $intlOpts['month'] = 'long';
2158 $intlOpts['calendar'] = $calendars[$code[1]];
2159 $s .= '{month}';
2160 break;
2161
2162 case 'm':
2163 $intlOpts['month'] = '2-digit';
2164 $s .= '{month}';
2165 break;
2166
2167 case 'M':
2168 $intlOpts['month'] = 'short';
2169 $s .= '{mwMonthAbbrev}';
2170 break;
2171
2172 case 'n':
2173 $intlOpts['month'] = 'numeric';
2174 $s .= '{month}';
2175 break;
2176
2177 case 'xin':
2178 case 'xmn':
2179 case 'xjn':
2180 // Numeric month in special calendar
2181 $intlOpts['month'] = 'numeric';
2182 $intlOpts['calendar'] = $calendars[$code[1]];
2183 $s .= '{month}';
2184 break;
2185
2186 case 'xjt':
2187 // Number of days in the given Hebrew month -- not supported
2188 $unsupported[] = $code;
2189 break;
2190
2191 case 'Y':
2192 $intlOpts['year'] = 'numeric';
2193 $s .= '{year}';
2194 break;
2195
2196 case 'xiY':
2197 case 'xmY':
2198 case 'xjY':
2199 case 'xkY':
2200 case 'xoY':
2201 // Year number in special calendar
2202 $intlOpts['year'] = 'numeric';
2203 $intlOpts['calendar'] = $calendars[$code[1]];
2204 $s .= '{year}';
2205 break;
2206
2207 case 'xtY':
2208 // Japanese year needs to be prefixed with the era name to
2209 // be consistent with tsToJapaneseGengo()
2210 $intlOpts['era'] = 'short';
2211 $intlOpts['year'] = 'numeric';
2212 $intlOpts['calendar'] = $calendars[$code[1]];
2213 $s .= '{era}{year}';
2214 break;
2215
2216 case 'y':
2217 $intlOpts['year'] = '2-digit';
2218 $s .= '{year}';
2219 break;
2220
2221 case 'xiy':
2222 // Iranian 2-digit year
2223 $intlOpts['calendar'] = 'persian';
2224 $intlOpts['year'] = '2-digit';
2225 $s .= '{year}';
2226 break;
2227
2228 case 'xit':
2229 // Number of days in Iranian month -- not supported
2230 $unsupported[] = $code;
2231 break;
2232
2233 case 'xiz':
2234 // Day of the year -- not supported
2235 $unsupported[] = $code;
2236 break;
2237
2238 case 'a':
2239 case 'A':
2240 // AM/PM
2241 $intlOpts['hour12'] = true;
2242 $s .= '{dayPeriod}';
2243 break;
2244
2245 case 'g':
2246 // Hour in 12-hour clock, without leading zero
2247 $intlOpts['hour'] = 'numeric';
2248 $intlOpts['hour12'] = true;
2249 $s .= '{hour}';
2250 break;
2251
2252 case 'G':
2253 // Hour in 24-hour clock, without leading zero
2254 $intlOpts['hour'] = 'numeric';
2255 $s .= '{hour}';
2256 break;
2257
2258 case 'h':
2259 // Hour in 12-hour clock, with leading zero
2260 $intlOpts['hour'] = '2-digit';
2261 $intlOpts['hour12'] = true;
2262 $s .= '{hour}';
2263 break;
2264
2265 case 'H':
2266 // Hour in 24-hour clock, without leading zero
2267 $intlOpts['hour'] = '2-digit';
2268 $intlOpts['hour12'] = false;
2269 $s .= '{hour}';
2270 break;
2271
2272 case 'i':
2273 $intlOpts['minute'] = '2-digit';
2274 $s .= '{minute}';
2275 break;
2276
2277 case 's':
2278 $intlOpts['second'] = '2-digit';
2279 $s .= '{second}';
2280 break;
2281
2282 case 'c':
2283 // ISO 8601
2284 $mwOpts['locale'] = 'en';
2285 $intlOpts += [
2286 'year' => 'numeric', 'month' => '2-digit', 'day' => '2-digit',
2287 'hour' => '2-digit', 'minute' => '2-digit', 'second' => '2-digit',
2288 'hour12' => false, 'timeZoneName' => 'longOffset'
2289 ];
2290 $s .= '{year}-{month}-{day}T{hour}:{minute}:{second}{timeZoneName}';
2291 break;
2292
2293 case 'r':
2294 // RFC 2822
2295 $mwOpts['locale'] = 'en';
2296 $intlOpts += [
2297 'year' => 'numeric', 'month' => 'short', 'day' => 'numeric',
2298 'hour' => '2-digit', 'minute' => '2-digit', 'second' => '2-digit',
2299 'hour12' => false, 'weekday' => 'short', 'timeZoneName' => 'longOffset',
2300 ];
2301 $s .= '{weekday}, {day} {month} {year} {hour}:{minute}:{second} {timeZoneName}';
2302 break;
2303
2304 case 'e':
2305 // Time zone identifier -- not supported, fall through to offset
2306 case 'O':
2307 // Offset without colon not supported
2308 $unsupported[] = $code;
2309 // Fall through to with colon
2310 case 'P':
2311 $intlOpts['timeZoneName'] = 'longOffset';
2312 $s .= '{timeZoneName}';
2313 break;
2314
2315 case 'T':
2316 // Time zone abbreviation
2317 $intlOpts['timeZoneName'] = 'short';
2318 $s .= '{timeZoneName}';
2319 break;
2320
2321 case 'w':
2322 case 'N':
2323 // Numeric day of week -- not supported
2324 $unsupported[] = $code;
2325 $intlOpts['weekday'] = 'short';
2326 $s .= '{weekday}';
2327 break;
2328
2329 case 'z':
2330 // Day of year
2331 case 'W':
2332 // Week number
2333 case 't':
2334 // Number of days in month
2335 case 'L':
2336 // Leap year flag
2337 case 'o':
2338 // ISO week numbering year
2339 case 'U':
2340 // Seconds since UNIX epoch
2341 case 'I':
2342 // DST flag
2343 case 'Z':
2344 // Timezone offset in seconds
2345 $unsupported[] = $code;
2346 break;
2347
2348 case '\\':
2349 # Backslash escaping
2350 if ( $p < $formatLength - 1 ) {
2351 $s .= $format[++$p];
2352 } else {
2353 $s .= '\\';
2354 }
2355 break;
2356
2357 case '"':
2358 # Quoted literal
2359 if ( $p < $formatLength - 1 ) {
2360 $endQuote = strpos( $format, '"', $p + 1 );
2361 if ( $endQuote === false ) {
2362 # No terminating quote, assume literal "
2363 $s .= '"';
2364 } else {
2365 $s .= substr( $format, $p + 1, $endQuote - $p - 1 );
2366 $p = $endQuote;
2367 }
2368 } else {
2369 # Quote at the end of the string, assume literal "
2370 $s .= '"';
2371 }
2372 break;
2373
2374 default:
2375 $s .= $format[$p];
2376 }
2377 }
2378 $mwOpts['options'] = $intlOpts;
2379 $mwOpts['pattern'] = $s;
2380 if ( $unsupported ) {
2381 $mwOpts['error'] = 'Unsupported format code(s): ' .
2382 implode( ', ', $unsupported );
2383 }
2384 return $mwOpts;
2385 }
2386
2396 private static function makeMediaWikiTimestamp( $fallback, $date ) {
2397 $ts = $date->format( 'YmdHis' );
2398 return strlen( $ts ) === 14 ? $ts : $fallback;
2399 }
2400
2416 public function dateFormat( $usePrefs = true ) {
2417 if ( is_bool( $usePrefs ) ) {
2418 if ( $usePrefs ) {
2419 $datePreference = RequestContext::getMain()
2420 ->getUser()
2421 ->getDatePreference();
2422 } else {
2423 $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
2424 $datePreference = (string)$userOptionsLookup->getDefaultOption( 'date' );
2425 }
2426 } else {
2427 $datePreference = (string)$usePrefs;
2428 }
2429
2430 // return int
2431 if ( $datePreference == '' ) {
2432 return 'default';
2433 }
2434
2435 return $datePreference;
2436 }
2437
2448 public function getDateFormatString( $type, $pref ) {
2449 $wasDefault = false;
2450 if ( $pref == 'default' ) {
2451 $wasDefault = true;
2452 $pref = $this->getDefaultDateFormat();
2453 }
2454
2455 if ( !isset( $this->dateFormatStrings[$type][$pref] ) ) {
2456 $df = $this->localisationCache
2457 ->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2458
2459 if ( $type === 'pretty' && $df === null ) {
2460 $df = $this->getDateFormatString( 'date', $pref );
2461 }
2462
2463 if ( !$wasDefault && $df === null ) {
2464 $pref = $this->getDefaultDateFormat();
2465 $df = $this->localisationCache
2466 ->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2467 }
2468
2469 $this->dateFormatStrings[$type][$pref] = $df;
2470 }
2471 return $this->dateFormatStrings[$type][$pref];
2472 }
2473
2484 public function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
2485 $ts = wfTimestamp( TS::MW, $ts );
2486 if ( $adj ) {
2487 $ts = $this->userAdjust( $ts, $timecorrection );
2488 }
2489 $df = $this->getDateFormatString( 'date', $this->dateFormat( $format ) );
2490 return $this->sprintfDate( $df, $ts );
2491 }
2492
2503 public function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
2504 $ts = wfTimestamp( TS::MW, $ts );
2505 if ( $adj ) {
2506 $ts = $this->userAdjust( $ts, $timecorrection );
2507 }
2508 $df = $this->getDateFormatString( 'time', $this->dateFormat( $format ) );
2509 return $this->sprintfDate( $df, $ts );
2510 }
2511
2523 public function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false ) {
2524 $ts = wfTimestamp( TS::MW, $ts );
2525 if ( $adj ) {
2526 $ts = $this->userAdjust( $ts, $timecorrection );
2527 }
2528 $df = $this->getDateFormatString( 'both', $this->dateFormat( $format ) );
2529 return $this->sprintfDate( $df, $ts );
2530 }
2531
2542 public function formatDuration( $seconds, array $chosenIntervals = [] ) {
2543 $intervals = $this->getDurationIntervals( $seconds, $chosenIntervals );
2544
2545 $segments = [];
2546
2547 foreach ( $intervals as $intervalName => $intervalValue ) {
2548 // Messages: duration-seconds, duration-minutes, duration-hours, duration-days, duration-weeks,
2549 // duration-years, duration-decades, duration-centuries, duration-millennia
2550 $message = $this->msg( 'duration-' . $intervalName )->numParams( $intervalValue );
2551 $segments[] = $message->escaped();
2552 }
2553
2554 return $this->listToText( $segments );
2555 }
2556
2567 int $timestamp1,
2568 int $timestamp2,
2569 ?int $precision = null
2570 ): string {
2571 $precision ??= count( self::DURATION_INTERVALS );
2572
2573 $sortedTimestamps = [ $timestamp1, $timestamp2 ];
2574 sort( $sortedTimestamps );
2575
2576 $tz = new DateTimeZone( 'UTC' );
2577 $date1 = ( new DateTimeImmutable() )
2578 ->setTimezone( $tz )
2579 ->setTimestamp( $sortedTimestamps[0] );
2580 $date2 = ( new DateTimeImmutable() )
2581 ->setTimezone( $tz )
2582 ->setTimestamp( $sortedTimestamps[1] );
2583
2584 $interval = $date1->diff( $date2 );
2585
2586 $format = [];
2587 if ( $interval->y >= 1000 ) {
2588 $millennia = floor( $interval->y / 1000 );
2589 $format[] = $this->msg( 'duration-millennia' )->numParams( $millennia )->text();
2590 $interval->y -= $millennia * 1000;
2591 }
2592 if ( $interval->y >= 100 ) {
2593 $centuries = floor( $interval->y / 100 );
2594 $format[] = $this->msg( 'duration-centuries' )->numParams( $centuries )->text();
2595 $interval->y -= $centuries * 100;
2596 }
2597 if ( $interval->y >= 10 ) {
2598 $decades = floor( $interval->y / 10 );
2599 $format[] = $this->msg( 'duration-decades' )->numParams( $decades )->text();
2600 $interval->y -= $decades * 10;
2601 }
2602 if ( $interval->y !== 0 ) {
2603 $format[] = $this->msg( 'duration-years' )->numParams( $interval->y )->text();
2604 }
2605 if ( $interval->m !== 0 ) {
2606 $format[] = $this->msg( 'duration-months' )->numParams( $interval->m )->text();
2607 }
2608 if ( $interval->d !== 0 ) {
2609 $format[] = $this->msg( 'duration-days' )->numParams( $interval->d )->text();
2610 }
2611 if ( $interval->h !== 0 ) {
2612 $format[] = $this->msg( 'duration-hours' )->numParams( $interval->h )->text();
2613 }
2614 if ( $interval->i !== 0 ) {
2615 $format[] = $this->msg( 'duration-minutes' )->numParams( $interval->i )->text();
2616 }
2617 if ( $interval->s !== 0 ) {
2618 $format[] = $this->msg( 'duration-seconds' )->numParams( $interval->s )->text();
2619 }
2620
2621 // slice the array to the provided precision
2622 $format = array_slice( $format, 0, $precision );
2623 // build the string from the array
2624 $format = $this->listToText( $format );
2625
2626 return $format ?: $this->msg( 'duration-seconds' )->numParams( 0 )->text();
2627 }
2628
2640 public function getDurationIntervals( $seconds, array $chosenIntervals = [] ) {
2641 if ( !$chosenIntervals ) {
2642 // Default intervals. Do not include `months` as they were not part of the original default implementation
2643 $chosenIntervals = [
2644 'millennia',
2645 'centuries',
2646 'decades',
2647 'years',
2648 'days',
2649 'hours',
2650 'minutes',
2651 'seconds'
2652 ];
2653 }
2654
2655 $intervals = array_intersect_key( self::DURATION_INTERVALS,
2656 array_fill_keys( $chosenIntervals, true ) );
2657 $sortedNames = array_keys( $intervals );
2658 $smallestInterval = array_pop( $sortedNames );
2659
2660 $segments = [];
2661
2662 foreach ( $intervals as $name => $length ) {
2663 $value = floor( $seconds / $length );
2664
2665 if ( $value > 0 || ( $name == $smallestInterval && !$segments ) ) {
2666 $seconds -= $value * $length;
2667 $segments[$name] = $value;
2668 }
2669 }
2670
2671 return $segments;
2672 }
2673
2693 private function internalUserTimeAndDate( $type, $ts, UserIdentity $user, array $options ) {
2694 $ts = wfTimestamp( TS::MW, $ts );
2695 $options += [ 'timecorrection' => true, 'format' => true ];
2696 if ( $options['timecorrection'] !== false ) {
2697 if ( $options['timecorrection'] === true ) {
2698 $offset = MediaWikiServices::getInstance()
2699 ->getUserOptionsLookup()
2700 ->getOption( $user, 'timecorrection' );
2701 } else {
2702 $offset = $options['timecorrection'];
2703 }
2704 $ts = $this->userAdjust( $ts, $offset );
2705 }
2706 if ( $options['format'] === true ) {
2707 $format = MediaWikiServices::getInstance()
2708 ->getUserFactory()
2709 ->newFromUserIdentity( $user )
2710 ->getDatePreference();
2711 } else {
2712 $format = $options['format'];
2713 }
2714 $df = $this->getDateFormatString( $type, $this->dateFormat( $format ) );
2715 return $this->sprintfDate( $df, $ts );
2716 }
2717
2737 public function userDate( $ts, UserIdentity $user, array $options = [] ) {
2738 return $this->internalUserTimeAndDate( 'date', $ts, $user, $options );
2739 }
2740
2760 public function userTime( $ts, UserIdentity $user, array $options = [] ) {
2761 return $this->internalUserTimeAndDate( 'time', $ts, $user, $options );
2762 }
2763
2783 public function userTimeAndDate( $ts, UserIdentity $user, array $options = [] ) {
2784 return $this->internalUserTimeAndDate( 'both', $ts, $user, $options );
2785 }
2786
2802 public function getHumanTimestamp(
2803 MWTimestamp $time, ?MWTimestamp $relativeTo = null, ?UserIdentity $user = null
2804 ) {
2805 $relativeTo ??= new MWTimestamp();
2806 if ( $user === null ) {
2807 $user = RequestContext::getMain()->getUser();
2808 } else {
2809 // For compatibility with the hook signature and self::getHumanTimestampInternal
2810 $user = MediaWikiServices::getInstance()
2811 ->getUserFactory()
2812 ->newFromUserIdentity( $user );
2813 }
2814
2815 // Adjust for the user's timezone.
2816 $offsetThis = $time->offsetForUser( $user );
2817 $offsetRel = $relativeTo->offsetForUser( $user );
2818
2819 $ts = '';
2820 if ( $this->getHookRunner()->onGetHumanTimestamp( $ts, $time, $relativeTo, $user, $this ) ) {
2821 $ts = $this->getHumanTimestampInternal( $time, $relativeTo, $user );
2822 }
2823
2824 // Reset the timezone on the objects.
2825 $time->timestamp->sub( $offsetThis );
2826 $relativeTo->timestamp->sub( $offsetRel );
2827
2828 return $ts;
2829 }
2830
2842 private function getHumanTimestampInternal(
2843 MWTimestamp $ts, MWTimestamp $relativeTo, User $user
2844 ) {
2845 $diff = $ts->diff( $relativeTo );
2846 $diffDay = (bool)( (int)$ts->timestamp->format( 'w' ) -
2847 (int)$relativeTo->timestamp->format( 'w' ) );
2848 $days = $diff->days ?: (int)$diffDay;
2849
2850 if ( $diff->invert ) {
2851 // Future dates: Use full timestamp
2855 $format = $this->getDateFormatString( 'both', $user->getDatePreference() ?: 'default' );
2856 $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS::MW ) );
2857 } elseif (
2858 $days > 5 &&
2859 $ts->timestamp->format( 'Y' ) !== $relativeTo->timestamp->format( 'Y' )
2860 ) {
2861 // Timestamps are in different years and more than 5 days apart: use full date
2862 $format = $this->getDateFormatString( 'date', $user->getDatePreference() ?: 'default' );
2863 $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS::MW ) );
2864 } elseif ( $days > 5 ) {
2865 // Timestamps are in same year and more than 5 days ago: show day and month only.
2866 $format = $this->getDateFormatString( 'pretty', $user->getDatePreference() ?: 'default' );
2867 $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS::MW ) );
2868 } elseif ( $days > 1 ) {
2869 // Timestamp within the past 5 days: show the day of the week and time
2870 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2871 $weekday = self::WEEKDAY_MESSAGES[(int)$ts->timestamp->format( 'w' )];
2872 // The following messages are used here:
2873 // * sunday-at, monday-at, tuesday-at, wednesday-at, thursday-at, friday-at, saturday-at
2874 $ts = $this->msg( "$weekday-at" )
2875 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS::MW ) ) )
2876 ->text();
2877 } elseif ( $days == 1 ) {
2878 // Timestamp was yesterday: say 'yesterday' and the time.
2879 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2880 $ts = $this->msg( 'yesterday-at' )
2881 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS::MW ) ) )
2882 ->text();
2883 } elseif ( $diff->h > 1 || ( $diff->h == 1 && $diff->i > 30 ) ) {
2884 // Timestamp was today, but more than 90 minutes ago: say 'today' and the time.
2885 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2886 $ts = $this->msg( 'today-at' )
2887 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS::MW ) ) )
2888 ->text();
2889
2890 // From here on in, the timestamp was soon enough ago so that we can simply say
2891 // XX units ago, e.g., "2 hours ago" or "5 minutes ago"
2892 } elseif ( $diff->h == 1 ) {
2893 // Less than 90 minutes, but more than an hour ago.
2894 $ts = $this->msg( 'hours-ago' )->numParams( 1 )->text();
2895 } elseif ( $diff->i >= 1 ) {
2896 // A few minutes ago.
2897 $ts = $this->msg( 'minutes-ago' )->numParams( $diff->i )->text();
2898 } elseif ( $diff->s >= 30 ) {
2899 // Less than a minute, but more than 30 sec ago.
2900 $ts = $this->msg( 'seconds-ago' )->numParams( $diff->s )->text();
2901 } else {
2902 // Less than 30 seconds ago.
2903 $ts = $this->msg( 'just-now' )->text();
2904 }
2905
2906 return $ts;
2907 }
2908
2917 public function getGroupName( $group ) {
2918 $msg = $this->msg( "group-$group" );
2919 return $msg->isBlank() ? $group : $msg->text();
2920 }
2921
2931 public function getGroupMemberName( string $group, $member ) {
2932 if ( $member instanceof UserIdentity ) {
2933 $member = $member->getName();
2934 }
2935 $msg = $this->msg( "group-$group-member", $member );
2936 return $msg->isBlank() ? $group : $msg->text();
2937 }
2938
2944 public function getMessage( $key ) {
2945 return $this->localisationCache->getSubitem( $this->mCode, 'messages', $key );
2946 }
2947
2952 public function getAllMessages() {
2953 return $this->localisationCache->getItem( $this->mCode, 'messages' );
2954 }
2955
2962 public function iconv( $in, $out, $string ) {
2963 # Even with //IGNORE iconv can whine about illegal characters in
2964 # *input* string. We just ignore those too.
2965 # REF: https://bugs.php.net/bug.php?id=37166
2966 # REF: https://phabricator.wikimedia.org/T18885
2967 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
2968 return @iconv( $in, $out . '//IGNORE', $string );
2969 }
2970
2975 public function ucfirst( $str ) {
2976 // T410920: ord() doesn't like an empty string, so just return early
2977 if ( $str === '' ) {
2978 return '';
2979 }
2980
2981 $octetCode = ord( $str[0] );
2982 // See https://en.wikipedia.org/wiki/ASCII#Printable_characters
2983 if ( $octetCode < 96 ) {
2984 // Assume this is an uppercase/uncased ASCII character
2985 return (string)$str;
2986 } elseif ( $octetCode < 128 ) {
2987 // Assume this is a lowercase/uncased ASCII character
2988 return ucfirst( $str );
2989 }
2990 $first = mb_substr( $str, 0, 1 );
2991 if ( strlen( $first ) === 1 ) {
2992 // Broken UTF-8?
2993 return ucfirst( $str );
2994 }
2995
2996 // Memoize the config table
2997 $overrides = $this->overrideUcfirstCharacters
2998 ??= $this->config->get( MainConfigNames::OverrideUcfirstCharacters );
2999
3000 // Use the config table and fall back to MB_CASE_TITLE
3001 $ucFirst = $overrides[$first] ?? mb_convert_case( $first, MB_CASE_TITLE );
3002 if ( $ucFirst !== $first ) {
3003 return $ucFirst . mb_substr( $str, 1 );
3004 } else {
3005 return $str;
3006 }
3007 }
3008
3014 public function uc( $str, $first = false ) {
3015 if ( $first ) {
3016 return $this->ucfirst( $str );
3017 } else {
3018 return $this->isMultibyte( $str ) ? mb_strtoupper( $str ) : strtoupper( $str );
3019 }
3020 }
3021
3026 public function lcfirst( $str ) {
3027 // T410920: ord() doesn't like an empty string, so just return early
3028 if ( $str === '' ) {
3029 return '';
3030 }
3031
3032 $octetCode = ord( $str[0] );
3033 // See https://en.wikipedia.org/wiki/ASCII#Printable_characters
3034 if ( $octetCode < 96 ) {
3035 // Assume this is an uppercase/uncased ASCII character
3036 return lcfirst( $str );
3037 } elseif ( $octetCode < 128 ) {
3038 // Assume this is a lowercase/uncased ASCII character
3039 return (string)$str;
3040 }
3041
3042 return $this->isMultibyte( $str )
3043 // Assume this is a multibyte character and mb_internal_encoding() is appropriate
3044 ? mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 )
3045 // Assume this is a non-multibyte character and LC_CASE is appropriate
3046 : lcfirst( $str );
3047 }
3048
3054 public function lc( $str, $first = false ) {
3055 if ( $first ) {
3056 return $this->lcfirst( $str );
3057 } else {
3058 return $this->isMultibyte( $str ) ? mb_strtolower( $str ) : strtolower( $str );
3059 }
3060 }
3061
3066 private function isMultibyte( $str ) {
3067 return strlen( $str ) !== mb_strlen( $str );
3068 }
3069
3074 public function ucwords( $str ) {
3075 if ( $this->isMultibyte( $str ) ) {
3076 $str = $this->lc( $str );
3077
3078 // regexp to find the first letter in each word (i.e., after each space)
3079 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
3080
3081 // function to use to capitalize a single char
3082 return preg_replace_callback(
3083 $replaceRegexp,
3084 static function ( $matches ) {
3085 return mb_strtoupper( $matches[0] );
3086 },
3087 $str
3088 );
3089 } else {
3090 return ucwords( strtolower( $str ) );
3091 }
3092 }
3093
3100 public function ucwordbreaks( $str ) {
3101 if ( $this->isMultibyte( $str ) ) {
3102 $str = $this->lc( $str );
3103
3104 // since \b doesn't work for UTF-8, we explicitly define word break chars
3105 $breaks = "[ \-\‍(\‍)\}\{\.,\?!]";
3106
3107 // find the first letter after word break
3108 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|" .
3109 "$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
3110
3111 return preg_replace_callback(
3112 $replaceRegexp,
3113 static function ( $matches ) {
3114 return mb_strtoupper( $matches[0] );
3115 },
3116 $str
3117 );
3118 } else {
3119 return preg_replace_callback(
3120 '/\b([\w\x80-\xff]+)\b/',
3121 function ( $matches ) {
3122 return $this->ucfirst( $matches[1] );
3123 },
3124 $str
3125 );
3126 }
3127 }
3128
3144 public function caseFold( $s ) {
3145 return $this->uc( $s );
3146 }
3147
3152 public function checkTitleEncoding( string $s ) {
3153 if ( StringUtils::isUtf8( $s ) ) {
3154 return $s;
3155 }
3156
3157 return $this->iconv( $this->fallback8bitEncoding(), 'utf-8', $s );
3158 }
3159
3163 public function fallback8bitEncoding() {
3164 return $this->localisationCache->getItem( $this->mCode, 'fallback8bitEncoding' );
3165 }
3166
3175 public function hasWordBreaks() {
3176 return true;
3177 }
3178
3186 public function segmentByWord( $string ) {
3187 return $string;
3188 }
3189
3195 protected function getSearchIndexVariant() {
3196 return null;
3197 }
3198
3210 public function normalizeForSearch( $text ) {
3211 $text = self::convertDoubleWidth( $text );
3212 if ( $this->getSearchIndexVariant() ) {
3213 return $this->getConverterInternal()->autoConvert( $text, $this->getSearchIndexVariant() );
3214 }
3215 return $text;
3216 }
3217
3225 protected static function convertDoubleWidth( $string ) {
3226 static $transTable = null;
3227 $transTable ??= array_combine(
3228 mb_str_split( '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' ),
3229 str_split( '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' )
3230 );
3231
3232 return strtr( $string, $transTable );
3233 }
3234
3240 protected static function insertSpace( $string, $pattern ) {
3241 $string = preg_replace( $pattern, " $1 ", $string );
3242 return preg_replace( '/ +/', ' ', $string );
3243 }
3244
3249 public function convertForSearchResult( $termsArray ) {
3250 # some languages, e.g., Chinese, need to do a conversion
3251 # in order for search results to be displayed correctly
3252 return $termsArray;
3253 }
3254
3261 public function firstChar( $s ) {
3262 $firstChar = mb_substr( $s, 0, 1 );
3263
3264 if ( $firstChar === '' || strlen( $firstChar ) != 3 ) {
3265 return $firstChar;
3266 }
3267
3268 // Break down Hangul syllables to grab the first jamo
3269 $code = mb_ord( $firstChar );
3270 if ( $code < 0xac00 || $code >= 0xd7a4 ) {
3271 return $firstChar;
3272 } elseif ( $code < 0xb098 ) {
3273 return "\u{3131}";
3274 } elseif ( $code < 0xb2e4 ) {
3275 return "\u{3134}";
3276 } elseif ( $code < 0xb77c ) {
3277 return "\u{3137}";
3278 } elseif ( $code < 0xb9c8 ) {
3279 return "\u{3139}";
3280 } elseif ( $code < 0xbc14 ) {
3281 return "\u{3141}";
3282 } elseif ( $code < 0xc0ac ) {
3283 return "\u{3142}";
3284 } elseif ( $code < 0xc544 ) {
3285 return "\u{3145}";
3286 } elseif ( $code < 0xc790 ) {
3287 return "\u{3147}";
3288 } elseif ( $code < 0xcc28 ) {
3289 return "\u{3148}";
3290 } elseif ( $code < 0xce74 ) {
3291 return "\u{314A}";
3292 } elseif ( $code < 0xd0c0 ) {
3293 return "\u{314B}";
3294 } elseif ( $code < 0xd30c ) {
3295 return "\u{314C}";
3296 } elseif ( $code < 0xd558 ) {
3297 return "\u{314D}";
3298 } else {
3299 return "\u{314E}";
3300 }
3301 }
3302
3312 public function normalize( $s ) {
3313 $allUnicodeFixes = $this->config->get( MainConfigNames::AllUnicodeFixes );
3314
3315 $s = UtfNormalValidator::cleanUp( $s );
3316 // Optimization: This is disabled by default to avoid negative performance impact.
3317 if ( $allUnicodeFixes ) {
3318 $s = $this->transformUsingPairFile( NormalizeAr::class, $s );
3319 $s = $this->transformUsingPairFile( NormalizeMl::class, $s );
3320 }
3321
3322 return $s;
3323 }
3324
3336 protected function transformUsingPairFile( string $dataClass, string $input ): string {
3337 if ( !isset( $this->transformData[$dataClass] ) ) {
3338 $this->transformData[$dataClass] = new ReplacementArray( $dataClass::PAIRS );
3339 }
3340
3341 return $this->transformData[$dataClass]->replace( $input );
3342 }
3343
3349 public function isRTL() {
3350 return $this->localisationCache->getItem( $this->mCode, 'rtl' );
3351 }
3352
3357 public function getDir() {
3358 return $this->isRTL() ? 'rtl' : 'ltr';
3359 }
3360
3369 public function alignStart() {
3370 return $this->isRTL() ? 'right' : 'left';
3371 }
3372
3381 public function alignEnd() {
3382 return $this->isRTL() ? 'left' : 'right';
3383 }
3384
3403 public function getDirMarkEntity( $opposite = false ) {
3404 wfDeprecated( __METHOD__, '1.43' );
3405
3406 if ( $opposite ) {
3407 return $this->isRTL() ? '&lrm;' : '&rlm;';
3408 }
3409 return $this->isRTL() ? '&rlm;' : '&lrm;';
3410 }
3411
3427 public function getDirMark( $opposite = false ) {
3428 if ( $opposite ) {
3429 return $this->isRTL() ? self::LRM : self::RLM;
3430 }
3431 return $this->isRTL() ? self::RLM : self::LRM;
3432 }
3433
3441 public function getArrow( $direction = 'forwards' ) {
3442 switch ( $direction ) {
3443 case 'forwards':
3444 return $this->isRTL() ? '←' : '→';
3445 case 'backwards':
3446 return $this->isRTL() ? '→' : '←';
3447 case 'left':
3448 return '←';
3449 case 'right':
3450 return '→';
3451 case 'up':
3452 return '↑';
3453 case 'down':
3454 return '↓';
3455 }
3456 }
3457
3463 public function linkPrefixExtension() {
3464 return $this->localisationCache->getItem( $this->mCode, 'linkPrefixExtension' );
3465 }
3466
3472 public function getMagicWords() {
3473 return $this->localisationCache->getItem( $this->mCode, 'magicWords' );
3474 }
3475
3481 public function getMagic( $mw ) {
3482 $rawEntry = $this->mMagicExtensions[$mw->mId] ??
3483 $this->localisationCache->getSubitem( $this->mCode, 'magicWords', $mw->mId );
3484
3485 if ( !is_array( $rawEntry ) ) {
3486 wfWarn( "\"$rawEntry\" is not a valid magic word for \"$mw->mId\"" );
3487 } else {
3488 $mw->mCaseSensitive = $rawEntry[0];
3489 $mw->mSynonyms = array_slice( $rawEntry, 1 );
3490 }
3491 }
3492
3498 public function getSpecialPageAliases() {
3499 // Cache aliases because it may be slow to load them
3500 $this->mExtendedSpecialPageAliases ??=
3501 $this->localisationCache->getItem( $this->mCode, 'specialPageAliases' );
3502
3503 return $this->mExtendedSpecialPageAliases;
3504 }
3505
3513 public function emphasize( $text ) {
3514 wfDeprecated( __METHOD__, '1.46' );
3515 return "<em>$text</em>";
3516 }
3517
3538 public function formatNum( $number ) {
3539 return $this->formatNumInternal( $number, false );
3540 }
3541
3550 private function formatNumInternal( $number, bool $noSeparators ): string {
3551 // From PHP 8.5, we can't cast NAN to string, and since the method
3552 // accepts float, there's no way to exclude NAN, which is subtype.
3553 // So handle it explicitly.
3554 $number = is_float( $number ) && is_nan( $number ) ? 'NAN' : (string)$number;
3555
3556 if ( $number === '' ) {
3557 return $number;
3558 }
3559 if ( $number === 'NAN' ) {
3560 return $this->msg( 'formatnum-nan' )->text();
3561 }
3562 if ( $number === 'INF' ) {
3563 return "∞";
3564 }
3565 if ( $number === '-INF' ) {
3566 return "\u{2212}∞";
3567 }
3568 if ( !is_numeric( $number ) ) {
3569 # T267587: downgrade this to level:warn while we chase down the long
3570 # trail of callers.
3571 # wfDeprecated( 'Language::formatNum with a non-numeric string', '1.36' );
3572 LoggerFactory::getInstance( 'formatnum' )->warning(
3573 'Language::formatNum with non-numeric string',
3574 [ 'number' => $number ]
3575 );
3576 $validNumberRe = '(-(?=[\d\.]))?(\d+|(?=\.\d))(\.\d*)?([Ee][-+]?\d+)?';
3577 // For backwards-compat, apply formatNum piecewise on the valid
3578 // numbers in the string. Don't split on NAN/INF in this legacy
3579 // case as they are likely to be found embedded inside non-numeric
3580 // text.
3581 return preg_replace_callback( "/{$validNumberRe}/", function ( $m ) use ( $noSeparators ) {
3582 return $this->formatNumInternal( $m[0], $noSeparators );
3583 }, $number );
3584 }
3585
3586 if ( !$noSeparators ) {
3587 $separatorTransformTable = $this->separatorTransformTable();
3588 $fmt = $this->getNumberFormatter();
3589
3590 // minimumGroupingDigits can be used to suppress groupings below a certain value.
3591 // This is used for languages such as Polish, where one would only write the grouping
3592 // separator for values above 9999 - numbers with more than 4 digits.
3593 // NumberFormatter is yet to support minimumGroupingDigits, ICU has it as experimental feature.
3594 // The attribute value is used by adding it to the grouping separator value. If
3595 // the input number has fewer integer digits, the grouping separator is suppressed.
3596 $minimumGroupingDigits = $this->minimumGroupingDigits();
3597 // Minimum length of a number to do digit grouping on.
3598 // http://unicode.org/reports/tr35/tr35-numbers.html#Examples_of_minimumGroupingDigits
3599 $minimumLength = $minimumGroupingDigits + $fmt->getAttribute( NumberFormatter::GROUPING_SIZE );
3600 if ( $minimumGroupingDigits > 1
3601 && !preg_match( '/^\-?\d{' . $minimumLength . '}/', $number )
3602 ) {
3603 // This number does not need commas inserted (even if
3604 // NumberFormatter thinks it does) because it's not long
3605 // enough. We still need to do decimal separator
3606 // transformation, though. For example, 1234.56 becomes 1234,56
3607 // in pl with $minimumGroupingDigits = 2.
3608 $number = strtr( $number, $separatorTransformTable ?: [] );
3609 } elseif ( $number === '-0' ) {
3610 // Special case to ensure we don't lose the minus sign by
3611 // converting to an int.
3612 $number = strtr( $number, $separatorTransformTable ?: [] );
3613 } else {
3614 // NumberFormatter supports separator transformation,
3615 // but it does not know all languages MW
3616 // supports. Example: arq. Also, languages like pl have
3617 // customisation. So manually set it.
3618 $fmt = clone $fmt;
3619
3621 $fmt->setSymbol(
3622 NumberFormatter::DECIMAL_SEPARATOR_SYMBOL,
3623 $separatorTransformTable[ '.' ] ?? '.'
3624 );
3625 $fmt->setSymbol(
3626 NumberFormatter::GROUPING_SEPARATOR_SYMBOL,
3627 $separatorTransformTable[ ',' ] ?? ','
3628 );
3629 }
3630
3631 // Maintain # of digits before and after the decimal point
3632 // (and presence of decimal point)
3633 if ( preg_match( '/^-?(\d*)(\.(\d*))?$/', $number, $m ) ) {
3634 $fmt->setAttribute( NumberFormatter::MIN_INTEGER_DIGITS, strlen( $m[1] ) );
3635 if ( isset( $m[2] ) ) {
3636 $fmt->setAttribute( NumberFormatter::DECIMAL_ALWAYS_SHOWN, 1 );
3637 }
3638 $fmt->setAttribute( NumberFormatter::FRACTION_DIGITS, strlen( $m[3] ?? '' ) );
3639 }
3640 $number = $fmt->format( (float)$number );
3641 }
3642 }
3643
3644 if ( $this->config->get( MainConfigNames::TranslateNumerals ) ) {
3645 // This is often unnecessary: PHP's NumberFormatter will often
3646 // do the digit transform itself (T267614)
3647 $s = $this->digitTransformTable();
3648 if ( $s ) {
3649 $number = strtr( $number, $s );
3650 }
3651 }
3652 # T10327: Make our formatted numbers prettier by using a
3653 # proper Unicode 'minus' character.
3654 $number = strtr( $number, [ '-' => "\u{2212}" ] );
3655
3656 // Remove any LRM or RLM characters generated from NumberFormatter,
3657 // since directionality is handled outside of this context.
3658 // Similarly remove \u61C (ALM) which is added starting PHP 7.3+
3659 return strtr( $number, [
3660 self::LRM => '',
3661 self::RLM => '',
3662 self::ALM => '',
3663 ] );
3664 }
3665
3674 public function formatNumNoSeparators( $number ) {
3675 return $this->formatNumInternal( $number, true );
3676 }
3677
3682 public function parseFormattedNumber( $number ) {
3683 if ( $number === $this->msg( 'formatnum-nan' )->text() ) {
3684 return "NAN";
3685 }
3686 if ( $number === "∞" ) {
3687 return "INF";
3688 }
3689 // Accept either ASCII hyphen-minus or the unicode minus emitted by
3690 // ::formatNum()
3691 $number = strtr( $number, [ "\u{2212}" => '-' ] );
3692 if ( $number === "-∞" ) {
3693 return "-INF";
3694 }
3695 $s = $this->digitTransformTable();
3696 if ( $s ) {
3697 // Eliminate empty array values such as ''. (T66347)
3698 $s = array_filter( $s );
3699 $number = strtr( $number, array_flip( $s ) );
3700 }
3701
3702 $s = $this->separatorTransformTable();
3703 if ( $s ) {
3704 // Eliminate empty array values such as ''. (T66347)
3705 $s = array_filter( $s );
3706 $number = strtr( $number, array_flip( $s ) );
3707 }
3708
3709 return strtr( $number, [ ',' => '' ] );
3710 }
3711
3715 public function digitGroupingPattern() {
3716 return $this->localisationCache->getItem( $this->mCode, 'digitGroupingPattern' );
3717 }
3718
3722 public function digitTransformTable() {
3723 return $this->localisationCache->getItem( $this->mCode, 'digitTransformTable' );
3724 }
3725
3729 public function separatorTransformTable() {
3730 return $this->localisationCache->getItem( $this->mCode, 'separatorTransformTable' );
3731 }
3732
3741 public function minimumGroupingDigits(): int {
3742 return $this->localisationCache->getItem( $this->mCode, 'minimumGroupingDigits' ) ?? 1;
3743 }
3744
3754 public function listToText( array $list ) {
3755 $itemCount = count( $list );
3756 if ( $itemCount < 1 ) {
3757 return '';
3758 }
3759 $text = array_pop( $list );
3760 if ( $itemCount > 1 ) {
3761 $and = $this->msg( 'and' )->escaped();
3762 $space = $this->msg( 'word-separator' )->escaped();
3763 $comma = '';
3764 if ( $itemCount > 2 ) {
3765 $comma = $this->msg( 'comma-separator' )->escaped();
3766 }
3767 $text = implode( $comma, $list ) . $and . $space . $text;
3768 }
3769 // @phan-suppress-next-line PhanTypeMismatchReturnNullable False positive
3770 return $text;
3771 }
3772
3780 public function commaList( array $list ): string {
3781 return implode(
3782 $this->msg( 'comma-separator' )->escaped(),
3783 $list
3784 );
3785 }
3786
3794 public function semicolonList( array $list ): string {
3795 return implode(
3796 $this->msg( 'semicolon-separator' )->escaped(),
3797 $list
3798 );
3799 }
3800
3807 public function pipeList( array $list ): string {
3808 return implode(
3809 $this->msg( 'pipe-separator' )->escaped(),
3810 $list
3811 );
3812 }
3813
3830 public function truncateForDatabase( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3831 return $this->truncateInternal(
3832 $string, $length, $ellipsis, $adjustLength, 'strlen', 'mb_strcut'
3833 );
3834 }
3835
3861 public function truncateForVisual( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3862 // Passing encoding to mb_strlen and mb_substr is optional.
3863 // Encoding defaults to mb_internal_encoding(), which is set to UTF-8 in Setup.php, so
3864 // explicit specification of encoding is skipped.
3865 // Note: Both multibyte methods are callables invoked in truncateInternal.
3866 return $this->truncateInternal(
3867 $string, $length, $ellipsis, $adjustLength, 'mb_strlen', 'mb_substr'
3868 );
3869 }
3870
3887 private function truncateInternal(
3888 $string, $length, $ellipsis, $adjustLength, callable $measureLength, callable $getSubstring
3889 ) {
3890 # Check if there is no need to truncate
3891 if ( $measureLength( $string ) <= abs( $length ) ) {
3892 return $string; // no need to truncate
3893 }
3894
3895 # Use the localized ellipsis character
3896 if ( $ellipsis == '...' ) {
3897 $ellipsis = $this->msg( 'ellipsis' )->text();
3898 }
3899 if ( $length == 0 ) {
3900 return $ellipsis; // convention
3901 }
3902
3903 $stringOriginal = $string;
3904 # If ellipsis length is >= $length then we can't apply $adjustLength
3905 if ( $adjustLength && $measureLength( $ellipsis ) >= abs( $length ) ) {
3906 $string = $ellipsis; // this can be slightly unexpected
3907 # Otherwise, truncate and add ellipsis...
3908 } else {
3909 $ellipsisLength = $adjustLength ? $measureLength( $ellipsis ) : 0;
3910 if ( $length > 0 ) {
3911 $length -= $ellipsisLength;
3912 $string = $getSubstring( $string, 0, $length ); // xyz...
3913 $string = rtrim( $string ) . $ellipsis;
3914 } else {
3915 $length += $ellipsisLength;
3916 $string = $getSubstring( $string, $length ); // ...xyz
3917 $string = $ellipsis . ltrim( $string );
3918 }
3919 }
3920
3921 # Do not truncate if the ellipsis makes the string longer/equal (T24181).
3922 # This check is *not* redundant if $adjustLength, due to the single case where
3923 # LEN($ellipsis) > ABS($limit arg); $stringOriginal could be shorter than $string.
3924 if ( $measureLength( $string ) < $measureLength( $stringOriginal ) ) {
3925 return $string;
3926 } else {
3927 return $stringOriginal;
3928 }
3929 }
3930
3938 protected function removeBadCharLast( $string ) {
3939 if ( $string != '' ) {
3940 $char = ord( substr( $string, -1 ) );
3941 $m = [];
3942 if ( $char >= 0xc0 ) {
3943 # We got the first byte only of a multibyte char; remove it.
3944 $string = substr( $string, 0, -1 );
3945 } elseif ( $char >= 0x80 &&
3946 // Use the /s modifier (PCRE_DOTALL) so (.*) also matches newlines
3947 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
3948 '[\xf0-\xf7][\x80-\xbf]{1,2})$/s', $string, $m )
3949 ) {
3950 # We chopped in the middle of a character; remove it
3951 $string = $m[1];
3952 }
3953 }
3954 return $string;
3955 }
3956
3972 public function truncateHtml( $text, $length, $ellipsis = '...' ) {
3973 # Use the localized ellipsis character
3974 if ( $ellipsis == '...' ) {
3975 $ellipsis = $this->msg( 'ellipsis' )->escaped();
3976 }
3977 # Check if there is clearly no need to truncate
3978 if ( $length <= 0 ) {
3979 return $ellipsis; // no text shown, nothing to format (convention)
3980 } elseif ( strlen( $text ) <= $length ) {
3981 return $text; // string short enough even *with* HTML (short-circuit)
3982 }
3983
3984 $dispLen = 0; // innerHTML length so far
3985 $testingEllipsis = false; // check if ellipses will make the string longer/equal?
3986 $tagType = 0; // 0-open, 1-close
3987 $bracketState = 0; // 1-tag start, 2-tag name, 0-neither
3988 $entityState = 0; // 0-not entity, 1-entity
3989 $tag = $ret = ''; // accumulated tag name, accumulated result string
3990 $openTags = []; // open tag stack
3991 $maybeState = null; // possible truncation state
3992
3993 $textLen = strlen( $text );
3994 $neLength = max( 0, $length - strlen( $ellipsis ) ); // non-ellipsis len if truncated
3995 for ( $pos = 0; true; ++$pos ) {
3996 # Consider truncation once the display length has reached the maximum.
3997 # We check if $dispLen > 0 to grab tags for the $neLength = 0 case.
3998 # Check that we're not in the middle of a bracket/entity...
3999 if ( $dispLen && $dispLen >= $neLength && $bracketState == 0 && !$entityState ) {
4000 if ( !$testingEllipsis ) {
4001 $testingEllipsis = true;
4002 # Save where we are; we will truncate here unless there turn out to
4003 # be so few remaining characters that truncation is not necessary.
4004 if ( !$maybeState ) { // already saved? ($neLength = 0 case)
4005 $maybeState = [ $ret, $openTags ]; // save state
4006 }
4007 } elseif ( $dispLen > $length && $dispLen > strlen( $ellipsis ) ) {
4008 # The string in fact does need truncation, the truncation point was OK.
4009 // @phan-suppress-next-line PhanTypeInvalidExpressionArrayDestructuring
4010 [ $ret, $openTags ] = $maybeState; // reload state
4011 $ret = $this->removeBadCharLast( $ret ); // multi-byte char fix
4012 $ret .= $ellipsis; // add ellipsis
4013 break;
4014 }
4015 }
4016 if ( $pos >= $textLen ) {
4017 break; // extra iteration just for the checks above
4018 }
4019
4020 # Read the next char...
4021 $ch = $text[$pos];
4022 $lastCh = $pos ? $text[$pos - 1] : '';
4023 $ret .= $ch; // add to result string
4024 if ( $ch == '<' ) {
4025 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags ); // for bad HTML
4026 $entityState = 0; // for bad HTML
4027 $bracketState = 1; // tag started (checking for backslash)
4028 } elseif ( $ch == '>' ) {
4029 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags );
4030 $entityState = 0; // for bad HTML
4031 $bracketState = 0; // out of brackets
4032 } elseif ( $bracketState == 1 ) {
4033 if ( $ch == '/' ) {
4034 $tagType = 1; // close tag (e.g. "</span>")
4035 } else {
4036 $tagType = 0; // open tag (e.g. "<span>")
4037 $tag .= $ch;
4038 }
4039 $bracketState = 2; // building tag name
4040 } elseif ( $bracketState == 2 ) {
4041 if ( $ch != ' ' ) {
4042 $tag .= $ch;
4043 } else {
4044 // Name found (e.g. "<a href=..."), add on tag attributes...
4045 $pos += $this->truncate_skip( $ret, $text, "<>", $pos + 1 );
4046 }
4047 } elseif ( $bracketState == 0 ) {
4048 if ( $entityState ) {
4049 if ( $ch == ';' ) {
4050 $entityState = 0;
4051 $dispLen++; // entity is one displayed char
4052 }
4053 } else {
4054 if ( $neLength == 0 && !$maybeState ) {
4055 // Save the state without $ch. We want to *hit* the first
4056 // display char (to get tags) but not *use* it if truncating.
4057 $maybeState = [ substr( $ret, 0, -1 ), $openTags ];
4058 }
4059 if ( $ch == '&' ) {
4060 $entityState = 1; // entity found, (e.g. "&#160;")
4061 } else {
4062 $dispLen++; // this char is displayed
4063 // Add the next $max display text chars after this in one swoop...
4064 $max = ( $testingEllipsis ? $length : $neLength ) - $dispLen;
4065 $skipped = $this->truncate_skip( $ret, $text, "<>&", $pos + 1, $max );
4066 $dispLen += $skipped;
4067 $pos += $skipped;
4068 }
4069 }
4070 }
4071 }
4072 // Close the last tag if left unclosed by bad HTML
4073 $this->truncate_endBracket( $tag, $tagType, $text[$textLen - 1], $openTags );
4074 while ( count( $openTags ) > 0 ) {
4075 $ret .= '</' . array_pop( $openTags ) . '>'; // close open tags
4076 }
4077 return $ret;
4078 }
4079
4091 private function truncate_skip( &$ret, $text, $search, $start, $len = null ) {
4092 if ( $len === null ) {
4093 // -1 means "no limit" for strcspn
4094 $len = -1;
4095 } elseif ( $len < 0 ) {
4096 $len = 0;
4097 }
4098 $skipCount = 0;
4099 if ( $start < strlen( $text ) ) {
4100 $skipCount = strcspn( $text, $search, $start, $len );
4101 $ret .= substr( $text, $start, $skipCount );
4102 }
4103 return $skipCount;
4104 }
4105
4116 private function truncate_endBracket( &$tag, $tagType, $lastCh, &$openTags ) {
4117 $tag = ltrim( $tag );
4118 if ( $tag != '' ) {
4119 if ( $tagType == 0 && $lastCh != '/' ) {
4120 $openTags[] = $tag; // tag opened (didn't close itself)
4121 } elseif ( $tagType == 1 ) {
4122 if ( $openTags && $tag == $openTags[count( $openTags ) - 1] ) {
4123 array_pop( $openTags ); // tag closed
4124 }
4125 }
4126 $tag = '';
4127 }
4128 }
4129
4138 public function convertGrammar( $word, $case ) {
4139 $grammarForms = $this->config->get( MainConfigNames::GrammarForms );
4140 if ( isset( $grammarForms[$this->getCode()][$case][$word] ) ) {
4141 return $grammarForms[$this->getCode()][$case][$word];
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 global $IP;
4211 if ( $this->grammarTransformCache !== null ) {
4212 return $this->grammarTransformCache;
4213 }
4214
4215 $grammarDataFile = $IP . "/languages/data/grammarTransformations/{$this->getCode()}.json";
4216 $this->grammarTransformCache = is_readable( $grammarDataFile )
4217 ? FormatJson::decode( file_get_contents( $grammarDataFile ), true )
4218 : [];
4219
4220 if ( $this->grammarTransformCache === null ) {
4221 throw new RuntimeException( "Invalid grammar data for \"{$this->getCode()}\"." );
4222 }
4223
4224 return $this->grammarTransformCache;
4225 }
4226
4246 public function gender( $gender, $forms ) {
4247 if ( !count( $forms ) ) {
4248 return '';
4249 }
4250 $forms = $this->preConvertPlural( $forms, 2 );
4251 if ( $gender === 'male' ) {
4252 return $forms[0];
4253 }
4254 if ( $gender === 'female' ) {
4255 return $forms[1];
4256 }
4257 return $forms[2] ?? $forms[0];
4258 }
4259
4275 public function convertPlural( $count, $forms ) {
4276 // Handle explicit n=pluralform cases
4277 $forms = $this->handleExplicitPluralForms( $count, $forms );
4278 if ( is_string( $forms ) ) {
4279 return $forms;
4280 }
4281 if ( !count( $forms ) ) {
4282 return '';
4283 }
4284
4285 $pluralForm = $this->getPluralRuleIndexNumber( $count );
4286 $pluralForm = min( $pluralForm, count( $forms ) - 1 );
4287 return $forms[$pluralForm];
4288 }
4289
4305 protected function handleExplicitPluralForms( $count, array $forms ) {
4306 foreach ( $forms as $index => $form ) {
4307 if ( preg_match( '/\d+=/i', $form ) ) {
4308 $pos = strpos( $form, '=' );
4309 if ( substr( $form, 0, $pos ) === (string)$count ) {
4310 return substr( $form, $pos + 1 );
4311 }
4312 unset( $forms[$index] );
4313 }
4314 }
4315 return array_values( $forms );
4316 }
4317
4326 protected function preConvertPlural( /* Array */ $forms, $count ) {
4327 return array_pad( $forms, $count, end( $forms ) );
4328 }
4329
4340 public function getFormalityIndex(): int {
4341 return $this->localisationCache->getItem( $this->mCode, 'formalityIndex' ) ?? 0;
4342 }
4343
4366 public function embedBidi( $text = '' ) {
4367 $dir = self::strongDirFromContent( $text );
4368 if ( $dir === 'ltr' ) {
4369 // Wrap in LEFT-TO-RIGHT EMBEDDING ... POP DIRECTIONAL FORMATTING
4370 return self::LRE . $text . self::PDF;
4371 }
4372 if ( $dir === 'rtl' ) {
4373 // Wrap in RIGHT-TO-LEFT EMBEDDING ... POP DIRECTIONAL FORMATTING
4374 return self::RLE . $text . self::PDF;
4375 }
4376 // No strong directionality: do not wrap
4377 return $text;
4378 }
4379
4389 public function getBlockDurations( $includeOther = true ): array {
4390 $msg = $this->msg( 'ipboptions' )->text();
4391
4392 if ( $msg == '-' ) {
4393 return [];
4394 }
4395
4396 $a = XmlSelect::parseOptionsMessage( $msg );
4397
4398 if ( $a && $includeOther ) {
4399 // If options exist, add other to the end instead of the beginning (which
4400 // is what happens by default).
4401 $a[ $this->msg( 'ipbother' )->text() ] = 'other';
4402 }
4403
4404 return $a;
4405 }
4406
4420 public function translateBlockExpiry( $str, ?UserIdentity $user = null, $now = 0 ) {
4421 $duration = $this->getBlockDurations();
4422 $show = array_search( $str, $duration, true );
4423 if ( $show !== false ) {
4424 return trim( $show );
4425 }
4426
4427 if ( wfIsInfinity( $str ) ) {
4428 foreach ( $duration as $show => $value ) {
4429 if ( wfIsInfinity( $value ) ) {
4430 return trim( $show );
4431 }
4432 }
4433 }
4434
4435 // If all else fails, return a standard duration or timestamp description.
4436 $time = strtotime( $str, $now );
4437 if ( $time === false ) { // Unknown format. Return it as-is in case.
4438 return $str;
4439 } elseif ( $time !== strtotime( $str, $now + 1 ) ) { // It's a relative timestamp.
4440 // The result differs based on current time, so the difference
4441 // is a fixed duration length.
4442 return $this->formatDurationBetweenTimestamps( $time, $now );
4443 } else { // It's an absolute timestamp.
4444 if ( $time === 0 ) {
4445 // wfTimestamp() handles 0 as current time instead of epoch.
4446 $time = '19700101000000';
4447 }
4448
4449 // Return the timestamp as-is if it is in an unknown format to ConvertibleTimestamp (T354663)
4450 $time = ConvertibleTimestamp::convert( TS::MW, $time );
4451 if ( $time === false ) {
4452 return $str;
4453 }
4454
4455 if ( $user ) {
4456 return $this->userTimeAndDate( $time, $user );
4457 }
4458 return $this->timeanddate( $time );
4459 }
4460 }
4461
4469 public function segmentForDiff( $text ) {
4470 return $text;
4471 }
4472
4479 public function unsegmentForDiff( $text ) {
4480 return $text;
4481 }
4482
4489 public function linkTrail() {
4490 return $this->localisationCache->getItem( $this->mCode, 'linkTrail' );
4491 }
4492
4499 public function linkPrefixCharset() {
4500 return $this->localisationCache->getItem( $this->mCode, 'linkPrefixCharset' );
4501 }
4502
4510 public function equals( Language $lang ) {
4511 return $lang === $this || $lang->getCode() === $this->mCode;
4512 }
4513
4520 public function getCode(): string {
4521 return $this->mCode;
4522 }
4523
4534 public function getHtmlCode() {
4535 $this->mHtmlCode ??= LanguageCode::bcp47( $this->getCode() );
4536 return $this->mHtmlCode;
4537 }
4538
4546 public function toBcp47Code(): string {
4547 return $this->getHtmlCode();
4548 }
4549
4557 public function isSameCodeAs( Bcp47Code $other ): bool {
4558 if ( $this === $other ) {
4559 return true;
4560 }
4561 if ( $other instanceof Language ) {
4562 // Compare the mediawiki-internal code
4563 return $this->equals( $other );
4564 }
4565 // Bcp-47 codes are case insensitive.
4566 // See Bcp47CodeValue::isSameCode()
4567 return strcasecmp( $this->toBcp47Code(), $other->toBcp47Code() ) === 0;
4568 }
4569
4578 public static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
4579 $m = null;
4580 preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
4581 preg_quote( $suffix, '/' ) . '/', $filename, $m );
4582 if ( !count( $m ) ) {
4583 return false;
4584 }
4585 return str_replace( '_', '-', strtolower( $m[1] ) );
4586 }
4587
4592 private function fixVariableInNamespace( $talk ) {
4593 if ( !str_contains( $talk, '$1' ) ) {
4594 return $talk;
4595 }
4596
4597 $talk = str_replace( '$1', $this->config->get( MainConfigNames::MetaNamespace ), $talk );
4598
4599 # Allow grammar transformations
4600 # Allowing full message-style parsing would make simple requests
4601 # such as action=raw much more expensive than they need to be.
4602 # This will hopefully cover most cases.
4603 $talk = preg_replace_callback(
4604 '/{{grammar:(.*?)\|(.*?)}}/i',
4605 function ( $m ) {
4606 return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
4607 },
4608 $talk
4609 );
4610 return str_replace( ' ', '_', $talk );
4611 }
4612
4625 public function formatExpiry( $expiry, $format = true, $infinity = 'infinity', $user = null ) {
4626 static $dbInfinity;
4627 $dbInfinity ??= MediaWikiServices::getInstance()->getConnectionProvider()
4628 ->getReplicaDatabase()
4629 ->getInfinity();
4630
4631 if ( $expiry == '' || $expiry === 'infinity' || $expiry == $dbInfinity ) {
4632 return $format === true
4633 ? $this->getMessageFromDB( 'infiniteblock' )
4634 : $infinity;
4635 } else {
4636 if ( $format === true ) {
4637 return $user
4638 ? $this->userTimeAndDate( $expiry, $user )
4639 : $this->timeanddate( $expiry, /* User preference timezone */ true );
4640 }
4641 return wfTimestamp( $format, $expiry );
4642 }
4643 }
4644
4659 public function formatTimePeriod( $seconds, $format = [] ) {
4660 if ( !is_array( $format ) ) {
4661 $format = [ 'avoid' => $format ]; // For backwards compatibility
4662 }
4663 if ( !isset( $format['avoid'] ) ) {
4664 $format['avoid'] = false;
4665 }
4666 if ( !isset( $format['noabbrevs'] ) ) {
4667 $format['noabbrevs'] = false;
4668 }
4669 $secondsMsg = $this->msg( $format['noabbrevs'] ? 'seconds' : 'seconds-abbrev' );
4670 $minutesMsg = $this->msg( $format['noabbrevs'] ? 'minutes' : 'minutes-abbrev' );
4671 $hoursMsg = $this->msg( $format['noabbrevs'] ? 'hours' : 'hours-abbrev' );
4672 $daysMsg = $this->msg( $format['noabbrevs'] ? 'days' : 'days-abbrev' );
4673 $space = $this->msg( 'word-separator' )->text();
4674
4675 if ( round( $seconds * 10 ) < 100 ) {
4676 $s = $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) );
4677 $s = $secondsMsg->params( $s )->text();
4678 } elseif ( round( $seconds ) < 60 ) {
4679 $s = $this->formatNum( round( $seconds ) );
4680 $s = $secondsMsg->params( $s )->text();
4681 } elseif ( round( $seconds ) < 3600 ) {
4682 $minutes = floor( $seconds / 60 );
4683 $secondsPart = round( fmod( $seconds, 60 ) );
4684 if ( $secondsPart == 60 ) {
4685 $secondsPart = 0;
4686 $minutes++;
4687 }
4688 $s = $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4689 $s .= $space;
4690 $s .= $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4691 } elseif ( round( $seconds ) <= 2 * 86400 ) {
4692 $hours = floor( $seconds / 3600 );
4693 $minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
4694 $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
4695 if ( $secondsPart == 60 ) {
4696 $secondsPart = 0;
4697 $minutes++;
4698 }
4699 if ( $minutes == 60 ) {
4700 $minutes = 0;
4701 $hours++;
4702 }
4703 $s = $hoursMsg->params( $this->formatNum( $hours ) )->text();
4704 $s .= $space;
4705 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4706 if ( !in_array( $format['avoid'], [ 'avoidseconds', 'avoidminutes', 'avoidhours' ] ) ) {
4707 $s .= $space . $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4708 }
4709 } else {
4710 $days = floor( $seconds / 86400 );
4711 if ( $format['avoid'] === 'avoidhours' ) {
4712 $hours = round( ( $seconds - $days * 86400 ) / 3600 );
4713 if ( $hours == 24 ) {
4714 $days++;
4715 }
4716 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4717 } elseif ( $format['avoid'] === 'avoidminutes' ) {
4718 $hours = round( ( $seconds - $days * 86400 ) / 3600 );
4719 if ( $hours == 24 ) {
4720 $hours = 0;
4721 $days++;
4722 }
4723 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4724 $s .= $space;
4725 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4726 } elseif ( $format['avoid'] === 'avoidseconds' ) {
4727 $hours = floor( ( $seconds - $days * 86400 ) / 3600 );
4728 $minutes = round( ( $seconds - $days * 86400 - $hours * 3600 ) / 60 );
4729 if ( $minutes == 60 ) {
4730 $minutes = 0;
4731 $hours++;
4732 }
4733 if ( $hours == 24 ) {
4734 $hours = 0;
4735 $days++;
4736 }
4737 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4738 $s .= $space;
4739 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4740 $s .= $space;
4741 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4742 } else {
4743 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4744 $s .= $space;
4745 $s .= $this->formatTimePeriod( $seconds - $days * 86400, $format );
4746 }
4747 }
4748 return $s;
4749 }
4750
4762 public function formatBitrate( $bps ) {
4763 // messages used: bitrate-bits, bitrate-kilobits, bitrate-megabits, bitrate-gigabits, bitrate-terabits,
4764 // bitrate-petabits, bitrate-exabits, bitrate-zettabits, bitrate-yottabits, bitrate-ronnabits,
4765 // bitrate-quettabits
4766 return $this->formatComputingNumbers( $bps, 1000, "bitrate-$1bits" );
4767 }
4768
4775 public function formatComputingNumbers( $size, $boundary, $messageKey ) {
4776 if ( $size <= 0 ) {
4777 return str_replace( '$1', $this->formatNum( $size ),
4778 $this->getMessageFromDB( str_replace( '$1', '', $messageKey ) )
4779 );
4780 }
4781 $sizes = [ '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zetta', 'yotta', 'ronna', 'quetta' ];
4782 $index = 0;
4783
4784 $maxIndex = count( $sizes ) - 1;
4785 while ( $size >= $boundary && $index < $maxIndex ) {
4786 $index++;
4787 $size /= $boundary;
4788 }
4789
4790 // For small sizes no decimal places necessary
4791 $round = 0;
4792 if ( $index > 1 ) {
4793 // For MB and larger units, two decimal places are smarter
4794 $round = 2;
4795 }
4796 $msg = str_replace( '$1', $sizes[$index], $messageKey );
4797
4798 $size = round( $size, $round );
4799 $text = $this->getMessageFromDB( $msg );
4800 return str_replace( '$1', $this->formatNum( $size ), $text );
4801 }
4802
4813 public function formatSize( $size ) {
4814 // messages used: size-bytes, size-kilobytes, size-megabytes, size-gigabytes, size-terabytes,
4815 // size-petabytes, size-exabytes, size-zettabytes, size-yottabytes, size-ronnabytes, size-quettabytes
4816 return $this->formatComputingNumbers( $size, 1024, "size-$1bytes" );
4817 }
4818
4826 public function specialList( $page, $details ) {
4827 if ( !$details ) {
4828 return $page;
4829 }
4830
4831 return Html::rawElement( 'bdi', [ 'dir' => $this->getDir() ], $page ) .
4832 $this->msg( 'word-separator' )->escaped() .
4833 $this->msg( 'parentheses' )->rawParams( $details )->escaped();
4834 }
4835
4836 private function getNumberFormatter(): NumberFormatter {
4837 if ( $this->numberFormatter === null ) {
4838 $digitGroupingPattern = $this->digitGroupingPattern();
4839 $code = $this->getCode();
4840 if ( !( $this->config->get( MainConfigNames::TranslateNumerals )
4841 && $this->langNameUtils->isValidCode( $code ) )
4842 ) {
4843 $code = Locale::getDefault(); // POSIX system default locale
4844 }
4845
4846 $fmt = $this->createNumberFormatter( $code, $digitGroupingPattern );
4847 if ( !$fmt ) {
4848 $fallbacks = $this->getFallbackLanguages();
4849 foreach ( $fallbacks as $fallbackCode ) {
4850 $fmt = $this->createNumberFormatter( $fallbackCode, $digitGroupingPattern );
4851 if ( $fmt ) {
4852 break;
4853 }
4854 }
4855 if ( !$fmt ) {
4856 throw new RuntimeException(
4857 'Could not instance NumberFormatter for ' . $code . ' and all fallbacks'
4858 );
4859 }
4860 }
4861
4862 $this->numberFormatter = $fmt;
4863 }
4864 return $this->numberFormatter;
4865 }
4866
4867 private function createNumberFormatter( string $code, ?string $digitGroupingPattern ): ?NumberFormatter {
4868 try {
4870 return new NumberFormatter(
4871 $code, NumberFormatter::PATTERN_DECIMAL, $digitGroupingPattern
4872 );
4873 }
4874 // @suppress PhanParamTooFew Phan thinks this always requires 3 parameters, that's wrong
4875 return new NumberFormatter( $code, NumberFormatter::DECIMAL );
4876 } catch ( \ValueError ) {
4877 // Value Errors are thrown since php8.4 for invalid locales
4878 return null;
4879 }
4880 }
4881
4888 public function getCompiledPluralRules() {
4889 $pluralRules =
4890 $this->localisationCache->getItem( strtolower( $this->mCode ), 'compiledPluralRules' );
4891 if ( !$pluralRules ) {
4892 $fallbacks = $this->getFallbackLanguages();
4893 foreach ( $fallbacks as $fallbackCode ) {
4894 $pluralRules = $this->localisationCache
4895 ->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' );
4896 if ( $pluralRules ) {
4897 break;
4898 }
4899 }
4900 }
4901 return $pluralRules;
4902 }
4903
4910 public function getPluralRules() {
4911 $pluralRules =
4912 $this->localisationCache->getItem( strtolower( $this->mCode ), 'pluralRules' );
4913 if ( !$pluralRules ) {
4914 $fallbacks = $this->getFallbackLanguages();
4915 foreach ( $fallbacks as $fallbackCode ) {
4916 $pluralRules = $this->localisationCache
4917 ->getItem( strtolower( $fallbackCode ), 'pluralRules' );
4918 if ( $pluralRules ) {
4919 break;
4920 }
4921 }
4922 }
4923 return $pluralRules;
4924 }
4925
4932 public function getPluralRuleTypes() {
4933 $pluralRuleTypes =
4934 $this->localisationCache->getItem( strtolower( $this->mCode ), 'pluralRuleTypes' );
4935 if ( !$pluralRuleTypes ) {
4936 $fallbacks = $this->getFallbackLanguages();
4937 foreach ( $fallbacks as $fallbackCode ) {
4938 $pluralRuleTypes = $this->localisationCache
4939 ->getItem( strtolower( $fallbackCode ), 'pluralRuleTypes' );
4940 if ( $pluralRuleTypes ) {
4941 break;
4942 }
4943 }
4944 }
4945 return $pluralRuleTypes;
4946 }
4947
4954 public function getPluralRuleIndexNumber( $number ) {
4955 $pluralRules = $this->getCompiledPluralRules();
4956 return Evaluator::evaluateCompiled( $number, $pluralRules );
4957 }
4958
4968 public function getPluralRuleType( $number ) {
4969 $index = $this->getPluralRuleIndexNumber( $number );
4970 $pluralRuleTypes = $this->getPluralRuleTypes();
4971 return $pluralRuleTypes[$index] ?? 'other';
4972 }
4973
4980 protected function getConverterInternal() {
4981 return $this->converterFactory->getLanguageConverter( $this );
4982 }
4983
4990 protected function getHookContainer() {
4991 return $this->hookContainer;
4992 }
4993
5002 protected function getHookRunner() {
5003 return $this->hookRunner;
5004 }
5005
5011 public function getJsData() {
5012 return [
5013 'digitTransformTable' => $this->digitTransformTable(),
5014 'separatorTransformTable' => $this->separatorTransformTable(),
5015 'minimumGroupingDigits' => $this->minimumGroupingDigits(),
5016 'formalityIndex' => $this->getFormalityIndex(),
5017 'grammarForms' => $this->getGrammarForms(),
5018 'grammarTransformations' => $this->getGrammarTransformations(),
5019 'pluralRules' => $this->getPluralRules(),
5020 'digitGroupingPattern' => $this->digitGroupingPattern(),
5021 'fallbackLanguages' => $this->getFallbackLanguages(),
5022 'bcp47Map' => LanguageCode::getNonstandardLanguageCodeMapping(),
5023 ];
5024 }
5025
5030 public function getJsDateFormats() {
5031 $jsLcFormats = $this->localisationCache->getItem( $this->mCode, 'jsDateFormats' );
5032 $phpLcFormats = $this->localisationCache->getItem( $this->mCode, 'dateFormats' );
5033
5034 $styles = $this->getDatePreferences() ?: [];
5035 $default = $this->getDefaultDateFormat();
5036 // Always include the default style
5037 if ( !in_array( $default, $styles, true ) ) {
5038 $styles[] = $default;
5039 }
5040 $results = [];
5041 foreach ( $styles as $style ) {
5042 if ( $style === 'default' ) {
5043 // Default is not a real style for our purposes
5044 continue;
5045 }
5046 foreach ( [ 'time', 'date', 'both', 'pretty' ] as $type ) {
5047 $key = "$style $type";
5048 $resolvedType = $type;
5049 $resolvedStyle = $style;
5050 $resolvedKey = $key;
5051
5052 // If the PHP localisation lacks the "pretty" type, fall back to "date"
5053 if ( !isset( $phpLcFormats[$key] ) && $type === 'pretty' ) {
5054 $resolvedType = 'date';
5055 $resolvedKey = "$resolvedStyle $resolvedType";
5056 }
5057
5058 // If $jsDateFormats has an alias, follow it.
5059 // This is used to gracefully remove formats that don't work in the browser.
5060 $alias = $jsLcFormats[$resolvedKey]['alias'] ?? '';
5061 if ( preg_match( '/^(.*) ([^ ]*)$/', $alias, $m ) ) {
5062 $resolvedType = $m[2];
5063 $resolvedStyle = $m[1];
5064 $resolvedKey = "$resolvedStyle $resolvedType";
5065 }
5066
5067 $jsFormat = $this->convertDateFormatToJs(
5068 $this->getDateFormatString( $resolvedType, $resolvedStyle ) );
5069 if ( isset( $jsLcFormats[$resolvedKey] ) ) {
5070 $results[$key] = array_merge_recursive( $jsFormat, $jsLcFormats[$resolvedKey] );
5071 } else {
5072 $results[$key] = $jsFormat;
5073 }
5074 }
5075 }
5076 return $results;
5077 }
5078}
5079
5081class_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')) global $IP
Environment checks.
Definition Setup.php:90
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:69
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:43
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:68
resetNamespaces()
Resets all the namespace caches.
Definition Language.php:439
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:99
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...
getNsText( $index)
Get a namespace value by key.
Definition Language.php:475
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:289
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:97
ucwordbreaks( $str)
capitalize words at word breaks
string[][] $dateFormatStrings
memoize
Definition Language.php:87
setNamespaces(array $namespaces)
Arbitrarily set all the namespace names at once.
Definition Language.php:430
getFormattedNamespaces()
A convenience function that returns {.
Definition Language.php:454
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 ...
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 special page names, as an associative array canonical name => array of valid names,...
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:249
getBookstoreList()
Exports $wgBookstoreListEn.
Definition Language.php:376
__construct(string $code, NamespaceInfo $namespaceInfo, LocalisationCache $localisationCache, LanguageNameUtils $langNameUtils, LanguageFallback $langFallback, LanguageConverterFactory $converterFactory, HookContainer $hookContainer, Config $config)
Definition Language.php:343
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:521
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:387
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:101
getFormattedNsText( $index)
A convenience function that returns the same thing as getNsText() except with '_' changed to ' ',...
Definition Language.php:493
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.
getBlockDurations( $includeOther=true)
Get an array of suggested block durations from MediaWiki:Ipboptions.
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:546
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:655
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:723
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:94
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:506
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:638
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:130
getDatePreference()
Get the user's preferred date format.
Definition User.php:2034
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, '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'=> '', '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'=>[], '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'=> false, '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 -o $output $input', 'ImagickExt'=>['SvgHandler::rasterizeImagickExt',],], 'SVGConverter'=> 'ImageMagick', 'SVGConverterPath'=> '', 'SVGMaxSize'=> 5120, 'SVGMetadataCutoff'=> 5242880, 'SVGNativeRendering'=> true, 'SVGNativeRenderingSizeLimit'=> 51200, 'MediaInTargetLanguage'=> true, 'MaxImageArea'=> 12500000, 'MaxAnimatedGifArea'=> 12500000, '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, 250, 300,], 'ThumbnailNamespaces'=>[6,], 'ThumbnailSteps'=> null, 'ThumbnailStepsRatio'=> null, 'ThumbnailBuckets'=> null, 'ThumbnailMinimumBucketDistance'=> 50, 'UploadThumbnailRenderMap'=>[], 'UploadThumbnailRenderMethod'=> 'jobqueue', 'UploadThumbnailRenderHttpCustomHost'=> false, 'UploadThumbnailRenderHttpCustomDomain'=> false, 'UseTinyRGBForJPGThumbnails'=> false, 'GalleryOptions'=>[], 'ThumbUpright'=> 0.75, 'DirectoryMode'=> 511, 'ResponsiveImages'=> true, 'ImagePreconnect'=> 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, 'UserEmailConfirmationUseHTML'=> false, 'PasswordExpirationDays'=> false, 'PasswordExpireGrace'=> 604800, 'SMTP'=> false, 'AdditionalMailParams'=> null, 'AllowHTMLEmail'=> false, 'EnotifFromEditor'=> false, 'EmailAuthentication'=> true, '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'=>[], 'FileSchemaMigrationStage'=> 3, 'ImageLinksSchemaMigrationStage'=> 769, 'ExternalLinksDomainGaps'=>[], 'ContentHandlers'=>['wikitext'=>['class'=> 'MediaWiki\\Content\\WikitextContentHandler', 'services'=>['TitleFactory', 'ParserFactory', 'GlobalIdGenerator', 'LanguageNameUtils', 'LinkRenderer', 'MagicWordFactory', 'ParsoidParserFactory',],], 'javascript'=>['class'=> 'MediaWiki\\Content\\JavaScriptContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'UserOptionsLookup',],], 'json'=>['class'=> 'MediaWiki\\Content\\JsonContentHandler', 'services'=>['ParsoidParserFactory', 'TitleFactory',],], 'css'=>['class'=> 'MediaWiki\\Content\\CssContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'UserOptionsLookup',],], 'vue'=>['class'=> 'MediaWiki\\Content\\VueContentHandler', 'services'=>['MainConfig', 'ParserFactory',],], '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, 'WarmParsoidParserCache'=> false,], 'ParsoidSelectiveUpdateSampleRate'=> 0, 'ParserCacheFilterConfig'=>['pcache'=>['default'=>['minCpuTime'=> 0,],], 'parsoid-pcache'=>['default'=>['minCpuTime'=> 0,],], 'postproc-pcache'=>['default'=>['minCpuTime'=> 9223372036854775807,],], 'postproc-parsoid-pcache'=>['default'=>['minCpuTime'=> 9223372036854775807,],],], 'ChronologyProtectorSecret'=> '', 'ParserCacheExpireTime'=> 86400, 'ParserCacheAsyncExpireTime'=> 60, 'ParserCacheAsyncRefreshJobs'=> true, 'OldRevisionParserCacheExpireTime'=> 3600, 'ObjectCacheSessionExpiry'=> 3600, 'PHPSessionHandling'=> 'warn', 'SuspiciousIpExpiry'=> false, 'SessionPbkdf2Iterations'=> 10001, 'UseSessionCookieJwt'=> false, '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',],],], '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, ], '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, 'UseLegacyMediaStyles' => 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, ], ], '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, ], 'AllowSecuritySensitiveOperationIfCannotReauthenticate' => [ 'default' => true, ], '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, '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, ], '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, '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' => [ ], '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, 'pagelang' => true, ], 'editprotected' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'editprotected' => true, ], 'editmycssjs' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'editmyusercss' => true, 'editmyuserjson' => true, 'editmyuserjs' => true, ], 'editmyoptions' => [ 'editmyoptions' => true, 'editmyuserjson' => true, ], 'editinterface' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'editinterface' => true, 'edituserjson' => true, 'editsitejson' => true, ], 'editsiteconfig' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => 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, '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, '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, 'editprotected' => true, 'protect' => true, ], 'viewmywatchlist' => [ 'viewmywatchlist' => true, ], 'editmywatchlist' => [ 'editmywatchlist' => true, ], 'sendemail' => [ 'sendemail' => true, ], 'createaccount' => [ 'createaccount' => true, ], 'privateinfo' => [ 'viewmyprivateinfo' => true, ], 'mergehistory' => [ 'mergehistory' => 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', 'editviewmywatchlist' => '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', ], 'GrantRiskGroups' => [ 'basic' => 'low', 'editpage' => 'low', 'createeditmovepage' => 'low', 'editprotected' => 'vandalism', 'patrol' => 'low', 'uploadfile' => 'low', 'uploadeditmovefile' => 'low', 'sendemail' => 'security', 'viewmywatchlist' => 'low', 'editviewmywatchlist' => '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', ], 'EnableBotPasswords' => true, 'BotPasswordsCluster' => false, 'BotPasswordsDatabase' => false, 'SecretKey' => false, 'JwtPrivateKey' => false, 'JwtPublicKey' => false, 'AllowUserJs' => false, 'AllowUserCss' => false, 'AllowUserCssPrefs' => true, 'UseSiteJs' => true, 'UseSiteCss' => true, 'BreakFrames' => false, 'EditPageFrameOptions' => 'DENY', 'ApiFrameOptions' => 'DENY', 'CSPHeader' => false, 'CSPReportOnlyHeader' => 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, 'CachePrefix' => false, 'DebugToolbar' => false, '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: ], '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' => [ ], 'RCEngines' => [ 'redis' => 'MediaWiki\\RCFeed\\RedisPubSubFeedEngine', 'udp' => 'MediaWiki\\RCFeed\\UDPRCFeedEngine', ], '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, ], '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, '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\\JobQueue\\Jobs\\CategoryMembershipChangeJob', 'services' => [ 'RecentChangeFactory', ], ], 'CategoryCountUpdateJob' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\CategoryCountUpdateJob', 'services' => [ 'ConnectionProvider', 'NamespaceInfo', ], ], 'clearUserWatchlist' => 'MediaWiki\\Watchlist\\ClearUserWatchlistJob', 'watchlistExpiry' => 'MediaWiki\\Watchlist\\WatchlistExpiryJob', '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', 'parsoidCachePrewarm' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\ParsoidCachePrewarmJob', 'services' => [ 'ParserOutputAccess', 'PageStore', 'RevisionLookup', 'ParsoidSiteConfig', ], 'needsPage' => false, ], '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', 'Access-Control-Max-Age', 'Authorization', ], 'RestAPIAdditionalRouteFiles' => [ ], 'RestSandboxSpecs' => [ ], '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, 'AsyncHTTPTimeout' => 25, 'HTTPProxy' => '', 'LocalVirtualHosts' => [ ], 'LocalHTTPProxy' => false, 'AllowExternalReqID' => false, 'JobRunRate' => 1, 'RunJobsAsync' => false, 'UpdateRowsPerJob' => 300, 'UpdateRowsPerQuery' => 100, 'RedirectOnLogin' => null, 'VirtualRestConfig' => [ 'paths' => [ ], 'modules' => [ ], 'global' => [ 'timeout' => 360, 'forwardCookies' => false, 'HTTPProxy' => 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' => [ ], 'CloneArticleParserOutput' => true, 'UseLeximorph' => false, 'UsePostprocCache' => false, 'UsePostprocCacheLegacy' => false, 'UsePostprocCacheParsoid' => false, 'ParserOptionsLogUnsafeSampleRate' => 0, ], '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', 'CacheSharedUploads' => 'boolean', 'ForeignUploadTargets' => 'array', 'UploadDialog' => 'object', 'FileBackends' => 'object', 'LockManagers' => 'array', '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', ], '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', ], 'ThumbnailStepsRatio' => [ 'number', 'null', ], 'ThumbnailBuckets' => [ 'array', 'null', ], 'UploadThumbnailRenderMap' => 'object', 'GalleryOptions' => 'object', 'DjvuDump' => [ 'string', 'null', ], 'DjvuRenderer' => [ 'string', 'null', ], 'DjvuTxt' => [ 'string', 'null', ], 'DjvuPostProcessor' => [ 'string', 'null', ], 'UserEmailConfirmationUseHTML' => 'boolean', 'SMTP' => [ 'boolean', 'object', ], 'EnotifFromEditor' => 'boolean', 'EnotifRevealEditorAddress' => 'boolean', 'UsersNotifiedOnAllChanges' => 'object', 'DBmwschema' => [ 'string', 'null', ], 'SharedTables' => 'array', 'DBservers' => [ 'boolean', 'array', ], 'LBFactoryConf' => 'object', 'LocalDatabases' => 'array', 'VirtualDomainsMapping' => 'object', 'FileSchemaMigrationStage' => 'integer', 'ImageLinksSchemaMigrationStage' => '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', 'ParserCacheFilterConfig' => 'object', 'ChronologyProtectorSecret' => 'string', 'PHPSessionHandling' => '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', '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', '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', 'AllowSecuritySensitiveOperationIfCannotReauthenticate' => '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', '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', ], 'CSPHeader' => [ 'boolean', 'object', ], 'CSPReportOnlyHeader' => [ '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', 'RCEngines' => 'object', 'OverrideSiteFeed' => 'object', 'FeedClasses' => 'object', 'AdvertisedFeedTypes' => 'array', 'SoftwareTags' => 'object', 'RecentChangesFlags' => 'object', 'WatchlistExpiry' => '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', 'RestSandboxSpecs' => 'object', 'ShellRestrictionMethod' => [ 'string', 'boolean', ], 'ShellboxUrls' => 'object', 'ShellboxSecretKey' => [ 'string', 'null', ], 'ShellboxShell' => [ 'string', 'null', ], 'HTTPTimeout' => 'number', 'HTTPConnectTimeout' => 'number', 'HTTPMaxTimeout' => 'number', 'HTTPMaxConnectTimeout' => 'number', 'LocalVirtualHosts' => 'object', 'LocalHTTPProxy' => [ 'string', 'boolean', ], 'VirtualRestConfig' => 'object', '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', 'CloneArticleParserOutput' => 'boolean', 'UseLeximorph' => 'boolean', 'UsePostprocCache' => 'boolean', 'UsePostprocCacheLegacy' => 'boolean', 'UsePostprocCacheParsoid' => 'boolean', 'ParserOptionsLogUnsafeSampleRate' => 'integer', ], 'mergeStrategy' => [ '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', 'VirtualRestConfig' => 'array_plus_2d', ], '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', ], ], ], ], ], 'PHPSessionHandling' => [ 'deprecated' => 'since 1.45 Integration with PHP session handling will be removed in the future', ], 'RawHtmlMessages' => [ 'items' => [ 'type' => 'string', ], ], 'InterwikiLogoOverride' => [ 'items' => [ 'type' => 'string', ], ], 'LegalTitleChars' => [ 'deprecated' => 'since 1.41; use Extension:TitleBlacklist to customize', ], 'ReauthenticateTime' => [ 'additionalProperties' => [ 'type' => 'integer', ], ], 'AllowSecuritySensitiveOperationIfCannotReauthenticate' => [ 'additionalProperties' => [ 'type' => 'boolean', ], ], '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', ], ], ], 'SoftwareTags' => [ 'additionalProperties' => [ 'type' => 'boolean', ], ], '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', ], ], 'RestSandboxSpecs' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'url' => [ 'type' => 'string', 'format' => 'url', ], 'name' => [ 'type' => 'string', ], 'msg' => [ 'type' => 'string', 'description' => 'a message key', ], ], 'required' => [ 'url', ], ], ], '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)