MediaWiki master
Language.php
Go to the documentation of this file.
1<?php
18namespace MediaWiki\Language;
19
20use CLDRPluralRuleParser\Evaluator;
21use DateTime;
22use DateTimeImmutable;
23use DateTimeInterface;
24use DateTimeZone;
25use InvalidArgumentException;
26use Locale;
46use NumberFormatter;
47use RuntimeException;
48use UtfNormal\Validator as UtfNormalValidator;
49use Wikimedia\Bcp47Code\Bcp47Code;
50use Wikimedia\DebugInfo\DebugInfoTrait;
55use Wikimedia\Timestamp\ConvertibleTimestamp;
56use Wikimedia\Timestamp\TimestampFormat as TS;
57
65class Language implements Bcp47Code {
66 use DebugInfoTrait;
67
68 public string $mCode;
69
73 public $mMagicExtensions = [];
74
76 private $mHtmlCode = null;
77
83 public $dateFormatStrings = [];
84
91
93 protected $namespaceNames;
95 protected $mNamespaceIds;
98
103 private $transformData = [];
104
109 private $grammarTransformCache;
110
112 private HookRunner $hookRunner;
113
117 private $overrideUcfirstCharacters;
118
123 private $numberFormatter = null;
124
128 public const WEEKDAY_MESSAGES = [
129 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
130 'friday', 'saturday'
131 ];
132
137 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
138 ];
139
143 public const MONTH_MESSAGES = [
144 'january', 'february', 'march', 'april', 'may_long', 'june',
145 'july', 'august', 'september', 'october', 'november',
146 'december'
147 ];
148
153
157 public const MONTH_GENITIVE_MESSAGES = [
158 'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
159 'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
160 'december-gen'
161 ];
162
167 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
168 'sep', 'oct', 'nov', 'dec'
169 ];
170
175
180 'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
181 'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
182 'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
183 'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
184 ];
185
190 'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
191 'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
192 'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
193 'hebrew-calendar-m10', 'hebrew-calendar-m11', 'hebrew-calendar-m12',
194 'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
195 ];
196
201 'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
202 'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
203 'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
204 'hebrew-calendar-m10-gen', 'hebrew-calendar-m11-gen', 'hebrew-calendar-m12-gen',
205 'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
206 ];
207
212 'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
213 'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
214 'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
215 'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12'
216 ];
217
221 protected const DURATION_INTERVALS = [
222 'millennia' => 1000 * 31_556_952,
223 'centuries' => 100 * 31_556_952,
224 'decades' => 10 * 31_556_952,
225 // The average year is 365.2425 days (365 + (24 * 3 + 25) / 400)
226 'years' => 31_556_952, // 365.2425 * 24 * 3600
227 // To simplify, we consider a month to be 1/12 of a year
228 'months' => 365.2425 * 24 * 3600 / 12,
229 'days' => 24 * 3600,
230 'hours' => 3600,
231 'minutes' => 60,
232 'seconds' => 1,
233 ];
234
241
243 private const DEADLINE_DATE_SPEC_BY_UNIT = [
244 'Y' => 'first day of January next year midnight',
245 'M' => 'first day of next month midnight',
246 'W' => 'monday next week midnight',
247 'D' => 'next day midnight',
248 // Note that this may be before the current time, in which case
249 // we advance by a calendar year; the ISOY is actually 1 week past
250 // this, since dec 28 is always in "the last week of the ISO year".
251 'ISOY' => 'december 28 midnight',
252 // Note that this may be before the current time, in which case 'D'
253 // is used.
254 'AM' => 'today noon',
255 // Note that this relative datetime specifier does not zero out
256 // minutes/seconds, but we will do so manually in
257 // ::computeUnitTimestampDeadline() when given the unit 'H'/'m'/'s'
258 'H' => 'next hour',
259 'm' => 'next minute',
260 's' => 'next second',
261 ];
262
266 private const LRM = "\u{200E}"; // U+200E LEFT-TO-RIGHT MARK
267 private const RLM = "\u{200F}"; // U+200F RIGHT-TO-LEFT MARK
268 private const LRE = "\u{202A}"; // U+202A LEFT-TO-RIGHT EMBEDDING
269 private const RLE = "\u{202B}"; // U+202B RIGHT-TO-LEFT EMBEDDING
270 private const PDF = "\u{202C}"; // U+202C POP DIRECTIONAL FORMATTING
271 // https://en.wikipedia.org/wiki/Arabic_letter_mark (Unicode 6.3.0)
272 private const ALM = "\u{061C}"; // U+061C ARABIC LETTER MARK
273
286 // @codeCoverageIgnoreStart
287 // phpcs:ignore Generic.Files.LineLength,MediaWiki.Commenting.PropertyDocumentation.MissingDocumentationPrivate
288 private static $strongDirRegex = '/(?:([\x{41}-\x{5a}\x{61}-\x{7a}\x{aa}\x{b5}\x{ba}\x{c0}-\x{d6}\x{d8}-\x{f6}\x{f8}-\x{2b8}\x{2bb}-\x{2c1}\x{2d0}\x{2d1}\x{2e0}-\x{2e4}\x{2ee}\x{370}-\x{373}\x{376}\x{377}\x{37a}-\x{37d}\x{37f}\x{386}\x{388}-\x{38a}\x{38c}\x{38e}-\x{3a1}\x{3a3}-\x{3f5}\x{3f7}-\x{482}\x{48a}-\x{52f}\x{531}-\x{556}\x{559}-\x{55f}\x{561}-\x{587}\x{589}\x{903}-\x{939}\x{93b}\x{93d}-\x{940}\x{949}-\x{94c}\x{94e}-\x{950}\x{958}-\x{961}\x{964}-\x{980}\x{982}\x{983}\x{985}-\x{98c}\x{98f}\x{990}\x{993}-\x{9a8}\x{9aa}-\x{9b0}\x{9b2}\x{9b6}-\x{9b9}\x{9bd}-\x{9c0}\x{9c7}\x{9c8}\x{9cb}\x{9cc}\x{9ce}\x{9d7}\x{9dc}\x{9dd}\x{9df}-\x{9e1}\x{9e6}-\x{9f1}\x{9f4}-\x{9fa}\x{a03}\x{a05}-\x{a0a}\x{a0f}\x{a10}\x{a13}-\x{a28}\x{a2a}-\x{a30}\x{a32}\x{a33}\x{a35}\x{a36}\x{a38}\x{a39}\x{a3e}-\x{a40}\x{a59}-\x{a5c}\x{a5e}\x{a66}-\x{a6f}\x{a72}-\x{a74}\x{a83}\x{a85}-\x{a8d}\x{a8f}-\x{a91}\x{a93}-\x{aa8}\x{aaa}-\x{ab0}\x{ab2}\x{ab3}\x{ab5}-\x{ab9}\x{abd}-\x{ac0}\x{ac9}\x{acb}\x{acc}\x{ad0}\x{ae0}\x{ae1}\x{ae6}-\x{af0}\x{af9}\x{b02}\x{b03}\x{b05}-\x{b0c}\x{b0f}\x{b10}\x{b13}-\x{b28}\x{b2a}-\x{b30}\x{b32}\x{b33}\x{b35}-\x{b39}\x{b3d}\x{b3e}\x{b40}\x{b47}\x{b48}\x{b4b}\x{b4c}\x{b57}\x{b5c}\x{b5d}\x{b5f}-\x{b61}\x{b66}-\x{b77}\x{b83}\x{b85}-\x{b8a}\x{b8e}-\x{b90}\x{b92}-\x{b95}\x{b99}\x{b9a}\x{b9c}\x{b9e}\x{b9f}\x{ba3}\x{ba4}\x{ba8}-\x{baa}\x{bae}-\x{bb9}\x{bbe}\x{bbf}\x{bc1}\x{bc2}\x{bc6}-\x{bc8}\x{bca}-\x{bcc}\x{bd0}\x{bd7}\x{be6}-\x{bf2}\x{c01}-\x{c03}\x{c05}-\x{c0c}\x{c0e}-\x{c10}\x{c12}-\x{c28}\x{c2a}-\x{c39}\x{c3d}\x{c41}-\x{c44}\x{c58}-\x{c5a}\x{c60}\x{c61}\x{c66}-\x{c6f}\x{c7f}\x{c82}\x{c83}\x{c85}-\x{c8c}\x{c8e}-\x{c90}\x{c92}-\x{ca8}\x{caa}-\x{cb3}\x{cb5}-\x{cb9}\x{cbd}-\x{cc4}\x{cc6}-\x{cc8}\x{cca}\x{ccb}\x{cd5}\x{cd6}\x{cde}\x{ce0}\x{ce1}\x{ce6}-\x{cef}\x{cf1}\x{cf2}\x{d02}\x{d03}\x{d05}-\x{d0c}\x{d0e}-\x{d10}\x{d12}-\x{d3a}\x{d3d}-\x{d40}\x{d46}-\x{d48}\x{d4a}-\x{d4c}\x{d4e}\x{d57}\x{d5f}-\x{d61}\x{d66}-\x{d75}\x{d79}-\x{d7f}\x{d82}\x{d83}\x{d85}-\x{d96}\x{d9a}-\x{db1}\x{db3}-\x{dbb}\x{dbd}\x{dc0}-\x{dc6}\x{dcf}-\x{dd1}\x{dd8}-\x{ddf}\x{de6}-\x{def}\x{df2}-\x{df4}\x{e01}-\x{e30}\x{e32}\x{e33}\x{e40}-\x{e46}\x{e4f}-\x{e5b}\x{e81}\x{e82}\x{e84}\x{e87}\x{e88}\x{e8a}\x{e8d}\x{e94}-\x{e97}\x{e99}-\x{e9f}\x{ea1}-\x{ea3}\x{ea5}\x{ea7}\x{eaa}\x{eab}\x{ead}-\x{eb0}\x{eb2}\x{eb3}\x{ebd}\x{ec0}-\x{ec4}\x{ec6}\x{ed0}-\x{ed9}\x{edc}-\x{edf}\x{f00}-\x{f17}\x{f1a}-\x{f34}\x{f36}\x{f38}\x{f3e}-\x{f47}\x{f49}-\x{f6c}\x{f7f}\x{f85}\x{f88}-\x{f8c}\x{fbe}-\x{fc5}\x{fc7}-\x{fcc}\x{fce}-\x{fda}\x{1000}-\x{102c}\x{1031}\x{1038}\x{103b}\x{103c}\x{103f}-\x{1057}\x{105a}-\x{105d}\x{1061}-\x{1070}\x{1075}-\x{1081}\x{1083}\x{1084}\x{1087}-\x{108c}\x{108e}-\x{109c}\x{109e}-\x{10c5}\x{10c7}\x{10cd}\x{10d0}-\x{1248}\x{124a}-\x{124d}\x{1250}-\x{1256}\x{1258}\x{125a}-\x{125d}\x{1260}-\x{1288}\x{128a}-\x{128d}\x{1290}-\x{12b0}\x{12b2}-\x{12b5}\x{12b8}-\x{12be}\x{12c0}\x{12c2}-\x{12c5}\x{12c8}-\x{12d6}\x{12d8}-\x{1310}\x{1312}-\x{1315}\x{1318}-\x{135a}\x{1360}-\x{137c}\x{1380}-\x{138f}\x{13a0}-\x{13f5}\x{13f8}-\x{13fd}\x{1401}-\x{167f}\x{1681}-\x{169a}\x{16a0}-\x{16f8}\x{1700}-\x{170c}\x{170e}-\x{1711}\x{1720}-\x{1731}\x{1735}\x{1736}\x{1740}-\x{1751}\x{1760}-\x{176c}\x{176e}-\x{1770}\x{1780}-\x{17b3}\x{17b6}\x{17be}-\x{17c5}\x{17c7}\x{17c8}\x{17d4}-\x{17da}\x{17dc}\x{17e0}-\x{17e9}\x{1810}-\x{1819}\x{1820}-\x{1877}\x{1880}-\x{18a8}\x{18aa}\x{18b0}-\x{18f5}\x{1900}-\x{191e}\x{1923}-\x{1926}\x{1929}-\x{192b}\x{1930}\x{1931}\x{1933}-\x{1938}\x{1946}-\x{196d}\x{1970}-\x{1974}\x{1980}-\x{19ab}\x{19b0}-\x{19c9}\x{19d0}-\x{19da}\x{1a00}-\x{1a16}\x{1a19}\x{1a1a}\x{1a1e}-\x{1a55}\x{1a57}\x{1a61}\x{1a63}\x{1a64}\x{1a6d}-\x{1a72}\x{1a80}-\x{1a89}\x{1a90}-\x{1a99}\x{1aa0}-\x{1aad}\x{1b04}-\x{1b33}\x{1b35}\x{1b3b}\x{1b3d}-\x{1b41}\x{1b43}-\x{1b4b}\x{1b50}-\x{1b6a}\x{1b74}-\x{1b7c}\x{1b82}-\x{1ba1}\x{1ba6}\x{1ba7}\x{1baa}\x{1bae}-\x{1be5}\x{1be7}\x{1bea}-\x{1bec}\x{1bee}\x{1bf2}\x{1bf3}\x{1bfc}-\x{1c2b}\x{1c34}\x{1c35}\x{1c3b}-\x{1c49}\x{1c4d}-\x{1c7f}\x{1cc0}-\x{1cc7}\x{1cd3}\x{1ce1}\x{1ce9}-\x{1cec}\x{1cee}-\x{1cf3}\x{1cf5}\x{1cf6}\x{1d00}-\x{1dbf}\x{1e00}-\x{1f15}\x{1f18}-\x{1f1d}\x{1f20}-\x{1f45}\x{1f48}-\x{1f4d}\x{1f50}-\x{1f57}\x{1f59}\x{1f5b}\x{1f5d}\x{1f5f}-\x{1f7d}\x{1f80}-\x{1fb4}\x{1fb6}-\x{1fbc}\x{1fbe}\x{1fc2}-\x{1fc4}\x{1fc6}-\x{1fcc}\x{1fd0}-\x{1fd3}\x{1fd6}-\x{1fdb}\x{1fe0}-\x{1fec}\x{1ff2}-\x{1ff4}\x{1ff6}-\x{1ffc}\x{200e}\x{2071}\x{207f}\x{2090}-\x{209c}\x{2102}\x{2107}\x{210a}-\x{2113}\x{2115}\x{2119}-\x{211d}\x{2124}\x{2126}\x{2128}\x{212a}-\x{212d}\x{212f}-\x{2139}\x{213c}-\x{213f}\x{2145}-\x{2149}\x{214e}\x{214f}\x{2160}-\x{2188}\x{2336}-\x{237a}\x{2395}\x{249c}-\x{24e9}\x{26ac}\x{2800}-\x{28ff}\x{2c00}-\x{2c2e}\x{2c30}-\x{2c5e}\x{2c60}-\x{2ce4}\x{2ceb}-\x{2cee}\x{2cf2}\x{2cf3}\x{2d00}-\x{2d25}\x{2d27}\x{2d2d}\x{2d30}-\x{2d67}\x{2d6f}\x{2d70}\x{2d80}-\x{2d96}\x{2da0}-\x{2da6}\x{2da8}-\x{2dae}\x{2db0}-\x{2db6}\x{2db8}-\x{2dbe}\x{2dc0}-\x{2dc6}\x{2dc8}-\x{2dce}\x{2dd0}-\x{2dd6}\x{2dd8}-\x{2dde}\x{3005}-\x{3007}\x{3021}-\x{3029}\x{302e}\x{302f}\x{3031}-\x{3035}\x{3038}-\x{303c}\x{3041}-\x{3096}\x{309d}-\x{309f}\x{30a1}-\x{30fa}\x{30fc}-\x{30ff}\x{3105}-\x{312d}\x{3131}-\x{318e}\x{3190}-\x{31ba}\x{31f0}-\x{321c}\x{3220}-\x{324f}\x{3260}-\x{327b}\x{327f}-\x{32b0}\x{32c0}-\x{32cb}\x{32d0}-\x{32fe}\x{3300}-\x{3376}\x{337b}-\x{33dd}\x{33e0}-\x{33fe}\x{3400}-\x{4db5}\x{4e00}-\x{9fd5}\x{a000}-\x{a48c}\x{a4d0}-\x{a60c}\x{a610}-\x{a62b}\x{a640}-\x{a66e}\x{a680}-\x{a69d}\x{a6a0}-\x{a6ef}\x{a6f2}-\x{a6f7}\x{a722}-\x{a787}\x{a789}-\x{a7ad}\x{a7b0}-\x{a7b7}\x{a7f7}-\x{a801}\x{a803}-\x{a805}\x{a807}-\x{a80a}\x{a80c}-\x{a824}\x{a827}\x{a830}-\x{a837}\x{a840}-\x{a873}\x{a880}-\x{a8c3}\x{a8ce}-\x{a8d9}\x{a8f2}-\x{a8fd}\x{a900}-\x{a925}\x{a92e}-\x{a946}\x{a952}\x{a953}\x{a95f}-\x{a97c}\x{a983}-\x{a9b2}\x{a9b4}\x{a9b5}\x{a9ba}\x{a9bb}\x{a9bd}-\x{a9cd}\x{a9cf}-\x{a9d9}\x{a9de}-\x{a9e4}\x{a9e6}-\x{a9fe}\x{aa00}-\x{aa28}\x{aa2f}\x{aa30}\x{aa33}\x{aa34}\x{aa40}-\x{aa42}\x{aa44}-\x{aa4b}\x{aa4d}\x{aa50}-\x{aa59}\x{aa5c}-\x{aa7b}\x{aa7d}-\x{aaaf}\x{aab1}\x{aab5}\x{aab6}\x{aab9}-\x{aabd}\x{aac0}\x{aac2}\x{aadb}-\x{aaeb}\x{aaee}-\x{aaf5}\x{ab01}-\x{ab06}\x{ab09}-\x{ab0e}\x{ab11}-\x{ab16}\x{ab20}-\x{ab26}\x{ab28}-\x{ab2e}\x{ab30}-\x{ab65}\x{ab70}-\x{abe4}\x{abe6}\x{abe7}\x{abe9}-\x{abec}\x{abf0}-\x{abf9}\x{ac00}-\x{d7a3}\x{d7b0}-\x{d7c6}\x{d7cb}-\x{d7fb}\x{e000}-\x{fa6d}\x{fa70}-\x{fad9}\x{fb00}-\x{fb06}\x{fb13}-\x{fb17}\x{ff21}-\x{ff3a}\x{ff41}-\x{ff5a}\x{ff66}-\x{ffbe}\x{ffc2}-\x{ffc7}\x{ffca}-\x{ffcf}\x{ffd2}-\x{ffd7}\x{ffda}-\x{ffdc}\x{10000}-\x{1000b}\x{1000d}-\x{10026}\x{10028}-\x{1003a}\x{1003c}\x{1003d}\x{1003f}-\x{1004d}\x{10050}-\x{1005d}\x{10080}-\x{100fa}\x{10100}\x{10102}\x{10107}-\x{10133}\x{10137}-\x{1013f}\x{101d0}-\x{101fc}\x{10280}-\x{1029c}\x{102a0}-\x{102d0}\x{10300}-\x{10323}\x{10330}-\x{1034a}\x{10350}-\x{10375}\x{10380}-\x{1039d}\x{1039f}-\x{103c3}\x{103c8}-\x{103d5}\x{10400}-\x{1049d}\x{104a0}-\x{104a9}\x{10500}-\x{10527}\x{10530}-\x{10563}\x{1056f}\x{10600}-\x{10736}\x{10740}-\x{10755}\x{10760}-\x{10767}\x{11000}\x{11002}-\x{11037}\x{11047}-\x{1104d}\x{11066}-\x{1106f}\x{11082}-\x{110b2}\x{110b7}\x{110b8}\x{110bb}-\x{110c1}\x{110d0}-\x{110e8}\x{110f0}-\x{110f9}\x{11103}-\x{11126}\x{1112c}\x{11136}-\x{11143}\x{11150}-\x{11172}\x{11174}-\x{11176}\x{11182}-\x{111b5}\x{111bf}-\x{111c9}\x{111cd}\x{111d0}-\x{111df}\x{111e1}-\x{111f4}\x{11200}-\x{11211}\x{11213}-\x{1122e}\x{11232}\x{11233}\x{11235}\x{11238}-\x{1123d}\x{11280}-\x{11286}\x{11288}\x{1128a}-\x{1128d}\x{1128f}-\x{1129d}\x{1129f}-\x{112a9}\x{112b0}-\x{112de}\x{112e0}-\x{112e2}\x{112f0}-\x{112f9}\x{11302}\x{11303}\x{11305}-\x{1130c}\x{1130f}\x{11310}\x{11313}-\x{11328}\x{1132a}-\x{11330}\x{11332}\x{11333}\x{11335}-\x{11339}\x{1133d}-\x{1133f}\x{11341}-\x{11344}\x{11347}\x{11348}\x{1134b}-\x{1134d}\x{11350}\x{11357}\x{1135d}-\x{11363}\x{11480}-\x{114b2}\x{114b9}\x{114bb}-\x{114be}\x{114c1}\x{114c4}-\x{114c7}\x{114d0}-\x{114d9}\x{11580}-\x{115b1}\x{115b8}-\x{115bb}\x{115be}\x{115c1}-\x{115db}\x{11600}-\x{11632}\x{1163b}\x{1163c}\x{1163e}\x{11641}-\x{11644}\x{11650}-\x{11659}\x{11680}-\x{116aa}\x{116ac}\x{116ae}\x{116af}\x{116b6}\x{116c0}-\x{116c9}\x{11700}-\x{11719}\x{11720}\x{11721}\x{11726}\x{11730}-\x{1173f}\x{118a0}-\x{118f2}\x{118ff}\x{11ac0}-\x{11af8}\x{12000}-\x{12399}\x{12400}-\x{1246e}\x{12470}-\x{12474}\x{12480}-\x{12543}\x{13000}-\x{1342e}\x{14400}-\x{14646}\x{16800}-\x{16a38}\x{16a40}-\x{16a5e}\x{16a60}-\x{16a69}\x{16a6e}\x{16a6f}\x{16ad0}-\x{16aed}\x{16af5}\x{16b00}-\x{16b2f}\x{16b37}-\x{16b45}\x{16b50}-\x{16b59}\x{16b5b}-\x{16b61}\x{16b63}-\x{16b77}\x{16b7d}-\x{16b8f}\x{16f00}-\x{16f44}\x{16f50}-\x{16f7e}\x{16f93}-\x{16f9f}\x{1b000}\x{1b001}\x{1bc00}-\x{1bc6a}\x{1bc70}-\x{1bc7c}\x{1bc80}-\x{1bc88}\x{1bc90}-\x{1bc99}\x{1bc9c}\x{1bc9f}\x{1d000}-\x{1d0f5}\x{1d100}-\x{1d126}\x{1d129}-\x{1d166}\x{1d16a}-\x{1d172}\x{1d183}\x{1d184}\x{1d18c}-\x{1d1a9}\x{1d1ae}-\x{1d1e8}\x{1d360}-\x{1d371}\x{1d400}-\x{1d454}\x{1d456}-\x{1d49c}\x{1d49e}\x{1d49f}\x{1d4a2}\x{1d4a5}\x{1d4a6}\x{1d4a9}-\x{1d4ac}\x{1d4ae}-\x{1d4b9}\x{1d4bb}\x{1d4bd}-\x{1d4c3}\x{1d4c5}-\x{1d505}\x{1d507}-\x{1d50a}\x{1d50d}-\x{1d514}\x{1d516}-\x{1d51c}\x{1d51e}-\x{1d539}\x{1d53b}-\x{1d53e}\x{1d540}-\x{1d544}\x{1d546}\x{1d54a}-\x{1d550}\x{1d552}-\x{1d6a5}\x{1d6a8}-\x{1d6da}\x{1d6dc}-\x{1d714}\x{1d716}-\x{1d74e}\x{1d750}-\x{1d788}\x{1d78a}-\x{1d7c2}\x{1d7c4}-\x{1d7cb}\x{1d800}-\x{1d9ff}\x{1da37}-\x{1da3a}\x{1da6d}-\x{1da74}\x{1da76}-\x{1da83}\x{1da85}-\x{1da8b}\x{1f110}-\x{1f12e}\x{1f130}-\x{1f169}\x{1f170}-\x{1f19a}\x{1f1e6}-\x{1f202}\x{1f210}-\x{1f23a}\x{1f240}-\x{1f248}\x{1f250}\x{1f251}\x{20000}-\x{2a6d6}\x{2a700}-\x{2b734}\x{2b740}-\x{2b81d}\x{2b820}-\x{2cea1}\x{2f800}-\x{2fa1d}\x{f0000}-\x{ffffd}\x{100000}-\x{10fffd}])|([\x{590}\x{5be}\x{5c0}\x{5c3}\x{5c6}\x{5c8}-\x{5ff}\x{7c0}-\x{7ea}\x{7f4}\x{7f5}\x{7fa}-\x{815}\x{81a}\x{824}\x{828}\x{82e}-\x{858}\x{85c}-\x{89f}\x{200f}\x{fb1d}\x{fb1f}-\x{fb28}\x{fb2a}-\x{fb4f}\x{10800}-\x{1091e}\x{10920}-\x{10a00}\x{10a04}\x{10a07}-\x{10a0b}\x{10a10}-\x{10a37}\x{10a3b}-\x{10a3e}\x{10a40}-\x{10ae4}\x{10ae7}-\x{10b38}\x{10b40}-\x{10e5f}\x{10e7f}-\x{10fff}\x{1e800}-\x{1e8cf}\x{1e8d7}-\x{1edff}\x{1ef00}-\x{1efff}\x{608}\x{60b}\x{60d}\x{61b}-\x{64a}\x{66d}-\x{66f}\x{671}-\x{6d5}\x{6e5}\x{6e6}\x{6ee}\x{6ef}\x{6fa}-\x{710}\x{712}-\x{72f}\x{74b}-\x{7a5}\x{7b1}-\x{7bf}\x{8a0}-\x{8e2}\x{fb50}-\x{fd3d}\x{fd40}-\x{fdcf}\x{fdf0}-\x{fdfc}\x{fdfe}\x{fdff}\x{fe70}-\x{fefe}\x{1ee00}-\x{1eeef}\x{1eef2}-\x{1eeff}]))/u';
289 // @codeCoverageIgnoreEnd
290
294 public function __construct(
295 string $code,
297 private readonly NamespaceInfo $namespaceInfo,
299 private LocalisationCache $localisationCache,
301 private readonly LanguageNameUtils $langNameUtils,
303 private readonly LanguageFallback $langFallback,
305 private readonly LanguageConverterFactory $converterFactory,
307 private readonly HookContainer $hookContainer,
309 private readonly Config $config,
311 private readonly LeximorphFactory $leximorphFactory,
312 ) {
313 $this->mCode = $code;
314 $this->hookRunner = new HookRunner( $hookContainer );
315 }
316
321 public function getFallbackLanguages() {
322 return $this->langFallback->getAll( $this->mCode );
323 }
324
329 public function getBookstoreList() {
330 return $this->localisationCache->getItem( $this->mCode, 'bookstoreList' );
331 }
332
340 public function getNamespaces() {
341 if ( $this->namespaceNames === null ) {
342 $metaNamespace = $this->config->get( MainConfigNames::MetaNamespace );
343 $metaNamespaceTalk = $this->config->get( MainConfigNames::MetaNamespaceTalk );
344 $extraNamespaces = $this->config->get( MainConfigNames::ExtraNamespaces );
345 $validNamespaces = $this->namespaceInfo->getCanonicalNamespaces();
346
347 // @phan-suppress-next-line PhanTypeMismatchProperty
348 $this->namespaceNames = $extraNamespaces +
349 $this->localisationCache->getItem( $this->mCode, 'namespaceNames' );
350 // @phan-suppress-next-line PhanTypeInvalidLeftOperand
351 $this->namespaceNames += $validNamespaces;
352
353 $this->namespaceNames[NS_PROJECT] = $metaNamespace;
354 if ( $metaNamespaceTalk ) {
355 $this->namespaceNames[NS_PROJECT_TALK] = $metaNamespaceTalk;
356 } else {
357 $talk = $this->namespaceNames[NS_PROJECT_TALK];
358 $this->namespaceNames[NS_PROJECT_TALK] =
359 $this->fixVariableInNamespace( $talk );
360 }
361
362 # Sometimes a language will be localised but not actually exist on this wiki.
363 foreach ( $this->namespaceNames as $key => $text ) {
364 if ( !isset( $validNamespaces[$key] ) ) {
365 unset( $this->namespaceNames[$key] );
366 }
367 }
368
369 # The above mixing may leave namespaces out of canonical order.
370 # Re-order by namespace ID number...
371 ksort( $this->namespaceNames );
372
373 $this->getHookRunner()->onLanguageGetNamespaces( $this->namespaceNames );
374 }
375
377 }
378
383 public function setNamespaces( array $namespaces ) {
384 $this->namespaceNames = $namespaces;
385 $this->mNamespaceIds = null;
386 }
387
392 public function resetNamespaces() {
393 $this->namespaceNames = null;
394 $this->mNamespaceIds = null;
395 $this->namespaceAliases = null;
396 }
397
407 public function getFormattedNamespaces() {
408 $ns = $this->getNamespaces();
409 foreach ( $ns as $k => $v ) {
410 $ns[$k] = strtr( $v, '_', ' ' );
411 }
412 return $ns;
413 }
414
428 public function getNsText( $index ) {
429 $ns = $this->getNamespaces();
430 return $ns[$index] ?? false;
431 }
432
446 public function getFormattedNsText( $index ) {
447 $ns = $this->getNsText( $index );
448 return $ns === false ? '' : strtr( $ns, '_', ' ' );
449 }
450
459 public function getGenderNsText( $index, $gender ) {
460 $extraGenderNamespaces = $this->config->get( MainConfigNames::ExtraGenderNamespaces );
461
462 $ns = $extraGenderNamespaces +
463 (array)$this->localisationCache->getItem( $this->mCode, 'namespaceGenderAliases' );
464
465 return $ns[$index][$gender] ?? $this->getNsText( $index );
466 }
467
474 public function needsGenderDistinction() {
475 $extraGenderNamespaces = $this->config->get( MainConfigNames::ExtraGenderNamespaces );
476 $extraNamespaces = $this->config->get( MainConfigNames::ExtraNamespaces );
477 if ( count( $extraGenderNamespaces ) > 0 ) {
478 // $wgExtraGenderNamespaces overrides everything
479 return true;
480 } elseif ( isset( $extraNamespaces[NS_USER] ) && isset( $extraNamespaces[NS_USER_TALK] ) ) {
481 // @todo There may be other gender namespace than NS_USER & NS_USER_TALK in the future
482 // $wgExtraNamespaces overrides any gender aliases specified in i18n files
483 return false;
484 } else {
485 // Check what is in i18n files
486 $aliases = (array)$this->localisationCache->getItem( $this->mCode, 'namespaceGenderAliases' );
487 return count( $aliases ) > 0;
488 }
489 }
490
499 public function getLocalNsIndex( $text ) {
500 $lctext = $this->lc( $text );
501 $ids = $this->getNamespaceIds();
502 return $ids[$lctext] ?? false;
503 }
504
509 public function getNamespaceAliases() {
510 if ( $this->namespaceAliases === null ) {
511 $aliases = $this->localisationCache->getItem( $this->mCode, 'namespaceAliases' );
512 if ( !$aliases ) {
513 $aliases = [];
514 } else {
515 foreach ( $aliases as $name => $index ) {
516 if ( $index === NS_PROJECT_TALK ) {
517 unset( $aliases[$name] );
518 $name = $this->fixVariableInNamespace( $name );
519 $aliases[$name] = $index;
520 }
521 }
522 }
523
524 $extraGenderNamespaces = $this->config->get( MainConfigNames::ExtraGenderNamespaces );
525 $genders = $extraGenderNamespaces + (array)$this->localisationCache
526 ->getItem( $this->mCode, 'namespaceGenderAliases' );
527 foreach ( $genders as $index => $forms ) {
528 foreach ( $forms as $alias ) {
529 $aliases[$alias] = $index;
530 }
531 }
532
533 $langConverter = $this->getConverterInternal();
534 # Also add converted namespace names as aliases, to avoid confusion.
535 $convertedNames = [];
536 foreach ( $langConverter->getVariants() as $variant ) {
537 if ( $variant === $this->mCode ) {
538 continue;
539 }
540 foreach ( $this->getNamespaces() as $ns => $_ ) {
541 $convertedNames[$langConverter->convertNamespace( $ns, $variant )] = $ns;
542 }
543 }
544
545 $this->namespaceAliases = $aliases + $convertedNames;
546
547 // In the case of conflicts between $wgNamespaceAliases and other sources
548 // of aliasing, $wgNamespaceAliases wins.
549 $this->namespaceAliases = $this->config->get( MainConfigNames::NamespaceAliases ) +
551
552 # Filter out aliases to namespaces that don't exist, e.g. from extensions
553 # that aren't loaded here but are included in the l10n cache.
554 # (array_intersect preserves keys from its first argument)
555 $this->namespaceAliases = array_intersect(
556 $this->namespaceAliases,
557 array_keys( $this->getNamespaces() )
558 );
559 }
560
562 }
563
567 public function getNamespaceIds() {
568 if ( $this->mNamespaceIds === null ) {
569 # Put namespace names and aliases into a hashtable.
570 # If this is too slow, then we should arrange it so that it is done
571 # before caching. The catch is that at pre-cache time, the above
572 # class-specific fixup hasn't been done.
573 $this->mNamespaceIds = [];
574 foreach ( $this->getNamespaces() as $index => $name ) {
575 $this->mNamespaceIds[$this->lc( $name )] = $index;
576 }
577 foreach ( $this->getNamespaceAliases() as $name => $index ) {
578 $this->mNamespaceIds[$this->lc( $name )] = $index;
579 }
580 }
582 }
583
591 public function getNsIndex( $text ) {
592 $lctext = $this->lc( $text );
593 $ns = $this->namespaceInfo->getCanonicalIndex( $lctext );
594 if ( $ns !== null ) {
595 return $ns;
596 }
597 $ids = $this->getNamespaceIds();
598 return $ids[$lctext] ?? false;
599 }
600
608 public function getVariantname( $code, $usemsg = true ) {
609 if ( $usemsg ) {
610 $msg = $this->msg( "variantname-$code" );
611 if ( $msg->exists() ) {
612 return $msg->text();
613 }
614 }
615 $name = $this->langNameUtils->getLanguageName( $code );
616 if ( $name ) {
617 return $name; # if it's defined as a language name, show that
618 } else {
619 # otherwise, output the language code
620 return $code;
621 }
622 }
623
627 public function getDatePreferences() {
628 return $this->localisationCache->getItem( $this->mCode, 'datePreferences' );
629 }
630
634 public function getDateFormats() {
635 return $this->localisationCache->getItem( $this->mCode, 'dateFormats' );
636 }
637
641 public function getDefaultDateFormat() {
642 $df = $this->localisationCache->getItem( $this->mCode, 'defaultDateFormat' );
643 if ( $df === 'dmy or mdy' ) {
644 return $this->config->get( MainConfigNames::AmericanDates ) ? 'mdy' : 'dmy';
645 } else {
646 return $df;
647 }
648 }
649
653 public function getDatePreferenceMigrationMap() {
654 return $this->localisationCache->getItem( $this->mCode, 'datePreferenceMigrationMap' );
655 }
656
663 public function getMessageFromDB( $msg ) {
664 return $this->msg( $msg )->text();
665 }
666
676 protected function msg( $msg, ...$params ) {
677 return wfMessage( $msg, ...$params )->inLanguage( $this );
678 }
679
684 public function getMonthName( $key ) {
685 return $this->getMessageFromDB( self::MONTH_MESSAGES[$key - 1] );
686 }
687
691 public function getMonthNamesArray() {
692 $monthNames = [ '' ];
693 for ( $i = 1; $i <= 12; $i++ ) {
694 $monthNames[] = $this->getMonthName( $i );
695 }
696 return $monthNames;
697 }
698
703 public function getMonthNameGen( $key ) {
704 return $this->getMessageFromDB( self::MONTH_GENITIVE_MESSAGES[$key - 1] );
705 }
706
711 public function getMonthAbbreviation( $key ) {
712 return $this->getMessageFromDB( self::MONTH_ABBREVIATED_MESSAGES[$key - 1] );
713 }
714
718 public function getMonthAbbreviationsArray() {
719 $monthNames = [ '' ];
720 for ( $i = 1; $i <= 12; $i++ ) {
721 $monthNames[] = $this->getMonthAbbreviation( $i );
722 }
723 return $monthNames;
724 }
725
730 public function getWeekdayName( $key ) {
731 return $this->getMessageFromDB( self::WEEKDAY_MESSAGES[$key - 1] );
732 }
733
738 public function getWeekdayAbbreviation( $key ) {
739 return $this->getMessageFromDB( self::WEEKDAY_ABBREVIATED_MESSAGES[$key - 1] );
740 }
741
750 private static function dateTimeObj( &$dateTimeObj, $ts, $zone ): DateTime {
751 if ( !$dateTimeObj ) {
752 $dateTimeObj = DateTime::createFromFormat(
753 'YmdHis', $ts, $zone ?: new DateTimeZone( 'UTC' )
754 );
755 }
756 return $dateTimeObj;
757 }
758
768 private static function dateTimeObjFormat( &$dateTimeObj, $ts, $zone, $code ) {
769 return self::dateTimeObj( $dateTimeObj, $ts, $zone )->format( $code );
770 }
771
841 public function sprintfDate( $format, $ts, ?DateTimeZone $zone = null, &$ttl = 'unused' ) {
842 // @phan-suppress-previous-line PhanTypeMismatchDefault Type mismatch on pass-by-ref args
843 $s = '';
844 $raw = false;
845 $roman = false;
846 $hebrewNum = false;
847 $dateTimeObj = false;
848 $rawToggle = false;
849 $iranian = false;
850 $hebrew = false;
851 $hijri = false;
852 $thai = false;
853 $minguo = false;
854 $tenno = false;
855
856 $usedSecond = false;
857 $usedMinute = false;
858 $usedHour = false;
859 $usedAMPM = false;
860 $usedDay = false;
861 $usedWeek = false;
862 $usedMonth = false;
863 $usedYear = false;
864 $usedISOYear = false;
865 $usedIsLeapYear = false;
866
867 $usedHebrewMonth = false;
868 $usedIranianMonth = false;
869 $usedHijriMonth = false;
870 $usedHebrewYear = false;
871 $usedIranianYear = false;
872 $usedHijriYear = false;
873 $usedTennoYear = false;
874
875 if ( strlen( $ts ) !== 14 ) {
876 throw new InvalidArgumentException( __METHOD__ . ": The timestamp $ts should have 14 characters" );
877 }
878
879 if ( !ctype_digit( $ts ) ) {
880 throw new InvalidArgumentException( __METHOD__ . ": The timestamp $ts should be a number" );
881 }
882
883 $formatLength = strlen( $format );
884 for ( $p = 0; $p < $formatLength; $p++ ) {
885 $num = false;
886 $code = $format[$p];
887 if ( $code == 'x' && $p < $formatLength - 1 ) {
888 $code .= $format[++$p];
889 }
890
891 if ( ( $code === 'xi'
892 || $code === 'xj'
893 || $code === 'xk'
894 || $code === 'xm'
895 || $code === 'xo'
896 || $code === 'xt' )
897 && $p < $formatLength - 1
898 ) {
899 $code .= $format[++$p];
900 }
901
902 switch ( $code ) {
903 case 'xx':
904 $s .= 'x';
905 break;
906
907 case 'xn':
908 $raw = true;
909 break;
910
911 case 'xN':
912 $rawToggle = !$rawToggle;
913 break;
914
915 case 'xr':
916 $roman = true;
917 break;
918
919 case 'xh':
920 $hebrewNum = true;
921 break;
922
923 case 'xg':
924 $usedMonth = true;
925 $s .= $this->getMonthNameGen( (int)substr( $ts, 4, 2 ) );
926 break;
927
928 case 'xjx':
929 $usedHebrewMonth = true;
930 if ( !$hebrew ) {
931 $hebrew = self::tsToHebrew( $ts );
932 }
933 $s .= $this->getMessageFromDB( self::HEBREW_CALENDAR_MONTH_GENITIVE_MESSAGES[$hebrew[1] - 1] );
934 break;
935
936 case 'd':
937 $usedDay = true;
938 $num = substr( $ts, 6, 2 );
939 break;
940
941 case 'D':
942 $usedDay = true;
943 $s .= $this->getWeekdayAbbreviation(
944 (int)self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1
945 );
946 break;
947
948 case 'j':
949 $usedDay = true;
950 $num = intval( substr( $ts, 6, 2 ) );
951 break;
952
953 case 'xij':
954 $usedDay = true;
955 if ( !$iranian ) {
956 $iranian = self::tsToIranian( $ts );
957 }
958 $num = $iranian[2];
959 break;
960
961 case 'xmj':
962 $usedDay = true;
963 if ( !$hijri ) {
964 $hijri = self::tsToHijri( $ts );
965 }
966 $num = $hijri[2];
967 break;
968
969 case 'xjj':
970 $usedDay = true;
971 if ( !$hebrew ) {
972 $hebrew = self::tsToHebrew( $ts );
973 }
974 $num = $hebrew[2];
975 break;
976
977 case 'l':
978 $usedDay = true;
979 $s .= $this->getWeekdayName(
980 (int)self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1
981 );
982 break;
983
984 case 'F':
985 $usedMonth = true;
986 $s .= $this->getMonthName( (int)substr( $ts, 4, 2 ) );
987 break;
988
989 case 'xiF':
990 $usedIranianMonth = true;
991 if ( !$iranian ) {
992 $iranian = self::tsToIranian( $ts );
993 }
994 $s .= $this->getMessageFromDB( self::IRANIAN_CALENDAR_MONTHS_MESSAGES[$iranian[1] - 1] );
995 break;
996
997 case 'xmF':
998 $usedHijriMonth = true;
999 if ( !$hijri ) {
1000 $hijri = self::tsToHijri( $ts );
1001 }
1002 $s .= $this->getMessageFromDB( self::HIJRI_CALENDAR_MONTH_MESSAGES[$hijri[1] - 1] );
1003 break;
1004
1005 case 'xjF':
1006 $usedHebrewMonth = true;
1007 if ( !$hebrew ) {
1008 $hebrew = self::tsToHebrew( $ts );
1009 }
1010 $s .= $this->getMessageFromDB( self::HEBREW_CALENDAR_MONTHS_MESSAGES[$hebrew[1] - 1] );
1011 break;
1012
1013 case 'm':
1014 $usedMonth = true;
1015 $num = substr( $ts, 4, 2 );
1016 break;
1017
1018 case 'M':
1019 $usedMonth = true;
1020 $s .= $this->getMonthAbbreviation( (int)substr( $ts, 4, 2 ) );
1021 break;
1022
1023 case 'n':
1024 $usedMonth = true;
1025 $num = intval( substr( $ts, 4, 2 ) );
1026 break;
1027
1028 case 'xin':
1029 $usedIranianMonth = true;
1030 if ( !$iranian ) {
1031 $iranian = self::tsToIranian( $ts );
1032 }
1033 $num = $iranian[1];
1034 break;
1035
1036 case 'xmn':
1037 $usedHijriMonth = true;
1038 if ( !$hijri ) {
1039 $hijri = self::tsToHijri( $ts );
1040 }
1041 $num = $hijri[1];
1042 break;
1043
1044 case 'xjn':
1045 $usedHebrewMonth = true;
1046 if ( !$hebrew ) {
1047 $hebrew = self::tsToHebrew( $ts );
1048 }
1049 $num = $hebrew[1];
1050 break;
1051
1052 case 'xjt':
1053 $usedHebrewMonth = true;
1054 if ( !$hebrew ) {
1055 $hebrew = self::tsToHebrew( $ts );
1056 }
1057 $num = $hebrew[3];
1058 break;
1059
1060 case 'Y':
1061 $usedYear = true;
1062 $num = substr( $ts, 0, 4 );
1063 break;
1064
1065 case 'xiY':
1066 $usedIranianYear = true;
1067 if ( !$iranian ) {
1068 $iranian = self::tsToIranian( $ts );
1069 }
1070 $num = $iranian[0];
1071 break;
1072
1073 case 'xmY':
1074 $usedHijriYear = true;
1075 if ( !$hijri ) {
1076 $hijri = self::tsToHijri( $ts );
1077 }
1078 $num = $hijri[0];
1079 break;
1080
1081 case 'xjY':
1082 $usedHebrewYear = true;
1083 if ( !$hebrew ) {
1084 $hebrew = self::tsToHebrew( $ts );
1085 }
1086 $num = $hebrew[0];
1087 break;
1088
1089 case 'xkY':
1090 $usedYear = true;
1091 if ( !$thai ) {
1092 $thai = self::tsToYear( $ts, 'thai' );
1093 }
1094 $num = $thai[0];
1095 break;
1096
1097 case 'xoY':
1098 $usedYear = true;
1099 if ( !$minguo ) {
1100 $minguo = self::tsToYear( $ts, 'minguo' );
1101 }
1102 $num = $minguo[0];
1103 break;
1104
1105 case 'xtY':
1106 $usedTennoYear = true;
1107 if ( !$tenno ) {
1108 $tenno = self::tsToJapaneseGengo( $ts );
1109 }
1110 $num = $tenno;
1111 break;
1112
1113 case 'y':
1114 $usedYear = true;
1115 $num = substr( $ts, 2, 2 );
1116 break;
1117
1118 case 'xiy':
1119 $usedIranianYear = true;
1120 if ( !$iranian ) {
1121 $iranian = self::tsToIranian( $ts );
1122 }
1123 $num = substr( (string)$iranian[0], -2 );
1124 break;
1125
1126 case 'xit':
1127 $usedIranianYear = true;
1128 if ( !$iranian ) {
1129 $iranian = self::tsToIranian( $ts );
1130 }
1131 $num = self::IRANIAN_DAYS[$iranian[1] - 1];
1132 break;
1133
1134 case 'xiz':
1135 $usedIranianYear = true;
1136 if ( !$iranian ) {
1137 $iranian = self::tsToIranian( $ts );
1138 }
1139 $num = $iranian[3];
1140 break;
1141
1142 case 'a':
1143 $usedAMPM = true;
1144 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'am' : 'pm';
1145 break;
1146
1147 case 'A':
1148 $usedAMPM = true;
1149 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'AM' : 'PM';
1150 break;
1151
1152 case 'g':
1153 $usedHour = true;
1154 $h = (int)substr( $ts, 8, 2 );
1155 $num = $h % 12 ?: 12;
1156 break;
1157
1158 case 'G':
1159 $usedHour = true;
1160 $num = intval( substr( $ts, 8, 2 ) );
1161 break;
1162
1163 case 'h':
1164 $usedHour = true;
1165 $h = (int)substr( $ts, 8, 2 );
1166 $num = sprintf( '%02d', $h % 12 ?: 12 );
1167 break;
1168
1169 case 'H':
1170 $usedHour = true;
1171 $num = substr( $ts, 8, 2 );
1172 break;
1173
1174 case 'i':
1175 $usedMinute = true;
1176 $num = substr( $ts, 10, 2 );
1177 break;
1178
1179 case 's':
1180 $usedSecond = true;
1181 $num = substr( $ts, 12, 2 );
1182 break;
1183
1184 case 'c':
1185 case 'r':
1186 $usedSecond = true;
1187 // fall through
1188 case 'e':
1189 case 'O':
1190 case 'P':
1191 case 'T':
1192 $s .= self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1193 break;
1194
1195 case 'w':
1196 case 'N':
1197 case 'z':
1198 $usedDay = true;
1199 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1200 break;
1201
1202 case 'W':
1203 $usedWeek = true;
1204 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1205 break;
1206
1207 case 't':
1208 $usedMonth = true;
1209 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1210 break;
1211
1212 case 'L':
1213 $usedIsLeapYear = true;
1214 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1215 break;
1216
1217 case 'o':
1218 $usedISOYear = true;
1219 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1220 break;
1221
1222 case 'U':
1223 $usedSecond = true;
1224 // fall through
1225 case 'I':
1226 case 'Z':
1227 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1228 break;
1229
1230 case '\\':
1231 # Backslash escaping
1232 if ( $p < $formatLength - 1 ) {
1233 $s .= $format[++$p];
1234 } else {
1235 $s .= '\\';
1236 }
1237 break;
1238
1239 case '"':
1240 # Quoted literal
1241 if ( $p < $formatLength - 1 ) {
1242 $endQuote = strpos( $format, '"', $p + 1 );
1243 if ( $endQuote === false ) {
1244 # No terminating quote, assume literal "
1245 $s .= '"';
1246 } else {
1247 $s .= substr( $format, $p + 1, $endQuote - $p - 1 );
1248 $p = $endQuote;
1249 }
1250 } else {
1251 # Quote at the end of the string, assume literal "
1252 $s .= '"';
1253 }
1254 break;
1255
1256 default:
1257 $s .= $format[$p];
1258 }
1259 if ( $num !== false ) {
1260 if ( $rawToggle || $raw ) {
1261 $s .= $num;
1262 $raw = false;
1263 } elseif ( $roman ) {
1264 $s .= self::romanNumeral( $num );
1265 $roman = false;
1266 } elseif ( $hebrewNum ) {
1267 $s .= self::hebrewNumeral( $num );
1268 $hebrewNum = false;
1269 } elseif ( preg_match( '/^[\d.]+$/', $num ) ) {
1270 $s .= $this->formatNumNoSeparators( $num );
1271 } else {
1272 $s .= $num;
1273 }
1274 }
1275 }
1276
1277 if ( $ttl !== 'unused' ) {
1278 // Ensure $dateTimeObj is set.
1279 self::dateTimeObj( $dateTimeObj, $ts, $zone );
1280 }
1281 '@phan-var DateTime $dateTimeObj';
1282 if ( $ttl === 'unused' ) {
1283 // No need to calculate the TTL, the caller won't use it anyway.
1284 } elseif ( $usedSecond ) {
1285 $ttl = self::computeUnitTimestampDeadline( $dateTimeObj, 's' );
1286 } elseif ( $usedMinute ) {
1287 $ttl = self::computeUnitTimestampDeadline( $dateTimeObj, 'm' );
1288 } elseif ( $usedHour ) {
1289 $ttl = self::computeUnitTimestampDeadline( $dateTimeObj, 'H' );
1290 } elseif ( $usedAMPM ) {
1291 $ttl = self::computeUnitTimestampDeadline( $dateTimeObj, 'AM' );
1292 } elseif (
1293 $usedDay ||
1294 $usedHebrewMonth ||
1295 $usedIranianMonth ||
1296 $usedHijriMonth ||
1297 $usedHebrewYear ||
1298 $usedIranianYear ||
1299 $usedHijriYear ||
1300 $usedTennoYear
1301 ) {
1302 // @todo Someone who understands the non-Gregorian calendars
1303 // should write proper logic for them so that they don't need purged every day.
1304 $ttl = self::computeUnitTimestampDeadline( $dateTimeObj, 'D' );
1305 } else {
1306 $possibleTtls = [];
1307 if ( $usedWeek ) {
1308 $possibleTtls[] =
1309 self::computeUnitTimestampDeadline( $dateTimeObj, 'W' );
1310 } elseif ( $usedISOYear ) {
1311 // December 28th falls on the last ISO week of the year, every year.
1312 // The last ISO week of a year can be 52 or 53.
1313 $possibleTtls[] =
1314 self::computeUnitTimestampDeadline( $dateTimeObj, 'ISOY' );
1315 }
1316
1317 if ( $usedMonth ) {
1318 $possibleTtls[] =
1319 self::computeUnitTimestampDeadline( $dateTimeObj, 'M' );
1320 } elseif ( $usedYear ) {
1321 $possibleTtls[] =
1322 self::computeUnitTimestampDeadline( $dateTimeObj, 'Y' );
1323 } elseif ( $usedIsLeapYear ) {
1324 $year = (int)substr( $ts, 0, 4 );
1325 $mod = $year % 4;
1326 $timeRemainingInYear =
1327 self::computeUnitTimestampDeadline( $dateTimeObj, 'Y' );
1328 if ( $mod || ( !( $year % 100 ) && $year % 400 ) ) {
1329 // this isn't a leap year. see when the next one starts
1330 $nextCandidate = $year - $mod + 4;
1331 if ( $nextCandidate % 100 || !( $nextCandidate % 400 ) ) {
1332 $possibleTtls[] = ( $nextCandidate - $year - 1 ) * 365 * 86400 +
1333 $timeRemainingInYear;
1334 } else {
1335 $possibleTtls[] = ( $nextCandidate - $year + 3 ) * 365 * 86400 +
1336 $timeRemainingInYear;
1337 }
1338 } else {
1339 // this is a leap year, so the next year isn't
1340 $possibleTtls[] = $timeRemainingInYear;
1341 }
1342 }
1343
1344 if ( $possibleTtls ) {
1345 $ttl = min( $possibleTtls );
1346 }
1347 }
1348
1349 return $s;
1350 }
1351
1361 public static function computeUnitTimestampDeadline(
1362 DateTimeInterface $date,
1363 string $unit
1364 ): int {
1365 // Ensure we don't mutate our argument
1366 $date = DateTimeImmutable::createFromInterface( $date );
1367 $tsUnix = $date->getTimestamp();
1368
1369 $date = $date->modify( self::DEADLINE_DATE_SPEC_BY_UNIT[$unit] );
1370 if ( $unit === 'AM' ) {
1371 if ( $date->getTimestamp() <= $tsUnix ) {
1372 $date = $date->modify( self::DEADLINE_DATE_SPEC_BY_UNIT['D'] );
1373 }
1374 } elseif ( $unit === 'ISOY' ) {
1375 // December 28th falls on the last ISO week of the year, every year.
1376 // but we could be in the same, next, or previous ISO year as
1377 // December 28 of this calendar year.
1378 // Compute all three and use the first which is in the future.
1379 $lastYear = $date->modify(
1380 'december 28 midnight last year'
1381 )->modify( 'monday next week midnight' );
1382 $nextYear = $date->modify(
1383 'december 28 midnight next year'
1384 )->modify( 'monday next week midnight' );
1385 // Advance to first week of next ISO year
1386 $date = $date->modify( 'monday next week midnight' );
1387 if ( $lastYear->getTimestamp() > $tsUnix ) {
1388 $date = $lastYear;
1389 } elseif ( $date->getTimestamp() <= $tsUnix ) {
1390 $date = $nextYear;
1391 }
1392 } elseif ( $unit === 'H' ) {
1393 // Zero out the minutes/seconds
1394 $date = $date->setTime( intval( $date->format( 'H' ), 10 ), 0, 0 );
1395 } elseif ( $unit === 'm' ) {
1396 // Zero out the seconds
1397 $date = $date->setTime( intval( $date->format( 'H' ), 10 ),
1398 intval( $date->format( 'i' ), 10 ), 0 );
1399 } elseif ( $unit === 's' ) {
1400 // Zero out the fractional seconds
1401 $date = $date->setTime( intval( $date->format( 'H' ), 10 ),
1402 intval( $date->format( 'i' ), 10 ),
1403 intval( $date->format( 's' ), 10 ) );
1404 } else {
1405 $date = $date->setTime( 0, 0, 0 );
1406 }
1407 $deadlineUnix = (int)$date->format( 'U' );
1408 return $deadlineUnix - $tsUnix;
1409 }
1410
1414 private const GREG_DAYS = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
1415
1419 private const IRANIAN_DAYS = [ 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 ];
1420
1433 private static function tsToIranian( $ts ) {
1434 $gy = (int)substr( $ts, 0, 4 ) - 1600;
1435 $gm = (int)substr( $ts, 4, 2 ) - 1;
1436 $gd = (int)substr( $ts, 6, 2 ) - 1;
1437
1438 # Days passed from the beginning (including leap years)
1439 $gDayNo = 365 * $gy
1440 + floor( ( $gy + 3 ) / 4 )
1441 - floor( ( $gy + 99 ) / 100 )
1442 + floor( ( $gy + 399 ) / 400 );
1443
1444 // Add the number of days for the past months of this year
1445 for ( $i = 0; $i < $gm; $i++ ) {
1446 $gDayNo += self::GREG_DAYS[$i];
1447 }
1448
1449 // Leap years
1450 if ( $gm > 1 && ( ( $gy % 4 === 0 && $gy % 100 !== 0 ) || $gy % 400 == 0 ) ) {
1451 $gDayNo++;
1452 }
1453
1454 // Days passed in the current month
1455 $gDayNo += $gd;
1456
1457 $jDayNo = $gDayNo - 79;
1458
1459 $jNp = (int)floor( $jDayNo / 12053 );
1460 $jDayNo %= 12053;
1461
1462 $jy = 979 + 33 * $jNp + 4 * (int)floor( $jDayNo / 1461 );
1463 $jDayNo %= 1461;
1464
1465 if ( $jDayNo >= 366 ) {
1466 $jy += (int)floor( ( $jDayNo - 1 ) / 365 );
1467 $jDayNo = (int)floor( ( $jDayNo - 1 ) % 365 );
1468 }
1469
1470 $jz = $jDayNo;
1471
1472 for ( $i = 0; $i < 11 && $jDayNo >= self::IRANIAN_DAYS[$i]; $i++ ) {
1473 $jDayNo -= self::IRANIAN_DAYS[$i];
1474 }
1475
1476 $jm = $i + 1;
1477 $jd = $jDayNo + 1;
1478
1479 return [ $jy, $jm, $jd, $jz ];
1480 }
1481
1493 private static function tsToHijri( $ts ) {
1494 $year = (int)substr( $ts, 0, 4 );
1495 $month = (int)substr( $ts, 4, 2 );
1496 $day = (int)substr( $ts, 6, 2 );
1497
1498 $zyr = $year;
1499 $zd = $day;
1500 $zm = $month;
1501 $zy = $zyr;
1502
1503 if (
1504 ( $zy > 1582 ) || ( ( $zy == 1582 ) && ( $zm > 10 ) ) ||
1505 ( ( $zy == 1582 ) && ( $zm == 10 ) && ( $zd > 14 ) )
1506 ) {
1507 $zjd = (int)( ( 1461 * ( $zy + 4800 + (int)( ( $zm - 14 ) / 12 ) ) ) / 4 ) +
1508 (int)( ( 367 * ( $zm - 2 - 12 * ( (int)( ( $zm - 14 ) / 12 ) ) ) ) / 12 ) -
1509 (int)( ( 3 * (int)( ( $zy + 4900 + (int)( ( $zm - 14 ) / 12 ) ) / 100 ) ) / 4 ) +
1510 $zd - 32075;
1511 } else {
1512 $zjd = 367 * $zy - (int)( ( 7 * ( $zy + 5001 + (int)( ( $zm - 9 ) / 7 ) ) ) / 4 ) +
1513 (int)( ( 275 * $zm ) / 9 ) + $zd + 1729777;
1514 }
1515
1516 $zl = $zjd - 1948440 + 10632;
1517 $zn = (int)( ( $zl - 1 ) / 10631 );
1518 $zl = $zl - 10631 * $zn + 354;
1519 $zj = ( (int)( ( 10985 - $zl ) / 5316 ) ) * ( (int)( ( 50 * $zl ) / 17719 ) ) +
1520 ( (int)( $zl / 5670 ) ) * ( (int)( ( 43 * $zl ) / 15238 ) );
1521 $zl = $zl - ( (int)( ( 30 - $zj ) / 15 ) ) * ( (int)( ( 17719 * $zj ) / 50 ) ) -
1522 ( (int)( $zj / 16 ) ) * ( (int)( ( 15238 * $zj ) / 43 ) ) + 29;
1523 $zm = (int)( ( 24 * $zl ) / 709 );
1524 $zd = $zl - (int)( ( 709 * $zm ) / 24 );
1525 $zy = 30 * $zn + $zj - 30;
1526
1527 return [ $zy, $zm, $zd ];
1528 }
1529
1545 private static function tsToHebrew( $ts ) {
1546 # Parse date
1547 $year = (int)substr( $ts, 0, 4 );
1548 $month = (int)substr( $ts, 4, 2 );
1549 $day = (int)substr( $ts, 6, 2 );
1550
1551 # Calculate Hebrew year
1552 $hebrewYear = $year + 3760;
1553
1554 # Month number when September = 1, August = 12
1555 $month += 4;
1556 if ( $month > 12 ) {
1557 # Next year
1558 $month -= 12;
1559 $year++;
1560 $hebrewYear++;
1561 }
1562
1563 # Calculate day of year from 1 September
1564 $dayOfYear = $day;
1565 for ( $i = 1; $i < $month; $i++ ) {
1566 if ( $i == 6 ) {
1567 # February
1568 $dayOfYear += 28;
1569 # Check if the year is a leap year
1570 if ( $year % 400 == 0 || ( $year % 4 == 0 && $year % 100 > 0 ) ) {
1571 $dayOfYear++;
1572 }
1573 } elseif ( $i == 8 || $i == 10 || $i == 1 || $i == 3 ) {
1574 $dayOfYear += 30;
1575 } else {
1576 $dayOfYear += 31;
1577 }
1578 }
1579
1580 # Calculate the start of the Hebrew year
1581 $start = self::hebrewYearStart( $hebrewYear );
1582
1583 # Calculate next year's start
1584 if ( $dayOfYear <= $start ) {
1585 # Day is before the start of the year - it is the previous year
1586 # Next year's start
1587 $nextStart = $start;
1588 # Previous year
1589 $year--;
1590 $hebrewYear--;
1591 # Add days since the previous year's 1 September
1592 $dayOfYear += 365;
1593 if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1594 # Leap year
1595 $dayOfYear++;
1596 }
1597 # Start of the new (previous) year
1598 $start = self::hebrewYearStart( $hebrewYear );
1599 } else {
1600 # Next year's start
1601 $nextStart = self::hebrewYearStart( $hebrewYear + 1 );
1602 }
1603
1604 # Calculate Hebrew day of year
1605 $hebrewDayOfYear = $dayOfYear - $start;
1606
1607 # Difference between year's days
1608 $diff = $nextStart - $start;
1609 # Add 12 (or 13 for leap years) days to ignore the difference between
1610 # Hebrew and Gregorian year (353 at least vs. 365/6) - now the
1611 # difference is only about the year type
1612 if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1613 $diff += 13;
1614 } else {
1615 $diff += 12;
1616 }
1617
1618 # Check the year pattern, and is leap year
1619 # 0 means an incomplete year, 1 means a regular year, 2 means a complete year
1620 # This is mod 30, to work on both leap years (which add 30 days of Adar I)
1621 # and non-leap years
1622 $yearPattern = $diff % 30;
1623 # Check if leap year
1624 $isLeap = $diff >= 30;
1625
1626 # Calculate day in the month from number of day in the Hebrew year
1627 # Don't check Adar - if the day is not in Adar, we will stop before;
1628 # if it is in Adar, we will use it to check if it is Adar I or Adar II
1629 $hebrewDay = $hebrewDayOfYear;
1630 $hebrewMonth = 1;
1631 $days = 0;
1632 while ( $hebrewMonth <= 12 ) {
1633 # Calculate days in this month
1634 if ( $isLeap && $hebrewMonth == 6 ) {
1635 # Leap year - has Adar I, with 30 days, and Adar II, with 29 days
1636 $days = 30;
1637 if ( $hebrewDay <= $days ) {
1638 # Day in Adar I
1639 $hebrewMonth = 13;
1640 } else {
1641 # Subtract the days of Adar I
1642 $hebrewDay -= $days;
1643 # Try Adar II
1644 $days = 29;
1645 if ( $hebrewDay <= $days ) {
1646 # Day in Adar II
1647 $hebrewMonth = 14;
1648 }
1649 }
1650 } elseif ( $hebrewMonth == 2 && $yearPattern == 2 ) {
1651 # Cheshvan in a complete year (otherwise as the rule below)
1652 $days = 30;
1653 } elseif ( $hebrewMonth == 3 && $yearPattern == 0 ) {
1654 # Kislev in an incomplete year (otherwise as the rule below)
1655 $days = 29;
1656 } else {
1657 # Odd months have 30 days, even have 29
1658 $days = 30 - ( $hebrewMonth - 1 ) % 2;
1659 }
1660 if ( $hebrewDay <= $days ) {
1661 # In the current month
1662 break;
1663 } else {
1664 # Subtract the days of the current month
1665 $hebrewDay -= $days;
1666 # Try in the next month
1667 $hebrewMonth++;
1668 }
1669 }
1670
1671 return [ $hebrewYear, $hebrewMonth, $hebrewDay, $days ];
1672 }
1673
1683 private static function hebrewYearStart( $year ) {
1684 $a = ( 12 * ( $year - 1 ) + 17 ) % 19;
1685 $b = ( $year - 1 ) % 4;
1686 $m = 32.044093161144 + 1.5542417966212 * $a + $b / 4.0 - 0.0031777940220923 * ( $year - 1 );
1687 if ( $m < 0 ) {
1688 $m--;
1689 }
1690 $Mar = intval( $m );
1691 if ( $m < 0 ) {
1692 $m++;
1693 }
1694 $m -= $Mar;
1695
1696 $c = ( $Mar + 3 * ( $year - 1 ) + 5 * $b + 5 ) % 7;
1697 if ( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
1698 $Mar++;
1699 } elseif ( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
1700 $Mar += 2;
1701 } elseif ( $c == 2 || $c == 4 || $c == 6 ) {
1702 $Mar++;
1703 }
1704
1705 $Mar += intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
1706 return $Mar;
1707 }
1708
1720 private static function tsToYear( $ts, $cName ) {
1721 $gy = (int)substr( $ts, 0, 4 );
1722 $gm = (int)substr( $ts, 4, 2 );
1723 $gd = (int)substr( $ts, 6, 2 );
1724
1725 if ( $cName === 'thai' ) {
1726 # Thai solar dates
1727 # Add 543 years to the Gregorian calendar
1728 # Months and days are identical
1729 $gy_offset = $gy + 543;
1730 # fix for dates between 1912 and 1941
1731 # https://en.wikipedia.org/?oldid=836596673#New_year
1732 if ( $gy >= 1912 && $gy <= 1940 ) {
1733 if ( $gm <= 3 ) {
1734 $gy_offset--;
1735 }
1736 $gm = ( $gm - 3 ) % 12;
1737 }
1738 } elseif ( $cName === 'minguo' || $cName === 'juche' ) {
1739 # Minguo dates
1740 # Deduct 1911 years from the Gregorian calendar
1741 # Months and days are identical
1742 $gy_offset = $gy - 1911;
1743 } else {
1744 $gy_offset = $gy;
1745 }
1746
1747 return [ $gy_offset, $gm, $gd ];
1748 }
1749
1758 private static function tsToJapaneseGengo( $ts ) {
1759 # Nengō dates up to Meiji period.
1760 # Deduct years from the Gregorian calendar
1761 # depending on the nengo periods
1762 # The months and days are identical
1763 $gy = (int)substr( $ts, 0, 4 );
1764 $ts = (int)$ts;
1765 if ( $ts >= 18730101000000 && $ts < 19120730000000 ) {
1766 # Meiji period; start from meiji 6 (1873) it starts using gregorian year
1767 return self::tsToJapaneseGengoCalculate( $gy, 1868, '明治' );
1768 } elseif ( $ts >= 19120730000000 && $ts < 19261225000000 ) {
1769 # Taishō period
1770 return self::tsToJapaneseGengoCalculate( $gy, 1912, '大正' );
1771 } elseif ( $ts >= 19261225000000 && $ts < 19890108000000 ) {
1772 # Shōwa period
1773 return self::tsToJapaneseGengoCalculate( $gy, 1926, '昭和' );
1774 } elseif ( $ts >= 19890108000000 && $ts < 20190501000000 ) {
1775 # Heisei period
1776 return self::tsToJapaneseGengoCalculate( $gy, 1989, '平成' );
1777 } elseif ( $ts >= 20190501000000 ) {
1778 # Reiwa period
1779 return self::tsToJapaneseGengoCalculate( $gy, 2019, '令和' );
1780 }
1781 return "西暦$gy";
1782 }
1783
1794 private static function tsToJapaneseGengoCalculate( $gy, $startYear, $gengo ) {
1795 $gy_offset = $gy - $startYear + 1;
1796 if ( $gy_offset == 1 ) {
1797 $gy_offset = '元';
1798 }
1799 return "$gengo$gy_offset";
1800 }
1801
1815 private static function strongDirFromContent( $text = '' ) {
1816 if ( !preg_match( self::$strongDirRegex, $text, $matches ) ) {
1817 return null;
1818 }
1819 if ( $matches[1] === '' ) {
1820 return 'rtl';
1821 }
1822 return 'ltr';
1823 }
1824
1832 public static function romanNumeral( $num ) {
1833 static $table = [
1834 [ '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ],
1835 [ '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ],
1836 [ '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ],
1837 [ '', 'M', 'MM', 'MMM', 'MMMM', 'MMMMM', 'MMMMMM', 'MMMMMMM',
1838 'MMMMMMMM', 'MMMMMMMMM', 'MMMMMMMMMM' ]
1839 ];
1840
1841 $num = intval( $num );
1842 if ( $num > 10000 || $num <= 0 ) {
1843 return (string)$num;
1844 }
1845
1846 $s = '';
1847 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1848 if ( $num >= $pow10 ) {
1849 $s .= $table[$i][(int)floor( $num / $pow10 )];
1850 }
1851 $num %= $pow10;
1852 }
1853 return $s;
1854 }
1855
1863 public static function hebrewNumeral( $num ) {
1864 static $table = [
1865 [ '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ],
1866 [ '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ],
1867 [ '',
1868 [ 'ק' ],
1869 [ 'ר' ],
1870 [ 'ש' ],
1871 [ 'ת' ],
1872 [ 'ת', 'ק' ],
1873 [ 'ת', 'ר' ],
1874 [ 'ת', 'ש' ],
1875 [ 'ת', 'ת' ],
1876 [ 'ת', 'ת', 'ק' ],
1877 [ 'ת', 'ת', 'ר' ],
1878 ],
1879 [ '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ]
1880 ];
1881
1882 $num = intval( $num );
1883 if ( $num > 9999 || $num <= 0 ) {
1884 return (string)$num;
1885 }
1886
1887 // Round thousands have special notations
1888 if ( $num === 1000 ) {
1889 return "א' אלף";
1890 } elseif ( $num % 1000 === 0 ) {
1891 return $table[0][$num / 1000] . "' אלפים";
1892 }
1893
1894 $letters = [];
1895
1896 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1897 if ( $num >= $pow10 ) {
1898 if ( $num === 15 || $num === 16 ) {
1899 $letters[] = $table[0][9];
1900 $letters[] = $table[0][$num - 9];
1901 $num = 0;
1902 } else {
1903 $letters = array_merge(
1904 $letters,
1905 (array)$table[$i][intval( $num / $pow10 )]
1906 );
1907
1908 if ( $pow10 === 1000 ) {
1909 $letters[] = "'";
1910 }
1911 }
1912 }
1913
1914 $num %= $pow10;
1915 }
1916
1917 $preTransformLength = count( $letters );
1918 if ( $preTransformLength === 1 ) {
1919 // Add geresh (single quote) to one-letter numbers
1920 $letters[] = "'";
1921 } else {
1922 $lastIndex = $preTransformLength - 1;
1923 $letters[$lastIndex] = strtr(
1924 $letters[$lastIndex],
1925 [ 'כ' => 'ך', 'מ' => 'ם', 'נ' => 'ן', 'פ' => 'ף', 'צ' => 'ץ' ]
1926 );
1927
1928 // Add gershayim (double quote) to multiple-letter numbers,
1929 // but exclude numbers with only one letter after the thousands
1930 // (1001-1009, 1020, 1030, 2001-2009, etc.)
1931 if ( $letters[1] === "'" && $preTransformLength === 3 ) {
1932 $letters[] = "'";
1933 } else {
1934 array_splice( $letters, -1, 0, '"' );
1935 }
1936 }
1937
1938 return implode( $letters );
1939 }
1940
1949 public function userAdjust( $ts, $tz = false ) {
1950 $localTZoffset = $this->config->get( MainConfigNames::LocalTZoffset );
1951 if ( $tz === false ) {
1952 $optionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
1953 $tz = $optionsLookup->getOption(
1954 RequestContext::getMain()->getUser(),
1955 'timecorrection'
1956 );
1957 }
1958
1959 $timeCorrection = new UserTimeCorrection( (string)$tz, null, $localTZoffset );
1960
1961 $tzObj = $timeCorrection->getTimeZone();
1962 if ( $tzObj ) {
1963 $date = new DateTime( $ts, new DateTimeZone( 'UTC' ) );
1964 $date->setTimezone( $tzObj );
1965 return self::makeMediaWikiTimestamp( $ts, $date );
1966 }
1967 $minDiff = $timeCorrection->getTimeOffset();
1968
1969 # No difference? Return the time unchanged
1970 if ( $minDiff === 0 ) {
1971 return $ts;
1972 }
1973
1974 $date = new DateTime( $ts );
1975 $date->modify( "{$minDiff} minutes" );
1976 return self::makeMediaWikiTimestamp( $ts, $date );
1977 }
1978
1994 private function convertDateFormatToJs( $format ) {
1995 // CLDR calendar IDs by format code
1996 $calendars = [
1997 'j' => 'hebrew',
1998 'i' => 'persian', // Iranian
1999 'm' => 'islamic', // Hijri
2000 'k' => 'buddhist', // Thai
2001 'o' => 'roc', // Minguo
2002 't' => 'japanese' // Tenno
2003 ];
2004
2005 $s = '';
2006 $mwOpts = [];
2007 $intlOpts = [
2008 'numberingSystem' => $this->localisationCache
2009 ->getItem( $this->mCode, 'numberingSystem' ) ?? 'latn'
2010 ];
2011 $unsupported = [];
2012 $formatLength = strlen( $format );
2013
2014 for ( $p = 0; $p < $formatLength; $p++ ) {
2015 $code = $format[$p];
2016 if ( $code == 'x' && $p < $formatLength - 1 ) {
2017 $code .= $format[++$p];
2018 }
2019
2020 if ( ( $code === 'xi'
2021 || $code === 'xj'
2022 || $code === 'xk'
2023 || $code === 'xm'
2024 || $code === 'xo'
2025 || $code === 'xt' )
2026 && $p < $formatLength - 1
2027 ) {
2028 $code .= $format[++$p];
2029 }
2030
2031 switch ( $code ) {
2032 case 'xx':
2033 $s .= 'x';
2034 break;
2035
2036 case 'xn':
2037 case 'xN':
2038 // Raw number -- usually safe enough in ISO 8601 styles
2039 // although we don't support switching
2040 $mwOpts['locale'] = 'en';
2041 unset( $intlOpts['numberingSystem'] );
2042 break;
2043
2044 case 'xr':
2045 // Roman numerals
2046 // Multiple numbering systems are usually used in the one format,
2047 // which is unsupported. Also, browsers do not implement it.
2048 $unsupported[] = $code;
2049 break;
2050
2051 case 'xh':
2052 // Hebrew numerals. Browsers do not implement it as of 2025,
2053 // and usually multiple numbering systems are desired in a
2054 // single format.
2055 $unsupported[] = $code;
2056 $intlOpts['numberingSystem'] = 'hebr';
2057 break;
2058
2059 case 'xg':
2060 // Genitive month name
2061 $intlOpts['month'] = 'long';
2062 $s .= '{mwMonthGen}';
2063 break;
2064
2065 case 'xjx':
2066 // Genitive month name in Hebrew calendar
2067 $intlOpts['calendar'] = 'hebrew';
2068 $intlOpts['month'] = 'long';
2069 $s .= '{mwMonthGen}';
2070 break;
2071
2072 case 'd':
2073 $intlOpts['day'] = '2-digit';
2074 $s .= '{day}';
2075 break;
2076
2077 case 'D':
2078 $intlOpts['weekday'] = 'short';
2079 $s .= '{weekday}';
2080 break;
2081
2082 case 'j':
2083 $intlOpts['day'] = 'numeric';
2084 $s .= '{day}';
2085 break;
2086
2087 case 'xij':
2088 case 'xmj':
2089 case 'xjj':
2090 // Day number in special calendar
2091 $intlOpts['day'] = 'numeric';
2092 $intlOpts['calendar'] = $calendars[$code[1]];
2093 $s .= '{day}';
2094 break;
2095
2096 case 'l':
2097 $intlOpts['weekday'] = 'long';
2098 $s .= '{weekday}';
2099 break;
2100
2101 case 'F':
2102 $intlOpts['month'] = 'long';
2103 $s .= '{mwMonth}';
2104 break;
2105
2106 case 'xiF':
2107 case 'xmF':
2108 case 'xjF':
2109 // Full month name in special calendar
2110 $intlOpts['month'] = 'long';
2111 $intlOpts['calendar'] = $calendars[$code[1]];
2112 $s .= '{month}';
2113 break;
2114
2115 case 'm':
2116 $intlOpts['month'] = '2-digit';
2117 $s .= '{month}';
2118 break;
2119
2120 case 'M':
2121 $intlOpts['month'] = 'short';
2122 $s .= '{mwMonthAbbrev}';
2123 break;
2124
2125 case 'n':
2126 $intlOpts['month'] = 'numeric';
2127 $s .= '{month}';
2128 break;
2129
2130 case 'xin':
2131 case 'xmn':
2132 case 'xjn':
2133 // Numeric month in special calendar
2134 $intlOpts['month'] = 'numeric';
2135 $intlOpts['calendar'] = $calendars[$code[1]];
2136 $s .= '{month}';
2137 break;
2138
2139 case 'xjt':
2140 // Number of days in the given Hebrew month -- not supported
2141 $unsupported[] = $code;
2142 break;
2143
2144 case 'Y':
2145 $intlOpts['year'] = 'numeric';
2146 $s .= '{year}';
2147 break;
2148
2149 case 'xiY':
2150 case 'xmY':
2151 case 'xjY':
2152 case 'xkY':
2153 case 'xoY':
2154 // Year number in special calendar
2155 $intlOpts['year'] = 'numeric';
2156 $intlOpts['calendar'] = $calendars[$code[1]];
2157 $s .= '{year}';
2158 break;
2159
2160 case 'xtY':
2161 // Japanese year needs to be prefixed with the era name to
2162 // be consistent with tsToJapaneseGengo()
2163 $intlOpts['era'] = 'short';
2164 $intlOpts['year'] = 'numeric';
2165 $intlOpts['calendar'] = $calendars[$code[1]];
2166 $s .= '{era}{year}';
2167 break;
2168
2169 case 'y':
2170 $intlOpts['year'] = '2-digit';
2171 $s .= '{year}';
2172 break;
2173
2174 case 'xiy':
2175 // Iranian 2-digit year
2176 $intlOpts['calendar'] = 'persian';
2177 $intlOpts['year'] = '2-digit';
2178 $s .= '{year}';
2179 break;
2180
2181 case 'xit':
2182 // Number of days in Iranian month -- not supported
2183 $unsupported[] = $code;
2184 break;
2185
2186 case 'xiz':
2187 // Day of the year -- not supported
2188 $unsupported[] = $code;
2189 break;
2190
2191 case 'a':
2192 case 'A':
2193 // AM/PM
2194 $intlOpts['hour12'] = true;
2195 $s .= '{dayPeriod}';
2196 break;
2197
2198 case 'g':
2199 // Hour in 12-hour clock, without leading zero
2200 $intlOpts['hour'] = 'numeric';
2201 $intlOpts['hour12'] = true;
2202 $s .= '{hour}';
2203 break;
2204
2205 case 'G':
2206 // Hour in 24-hour clock, without leading zero
2207 $intlOpts['hour'] = 'numeric';
2208 $s .= '{hour}';
2209 break;
2210
2211 case 'h':
2212 // Hour in 12-hour clock, with leading zero
2213 $intlOpts['hour'] = '2-digit';
2214 $intlOpts['hour12'] = true;
2215 $s .= '{hour}';
2216 break;
2217
2218 case 'H':
2219 // Hour in 24-hour clock, without leading zero
2220 $intlOpts['hour'] = '2-digit';
2221 $intlOpts['hour12'] = false;
2222 $s .= '{hour}';
2223 break;
2224
2225 case 'i':
2226 $intlOpts['minute'] = '2-digit';
2227 $s .= '{minute}';
2228 break;
2229
2230 case 's':
2231 $intlOpts['second'] = '2-digit';
2232 $s .= '{second}';
2233 break;
2234
2235 case 'c':
2236 // ISO 8601
2237 $mwOpts['locale'] = 'en';
2238 $intlOpts += [
2239 'year' => 'numeric', 'month' => '2-digit', 'day' => '2-digit',
2240 'hour' => '2-digit', 'minute' => '2-digit', 'second' => '2-digit',
2241 'hour12' => false, 'timeZoneName' => 'longOffset'
2242 ];
2243 $s .= '{year}-{month}-{day}T{hour}:{minute}:{second}{timeZoneName}';
2244 break;
2245
2246 case 'r':
2247 // RFC 2822
2248 $mwOpts['locale'] = 'en';
2249 $intlOpts += [
2250 'year' => 'numeric', 'month' => 'short', 'day' => 'numeric',
2251 'hour' => '2-digit', 'minute' => '2-digit', 'second' => '2-digit',
2252 'hour12' => false, 'weekday' => 'short', 'timeZoneName' => 'longOffset',
2253 ];
2254 $s .= '{weekday}, {day} {month} {year} {hour}:{minute}:{second} {timeZoneName}';
2255 break;
2256
2257 case 'e':
2258 // Time zone identifier -- not supported, fall through to offset
2259 case 'O':
2260 // Offset without colon not supported
2261 $unsupported[] = $code;
2262 // Fall through to with colon
2263 case 'P':
2264 $intlOpts['timeZoneName'] = 'longOffset';
2265 $s .= '{timeZoneName}';
2266 break;
2267
2268 case 'T':
2269 // Time zone abbreviation
2270 $intlOpts['timeZoneName'] = 'short';
2271 $s .= '{timeZoneName}';
2272 break;
2273
2274 case 'w':
2275 case 'N':
2276 // Numeric day of week -- not supported
2277 $unsupported[] = $code;
2278 $intlOpts['weekday'] = 'short';
2279 $s .= '{weekday}';
2280 break;
2281
2282 case 'z':
2283 // Day of year
2284 case 'W':
2285 // Week number
2286 case 't':
2287 // Number of days in month
2288 case 'L':
2289 // Leap year flag
2290 case 'o':
2291 // ISO week numbering year
2292 case 'U':
2293 // Seconds since UNIX epoch
2294 case 'I':
2295 // DST flag
2296 case 'Z':
2297 // Timezone offset in seconds
2298 $unsupported[] = $code;
2299 break;
2300
2301 case '\\':
2302 # Backslash escaping
2303 if ( $p < $formatLength - 1 ) {
2304 $s .= $format[++$p];
2305 } else {
2306 $s .= '\\';
2307 }
2308 break;
2309
2310 case '"':
2311 # Quoted literal
2312 if ( $p < $formatLength - 1 ) {
2313 $endQuote = strpos( $format, '"', $p + 1 );
2314 if ( $endQuote === false ) {
2315 # No terminating quote, assume literal "
2316 $s .= '"';
2317 } else {
2318 $s .= substr( $format, $p + 1, $endQuote - $p - 1 );
2319 $p = $endQuote;
2320 }
2321 } else {
2322 # Quote at the end of the string, assume literal "
2323 $s .= '"';
2324 }
2325 break;
2326
2327 default:
2328 $s .= $format[$p];
2329 }
2330 }
2331 $mwOpts['options'] = $intlOpts;
2332 $mwOpts['pattern'] = $s;
2333 if ( $unsupported ) {
2334 $mwOpts['error'] = 'Unsupported format code(s): ' .
2335 implode( ', ', $unsupported );
2336 }
2337 return $mwOpts;
2338 }
2339
2349 private static function makeMediaWikiTimestamp( $fallback, $date ) {
2350 $ts = $date->format( 'YmdHis' );
2351 return strlen( $ts ) === 14 ? $ts : $fallback;
2352 }
2353
2369 public function dateFormat( $usePrefs = true ) {
2370 if ( is_bool( $usePrefs ) ) {
2371 if ( $usePrefs ) {
2372 $datePreference = RequestContext::getMain()
2373 ->getUser()
2374 ->getDatePreference();
2375 } else {
2376 $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
2377 $datePreference = (string)$userOptionsLookup->getDefaultOption( 'date' );
2378 }
2379 } else {
2380 $datePreference = (string)$usePrefs;
2381 }
2382
2383 // return int
2384 if ( $datePreference == '' ) {
2385 return 'default';
2386 }
2387
2388 return $datePreference;
2389 }
2390
2401 public function getDateFormatString( $type, $pref ) {
2402 $wasDefault = false;
2403 if ( $pref == 'default' ) {
2404 $wasDefault = true;
2405 $pref = $this->getDefaultDateFormat();
2406 }
2407
2408 if ( !isset( $this->dateFormatStrings[$type][$pref] ) ) {
2409 $df = $this->localisationCache
2410 ->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2411
2412 if ( $type === 'pretty' && $df === null ) {
2413 $df = $this->getDateFormatString( 'date', $pref );
2414 }
2415
2416 if ( !$wasDefault && $df === null ) {
2417 $pref = $this->getDefaultDateFormat();
2418 $df = $this->localisationCache
2419 ->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2420 }
2421
2422 $this->dateFormatStrings[$type][$pref] = $df;
2423 }
2424 return $this->dateFormatStrings[$type][$pref];
2425 }
2426
2437 public function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
2438 $ts = wfTimestamp( TS::MW, $ts );
2439 if ( $adj ) {
2440 $ts = $this->userAdjust( $ts, $timecorrection );
2441 }
2442 $df = $this->getDateFormatString( 'date', $this->dateFormat( $format ) );
2443 return $this->sprintfDate( $df, $ts );
2444 }
2445
2456 public function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
2457 $ts = wfTimestamp( TS::MW, $ts );
2458 if ( $adj ) {
2459 $ts = $this->userAdjust( $ts, $timecorrection );
2460 }
2461 $df = $this->getDateFormatString( 'time', $this->dateFormat( $format ) );
2462 return $this->sprintfDate( $df, $ts );
2463 }
2464
2476 public function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false ) {
2477 $ts = wfTimestamp( TS::MW, $ts );
2478 if ( $adj ) {
2479 $ts = $this->userAdjust( $ts, $timecorrection );
2480 }
2481 $df = $this->getDateFormatString( 'both', $this->dateFormat( $format ) );
2482 return $this->sprintfDate( $df, $ts );
2483 }
2484
2495 public function formatDuration( $seconds, array $chosenIntervals = [] ) {
2496 $intervals = $this->getDurationIntervals( $seconds, $chosenIntervals );
2497
2498 $segments = [];
2499
2500 foreach ( $intervals as $intervalName => $intervalValue ) {
2501 // Messages: duration-seconds, duration-minutes, duration-hours, duration-days, duration-weeks,
2502 // duration-years, duration-decades, duration-centuries, duration-millennia
2503 $message = $this->msg( 'duration-' . $intervalName )->numParams( $intervalValue );
2504 $segments[] = $message->escaped();
2505 }
2506
2507 return $this->listToText( $segments );
2508 }
2509
2520 int $timestamp1,
2521 int $timestamp2,
2522 ?int $precision = null
2523 ): string {
2524 $precision ??= count( self::DURATION_INTERVALS );
2525
2526 $sortedTimestamps = [ $timestamp1, $timestamp2 ];
2527 sort( $sortedTimestamps );
2528
2529 $tz = new DateTimeZone( 'UTC' );
2530 $date1 = ( new DateTimeImmutable() )
2531 ->setTimezone( $tz )
2532 ->setTimestamp( $sortedTimestamps[0] );
2533 $date2 = ( new DateTimeImmutable() )
2534 ->setTimezone( $tz )
2535 ->setTimestamp( $sortedTimestamps[1] );
2536
2537 $interval = $date1->diff( $date2 );
2538
2539 $format = [];
2540 if ( $interval->y >= 1000 ) {
2541 $millennia = floor( $interval->y / 1000 );
2542 $format[] = $this->msg( 'duration-millennia' )->numParams( $millennia )->text();
2543 $interval->y -= $millennia * 1000;
2544 }
2545 if ( $interval->y >= 100 ) {
2546 $centuries = floor( $interval->y / 100 );
2547 $format[] = $this->msg( 'duration-centuries' )->numParams( $centuries )->text();
2548 $interval->y -= $centuries * 100;
2549 }
2550 if ( $interval->y >= 10 ) {
2551 $decades = floor( $interval->y / 10 );
2552 $format[] = $this->msg( 'duration-decades' )->numParams( $decades )->text();
2553 $interval->y -= $decades * 10;
2554 }
2555 if ( $interval->y !== 0 ) {
2556 $format[] = $this->msg( 'duration-years' )->numParams( $interval->y )->text();
2557 }
2558 if ( $interval->m !== 0 ) {
2559 $format[] = $this->msg( 'duration-months' )->numParams( $interval->m )->text();
2560 }
2561 if ( $interval->d !== 0 ) {
2562 $format[] = $this->msg( 'duration-days' )->numParams( $interval->d )->text();
2563 }
2564 if ( $interval->h !== 0 ) {
2565 $format[] = $this->msg( 'duration-hours' )->numParams( $interval->h )->text();
2566 }
2567 if ( $interval->i !== 0 ) {
2568 $format[] = $this->msg( 'duration-minutes' )->numParams( $interval->i )->text();
2569 }
2570 if ( $interval->s !== 0 ) {
2571 $format[] = $this->msg( 'duration-seconds' )->numParams( $interval->s )->text();
2572 }
2573
2574 // slice the array to the provided precision
2575 $format = array_slice( $format, 0, $precision );
2576 // build the string from the array
2577 $format = $this->listToText( $format );
2578
2579 return $format ?: $this->msg( 'duration-seconds' )->numParams( 0 )->text();
2580 }
2581
2593 public function getDurationIntervals( $seconds, array $chosenIntervals = [] ) {
2594 if ( !$chosenIntervals ) {
2595 // Default intervals. Do not include `months` as they were not part of the original default implementation
2596 $chosenIntervals = [
2597 'millennia',
2598 'centuries',
2599 'decades',
2600 'years',
2601 'days',
2602 'hours',
2603 'minutes',
2604 'seconds'
2605 ];
2606 }
2607
2608 $intervals = array_intersect_key( self::DURATION_INTERVALS,
2609 array_fill_keys( $chosenIntervals, true ) );
2610 $smallestInterval = array_key_last( $intervals );
2611
2612 $segments = [];
2613
2614 foreach ( $intervals as $name => $length ) {
2615 $value = floor( $seconds / $length );
2616
2617 if ( $value > 0 || ( $name == $smallestInterval && !$segments ) ) {
2618 $seconds -= $value * $length;
2619 $segments[$name] = $value;
2620 }
2621 }
2622
2623 return $segments;
2624 }
2625
2645 private function internalUserTimeAndDate( $type, $ts, UserIdentity $user, array $options ) {
2646 $ts = wfTimestamp( TS::MW, $ts );
2647 $options += [ 'timecorrection' => true, 'format' => true ];
2648 if ( $options['timecorrection'] !== false ) {
2649 if ( $options['timecorrection'] === true ) {
2650 $offset = MediaWikiServices::getInstance()
2651 ->getUserOptionsLookup()
2652 ->getOption( $user, 'timecorrection' );
2653 } else {
2654 $offset = $options['timecorrection'];
2655 }
2656 $ts = $this->userAdjust( $ts, $offset );
2657 }
2658 if ( $options['format'] === true ) {
2659 $format = MediaWikiServices::getInstance()
2660 ->getUserFactory()
2661 ->newFromUserIdentity( $user )
2662 ->getDatePreference();
2663 } else {
2664 $format = $options['format'];
2665 }
2666 $df = $this->getDateFormatString( $type, $this->dateFormat( $format ) );
2667 return $this->sprintfDate( $df, $ts );
2668 }
2669
2689 public function userDate( $ts, UserIdentity $user, array $options = [] ) {
2690 return $this->internalUserTimeAndDate( 'date', $ts, $user, $options );
2691 }
2692
2712 public function userTime( $ts, UserIdentity $user, array $options = [] ) {
2713 return $this->internalUserTimeAndDate( 'time', $ts, $user, $options );
2714 }
2715
2735 public function userTimeAndDate( $ts, UserIdentity $user, array $options = [] ) {
2736 return $this->internalUserTimeAndDate( 'both', $ts, $user, $options );
2737 }
2738
2754 public function getHumanTimestamp(
2755 MWTimestamp $time, ?MWTimestamp $relativeTo = null, ?UserIdentity $user = null
2756 ) {
2757 $relativeTo ??= new MWTimestamp();
2758 if ( $user === null ) {
2759 $user = RequestContext::getMain()->getUser();
2760 } else {
2761 // For compatibility with the hook signature and self::getHumanTimestampInternal
2762 $user = MediaWikiServices::getInstance()
2763 ->getUserFactory()
2764 ->newFromUserIdentity( $user );
2765 }
2766
2767 // Adjust for the user's timezone.
2768 $offsetThis = $time->offsetForUser( $user );
2769 $offsetRel = $relativeTo->offsetForUser( $user );
2770
2771 $ts = '';
2772 if ( $this->getHookRunner()->onGetHumanTimestamp( $ts, $time, $relativeTo, $user, $this ) ) {
2773 $ts = $this->getHumanTimestampInternal( $time, $relativeTo, $user );
2774 }
2775
2776 // Reset the timezone on the objects.
2777 $time->timestamp->sub( $offsetThis );
2778 $relativeTo->timestamp->sub( $offsetRel );
2779
2780 return $ts;
2781 }
2782
2794 private function getHumanTimestampInternal(
2795 MWTimestamp $ts, MWTimestamp $relativeTo, User $user
2796 ) {
2797 $diff = $ts->diff( $relativeTo );
2798 $diffDay = (bool)( (int)$ts->timestamp->format( 'w' ) -
2799 (int)$relativeTo->timestamp->format( 'w' ) );
2800 $days = $diff->days ?: (int)$diffDay;
2801
2802 if ( $diff->invert ) {
2803 // Future dates: Use full timestamp
2807 $format = $this->getDateFormatString( 'both', $user->getDatePreference() ?: 'default' );
2808 $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS::MW ) );
2809 } elseif (
2810 $days > 5 &&
2811 $ts->timestamp->format( 'Y' ) !== $relativeTo->timestamp->format( 'Y' )
2812 ) {
2813 // Timestamps are in different years and more than 5 days apart: use full date
2814 $format = $this->getDateFormatString( 'date', $user->getDatePreference() ?: 'default' );
2815 $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS::MW ) );
2816 } elseif ( $days > 5 ) {
2817 // Timestamps are in same year and more than 5 days ago: show day and month only.
2818 $format = $this->getDateFormatString( 'pretty', $user->getDatePreference() ?: 'default' );
2819 $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS::MW ) );
2820 } elseif ( $days > 1 ) {
2821 // Timestamp within the past 5 days: show the day of the week and time
2822 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2823 $weekday = self::WEEKDAY_MESSAGES[(int)$ts->timestamp->format( 'w' )];
2824 // The following messages are used here:
2825 // * sunday-at, monday-at, tuesday-at, wednesday-at, thursday-at, friday-at, saturday-at
2826 $ts = $this->msg( "$weekday-at" )
2827 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS::MW ) ) )
2828 ->text();
2829 } elseif ( $days == 1 ) {
2830 // Timestamp was yesterday: say 'yesterday' and the time.
2831 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2832 $ts = $this->msg( 'yesterday-at' )
2833 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS::MW ) ) )
2834 ->text();
2835 } elseif ( $diff->h > 1 || ( $diff->h == 1 && $diff->i > 30 ) ) {
2836 // Timestamp was today, but more than 90 minutes ago: say 'today' and the time.
2837 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2838 $ts = $this->msg( 'today-at' )
2839 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS::MW ) ) )
2840 ->text();
2841
2842 // From here on in, the timestamp was soon enough ago so that we can simply say
2843 // XX units ago, e.g., "2 hours ago" or "5 minutes ago"
2844 } elseif ( $diff->h == 1 ) {
2845 // Less than 90 minutes, but more than an hour ago.
2846 $ts = $this->msg( 'hours-ago' )->numParams( 1 )->text();
2847 } elseif ( $diff->i >= 1 ) {
2848 // A few minutes ago.
2849 $ts = $this->msg( 'minutes-ago' )->numParams( $diff->i )->text();
2850 } elseif ( $diff->s >= 30 ) {
2851 // Less than a minute, but more than 30 sec ago.
2852 $ts = $this->msg( 'seconds-ago' )->numParams( $diff->s )->text();
2853 } else {
2854 // Less than 30 seconds ago.
2855 $ts = $this->msg( 'just-now' )->text();
2856 }
2857
2858 return $ts;
2859 }
2860
2869 public function getGroupName( $group ) {
2870 $msg = $this->msg( "group-$group" );
2871 return $msg->isBlank() ? $group : $msg->text();
2872 }
2873
2883 public function getGroupMemberName( string $group, $member ) {
2884 if ( $member instanceof UserIdentity ) {
2885 $member = $member->getName();
2886 }
2887 $msg = $this->msg( "group-$group-member", $member );
2888 return $msg->isBlank() ? $group : $msg->text();
2889 }
2890
2896 public function getMessage( $key ) {
2897 return $this->localisationCache->getSubitem( $this->mCode, 'messages', $key );
2898 }
2899
2904 public function getAllMessages() {
2905 return $this->localisationCache->getItem( $this->mCode, 'messages' );
2906 }
2907
2914 public function iconv( $in, $out, $string ) {
2915 # Even with //IGNORE iconv can whine about illegal characters in
2916 # *input* string. We just ignore those too.
2917 # REF: https://bugs.php.net/bug.php?id=37166
2918 # REF: https://phabricator.wikimedia.org/T18885
2919 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
2920 return @iconv( $in, $out . '//IGNORE', $string );
2921 }
2922
2927 public function ucfirst( $str ) {
2928 // T410920: ord() doesn't like an empty string, so just return early
2929 if ( $str === '' ) {
2930 return '';
2931 }
2932
2933 $octetCode = ord( $str[0] );
2934 // See https://en.wikipedia.org/wiki/ASCII#Printable_characters
2935 if ( $octetCode < 96 ) {
2936 // Assume this is an uppercase/uncased ASCII character
2937 return (string)$str;
2938 } elseif ( $octetCode < 128 ) {
2939 // Assume this is a lowercase/uncased ASCII character
2940 return ucfirst( $str );
2941 }
2942 $first = mb_substr( $str, 0, 1 );
2943 if ( strlen( $first ) === 1 ) {
2944 // Broken UTF-8?
2945 return ucfirst( $str );
2946 }
2947
2948 // Memoize the config table
2949 $overrides = $this->overrideUcfirstCharacters
2950 ??= $this->config->get( MainConfigNames::OverrideUcfirstCharacters );
2951
2952 // Use the config table and fall back to MB_CASE_TITLE
2953 $ucFirst = $overrides[$first] ?? mb_convert_case( $first, MB_CASE_TITLE );
2954 if ( $ucFirst !== $first ) {
2955 return $ucFirst . mb_substr( $str, 1 );
2956 } else {
2957 return $str;
2958 }
2959 }
2960
2966 public function uc( $str, $first = false ) {
2967 if ( $first ) {
2968 return $this->ucfirst( $str );
2969 } else {
2970 return $this->isMultibyte( $str ) ? mb_strtoupper( $str ) : strtoupper( $str );
2971 }
2972 }
2973
2978 public function lcfirst( $str ) {
2979 // T410920: ord() doesn't like an empty string, so just return early
2980 if ( $str === '' ) {
2981 return '';
2982 }
2983
2984 $octetCode = ord( $str[0] );
2985 // See https://en.wikipedia.org/wiki/ASCII#Printable_characters
2986 if ( $octetCode < 96 ) {
2987 // Assume this is an uppercase/uncased ASCII character
2988 return lcfirst( $str );
2989 } elseif ( $octetCode < 128 ) {
2990 // Assume this is a lowercase/uncased ASCII character
2991 return (string)$str;
2992 }
2993
2994 return $this->isMultibyte( $str )
2995 // Assume this is a multibyte character and mb_internal_encoding() is appropriate
2996 ? mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 )
2997 // Assume this is a non-multibyte character and LC_CASE is appropriate
2998 : lcfirst( $str );
2999 }
3000
3006 public function lc( $str, $first = false ) {
3007 if ( $first ) {
3008 return $this->lcfirst( $str );
3009 } else {
3010 return $this->isMultibyte( $str ) ? mb_strtolower( $str ) : strtolower( $str );
3011 }
3012 }
3013
3018 private function isMultibyte( $str ) {
3019 return strlen( $str ) !== mb_strlen( $str );
3020 }
3021
3026 public function ucwords( $str ) {
3027 if ( $this->isMultibyte( $str ) ) {
3028 $str = $this->lc( $str );
3029
3030 // regexp to find the first letter in each word (i.e., after each space)
3031 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
3032
3033 // function to use to capitalize a single char
3034 return preg_replace_callback(
3035 $replaceRegexp,
3036 static function ( $matches ) {
3037 return mb_strtoupper( $matches[0] );
3038 },
3039 $str
3040 );
3041 } else {
3042 return ucwords( strtolower( $str ) );
3043 }
3044 }
3045
3052 public function ucwordbreaks( $str ) {
3053 if ( $this->isMultibyte( $str ) ) {
3054 $str = $this->lc( $str );
3055
3056 // since \b doesn't work for UTF-8, we explicitly define word break chars
3057 $breaks = "[ \-\‍(\‍)\}\{\.,\?!]";
3058
3059 // find the first letter after word break
3060 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|" .
3061 "$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
3062
3063 return preg_replace_callback(
3064 $replaceRegexp,
3065 static function ( $matches ) {
3066 return mb_strtoupper( $matches[0] );
3067 },
3068 $str
3069 );
3070 } else {
3071 return preg_replace_callback(
3072 '/\b([\w\x80-\xff]+)\b/',
3073 function ( $matches ) {
3074 return $this->ucfirst( $matches[1] );
3075 },
3076 $str
3077 );
3078 }
3079 }
3080
3096 public function caseFold( $s ) {
3097 return $this->uc( $s );
3098 }
3099
3104 public function checkTitleEncoding( string $s ) {
3105 if ( StringUtils::isUtf8( $s ) ) {
3106 return $s;
3107 }
3108
3109 return $this->iconv( $this->fallback8bitEncoding(), 'utf-8', $s );
3110 }
3111
3115 public function fallback8bitEncoding() {
3116 return $this->localisationCache->getItem( $this->mCode, 'fallback8bitEncoding' );
3117 }
3118
3127 public function hasWordBreaks() {
3128 return true;
3129 }
3130
3138 public function segmentByWord( $string ) {
3139 return $string;
3140 }
3141
3147 protected function getSearchIndexVariant() {
3148 return null;
3149 }
3150
3162 public function normalizeForSearch( $text ) {
3163 $text = self::convertDoubleWidth( $text );
3164 if ( $this->getSearchIndexVariant() ) {
3165 return $this->getConverterInternal()->autoConvert( $text, $this->getSearchIndexVariant() );
3166 }
3167 return $text;
3168 }
3169
3177 protected static function convertDoubleWidth( $string ) {
3178 static $transTable = null;
3179 $transTable ??= array_combine(
3180 mb_str_split( '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' ),
3181 str_split( '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' )
3182 );
3183
3184 return strtr( $string, $transTable );
3185 }
3186
3192 protected static function insertSpace( $string, $pattern ) {
3193 $string = preg_replace( $pattern, " $1 ", $string );
3194 return preg_replace( '/ +/', ' ', $string );
3195 }
3196
3201 public function convertForSearchResult( $termsArray ) {
3202 # some languages, e.g., Chinese, need to do a conversion
3203 # in order for search results to be displayed correctly
3204 return $termsArray;
3205 }
3206
3213 public function firstChar( $s ) {
3214 $firstChar = mb_substr( $s, 0, 1 );
3215
3216 if ( $firstChar === '' || strlen( $firstChar ) != 3 ) {
3217 return $firstChar;
3218 }
3219
3220 // Break down Hangul syllables to grab the first jamo
3221 $code = mb_ord( $firstChar );
3222 if ( $code < 0xac00 || $code >= 0xd7a4 ) {
3223 return $firstChar;
3224 } elseif ( $code < 0xb098 ) {
3225 return "\u{3131}";
3226 } elseif ( $code < 0xb2e4 ) {
3227 return "\u{3134}";
3228 } elseif ( $code < 0xb77c ) {
3229 return "\u{3137}";
3230 } elseif ( $code < 0xb9c8 ) {
3231 return "\u{3139}";
3232 } elseif ( $code < 0xbc14 ) {
3233 return "\u{3141}";
3234 } elseif ( $code < 0xc0ac ) {
3235 return "\u{3142}";
3236 } elseif ( $code < 0xc544 ) {
3237 return "\u{3145}";
3238 } elseif ( $code < 0xc790 ) {
3239 return "\u{3147}";
3240 } elseif ( $code < 0xcc28 ) {
3241 return "\u{3148}";
3242 } elseif ( $code < 0xce74 ) {
3243 return "\u{314A}";
3244 } elseif ( $code < 0xd0c0 ) {
3245 return "\u{314B}";
3246 } elseif ( $code < 0xd30c ) {
3247 return "\u{314C}";
3248 } elseif ( $code < 0xd558 ) {
3249 return "\u{314D}";
3250 } else {
3251 return "\u{314E}";
3252 }
3253 }
3254
3264 public function normalize( $s ) {
3265 $allUnicodeFixes = $this->config->get( MainConfigNames::AllUnicodeFixes );
3266
3267 $s = UtfNormalValidator::cleanUp( $s );
3268 // Optimization: This is disabled by default to avoid negative performance impact.
3269 if ( $allUnicodeFixes ) {
3270 $s = $this->transformUsingPairFile( NormalizeAr::class, $s );
3271 $s = $this->transformUsingPairFile( NormalizeMl::class, $s );
3272 }
3273
3274 return $s;
3275 }
3276
3288 protected function transformUsingPairFile( string $dataClass, string $input ): string {
3289 if ( !isset( $this->transformData[$dataClass] ) ) {
3290 $this->transformData[$dataClass] = new ReplacementArray( $dataClass::PAIRS );
3291 }
3292
3293 return $this->transformData[$dataClass]->replace( $input );
3294 }
3295
3301 public function isRTL() {
3302 return $this->localisationCache->getItem( $this->mCode, 'rtl' );
3303 }
3304
3309 public function getDir() {
3310 return $this->isRTL() ? 'rtl' : 'ltr';
3311 }
3312
3321 public function alignStart() {
3322 return $this->isRTL() ? 'right' : 'left';
3323 }
3324
3333 public function alignEnd() {
3334 return $this->isRTL() ? 'left' : 'right';
3335 }
3336
3355 public function getDirMarkEntity( $opposite = false ) {
3356 wfDeprecated( __METHOD__, '1.43' );
3357
3358 if ( $opposite ) {
3359 return $this->isRTL() ? '&lrm;' : '&rlm;';
3360 }
3361 return $this->isRTL() ? '&rlm;' : '&lrm;';
3362 }
3363
3379 public function getDirMark( $opposite = false ) {
3380 if ( $opposite ) {
3381 return $this->isRTL() ? self::LRM : self::RLM;
3382 }
3383 return $this->isRTL() ? self::RLM : self::LRM;
3384 }
3385
3393 public function getArrow( $direction = 'forwards' ) {
3394 switch ( $direction ) {
3395 case 'forwards':
3396 return $this->isRTL() ? '←' : '→';
3397 case 'backwards':
3398 return $this->isRTL() ? '→' : '←';
3399 case 'left':
3400 return '←';
3401 case 'right':
3402 return '→';
3403 case 'up':
3404 return '↑';
3405 case 'down':
3406 return '↓';
3407 }
3408 }
3409
3415 public function linkPrefixExtension() {
3416 return $this->localisationCache->getItem( $this->mCode, 'linkPrefixExtension' );
3417 }
3418
3424 public function getMagicWords() {
3425 return $this->localisationCache->getItem( $this->mCode, 'magicWords' );
3426 }
3427
3433 public function getMagic( $mw ) {
3434 $rawEntry = $this->mMagicExtensions[$mw->mId] ??
3435 $this->localisationCache->getSubitem( $this->mCode, 'magicWords', $mw->mId );
3436
3437 if ( !is_array( $rawEntry ) ) {
3438 wfWarn( "\"$rawEntry\" is not a valid magic word for \"$mw->mId\"" );
3439 } else {
3440 $mw->mCaseSensitive = $rawEntry[0];
3441 $mw->mSynonyms = array_slice( $rawEntry, 1 );
3442 }
3443 }
3444
3450 public function getSpecialPageAliases() {
3451 // Cache aliases because it may be slow to load them
3452 $this->mExtendedSpecialPageAliases ??=
3453 $this->localisationCache->getItem( $this->mCode, 'specialPageAliases' );
3454
3455 return $this->mExtendedSpecialPageAliases;
3456 }
3457
3465 public function emphasize( $text ) {
3466 wfDeprecated( __METHOD__, '1.46' );
3467 return "<em>$text</em>";
3468 }
3469
3490 public function formatNum( $number ) {
3491 return $this->formatNumInternal( $number, false );
3492 }
3493
3502 private function formatNumInternal( $number, bool $noSeparators ): string {
3503 // From PHP 8.5, we can't cast NAN to string, and since the method
3504 // accepts float, there's no way to exclude NAN, which is subtype.
3505 // So handle it explicitly.
3506 $number = is_float( $number ) && is_nan( $number ) ? 'NAN' : (string)$number;
3507
3508 if ( $number === '' ) {
3509 return $number;
3510 }
3511 if ( $number === 'NAN' ) {
3512 return $this->msg( 'formatnum-nan' )->text();
3513 }
3514 if ( $number === 'INF' ) {
3515 return "∞";
3516 }
3517 if ( $number === '-INF' ) {
3518 return "\u{2212}∞";
3519 }
3520 if ( !is_numeric( $number ) ) {
3521 # T267587: downgrade this to level:warn while we chase down the long
3522 # trail of callers.
3523 # wfDeprecated( 'Language::formatNum with a non-numeric string', '1.36' );
3524 LoggerFactory::getInstance( 'formatnum' )->warning(
3525 'Language::formatNum with non-numeric string',
3526 [ 'number' => $number ]
3527 );
3528 $validNumberRe = '(-(?=[\d\.]))?(\d+|(?=\.\d))(\.\d*)?([Ee][-+]?\d+)?';
3529 // For backwards-compat, apply formatNum piecewise on the valid
3530 // numbers in the string. Don't split on NAN/INF in this legacy
3531 // case as they are likely to be found embedded inside non-numeric
3532 // text.
3533 return preg_replace_callback( "/{$validNumberRe}/", function ( $m ) use ( $noSeparators ) {
3534 return $this->formatNumInternal( $m[0], $noSeparators );
3535 }, $number );
3536 }
3537
3538 if ( !$noSeparators ) {
3539 $separatorTransformTable = $this->separatorTransformTable();
3540 $fmt = $this->getNumberFormatter();
3541
3542 // minimumGroupingDigits can be used to suppress groupings below a certain value.
3543 // This is used for languages such as Polish, where one would only write the grouping
3544 // separator for values above 9999 - numbers with more than 4 digits.
3545 // NumberFormatter is yet to support minimumGroupingDigits, ICU has it as experimental feature.
3546 // The attribute value is used by adding it to the grouping separator value. If
3547 // the input number has fewer integer digits, the grouping separator is suppressed.
3548 $minimumGroupingDigits = $this->minimumGroupingDigits();
3549 // Minimum length of a number to do digit grouping on.
3550 // http://unicode.org/reports/tr35/tr35-numbers.html#Examples_of_minimumGroupingDigits
3551 $minimumLength = $minimumGroupingDigits + $fmt->getAttribute( NumberFormatter::GROUPING_SIZE );
3552 if ( $minimumGroupingDigits > 1
3553 && !preg_match( '/^\-?\d{' . $minimumLength . '}/', $number )
3554 ) {
3555 // This number does not need commas inserted (even if
3556 // NumberFormatter thinks it does) because it's not long
3557 // enough. We still need to do decimal separator
3558 // transformation, though. For example, 1234.56 becomes 1234,56
3559 // in pl with $minimumGroupingDigits = 2.
3560 $number = strtr( $number, $separatorTransformTable ?: [] );
3561 } elseif ( $number === '-0' ) {
3562 // Special case to ensure we don't lose the minus sign by
3563 // converting to an int.
3564 $number = strtr( $number, $separatorTransformTable ?: [] );
3565 } else {
3566 // NumberFormatter supports separator transformation,
3567 // but it does not know all languages MW
3568 // supports. Example: arq. Also, languages like pl have
3569 // customisation. So manually set it.
3570 $fmt = clone $fmt;
3571
3573 $fmt->setSymbol(
3574 NumberFormatter::DECIMAL_SEPARATOR_SYMBOL,
3575 $separatorTransformTable[ '.' ] ?? '.'
3576 );
3577 $fmt->setSymbol(
3578 NumberFormatter::GROUPING_SEPARATOR_SYMBOL,
3579 $separatorTransformTable[ ',' ] ?? ','
3580 );
3581 }
3582
3583 // Maintain # of digits before and after the decimal point
3584 // (and presence of decimal point)
3585 if ( preg_match( '/^-?(\d*)(\.(\d*))?$/', $number, $m ) ) {
3586 $fmt->setAttribute( NumberFormatter::MIN_INTEGER_DIGITS, strlen( $m[1] ) );
3587 if ( isset( $m[2] ) ) {
3588 $fmt->setAttribute( NumberFormatter::DECIMAL_ALWAYS_SHOWN, 1 );
3589 }
3590 $fmt->setAttribute( NumberFormatter::FRACTION_DIGITS, strlen( $m[3] ?? '' ) );
3591 }
3592 $number = $fmt->format( (float)$number );
3593 }
3594 }
3595
3596 if ( $this->config->get( MainConfigNames::TranslateNumerals ) ) {
3597 // This is often unnecessary: PHP's NumberFormatter will often
3598 // do the digit transform itself (T267614)
3599 $s = $this->digitTransformTable();
3600 if ( $s ) {
3601 $number = strtr( $number, $s );
3602 }
3603 }
3604 # T10327: Make our formatted numbers prettier by using a
3605 # proper Unicode 'minus' character.
3606 $number = strtr( $number, [ '-' => "\u{2212}" ] );
3607
3608 // Remove any LRM or RLM characters generated from NumberFormatter,
3609 // since directionality is handled outside of this context.
3610 // Similarly remove \u61C (ALM) which is added starting PHP 7.3+
3611 return strtr( $number, [
3612 self::LRM => '',
3613 self::RLM => '',
3614 self::ALM => '',
3615 ] );
3616 }
3617
3626 public function formatNumNoSeparators( $number ) {
3627 return $this->formatNumInternal( $number, true );
3628 }
3629
3634 public function parseFormattedNumber( $number ) {
3635 if ( $number === $this->msg( 'formatnum-nan' )->text() ) {
3636 return "NAN";
3637 }
3638 if ( $number === "∞" ) {
3639 return "INF";
3640 }
3641 // Accept either ASCII hyphen-minus or the unicode minus emitted by
3642 // ::formatNum()
3643 $number = strtr( $number, [ "\u{2212}" => '-' ] );
3644 if ( $number === "-∞" ) {
3645 return "-INF";
3646 }
3647 $s = $this->digitTransformTable();
3648 if ( $s ) {
3649 // Eliminate empty array values such as ''. (T66347)
3650 $s = array_filter( $s );
3651 $number = strtr( $number, array_flip( $s ) );
3652 }
3653
3654 $s = $this->separatorTransformTable();
3655 if ( $s ) {
3656 // Eliminate empty array values such as ''. (T66347)
3657 $s = array_filter( $s );
3658 $number = strtr( $number, array_flip( $s ) );
3659 }
3660
3661 return strtr( $number, [ ',' => '' ] );
3662 }
3663
3667 public function digitGroupingPattern() {
3668 return $this->localisationCache->getItem( $this->mCode, 'digitGroupingPattern' );
3669 }
3670
3674 public function digitTransformTable() {
3675 return $this->localisationCache->getItem( $this->mCode, 'digitTransformTable' );
3676 }
3677
3681 public function separatorTransformTable() {
3682 return $this->localisationCache->getItem( $this->mCode, 'separatorTransformTable' );
3683 }
3684
3693 public function minimumGroupingDigits(): int {
3694 return $this->localisationCache->getItem( $this->mCode, 'minimumGroupingDigits' ) ?? 1;
3695 }
3696
3706 public function listToText( array $list ) {
3707 $itemCount = count( $list );
3708 if ( $itemCount < 1 ) {
3709 return '';
3710 }
3711 $text = array_pop( $list );
3712 if ( $itemCount > 1 ) {
3713 $and = $this->msg( 'and' )->escaped();
3714 $space = $this->msg( 'word-separator' )->escaped();
3715 $comma = '';
3716 if ( $itemCount > 2 ) {
3717 $comma = $this->msg( 'comma-separator' )->escaped();
3718 }
3719 $text = implode( $comma, $list ) . $and . $space . $text;
3720 }
3721 // @phan-suppress-next-line PhanTypeMismatchReturnNullable False positive
3722 return $text;
3723 }
3724
3732 public function commaList( array $list ): string {
3733 return implode(
3734 $this->msg( 'comma-separator' )->escaped(),
3735 $list
3736 );
3737 }
3738
3746 public function semicolonList( array $list ): string {
3747 return implode(
3748 $this->msg( 'semicolon-separator' )->escaped(),
3749 $list
3750 );
3751 }
3752
3759 public function pipeList( array $list ): string {
3760 return implode(
3761 $this->msg( 'pipe-separator' )->escaped(),
3762 $list
3763 );
3764 }
3765
3782 public function truncateForDatabase( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3783 return $this->truncateInternal(
3784 $string, $length, $ellipsis, $adjustLength, 'strlen', 'mb_strcut'
3785 );
3786 }
3787
3813 public function truncateForVisual( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3814 // Passing encoding to mb_strlen and mb_substr is optional.
3815 // Encoding defaults to mb_internal_encoding(), which is set to UTF-8 in Setup.php, so
3816 // explicit specification of encoding is skipped.
3817 // Note: Both multibyte methods are callables invoked in truncateInternal.
3818 return $this->truncateInternal(
3819 $string, $length, $ellipsis, $adjustLength, 'mb_strlen', 'mb_substr'
3820 );
3821 }
3822
3839 private function truncateInternal(
3840 $string, $length, $ellipsis, $adjustLength, callable $measureLength, callable $getSubstring
3841 ) {
3842 # Check if there is no need to truncate
3843 if ( $measureLength( $string ) <= abs( $length ) ) {
3844 return $string; // no need to truncate
3845 }
3846
3847 # Use the localized ellipsis character
3848 if ( $ellipsis == '...' ) {
3849 $ellipsis = $this->msg( 'ellipsis' )->text();
3850 }
3851 if ( $length == 0 ) {
3852 return $ellipsis; // convention
3853 }
3854
3855 $stringOriginal = $string;
3856 # If ellipsis length is >= $length then we can't apply $adjustLength
3857 if ( $adjustLength && $measureLength( $ellipsis ) >= abs( $length ) ) {
3858 $string = $ellipsis; // this can be slightly unexpected
3859 # Otherwise, truncate and add ellipsis...
3860 } else {
3861 $ellipsisLength = $adjustLength ? $measureLength( $ellipsis ) : 0;
3862 if ( $length > 0 ) {
3863 $length -= $ellipsisLength;
3864 $string = $getSubstring( $string, 0, $length ); // xyz...
3865 $string = rtrim( $string ) . $ellipsis;
3866 } else {
3867 $length += $ellipsisLength;
3868 $string = $getSubstring( $string, $length ); // ...xyz
3869 $string = $ellipsis . ltrim( $string );
3870 }
3871 }
3872
3873 # Do not truncate if the ellipsis makes the string longer/equal (T24181).
3874 # This check is *not* redundant if $adjustLength, due to the single case where
3875 # LEN($ellipsis) > ABS($limit arg); $stringOriginal could be shorter than $string.
3876 if ( $measureLength( $string ) < $measureLength( $stringOriginal ) ) {
3877 return $string;
3878 } else {
3879 return $stringOriginal;
3880 }
3881 }
3882
3890 protected function removeBadCharLast( $string ) {
3891 if ( $string != '' ) {
3892 $char = ord( substr( $string, -1 ) );
3893 $m = [];
3894 if ( $char >= 0xc0 ) {
3895 # We got the first byte only of a multibyte char; remove it.
3896 $string = substr( $string, 0, -1 );
3897 } elseif ( $char >= 0x80 &&
3898 // Use the /s modifier (PCRE_DOTALL) so (.*) also matches newlines
3899 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
3900 '[\xf0-\xf7][\x80-\xbf]{1,2})$/s', $string, $m )
3901 ) {
3902 # We chopped in the middle of a character; remove it
3903 $string = $m[1];
3904 }
3905 }
3906 return $string;
3907 }
3908
3924 public function truncateHtml( $text, $length, $ellipsis = '...' ) {
3925 # Use the localized ellipsis character
3926 if ( $ellipsis == '...' ) {
3927 $ellipsis = $this->msg( 'ellipsis' )->escaped();
3928 }
3929 # Check if there is clearly no need to truncate
3930 if ( $length <= 0 ) {
3931 return $ellipsis; // no text shown, nothing to format (convention)
3932 } elseif ( strlen( $text ) <= $length ) {
3933 return $text; // string short enough even *with* HTML (short-circuit)
3934 }
3935
3936 $dispLen = 0; // innerHTML length so far
3937 $testingEllipsis = false; // check if ellipses will make the string longer/equal?
3938 $tagType = 0; // 0-open, 1-close
3939 $bracketState = 0; // 1-tag start, 2-tag name, 0-neither
3940 $entityState = 0; // 0-not entity, 1-entity
3941 $tag = $ret = ''; // accumulated tag name, accumulated result string
3942 $openTags = []; // open tag stack
3943 $maybeState = null; // possible truncation state
3944
3945 $textLen = strlen( $text );
3946 $neLength = max( 0, $length - strlen( $ellipsis ) ); // non-ellipsis len if truncated
3947 for ( $pos = 0; true; ++$pos ) {
3948 # Consider truncation once the display length has reached the maximum.
3949 # We check if $dispLen > 0 to grab tags for the $neLength = 0 case.
3950 # Check that we're not in the middle of a bracket/entity...
3951 if ( $dispLen && $dispLen >= $neLength && $bracketState == 0 && !$entityState ) {
3952 if ( !$testingEllipsis ) {
3953 $testingEllipsis = true;
3954 # Save where we are; we will truncate here unless there turn out to
3955 # be so few remaining characters that truncation is not necessary.
3956 if ( !$maybeState ) { // already saved? ($neLength = 0 case)
3957 $maybeState = [ $ret, $openTags ]; // save state
3958 }
3959 } elseif ( $dispLen > $length && $dispLen > strlen( $ellipsis ) ) {
3960 # The string in fact does need truncation, the truncation point was OK.
3961 // @phan-suppress-next-line PhanTypeInvalidExpressionArrayDestructuring
3962 [ $ret, $openTags ] = $maybeState; // reload state
3963 $ret = $this->removeBadCharLast( $ret ); // multi-byte char fix
3964 $ret .= $ellipsis; // add ellipsis
3965 break;
3966 }
3967 }
3968 if ( $pos >= $textLen ) {
3969 break; // extra iteration just for the checks above
3970 }
3971
3972 # Read the next char...
3973 $ch = $text[$pos];
3974 $lastCh = $pos ? $text[$pos - 1] : '';
3975 $ret .= $ch; // add to result string
3976 if ( $ch == '<' ) {
3977 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags ); // for bad HTML
3978 $entityState = 0; // for bad HTML
3979 $bracketState = 1; // tag started (checking for backslash)
3980 } elseif ( $ch == '>' ) {
3981 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags );
3982 $entityState = 0; // for bad HTML
3983 $bracketState = 0; // out of brackets
3984 } elseif ( $bracketState == 1 ) {
3985 if ( $ch == '/' ) {
3986 $tagType = 1; // close tag (e.g. "</span>")
3987 } else {
3988 $tagType = 0; // open tag (e.g. "<span>")
3989 $tag .= $ch;
3990 }
3991 $bracketState = 2; // building tag name
3992 } elseif ( $bracketState == 2 ) {
3993 if ( $ch != ' ' ) {
3994 $tag .= $ch;
3995 } else {
3996 // Name found (e.g. "<a href=..."), add on tag attributes...
3997 $pos += $this->truncate_skip( $ret, $text, "<>", $pos + 1 );
3998 }
3999 } elseif ( $bracketState == 0 ) {
4000 if ( $entityState ) {
4001 if ( $ch == ';' ) {
4002 $entityState = 0;
4003 $dispLen++; // entity is one displayed char
4004 }
4005 } else {
4006 if ( $neLength == 0 && !$maybeState ) {
4007 // Save the state without $ch. We want to *hit* the first
4008 // display char (to get tags) but not *use* it if truncating.
4009 $maybeState = [ substr( $ret, 0, -1 ), $openTags ];
4010 }
4011 if ( $ch == '&' ) {
4012 $entityState = 1; // entity found, (e.g. "&#160;")
4013 } else {
4014 $dispLen++; // this char is displayed
4015 // Add the next $max display text chars after this in one swoop...
4016 $max = ( $testingEllipsis ? $length : $neLength ) - $dispLen;
4017 $skipped = $this->truncate_skip( $ret, $text, "<>&", $pos + 1, $max );
4018 $dispLen += $skipped;
4019 $pos += $skipped;
4020 }
4021 }
4022 }
4023 }
4024 // Close the last tag if left unclosed by bad HTML
4025 $this->truncate_endBracket( $tag, $tagType, $text[$textLen - 1], $openTags );
4026 while ( count( $openTags ) > 0 ) {
4027 $ret .= '</' . array_pop( $openTags ) . '>'; // close open tags
4028 }
4029 return $ret;
4030 }
4031
4043 private function truncate_skip( &$ret, $text, $search, $start, $len = null ) {
4044 if ( $len === null ) {
4045 // -1 means "no limit" for strcspn
4046 $len = -1;
4047 } elseif ( $len < 0 ) {
4048 $len = 0;
4049 }
4050 $skipCount = 0;
4051 if ( $start < strlen( $text ) ) {
4052 $skipCount = strcspn( $text, $search, $start, $len );
4053 $ret .= substr( $text, $start, $skipCount );
4054 }
4055 return $skipCount;
4056 }
4057
4068 private function truncate_endBracket( &$tag, $tagType, $lastCh, &$openTags ) {
4069 $tag = ltrim( $tag );
4070 if ( $tag != '' ) {
4071 if ( $tagType == 0 && $lastCh != '/' ) {
4072 $openTags[] = $tag; // tag opened (didn't close itself)
4073 } elseif ( $tagType == 1 ) {
4074 if ( $openTags && $tag == $openTags[count( $openTags ) - 1] ) {
4075 array_pop( $openTags ); // tag closed
4076 }
4077 }
4078 $tag = '';
4079 }
4080 }
4081
4090 public function convertGrammar( $word, $case ) {
4091 $grammarForms = $this->config->get( MainConfigNames::GrammarForms );
4092 if ( isset( $grammarForms[$this->getCode()][$case][$word] ) ) {
4093 return $grammarForms[$this->getCode()][$case][$word];
4094 }
4095
4096 $manager = $this->leximorphFactory->getManager( $this );
4097 if ( $manager ) {
4098 return $manager->getGrammar()->process( $word, $case, $grammarForms );
4099 }
4100
4101 $grammarTransformations = $this->getGrammarTransformations();
4102
4103 if ( isset( $grammarTransformations[$case] ) ) {
4104 $forms = $grammarTransformations[$case];
4105
4106 // Some names of grammar rules are aliases for other rules.
4107 // In such cases the value is a string rather than object,
4108 // so load the actual rules.
4109 if ( is_string( $forms ) ) {
4110 $forms = $grammarTransformations[$forms];
4111 }
4112
4113 foreach ( $forms as $rule ) {
4114 $form = $rule[0];
4115
4116 if ( $form === '@metadata' ) {
4117 continue;
4118 }
4119
4120 $replacement = $rule[1];
4121
4122 $regex = '/' . addcslashes( $form, '/' ) . '/u';
4123 $patternMatches = preg_match( $regex, $word );
4124
4125 if ( $patternMatches === false ) {
4127 'An error occurred while processing grammar. ' .
4128 "Word: '$word'. Regex: /$form/."
4129 );
4130 } elseif ( $patternMatches === 1 ) {
4131 $word = preg_replace( $regex, $replacement, $word );
4132
4133 break;
4134 }
4135 }
4136 }
4137
4138 return $word;
4139 }
4140
4147 public function getGrammarForms() {
4148 $grammarForms = $this->config->get( MainConfigNames::GrammarForms );
4149 if ( isset( $grammarForms[$this->getCode()] )
4150 && is_array( $grammarForms[$this->getCode()] )
4151 ) {
4152 return $grammarForms[$this->getCode()];
4153 }
4154
4155 return [];
4156 }
4157
4166 public function getGrammarTransformations() {
4167 $provider = $this->leximorphFactory->getProvider( $this );
4168 if ( $provider ) {
4169 return $provider->getGrammarTransformationsProvider()->getTransformations();
4170 }
4171
4172 global $IP;
4173 if ( $this->grammarTransformCache !== null ) {
4174 return $this->grammarTransformCache;
4175 }
4176
4177 $grammarDataFile = $IP . "/languages/data/grammarTransformations/{$this->getCode()}.json";
4178 $this->grammarTransformCache = is_readable( $grammarDataFile )
4179 ? FormatJson::decode( file_get_contents( $grammarDataFile ), true )
4180 : [];
4181
4182 if ( $this->grammarTransformCache === null ) {
4183 throw new RuntimeException( "Invalid grammar data for \"{$this->getCode()}\"." );
4184 }
4185
4186 return $this->grammarTransformCache;
4187 }
4188
4208 public function gender( $gender, $forms ) {
4209 if ( !count( $forms ) ) {
4210 return '';
4211 }
4212
4213 $manager = $this->leximorphFactory->getManager( $this );
4214 if ( $manager ) {
4215 return $manager->getGender()->process( $gender, $forms );
4216 }
4217
4218 $forms = $this->preConvertPlural( $forms, 2 );
4219 if ( $gender === 'male' ) {
4220 return $forms[0];
4221 }
4222 if ( $gender === 'female' ) {
4223 return $forms[1];
4224 }
4225 return $forms[2] ?? $forms[0];
4226 }
4227
4243 public function convertPlural( $count, $forms ) {
4244 $manager = $this->leximorphFactory->getManager( $this );
4245 if ( $manager ) {
4246 return $manager->getPlural()->process( $count, $forms );
4247 }
4248
4249 // Handle explicit n=pluralform cases
4250 $forms = $this->handleExplicitPluralForms( $count, $forms );
4251 if ( is_string( $forms ) ) {
4252 return $forms;
4253 }
4254 if ( !count( $forms ) ) {
4255 return '';
4256 }
4257
4258 $pluralForm = $this->getPluralRuleIndexNumber( $count );
4259 $pluralForm = min( $pluralForm, count( $forms ) - 1 );
4260 return $forms[$pluralForm];
4261 }
4262
4278 protected function handleExplicitPluralForms( $count, array $forms ) {
4279 foreach ( $forms as $index => $form ) {
4280 if ( preg_match( '/\d+=/i', $form ) ) {
4281 $pos = strpos( $form, '=' );
4282 if ( substr( $form, 0, $pos ) === (string)$count ) {
4283 return substr( $form, $pos + 1 );
4284 }
4285 unset( $forms[$index] );
4286 }
4287 }
4288 return array_values( $forms );
4289 }
4290
4299 protected function preConvertPlural( /* Array */ $forms, $count ) {
4300 return array_pad( $forms, $count, end( $forms ) );
4301 }
4302
4313 public function getFormalityIndex(): int {
4314 $provider = $this->leximorphFactory->getProvider( $this );
4315 if ( $provider ) {
4316 return $provider->getFormalityIndexProvider()->getFormalityIndex();
4317 }
4318
4319 return $this->localisationCache->getItem( $this->mCode, 'formalityIndex' ) ?? 0;
4320 }
4321
4344 public function embedBidi( $text = '' ) {
4345 $manager = $this->leximorphFactory->getManager( $this );
4346 if ( $manager ) {
4347 return $manager->getBidi()->process( $text );
4348 }
4349
4350 $dir = self::strongDirFromContent( $text );
4351 if ( $dir === 'ltr' ) {
4352 // Wrap in LEFT-TO-RIGHT EMBEDDING ... POP DIRECTIONAL FORMATTING
4353 return self::LRE . $text . self::PDF;
4354 }
4355 if ( $dir === 'rtl' ) {
4356 // Wrap in RIGHT-TO-LEFT EMBEDDING ... POP DIRECTIONAL FORMATTING
4357 return self::RLE . $text . self::PDF;
4358 }
4359 // No strong directionality: do not wrap
4360 return $text;
4361 }
4362
4372 public function getBlockDurations( bool $includeOther = true ): array {
4373 $msg = $this->msg( 'ipboptions' );
4374 $a = $msg->isDisabled() ? [] : XmlSelect::parseOptionsMessage( $msg->text() );
4375
4376 if ( $a && $includeOther ) {
4377 // If options exist, add other to the end instead of the beginning (which
4378 // is what happens by default).
4379 $a[ $this->msg( 'ipbother' )->text() ] = 'other';
4380 }
4381
4382 return $a;
4383 }
4384
4398 public function translateBlockExpiry( $str, ?UserIdentity $user = null, $now = 0 ) {
4399 $duration = $this->getBlockDurations();
4400 $show = array_search( $str, $duration, true );
4401 if ( $show !== false ) {
4402 return trim( $show );
4403 }
4404
4405 if ( wfIsInfinity( $str ) ) {
4406 foreach ( $duration as $show => $value ) {
4407 if ( wfIsInfinity( $value ) ) {
4408 return trim( $show );
4409 }
4410 }
4411 }
4412
4413 // If all else fails, return a standard duration or timestamp description.
4414 $time = strtotime( $str, $now );
4415 if ( $time === false ) { // Unknown format. Return it as-is in case.
4416 return $str;
4417 } elseif ( $time !== strtotime( $str, $now + 1 ) ) { // It's a relative timestamp.
4418 // The result differs based on current time, so the difference
4419 // is a fixed duration length.
4420 return $this->formatDurationBetweenTimestamps( $time, $now );
4421 } else { // It's an absolute timestamp.
4422 if ( $time === 0 ) {
4423 // wfTimestamp() handles 0 as current time instead of epoch.
4424 $time = '19700101000000';
4425 }
4426
4427 // Return the timestamp as-is if it is in an unknown format to ConvertibleTimestamp (T354663)
4428 $time = ConvertibleTimestamp::convert( TS::MW, $time );
4429 if ( $time === false ) {
4430 return $str;
4431 }
4432
4433 if ( $user ) {
4434 return $this->userTimeAndDate( $time, $user );
4435 }
4436 return $this->timeanddate( $time );
4437 }
4438 }
4439
4447 public function segmentForDiff( $text ) {
4448 return $text;
4449 }
4450
4457 public function unsegmentForDiff( $text ) {
4458 return $text;
4459 }
4460
4467 public function linkTrail() {
4468 return $this->localisationCache->getItem( $this->mCode, 'linkTrail' );
4469 }
4470
4477 public function linkPrefixCharset() {
4478 return $this->localisationCache->getItem( $this->mCode, 'linkPrefixCharset' );
4479 }
4480
4488 public function equals( Language $lang ) {
4489 return $lang === $this || $lang->getCode() === $this->mCode;
4490 }
4491
4498 public function getCode(): string {
4499 return $this->mCode;
4500 }
4501
4512 public function getHtmlCode() {
4513 $this->mHtmlCode ??= LanguageCode::bcp47( $this->getCode() );
4514 return $this->mHtmlCode;
4515 }
4516
4524 public function toBcp47Code(): string {
4525 return $this->getHtmlCode();
4526 }
4527
4535 public function isSameCodeAs( Bcp47Code $other ): bool {
4536 if ( $this === $other ) {
4537 return true;
4538 }
4539 if ( $other instanceof Language ) {
4540 // Compare the mediawiki-internal code
4541 return $this->equals( $other );
4542 }
4543 // Bcp-47 codes are case insensitive.
4544 // See Bcp47CodeValue::isSameCode()
4545 return strcasecmp( $this->toBcp47Code(), $other->toBcp47Code() ) === 0;
4546 }
4547
4556 public static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
4557 $m = null;
4558 preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
4559 preg_quote( $suffix, '/' ) . '/', $filename, $m );
4560 if ( !count( $m ) ) {
4561 return false;
4562 }
4563 return str_replace( '_', '-', strtolower( $m[1] ) );
4564 }
4565
4570 private function fixVariableInNamespace( $talk ) {
4571 if ( !str_contains( $talk, '$1' ) ) {
4572 return $talk;
4573 }
4574
4575 $talk = str_replace( '$1', $this->config->get( MainConfigNames::MetaNamespace ), $talk );
4576
4577 # Allow grammar transformations
4578 # Allowing full message-style parsing would make simple requests
4579 # such as action=raw much more expensive than they need to be.
4580 # This will hopefully cover most cases.
4581 $talk = preg_replace_callback(
4582 '/{{grammar:(.*?)\|(.*?)}}/i',
4583 function ( $m ) {
4584 return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
4585 },
4586 $talk
4587 );
4588 return str_replace( ' ', '_', $talk );
4589 }
4590
4603 public function formatExpiry( $expiry, $format = true, $infinity = 'infinity', $user = null ) {
4604 static $dbInfinity;
4605 $dbInfinity ??= MediaWikiServices::getInstance()->getConnectionProvider()
4606 ->getReplicaDatabase()
4607 ->getInfinity();
4608
4609 if ( $expiry == '' || $expiry === 'infinity' || $expiry == $dbInfinity ) {
4610 return $format === true
4611 ? $this->getMessageFromDB( 'infiniteblock' )
4612 : $infinity;
4613 } else {
4614 if ( $format === true ) {
4615 return $user
4616 ? $this->userTimeAndDate( $expiry, $user )
4617 : $this->timeanddate( $expiry, /* User preference timezone */ true );
4618 }
4619 return wfTimestamp( $format, $expiry );
4620 }
4621 }
4622
4637 public function formatTimePeriod( $seconds, $format = [] ) {
4638 if ( !is_array( $format ) ) {
4639 $format = [ 'avoid' => $format ]; // For backwards compatibility
4640 }
4641 if ( !isset( $format['avoid'] ) ) {
4642 $format['avoid'] = false;
4643 }
4644 if ( !isset( $format['noabbrevs'] ) ) {
4645 $format['noabbrevs'] = false;
4646 }
4647 $secondsMsg = $this->msg( $format['noabbrevs'] ? 'seconds' : 'seconds-abbrev' );
4648 $minutesMsg = $this->msg( $format['noabbrevs'] ? 'minutes' : 'minutes-abbrev' );
4649 $hoursMsg = $this->msg( $format['noabbrevs'] ? 'hours' : 'hours-abbrev' );
4650 $daysMsg = $this->msg( $format['noabbrevs'] ? 'days' : 'days-abbrev' );
4651 $space = $this->msg( 'word-separator' )->text();
4652
4653 if ( round( $seconds * 10 ) < 100 ) {
4654 $s = $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) );
4655 $s = $secondsMsg->params( $s )->text();
4656 } elseif ( round( $seconds ) < 60 ) {
4657 $s = $this->formatNum( round( $seconds ) );
4658 $s = $secondsMsg->params( $s )->text();
4659 } elseif ( round( $seconds ) < 3600 ) {
4660 $minutes = floor( $seconds / 60 );
4661 $secondsPart = round( fmod( $seconds, 60 ) );
4662 if ( $secondsPart == 60 ) {
4663 $secondsPart = 0;
4664 $minutes++;
4665 }
4666 $s = $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4667 $s .= $space;
4668 $s .= $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4669 } elseif ( round( $seconds ) <= 2 * 86400 ) {
4670 $hours = floor( $seconds / 3600 );
4671 $minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
4672 $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
4673 if ( $secondsPart == 60 ) {
4674 $secondsPart = 0;
4675 $minutes++;
4676 }
4677 if ( $minutes == 60 ) {
4678 $minutes = 0;
4679 $hours++;
4680 }
4681 $s = $hoursMsg->params( $this->formatNum( $hours ) )->text();
4682 $s .= $space;
4683 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4684 if ( !in_array( $format['avoid'], [ 'avoidseconds', 'avoidminutes', 'avoidhours' ] ) ) {
4685 $s .= $space . $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4686 }
4687 } else {
4688 $days = floor( $seconds / 86400 );
4689 if ( $format['avoid'] === 'avoidhours' ) {
4690 $hours = round( ( $seconds - $days * 86400 ) / 3600 );
4691 if ( $hours == 24 ) {
4692 $days++;
4693 }
4694 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4695 } elseif ( $format['avoid'] === 'avoidminutes' ) {
4696 $hours = round( ( $seconds - $days * 86400 ) / 3600 );
4697 if ( $hours == 24 ) {
4698 $hours = 0;
4699 $days++;
4700 }
4701 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4702 $s .= $space;
4703 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4704 } elseif ( $format['avoid'] === 'avoidseconds' ) {
4705 $hours = floor( ( $seconds - $days * 86400 ) / 3600 );
4706 $minutes = round( ( $seconds - $days * 86400 - $hours * 3600 ) / 60 );
4707 if ( $minutes == 60 ) {
4708 $minutes = 0;
4709 $hours++;
4710 }
4711 if ( $hours == 24 ) {
4712 $hours = 0;
4713 $days++;
4714 }
4715 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4716 $s .= $space;
4717 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4718 $s .= $space;
4719 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4720 } else {
4721 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4722 $s .= $space;
4723 $s .= $this->formatTimePeriod( $seconds - $days * 86400, $format );
4724 }
4725 }
4726 return $s;
4727 }
4728
4740 public function formatBitrate( $bps ) {
4741 // messages used: bitrate-bits, bitrate-kilobits, bitrate-megabits, bitrate-gigabits, bitrate-terabits,
4742 // bitrate-petabits, bitrate-exabits, bitrate-zettabits, bitrate-yottabits, bitrate-ronnabits,
4743 // bitrate-quettabits
4744 return $this->formatComputingNumbers( $bps, 1000, "bitrate-$1bits" );
4745 }
4746
4753 public function formatComputingNumbers( $size, $boundary, $messageKey ) {
4754 if ( $size <= 0 ) {
4755 return str_replace( '$1', $this->formatNum( $size ),
4756 $this->getMessageFromDB( str_replace( '$1', '', $messageKey ) )
4757 );
4758 }
4759 $sizes = [ '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zetta', 'yotta', 'ronna', 'quetta' ];
4760 $index = 0;
4761
4762 $maxIndex = count( $sizes ) - 1;
4763 while ( $size >= $boundary && $index < $maxIndex ) {
4764 $index++;
4765 $size /= $boundary;
4766 }
4767
4768 // For small sizes no decimal places necessary
4769 $round = 0;
4770 if ( $index > 1 ) {
4771 // For MB and larger units, two decimal places are smarter
4772 $round = 2;
4773 }
4774 $msg = str_replace( '$1', $sizes[$index], $messageKey );
4775
4776 $size = round( $size, $round );
4777 $text = $this->getMessageFromDB( $msg );
4778 return str_replace( '$1', $this->formatNum( $size ), $text );
4779 }
4780
4791 public function formatSize( $size ) {
4792 // messages used: size-bytes, size-kilobytes, size-megabytes, size-gigabytes, size-terabytes,
4793 // size-petabytes, size-exabytes, size-zettabytes, size-yottabytes, size-ronnabytes, size-quettabytes
4794 return $this->formatComputingNumbers( $size, 1024, "size-$1bytes" );
4795 }
4796
4804 public function specialList( $page, $details ) {
4805 if ( !$details ) {
4806 return $page;
4807 }
4808
4809 return Html::rawElement( 'bdi', [ 'dir' => $this->getDir() ], $page ) .
4810 $this->msg( 'word-separator' )->escaped() .
4811 $this->msg( 'parentheses' )->rawParams( $details )->escaped();
4812 }
4813
4814 private function getNumberFormatter(): NumberFormatter {
4815 if ( $this->numberFormatter === null ) {
4816 $digitGroupingPattern = $this->digitGroupingPattern();
4817 $code = $this->getCode();
4818 if ( !( $this->config->get( MainConfigNames::TranslateNumerals )
4819 && $this->langNameUtils->isValidCode( $code ) )
4820 ) {
4821 $code = Locale::getDefault(); // POSIX system default locale
4822 }
4823
4824 $fmt = $this->createNumberFormatter( $code, $digitGroupingPattern );
4825 if ( !$fmt ) {
4826 $fallbacks = $this->getFallbackLanguages();
4827 foreach ( $fallbacks as $fallbackCode ) {
4828 $fmt = $this->createNumberFormatter( $fallbackCode, $digitGroupingPattern );
4829 if ( $fmt ) {
4830 break;
4831 }
4832 }
4833 if ( !$fmt ) {
4834 throw new RuntimeException(
4835 'Could not instance NumberFormatter for ' . $code . ' and all fallbacks'
4836 );
4837 }
4838 }
4839
4840 $this->numberFormatter = $fmt;
4841 }
4842 return $this->numberFormatter;
4843 }
4844
4845 private function createNumberFormatter( string $code, ?string $digitGroupingPattern ): ?NumberFormatter {
4846 try {
4848 return new NumberFormatter(
4849 $code, NumberFormatter::PATTERN_DECIMAL, $digitGroupingPattern
4850 );
4851 }
4852 // @suppress PhanParamTooFew Phan thinks this always requires 3 parameters, that's wrong
4853 return new NumberFormatter( $code, NumberFormatter::DECIMAL );
4854 } catch ( \ValueError ) {
4855 // Value Errors are thrown since php8.4 for invalid locales
4856 return null;
4857 }
4858 }
4859
4866 public function getCompiledPluralRules() {
4867 $provider = $this->leximorphFactory->getProvider( $this );
4868 if ( $provider ) {
4869 return $provider->getPluralProvider()->getCompiledPluralRules();
4870 }
4871
4872 $pluralRules =
4873 $this->localisationCache->getItem( strtolower( $this->mCode ), 'compiledPluralRules' );
4874 if ( !$pluralRules ) {
4875 $fallbacks = $this->getFallbackLanguages();
4876 foreach ( $fallbacks as $fallbackCode ) {
4877 $pluralRules = $this->localisationCache
4878 ->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' );
4879 if ( $pluralRules ) {
4880 break;
4881 }
4882 }
4883 }
4884 return $pluralRules;
4885 }
4886
4893 public function getPluralRules() {
4894 $pluralRules =
4895 $this->localisationCache->getItem( strtolower( $this->mCode ), 'pluralRules' );
4896 if ( !$pluralRules ) {
4897 $fallbacks = $this->getFallbackLanguages();
4898 foreach ( $fallbacks as $fallbackCode ) {
4899 $pluralRules = $this->localisationCache
4900 ->getItem( strtolower( $fallbackCode ), 'pluralRules' );
4901 if ( $pluralRules ) {
4902 break;
4903 }
4904 }
4905 }
4906 return $pluralRules;
4907 }
4908
4915 public function getPluralRuleTypes() {
4916 $provider = $this->leximorphFactory->getProvider( $this );
4917 if ( $provider ) {
4918 return $provider->getPluralProvider()->getPluralRuleTypes();
4919 }
4920
4921 $pluralRuleTypes =
4922 $this->localisationCache->getItem( strtolower( $this->mCode ), 'pluralRuleTypes' );
4923 if ( !$pluralRuleTypes ) {
4924 $fallbacks = $this->getFallbackLanguages();
4925 foreach ( $fallbacks as $fallbackCode ) {
4926 $pluralRuleTypes = $this->localisationCache
4927 ->getItem( strtolower( $fallbackCode ), 'pluralRuleTypes' );
4928 if ( $pluralRuleTypes ) {
4929 break;
4930 }
4931 }
4932 }
4933 return $pluralRuleTypes;
4934 }
4935
4942 public function getPluralRuleIndexNumber( $number ) {
4943 $provider = $this->leximorphFactory->getProvider( $this );
4944 if ( $provider ) {
4945 return $provider->getPluralProvider()->getPluralRuleIndexNumber( $number );
4946 }
4947
4948 $pluralRules = $this->getCompiledPluralRules();
4949 return Evaluator::evaluateCompiled( $number, $pluralRules );
4950 }
4951
4961 public function getPluralRuleType( $number ) {
4962 $provider = $this->leximorphFactory->getProvider( $this );
4963 if ( $provider ) {
4964 return $provider->getPluralProvider()->getPluralRuleType( $number );
4965 }
4966
4967 $index = $this->getPluralRuleIndexNumber( $number );
4968 $pluralRuleTypes = $this->getPluralRuleTypes();
4969 return $pluralRuleTypes[$index] ?? 'other';
4970 }
4971
4978 protected function getConverterInternal() {
4979 return $this->converterFactory->getLanguageConverter( $this );
4980 }
4981
4988 protected function getHookContainer() {
4989 return $this->hookContainer;
4990 }
4991
5000 protected function getHookRunner() {
5001 return $this->hookRunner;
5002 }
5003
5009 public function getJsData() {
5010 return [
5011 'digitTransformTable' => $this->digitTransformTable(),
5012 'separatorTransformTable' => $this->separatorTransformTable(),
5013 'minimumGroupingDigits' => $this->minimumGroupingDigits(),
5014 'formalityIndex' => $this->getFormalityIndex(),
5015 'grammarForms' => $this->getGrammarForms(),
5016 'grammarTransformations' => $this->getGrammarTransformations(),
5017 'pluralRules' => $this->getPluralRules(),
5018 'digitGroupingPattern' => $this->digitGroupingPattern(),
5019 'fallbackLanguages' => $this->getFallbackLanguages(),
5020 'bcp47Map' => LanguageCode::getNonstandardLanguageCodeMapping(),
5021 ];
5022 }
5023
5028 public function getJsDateFormats() {
5029 $jsLcFormats = $this->localisationCache->getItem( $this->mCode, 'jsDateFormats' );
5030 $phpLcFormats = $this->localisationCache->getItem( $this->mCode, 'dateFormats' );
5031
5032 $styles = $this->getDatePreferences() ?: [];
5033 $default = $this->getDefaultDateFormat();
5034 // Always include the default style
5035 if ( !in_array( $default, $styles, true ) ) {
5036 $styles[] = $default;
5037 }
5038 $results = [];
5039 foreach ( $styles as $style ) {
5040 if ( $style === 'default' ) {
5041 // Default is not a real style for our purposes
5042 continue;
5043 }
5044 foreach ( [ 'time', 'date', 'both', 'pretty' ] as $type ) {
5045 $key = "$style $type";
5046 $resolvedType = $type;
5047 $resolvedStyle = $style;
5048 $resolvedKey = $key;
5049
5050 // If the PHP localisation lacks the "pretty" type, fall back to "date"
5051 if ( !isset( $phpLcFormats[$key] ) && $type === 'pretty' ) {
5052 $resolvedType = 'date';
5053 $resolvedKey = "$resolvedStyle $resolvedType";
5054 }
5055
5056 // If $jsDateFormats has an alias, follow it.
5057 // This is used to gracefully remove formats that don't work in the browser.
5058 $alias = $jsLcFormats[$resolvedKey]['alias'] ?? '';
5059 if ( preg_match( '/^(.*) ([^ ]*)$/', $alias, $m ) ) {
5060 $resolvedType = $m[2];
5061 $resolvedStyle = $m[1];
5062 $resolvedKey = "$resolvedStyle $resolvedType";
5063 }
5064
5065 $jsFormat = $this->convertDateFormatToJs(
5066 $this->getDateFormatString( $resolvedType, $resolvedStyle ) );
5067 if ( isset( $jsLcFormats[$resolvedKey] ) ) {
5068 $results[$key] = array_merge_recursive( $jsFormat, $jsLcFormats[$resolvedKey] );
5069 } else {
5070 $results[$key] = $jsFormat;
5071 }
5072 }
5073 }
5074 return $results;
5075 }
5076}
5077
5079class_alias( Language::class, 'Language' );
const NS_USER
Definition Defines.php:53
const NS_PROJECT_TALK
Definition Defines.php:56
const NS_USER_TALK
Definition Defines.php:54
const NS_PROJECT
Definition Defines.php:55
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
wfTimestamp( $outputtype=TS::UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfIsInfinity( $str)
Determine input string is represents as infinity.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
$fallback
$separatorTransformTable
$digitGroupingPattern
$minimumGroupingDigits
if(!defined('MEDIAWIKI')) if(!defined( 'MW_ENTRY_POINT')) $IP
Environment checks.
Definition Setup.php:103
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:44
JSON formatter wrapper class.
An interface for creating language converters.
A service that provides utilities to do with language names and codes.
Base class for language-specific code.
Definition Language.php:65
resetNamespaces()
Resets all the namespace caches.
Definition Language.php:392
formatDuration( $seconds, array $chosenIntervals=[])
Takes a number of seconds and turns it into a text using values such as hours and minutes.
hasWordBreaks()
Most writing systems use whitespace to break up words.
date( $ts, $adj=false, $format=true, $timecorrection=false)
formatDurationBetweenTimestamps(int $timestamp1, int $timestamp2, ?int $precision=null)
Takes two timestamps and turns the difference between them into a text using values such as hours and...
formatNumNoSeparators( $number)
Front-end for non-commafied formatNum.
static computeUnitTimestampDeadline(DateTimeInterface $date, string $unit)
Compute a cache expiry to account for a dynamic timestamp displayed in output.
array< string, int > null $mNamespaceIds
Indexed by localized lower-cased namespace name.
Definition Language.php:95
userTimeAndDate( $ts, UserIdentity $user, array $options=[])
Get the formatted date and time for the given timestamp and formatted for the given user.
convertGrammar( $word, $case)
Grammatical transformations, needed for inflected languages Invoked by putting {{grammar:case|word}} ...
formatTimePeriod( $seconds, $format=[])
Formats a time given in seconds into a string representation of that time.
formatExpiry( $expiry, $format=true, $infinity='infinity', $user=null)
Decode an expiry (block, protection, etc.) which has come from the DB.
truncateHtml( $text, $length, $ellipsis='...')
Truncate a string of valid HTML to a specified length in bytes, appending an optional string (e....
isRTL()
For right-to-left language support.
getPluralRuleIndexNumber( $number)
Find the index number of the plural rule appropriate for the given number.
formatBitrate( $bps)
Format a bitrate for output, using an appropriate unit (bps, kbps, Mbps, Gbps, Tbps,...
linkPrefixCharset()
A regular expression character set to match legal word-prefixing characters which should be merged on...
equals(Language $lang)
Compare with another language object.
getDir()
Return the correct HTML 'dir' attribute value for this language.
getGrammarForms()
Get the grammar forms for the content language.
alignStart()
Return 'left' or 'right' as appropriate alignment for line-start for this language's text direction.
timeanddate( $ts, $adj=false, $format=true, $timecorrection=false)
userAdjust( $ts, $tz=false)
Used by date() and time() to adjust the time output.
alignEnd()
Return 'right' or 'left' as appropriate alignment for line-end for this language's text direction.
formatNum( $number)
Normally we output all numbers in plain en_US style, that is 293,291.235 for two hundred ninety-three...
isSameCodeAs(Bcp47Code $other)
Compare this Language object to a Bcp47Code.
dateFormat( $usePrefs=true)
This is meant to be used by time(), date(), and timeanddate() to get the date preference they're supp...
getBlockDurations(bool $includeOther=true)
Get an array of suggested block durations from MediaWiki:Ipboptions.
getNsText( $index)
Get a namespace value by key.
Definition Language.php:428
getHumanTimestamp(MWTimestamp $time, ?MWTimestamp $relativeTo=null, ?UserIdentity $user=null)
Get the timestamp in a human-friendly relative format, e.g., "3 days ago".
firstChar( $s)
Get the first character of a string.
handleExplicitPluralForms( $count, array $forms)
Handles explicit plural forms for Language::convertPlural()
normalize( $s)
Convert a UTF-8 string to normal form C.
static int[] $durationIntervals
Definition Language.php:240
truncateForVisual( $string, $length, $ellipsis='...', $adjustLength=true)
Truncate a string to a specified number of characters, appending an optional string (e....
convertPlural( $count, $forms)
Plural form transformations, needed for some languages.
static hebrewNumeral( $num)
Hebrew Gematria number formatting up to 9999.
array< int, string > null $namespaceNames
Indexed by numeric namespace ID.
Definition Language.php:93
ucwordbreaks( $str)
capitalize words at word breaks
string[][] $dateFormatStrings
memoize
Definition Language.php:83
setNamespaces(array $namespaces)
Arbitrarily set all the namespace names at once.
Definition Language.php:383
getFormattedNamespaces()
A convenience function that returns {.
Definition Language.php:407
userDate( $ts, UserIdentity $user, array $options=[])
Get the formatted date for the given timestamp and formatted for the given user.
getConverterInternal()
Return the LanguageConverter for this language, convenience function for use in the language classes ...
__construct(string $code, private readonly NamespaceInfo $namespaceInfo, private LocalisationCache $localisationCache, private readonly LanguageNameUtils $langNameUtils, private readonly LanguageFallback $langFallback, private readonly LanguageConverterFactory $converterFactory, private readonly HookContainer $hookContainer, private readonly Config $config, private readonly LeximorphFactory $leximorphFactory,)
Definition Language.php:294
getCompiledPluralRules()
Get the compiled plural rules for the language.
getDirMark( $opposite=false)
A hidden direction mark (LRM or RLM), depending on the language direction.
static getCodeFromFileName( $filename, $prefix='Language', $suffix='.php')
Get the language code from a file name.
getSpecialPageAliases()
Get 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:200
getBookstoreList()
Exports $wgBookstoreListEn.
Definition Language.php:329
getArrow( $direction='forwards')
An arrow, depending on the language direction.
emphasize( $text)
Italic is unsuitable for some languages.
needsGenderDistinction()
Whether this language uses gender-dependent namespace aliases.
Definition Language.php:474
getDirMarkEntity( $opposite=false)
A hidden direction mark (LRM or RLM), depending on the language direction.
semicolonList(array $list)
Take a list of strings and build a locale-friendly semicolon-separated list, using the local semicolo...
iconv( $in, $out, $string)
convertForSearchResult( $termsArray)
getNamespaces()
Returns an array of localised namespaces (with underscores, without considering language variants) in...
Definition Language.php:340
linkPrefixExtension()
To allow "foo[[bar]]" to extend the link over the whole word "foobar".
array< string, int > null $namespaceAliases
Map from alias to namespace ID.
Definition Language.php:97
getFormattedNsText( $index)
A convenience function that returns the same thing as getNsText() except with '_' changed to ' ',...
Definition Language.php:446
getMagic( $mw)
Fill a MagicWord object with data from this instance.
segmentForDiff( $text)
Languages like Chinese need to be segmented in order for the diff to be of any use.
formatSize( $size)
Format a size in bytes for output, using an appropriate unit (B, KB, MB, GB, TB, PB,...
commaList(array $list)
Take a list of strings and build a locale-friendly comma-separated list, using the local comma-separa...
pipeList(array $list)
Same as commaList, but separate it with the pipe instead.
static convertDoubleWidth( $string)
Convert double-width roman characters to single-width.
getGrammarTransformations()
Get the grammar transformations data for the language.
getMagicWords()
Get all the magic words from the localisation cache.
transformUsingPairFile(string $dataClass, string $input)
Transform a string using serialized data stored in the given file (which must be in the serialized su...
unsegmentForDiff( $text)
And unsegment to show the result.
specialList( $page, $details)
Make a list item, used by various special pages.
normalizeForSearch( $text)
Some languages have special punctuation need to be normalized.
getHtmlCode()
Get the code in BCP 47 format which we can use inside html lang="" tags.
getPluralRules()
Get the plural rules for the language.
getLocalNsIndex( $text)
Get a namespace key by case-insensitive value.
Definition Language.php:499
getHookContainer()
Get a HookContainer, for hook metadata and running extension hooks.
caseFold( $s)
Return a case-folded representation of $s.
preConvertPlural($forms, $count)
Checks that convertPlural was given an array and pads it to requested number of forms by copying the ...
formatComputingNumbers( $size, $boundary, $messageKey)
uc( $str, $first=false)
getVariantname( $code, $usemsg=true)
Short names for language variants used for language conversion links.
Definition Language.php:608
getDateFormatString( $type, $pref)
Get a format string for a given type and preference.
msg( $msg,... $params)
Gets the Message object from this language.
Definition Language.php:676
minimumGroupingDigits()
The minimum number of digits a number must have, in addition to the grouping size,...
toBcp47Code()
Implement the Bcp47Code interface.
string[][] null $mExtendedSpecialPageAliases
memoize
Definition Language.php:90
gender( $gender, $forms)
Provides an alternative text depending on specified gender.
getHookRunner()
Get a HookRunner, for running core hooks.
listToText(array $list)
Take a list of strings and build a locale-friendly comma-separated list, using the local comma-separa...
lc( $str, $first=false)
embedBidi( $text='')
Wraps argument with unicode control characters for directionality safety.
static romanNumeral( $num)
Roman number formatting up to 10000.
static insertSpace( $string, $pattern)
getDurationIntervals( $seconds, array $chosenIntervals=[])
Takes a number of seconds and returns an array with a set of corresponding intervals.
segmentByWord( $string)
Some languages such as Chinese require word segmentation, Specify such segmentation when overridden i...
getGenderNsText( $index, $gender)
Returns gender-dependent namespace alias if available.
Definition Language.php:459
getFormalityIndex()
Some languages provide translations in different levels of formality (or manner of address),...
time( $ts, $adj=false, $format=true, $timecorrection=false)
getNsIndex( $text)
Get a namespace key by case-insensitive value.
Definition Language.php:591
Create and cache Manager and Provider instances.
Caching for the contents of localisation files.
Create PSR-3 logger objects.
A class containing constants representing the names of configuration variables.
const ExtraGenderNamespaces
Name constant for the ExtraGenderNamespaces setting, for use with Config::get()
const NamespaceAliases
Name constant for the NamespaceAliases setting, for use with Config::get()
const MetaNamespace
Name constant for the MetaNamespace setting, for use with Config::get()
const ExtraNamespaces
Name constant for the ExtraNamespaces setting, for use with Config::get()
const MetaNamespaceTalk
Name constant for the MetaNamespaceTalk setting, for use with Config::get()
Service locator for MediaWiki core services.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:144
This class encapsulates "magic words" such as "#redirect", NOTOC, etc.
Definition MagicWord.php:51
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
getDefaultOption(string $opt, ?UserIdentity $userIdentity=null)
Get a given default option value.
Utility class to parse the TimeCorrection string value.
User class for the MediaWiki software.
Definition User.php: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, 220, 250, 300, 400,], 'ThumbnailNamespaces'=>[6,], 'ThumbnailSteps'=> null, 'ThumbnailBuckets'=> null, 'ThumbnailMinimumBucketDistance'=> 50, 'UploadThumbnailRenderMap'=>[], 'UploadThumbnailRenderMethod'=> 'jobqueue', 'UploadThumbnailRenderHttpCustomHost'=> false, 'UploadThumbnailRenderHttpCustomDomain'=> false, 'UseTinyRGBForJPGThumbnails'=> false, 'GalleryOptions'=>[], 'ThumbUpright'=> 0.75, 'DirectoryMode'=> 511, 'ResponsiveImages'=> true, 'ImagePreconnect'=> false, 'TrackMediaRequestProvenance'=> false, 'DjvuUseBoxedCommand'=> false, 'DjvuDump'=> null, 'DjvuRenderer'=> null, 'DjvuTxt'=> null, 'DjvuPostProcessor'=> 'pnmtojpeg', 'DjvuOutputExtension'=> 'jpg', 'EmergencyContact'=> false, 'PasswordSender'=> false, 'NoReplyAddress'=> false, 'EnableEmail'=> true, 'EnableUserEmail'=> true, 'UserEmailUseReplyTo'=> true, 'PasswordReminderResendTime'=> 24, 'NewPasswordExpiry'=> 604800, 'UserEmailConfirmationTokenExpiry'=> 604800, 'PasswordExpirationDays'=> false, 'PasswordExpireGrace'=> 604800, 'SMTP'=> false, 'AdditionalMailParams'=> null, 'AllowHTMLEmail'=> false, 'EnotifFromEditor'=> false, 'EmailAuthentication'=> true, 'EmailConfirmationBanner'=> false, 'EnotifWatchlist'=> false, 'EnotifUserTalk'=> false, 'EnotifRevealEditorAddress'=> false, 'EnotifMinorEdits'=> true, 'EnotifUseRealName'=> false, 'UsersNotifiedOnAllChanges'=>[], 'DBname'=> 'my_wiki', 'DBmwschema'=> null, 'DBprefix'=> '', 'DBserver'=> 'localhost', 'DBport'=> 5432, 'DBuser'=> 'wikiuser', 'DBpassword'=> '', 'DBtype'=> 'mysql', 'DBssl'=> false, 'DBcompress'=> false, 'DBStrictWarnings'=> false, 'DBadminuser'=> null, 'DBadminpassword'=> null, 'SearchType'=> null, 'SearchTypeAlternatives'=> null, 'DBTableOptions'=> 'ENGINE=InnoDB, DEFAULT CHARSET=binary', 'SQLMode'=> '', 'SQLiteDataDir'=> '', 'SharedDB'=> null, 'SharedPrefix'=> false, 'SharedTables'=>['user', 'user_properties', 'user_autocreate_serial',], 'SharedSchema'=> false, 'DBservers'=> false, 'LBFactoryConf'=>['class'=> 'Wikimedia\\Rdbms\\LBFactorySimple',], 'DataCenterUpdateStickTTL'=> 10, 'DBerrorLog'=> false, 'DBerrorLogTZ'=> false, 'LocalDatabases'=>[], 'DatabaseReplicaLagWarning'=> 10, 'DatabaseReplicaLagCritical'=> 30, 'MaxExecutionTimeForExpensiveQueries'=> 0, 'VirtualDomainsMapping'=>[], 'FileSchemaMigrationStage'=> 3, 'ExternalLinksDomainGaps'=>[], 'ContentHandlers'=>['wikitext'=>['class'=> 'MediaWiki\\Content\\WikitextContentHandler', 'services'=>['TitleFactory', 'ParserFactory', 'GlobalIdGenerator', 'LanguageNameUtils', 'LinkRenderer', 'MagicWordFactory', 'ParsoidParserFactory',],], 'javascript'=>['class'=> 'MediaWiki\\Content\\JavaScriptContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'UserOptionsLookup', 'CodeHighlighter',],], 'json'=>['class'=> 'MediaWiki\\Content\\JsonContentHandler', 'services'=>['ParsoidParserFactory', 'TitleFactory',],], 'css'=>['class'=> 'MediaWiki\\Content\\CssContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'UserOptionsLookup', 'CodeHighlighter',],], 'vue'=>['class'=> 'MediaWiki\\Content\\VueContentHandler', 'services'=>['MainConfig', 'ParserFactory', 'CodeHighlighter',],], 'text'=> 'MediaWiki\\Content\\TextContentHandler', 'unknown'=> 'MediaWiki\\Content\\FallbackContentHandler',], 'NamespaceContentModels'=>[], 'TextModelsToParse'=>['wikitext', 'javascript', 'css',], 'CompressRevisions'=> false, 'ExternalStores'=>[], 'ExternalServers'=>[], 'DefaultExternalStore'=> false, 'RevisionCacheExpiry'=> 604800, 'PageLanguageUseDB'=> false, 'DiffEngine'=> null, 'ExternalDiffEngine'=> false, 'Wikidiff2Options'=>[], 'RequestTimeLimit'=> null, 'TransactionalTimeLimit'=> 120, 'CriticalSectionTimeLimit'=> 180.0, 'MiserMode'=> false, 'DisableQueryPages'=> false, 'QueryCacheLimit'=> 1000, 'WantedPagesThreshold'=> 1, 'AllowSlowParserFunctions'=> false, 'AllowSchemaUpdates'=> true, 'MaxArticleSize'=> 2048, 'MemoryLimit'=> '50M', 'PoolCounterConf'=> null, 'PoolCountClientConf'=>['servers'=>['127.0.0.1',], 'timeout'=> 0.1,], 'MaxUserDBWriteDuration'=> false, 'MaxJobDBWriteDuration'=> false, 'LinkHolderBatchSize'=> 1000, 'MaximumMovedPages'=> 100, 'ForceDeferredUpdatesPreSend'=> false, 'MultiShardSiteStats'=> false, 'CacheDirectory'=> false, 'MainCacheType'=> 0, 'MessageCacheType'=> -1, 'ParserCacheType'=> -1, 'SessionCacheType'=> -1, 'AnonSessionCacheType'=> false, 'LanguageConverterCacheType'=> -1, 'ObjectCaches'=>[0=>['class'=> 'Wikimedia\\ObjectCache\\EmptyBagOStuff', 'reportDupes'=> false,], 1=>['class'=> 'MediaWiki\\ObjectCache\\SqlBagOStuff', 'loggroup'=> 'SQLBagOStuff',], 'memcached-php'=>['class'=> 'Wikimedia\\ObjectCache\\MemcachedPhpBagOStuff', 'loggroup'=> 'memcached',], 'memcached-pecl'=>['class'=> 'Wikimedia\\ObjectCache\\MemcachedPeclBagOStuff', 'loggroup'=> 'memcached',], 'hash'=>['class'=> 'Wikimedia\\ObjectCache\\HashBagOStuff', 'reportDupes'=> false,], 'apc'=>['class'=> 'Wikimedia\\ObjectCache\\APCUBagOStuff', 'reportDupes'=> false,], 'apcu'=>['class'=> 'Wikimedia\\ObjectCache\\APCUBagOStuff', 'reportDupes'=> false,],], 'WANObjectCache'=>[], 'MicroStashType'=> -1, 'MainStash'=> 1, 'ParsoidCacheConfig'=>['StashType'=> null, 'StashDuration'=> 86400, 'WarmParsoidParserCache'=> false,], 'ParsoidSelectiveUpdateSampleRate'=> 0, 'ParserCacheFilterConfig'=>['pcache'=>['default'=>['minCpuTime'=> 0,],], 'postproc-pcache'=>['default'=>['minCpuTime'=> 9223372036854775807,],], 'parsoid-pcache'=>['default'=>['minCpuTime'=> 0,],], 'postproc-parsoid-pcache'=>['default'=>['minCpuTime'=> 0,],],], 'ChronologyProtectorSecret'=> '', 'ParserCacheExpireTime'=> 86400, 'ParserCacheAsyncExpireTime'=> 60, 'ParserCacheAsyncRefreshJobs'=> true, 'OldRevisionParserCacheExpireTime'=> 3600, 'ObjectCacheSessionExpiry'=> 3600, 'PHPSessionHandling'=> 'warn', 'SuspiciousIpExpiry'=> false, 'SessionPbkdf2Iterations'=> 10001, 'UseSessionCookieJwt'=> false, 'JwtSessionCookieIssuer'=> null, 'MemCachedServers'=>['127.0.0.1:11211',], 'MemCachedPersistent'=> false, 'MemCachedTimeout'=> 500000, 'UseLocalMessageCache'=> false, 'AdaptiveMessageCache'=> false, 'LocalisationCacheConf'=>['class'=> 'MediaWiki\\Language\\LocalisationCache', 'store'=> 'detect', 'storeClass'=> false, 'storeDirectory'=> false, 'storeServer'=>[], 'forceRecache'=> false, 'manualRecache'=> false,], 'CachePages'=> true, 'CacheEpoch'=> '20030516000000', 'GitInfoCacheDirectory'=> false, 'UseFileCache'=> false, 'FileCacheDepth'=> 2, 'RenderHashAppend'=> '', 'EnableSidebarCache'=> false, 'SidebarCacheExpiry'=> 86400, 'UseGzip'=> false, 'InvalidateCacheOnLocalSettingsChange'=> true, 'ExtensionInfoMTime'=> false, 'EnableRemoteBagOStuffTests'=> false, 'UseCdn'=> false, 'VaryOnXFP'=> false, 'InternalServer'=> false, 'CdnMaxAge'=> 18000, 'CdnMaxageLagged'=> 30, 'CdnMaxageStale'=> 10, 'CdnReboundPurgeDelay'=> 0, 'CdnMaxageSubstitute'=> 60, 'ForcedRawSMaxage'=> 300, 'CdnServers'=>[], 'CdnServersNoPurge'=>[], 'HTCPRouting'=>[], 'HTCPMulticastTTL'=> 1, 'UsePrivateIPs'=> false, 'CdnMatchParameterOrder'=> true, 'LanguageCode'=> 'en', 'GrammarForms'=>[], 'InterwikiMagic'=> true, 'HideInterlanguageLinks'=> false, 'ExtraInterlanguageLinkPrefixes'=>[], 'InterlanguageLinkCodeMap'=>[], 'ExtraLanguageNames'=>[], 'ExtraLanguageCodes'=>['bh'=> 'bho', 'no'=> 'nb', 'simple'=> 'en',], 'DummyLanguageCodes'=>[], 'AllUnicodeFixes'=> false, 'LegacyEncoding'=> false, 'AmericanDates'=> false, 'TranslateNumerals'=> true, 'UseDatabaseMessages'=> true, 'MaxMsgCacheEntrySize'=> 10000, 'DisableLangConversion'=> false, 'DisableTitleConversion'=> false, 'DefaultLanguageVariant'=> false, 'UsePigLatinVariant'=> false, 'DisabledVariants'=>[], 'VariantArticlePath'=> false, 'UseXssLanguage'=> false, 'LoginLanguageSelector'=> false, 'ForceUIMsgAsContentMsg'=>[], 'RawHtmlMessages'=>[], 'Localtimezone'=> null, 'LocalTZoffset'=> null, 'OverrideUcfirstCharacters'=>[], 'MimeType'=> 'text/html', 'Html5Version'=> null, 'EditSubmitButtonLabelPublish'=> false, 'XhtmlNamespaces'=>[], 'SiteNotice'=> '', 'BrowserFormatDetection'=> 'telephone=no', 'SkinMetaTags'=>[], 'DefaultSkin'=> 'vector-2022', 'FallbackSkin'=> 'fallback', 'SkipSkins'=>[], 'DisableOutputCompression'=> false, 'FragmentMode'=>['html5', 'legacy',], 'ExternalInterwikiFragmentMode'=> 'legacy', 'FooterIcons'=>['copyright'=>['copyright'=>[],], 'poweredby'=>['mediawiki'=>['src'=> null, 'url'=> 'https:'alt'=> 'Powered by MediaWiki', 'lang'=> 'en',],],], 'UseCombinedLoginLink'=> false, 'Edititis'=> false, 'Send404Code'=> true, 'ShowRollbackEditCount'=> 10, 'EnableCanonicalServerLink'=> false, 'InterwikiLogoOverride'=>[], 'ResourceModules'=>[], 'ResourceModuleSkinStyles'=>[], 'ResourceLoaderSources'=>[], 'ResourceBasePath'=> null, 'ResourceLoaderMaxage'=>[], 'ResourceLoaderDebug'=> false, 'ResourceLoaderMaxQueryLength'=> false, 'ResourceLoaderValidateJS'=> true, 'ResourceLoaderEnableJSProfiler'=> false, 'ResourceLoaderStorageEnabled'=> true, 'ResourceLoaderStorageVersion'=> 1, 'ResourceLoaderEnableSourceMapLinks'=> true, 'AllowSiteCSSOnRestrictedPages'=> false, 'VueDevelopmentMode'=> false, 'CodexDevelopmentDir'=> null, 'MetaNamespace'=> false, 'MetaNamespaceTalk'=> false, 'CanonicalNamespaceNames'=>[-2=> 'Media', -1=> 'Special', 0=> '', 1=> 'Talk', 2=> 'User', 3=> 'User_talk', 4=> 'Project', 5=> 'Project_talk', 6=> 'File', 7=> 'File_talk', 8=> 'MediaWiki', 9=> 'MediaWiki_talk', 10=> 'Template', 11=> 'Template_talk', 12=> 'Help', 13=> 'Help_talk', 14=> 'Category', 15=> 'Category_talk',], 'ExtraNamespaces'=>[], 'ExtraGenderNamespaces'=>[], 'NamespaceAliases'=>[], 'LegalTitleChars'=> ' %!"$&\'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+', 'CapitalLinks' => true, 'CapitalLinkOverrides' => [ ], 'NamespacesWithSubpages' => [ 1 => true, 2 => true, 3 => true, 4 => true, 5 => true, 7 => true, 8 => true, 9 => true, 10 => true, 11 => true, 12 => true, 13 => true, 15 => true, ], 'NamespacesWithoutAutoSummaries' => [ ], 'ContentNamespaces' => [ 0, ], 'ShortPagesNamespaceExclusions' => [ ], 'ExtraSignatureNamespaces' => [ ], 'InvalidRedirectTargets' => [ 'Filepath', 'Mypage', 'Mytalk', 'Redirect', 'Mylog', ], 'DisableHardRedirects' => false, 'FixDoubleRedirects' => false, 'LocalInterwikis' => [ ], 'InterwikiExpiry' => 10800, 'InterwikiCache' => false, 'InterwikiScopes' => 3, 'InterwikiFallbackSite' => 'wiki', 'RedirectSources' => false, 'SiteTypes' => [ 'mediawiki' => 'MediaWiki\\Site\\MediaWikiSite', ], 'MaxTocLevel' => 999, 'MaxPPNodeCount' => 1000000, 'MaxTemplateDepth' => 100, 'MaxPPExpandDepth' => 100, 'UrlProtocols' => [ 'bitcoin:', 'ftp: 'ftps: 'geo:', 'git: 'gopher: 'http: 'https: 'irc: 'ircs: 'magnet:', 'mailto:', 'matrix:', 'mms: 'news:', 'nntp: 'redis: 'sftp: 'sip:', 'sips:', 'sms:', 'ssh: 'svn: 'tel:', 'telnet: 'urn:', 'wikipedia: 'worldwind: 'xmpp:', ' ], 'CleanSignatures' => true, 'AllowExternalImages' => false, 'AllowExternalImagesFrom' => '', 'EnableImageWhitelist' => false, 'TidyConfig' => [ ], 'ParsoidSettings' => [ 'useSelser' => true, ], 'ParsoidExperimentalParserFunctionOutput' => false, 'RawHtml' => false, 'ExternalLinkTarget' => false, 'NoFollowLinks' => true, 'NoFollowNsExceptions' => [ ], 'NoFollowDomainExceptions' => [ 'mediawiki.org', ], 'RegisterInternalExternals' => false, 'ExternalLinksIgnoreDomains' => [ ], 'AllowDisplayTitle' => true, 'RestrictDisplayTitle' => true, 'ExpensiveParserFunctionLimit' => 100, 'PreprocessorCacheThreshold' => 1000, 'EnableScaryTranscluding' => false, 'TranscludeCacheExpiry' => 3600, 'EnableMagicLinks' => [ 'ISBN' => false, 'PMID' => false, 'RFC' => false, ], 'ParserEnableUserLanguage' => false, 'ArticleCountMethod' => 'link', 'ActiveUserDays' => 30, 'LearnerEdits' => 10, 'LearnerMemberSince' => 4, 'ExperiencedUserEdits' => 500, 'ExperiencedUserMemberSince' => 30, 'ManualRevertSearchRadius' => 15, 'RevertedTagMaxDepth' => 15, 'CentralIdLookupProviders' => [ 'local' => [ 'class' => 'MediaWiki\\User\\CentralId\\LocalIdLookup', 'services' => [ 'MainConfig', 'DBLoadBalancerFactory', 'HideUserUtils', ], ], ], 'CentralIdLookupProvider' => 'local', 'UserRegistrationProviders' => [ 'local' => [ 'class' => 'MediaWiki\\User\\Registration\\LocalUserRegistrationProvider', 'services' => [ 'ConnectionProvider', ], ], ], 'PasswordPolicy' => [ 'policies' => [ 'bureaucrat' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'sysop' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'interface-admin' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'bot' => [ 'MinimalPasswordLength' => 10, 'MinimumPasswordLengthToLogin' => 1, ], 'default' => [ 'MinimalPasswordLength' => [ 'value' => 8, 'suggestChangeOnLogin' => true, ], 'PasswordCannotBeSubstringInUsername' => [ 'value' => true, 'suggestChangeOnLogin' => true, ], 'PasswordCannotMatchDefaults' => [ 'value' => true, 'suggestChangeOnLogin' => true, ], 'MaximalPasswordLength' => [ 'value' => 4096, 'suggestChangeOnLogin' => true, ], 'PasswordNotInCommonList' => [ 'value' => true, 'suggestChangeOnLogin' => true, ], ], ], 'checks' => [ 'MinimalPasswordLength' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkMinimalPasswordLength', ], 'MinimumPasswordLengthToLogin' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkMinimumPasswordLengthToLogin', ], 'PasswordCannotBeSubstringInUsername' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkPasswordCannotBeSubstringInUsername', ], 'PasswordCannotMatchDefaults' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkPasswordCannotMatchDefaults', ], 'MaximalPasswordLength' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkMaximalPasswordLength', ], 'PasswordNotInCommonList' => [ 'MediaWiki\\Password\\PasswordPolicyChecks', 'checkPasswordNotInCommonList', ], ], ], 'AuthManagerConfig' => null, 'AuthManagerAutoConfig' => [ 'preauth' => [ 'MediaWiki\\Auth\\ThrottlePreAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\ThrottlePreAuthenticationProvider', 'sort' => 0, ], 'MediaWiki\\Auth\\PreviouslyRenamedAccountPreAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\PreviouslyRenamedAccountPreAuthenticationProvider', 'services' => [ 'ConnectionProvider', 'UserFactory', ], 'sort' => 0, ], ], 'primaryauth' => [ 'MediaWiki\\Auth\\TemporaryPasswordPrimaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\TemporaryPasswordPrimaryAuthenticationProvider', 'services' => [ 'DBLoadBalancerFactory', 'UserOptionsLookup', ], 'args' => [ [ 'authoritative' => false, ], ], 'sort' => 0, ], 'MediaWiki\\Auth\\LocalPasswordPrimaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\LocalPasswordPrimaryAuthenticationProvider', 'services' => [ 'DBLoadBalancerFactory', ], 'args' => [ [ 'authoritative' => true, ], ], 'sort' => 100, ], ], 'secondaryauth' => [ 'MediaWiki\\Auth\\CheckBlocksSecondaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\CheckBlocksSecondaryAuthenticationProvider', 'sort' => 0, ], 'MediaWiki\\Auth\\ResetPasswordSecondaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\ResetPasswordSecondaryAuthenticationProvider', 'sort' => 100, ], 'MediaWiki\\Auth\\EmailNotificationSecondaryAuthenticationProvider' => [ 'class' => 'MediaWiki\\Auth\\EmailNotificationSecondaryAuthenticationProvider', 'services' => [ 'DBLoadBalancerFactory', ], 'sort' => 200, ], ], ], 'RememberMe' => 'choose', 'ReauthenticateTime' => [ 'default' => 3600, ], '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, 'autocreateaccount' => true, 'read' => true, 'edit' => true, 'createpage' => true, 'createtalk' => true, 'viewmyprivateinfo' => true, 'editmyprivateinfo' => true, 'editmyoptions' => true, ], 'user' => [ 'move' => true, 'move-subpages' => true, 'move-rootuserpages' => true, 'move-categorypages' => true, 'movefile' => true, 'read' => true, 'edit' => true, 'createpage' => true, 'createtalk' => true, 'upload' => true, 'reupload' => true, 'reupload-shared' => true, 'minoredit' => true, 'editmyusercss' => true, 'editmyuserjson' => true, 'editmyuserjs' => true, 'editmyuserjsredirect' => true, 'sendemail' => true, 'applychangetags' => true, 'changetags' => true, 'viewmywatchlist' => true, 'editmywatchlist' => true, 'createwithcontentmodel' => true, 'logout' => true, ], 'autoconfirmed' => [ 'autoconfirmed' => true, 'editsemiprotected' => true, ], 'bot' => [ 'bot' => true, 'autoconfirmed' => true, 'editsemiprotected' => true, 'nominornewtalk' => true, 'autopatrol' => true, 'suppressredirect' => true, 'apihighlimits' => true, ], 'sysop' => [ 'block' => true, 'createaccount' => true, 'createpreviouslyrenamedaccount' => true, 'delete' => true, 'bigdelete' => true, 'deletedhistory' => true, 'deletedtext' => true, 'undelete' => true, 'editcontentmodel' => true, 'editinterface' => true, 'editsitejson' => true, 'edituserjson' => true, 'import' => true, 'importupload' => true, 'move' => true, 'move-subpages' => true, 'move-rootuserpages' => true, 'move-categorypages' => true, 'patrol' => true, 'autopatrol' => true, 'protect' => true, 'editprotected' => true, 'rollback' => true, 'upload' => true, 'reupload' => true, 'reupload-shared' => true, 'unwatchedpages' => true, 'autoconfirmed' => true, 'editsemiprotected' => true, 'ipblock-exempt' => true, 'blockemail' => true, 'markbotedits' => true, 'apihighlimits' => true, 'browsearchive' => true, 'noratelimit' => true, 'movefile' => true, 'unblockself' => true, 'suppressredirect' => true, 'mergehistory' => true, 'managechangetags' => true, 'deletechangetags' => true, ], 'interface-admin' => [ 'editinterface' => true, 'editsitecss' => true, 'editsitejson' => true, 'editsitejs' => true, 'editusercss' => true, 'edituserjson' => true, 'edituserjs' => true, ], 'bureaucrat' => [ 'userrights' => true, 'noratelimit' => true, 'renameuser' => true, ], 'suppress' => [ 'hideuser' => true, 'suppressrevision' => true, 'viewsuppressed' => true, 'suppressionlog' => true, 'deleterevision' => true, 'deletelogentry' => true, ], ], 'PrivilegedGroups' => [ 'bureaucrat', 'interface-admin', 'suppress', 'sysop', ], 'RevokePermissions' => [ ], 'GroupInheritsPermissions' => [ ], 'ImplicitGroups' => [ '*', 'user', 'autoconfirmed', ], 'GroupsAddToSelf' => [ ], 'GroupsRemoveFromSelf' => [ ], 'RestrictedGroups' => [ ], 'UserRequirementsPrivateConditions' => [ ], 'RestrictionTypes' => [ 'create', 'edit', 'move', 'upload', ], 'RestrictionLevels' => [ '', 'autoconfirmed', 'sysop', ], 'CascadingRestrictionLevels' => [ 'sysop', ], 'SemiprotectedRestrictionLevels' => [ 'autoconfirmed', ], 'NamespaceProtection' => [ ], 'NonincludableNamespaces' => [ ], 'AutoConfirmAge' => 0, 'AutoConfirmCount' => 0, 'Autopromote' => [ 'autoconfirmed' => [ '&', [ 1, null, ], [ 2, null, ], ], ], 'AutopromoteOnce' => [ 'onEdit' => [ ], ], 'AutopromoteOnceLogInRC' => true, 'AutopromoteOnceRCExcludedGroups' => [ ], 'AddGroups' => [ ], 'RemoveGroups' => [ ], 'AvailableRights' => [ ], 'ImplicitRights' => [ ], 'DeleteRevisionsLimit' => 0, 'DeleteRevisionsBatchSize' => 1000, 'HideUserContribLimit' => 1000, 'AccountCreationThrottle' => [ [ 'count' => 0, 'seconds' => 86400, ], ], 'TempAccountCreationThrottle' => [ [ 'count' => 1, 'seconds' => 600, ], [ 'count' => 6, 'seconds' => 86400, ], ], 'TempAccountNameAcquisitionThrottle' => [ [ 'count' => 60, 'seconds' => 86400, ], ], 'SpamRegex' => [ ], 'SummarySpamRegex' => [ ], 'EnableDnsBlacklist' => false, 'DnsBlacklistUrls' => [ ], 'ProxyList' => [ ], 'ProxyWhitelist' => [ ], 'SoftBlockRanges' => [ ], 'ApplyIpBlocksToXff' => false, 'RateLimits' => [ 'edit' => [ 'ip' => [ 8, 60, ], 'newbie' => [ 8, 60, ], 'user' => [ 90, 60, ], ], 'move' => [ 'newbie' => [ 2, 120, ], 'user' => [ 8, 60, ], ], 'upload' => [ 'ip' => [ 8, 60, ], 'newbie' => [ 8, 60, ], ], 'rollback' => [ 'user' => [ 10, 60, ], 'newbie' => [ 5, 120, ], ], 'mailpassword' => [ 'ip' => [ 5, 3600, ], ], 'sendemail' => [ 'ip' => [ 5, 86400, ], 'newbie' => [ 5, 86400, ], 'user' => [ 20, 86400, ], ], 'changeemail' => [ 'ip-all' => [ 10, 3600, ], 'user' => [ 4, 86400, ], ], 'confirmemail' => [ 'ip-all' => [ 10, 3600, ], 'user' => [ 4, 86400, ], ], 'purge' => [ 'ip' => [ 30, 60, ], 'user' => [ 30, 60, ], ], 'linkpurge' => [ 'ip' => [ 30, 60, ], 'user' => [ 30, 60, ], ], 'renderfile' => [ 'ip' => [ 700, 30, ], 'user' => [ 700, 30, ], ], 'renderfile-nonstandard' => [ 'ip' => [ 70, 30, ], 'user' => [ 70, 30, ], ], 'stashedit' => [ 'ip' => [ 30, 60, ], 'newbie' => [ 30, 60, ], ], 'stashbasehtml' => [ 'ip' => [ 5, 60, ], 'newbie' => [ 5, 60, ], ], 'changetags' => [ 'ip' => [ 8, 60, ], 'newbie' => [ 8, 60, ], ], 'editcontentmodel' => [ 'newbie' => [ 2, 120, ], 'user' => [ 8, 60, ], ], ], 'RateLimitsExcludedIPs' => [ ], 'PutIPinRC' => true, 'QueryPageDefaultLimit' => 50, 'ExternalQuerySources' => [ ], 'PasswordAttemptThrottle' => [ [ 'count' => 5, 'seconds' => 300, ], [ 'count' => 150, 'seconds' => 172800, ], ], 'GrantPermissions' => [ 'basic' => [ 'autocreateaccount' => true, 'autoconfirmed' => true, 'autopatrol' => true, 'editsemiprotected' => true, 'ipblock-exempt' => true, 'nominornewtalk' => true, 'patrolmarks' => true, 'read' => true, 'unwatchedpages' => true, ], 'highvolume' => [ 'bot' => true, 'apihighlimits' => true, 'noratelimit' => true, 'markbotedits' => true, ], 'import' => [ 'import' => true, 'importupload' => true, ], 'editpage' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'pagelang' => true, ], 'editprotected' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editprotected' => true, ], 'editmycssjs' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editmyusercss' => true, 'editmyuserjson' => true, 'editmyuserjs' => true, ], 'editmyoptions' => [ 'editmyoptions' => true, 'editmyuserjson' => true, ], 'editinterface' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editinterface' => true, 'edituserjson' => true, 'editsitejson' => true, ], 'editsiteconfig' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editinterface' => true, 'edituserjson' => true, 'editsitejson' => true, 'editusercss' => true, 'edituserjs' => true, 'editsitecss' => true, 'editsitejs' => true, ], 'createeditmovepage' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'createpage' => true, 'createtalk' => true, 'delete-redirect' => true, 'move' => true, 'move-rootuserpages' => true, 'move-subpages' => true, 'move-categorypages' => true, 'suppressredirect' => true, ], 'uploadfile' => [ 'upload' => true, 'reupload-own' => true, ], 'uploadeditmovefile' => [ 'upload' => true, 'reupload-own' => true, 'reupload' => true, 'reupload-shared' => true, 'upload_by_url' => true, 'movefile' => true, 'suppressredirect' => true, ], 'patrol' => [ 'patrol' => true, ], 'rollback' => [ 'rollback' => true, ], 'blockusers' => [ 'block' => true, 'blockemail' => true, ], 'viewdeleted' => [ 'browsearchive' => true, 'deletedhistory' => true, 'deletedtext' => true, ], 'viewrestrictedlogs' => [ 'suppressionlog' => true, ], 'delete' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'browsearchive' => true, 'deletedhistory' => true, 'deletedtext' => true, 'delete' => true, 'bigdelete' => true, 'deletelogentry' => true, 'deleterevision' => true, 'undelete' => true, ], 'oversight' => [ 'suppressrevision' => true, 'viewsuppressed' => true, ], 'protect' => [ 'edit' => true, 'minoredit' => true, 'applychangetags' => true, 'changetags' => true, 'editcontentmodel' => true, 'createwithcontentmodel' => true, 'editprotected' => true, 'protect' => true, ], 'viewmywatchlist' => [ 'viewmywatchlist' => true, ], 'editmywatchlist' => [ 'editmywatchlist' => true, ], 'sendemail' => [ 'sendemail' => true, ], 'createaccount' => [ 'createaccount' => true, ], 'privateinfo' => [ 'viewmyprivateinfo' => true, ], 'mergehistory' => [ 'mergehistory' => true, ], 'managesessions' => [ 'logout' => true, ], ], 'GrantPermissionGroups' => [ 'basic' => 'hidden', 'editpage' => 'page-interaction', 'createeditmovepage' => 'page-interaction', 'editprotected' => 'page-interaction', 'patrol' => 'page-interaction', 'uploadfile' => 'file-interaction', 'uploadeditmovefile' => 'file-interaction', 'sendemail' => 'email', 'viewmywatchlist' => 'watchlist-interaction', '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', 'managesessions' => '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, 'BotPasswordsLimit' => 100, '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, 'CSPUseReportURIDirective' => false, 'CSPFalsePositiveUrls' => [ 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'https: 'chrome-extension' => true, ], 'AllowCrossOrigin' => false, 'RestAllowCrossOriginCookieAuth' => false, 'SessionSecret' => false, 'CookieExpiration' => 2592000, 'ExtendedLoginCookieExpiration' => 15552000, 'SessionCookieJwtExpiration' => 14400, 'CookieDomain' => '', 'CookiePath' => '/', 'CookieSecure' => 'detect', 'CookiePrefix' => false, 'CookieHttpOnly' => true, 'CookieSameSite' => null, 'CacheVaryCookies' => [ ], 'SessionName' => false, 'CookieSetOnAutoblock' => true, 'CookieSetOnIpBlock' => true, 'DebugLogFile' => '', 'DebugLogPrefix' => '', 'DebugRedirects' => false, 'DebugRawPage' => false, 'DebugComments' => false, 'DebugDumpSql' => false, 'TrxProfilerLimits' => [ 'GET' => [ 'masterConns' => 0, 'writes' => 0, 'readQueryTime' => 5, 'readQueryRows' => 10000, ], 'POST' => [ 'readQueryTime' => 5, 'writeQueryTime' => 1, 'readQueryRows' => 100000, 'maxAffected' => 1000, ], 'POST-nonwrite' => [ 'writes' => 0, 'readQueryTime' => 5, 'readQueryRows' => 10000, ], 'PostSend-GET' => [ 'readQueryTime' => 5, 'writeQueryTime' => 1, 'readQueryRows' => 10000, 'maxAffected' => 1000, 'masterConns' => 0, 'writes' => 0, ], 'PostSend-POST' => [ 'readQueryTime' => 5, 'writeQueryTime' => 1, 'readQueryRows' => 100000, 'maxAffected' => 1000, ], 'JobRunner' => [ 'readQueryTime' => 30, 'writeQueryTime' => 5, 'readQueryRows' => 100000, 'maxAffected' => 500, ], 'Maintenance' => [ 'writeQueryTime' => 5, 'maxAffected' => 1000, ], ], 'DebugLogGroups' => [ ], 'MWLoggerDefaultSpi' => [ 'class' => 'MediaWiki\\Logger\\LegacySpi', ], 'ShowDebug' => false, 'SpecialVersionShowHooks' => false, 'ShowExceptionDetails' => false, 'LogExceptionBacktrace' => true, 'PropagateErrors' => true, 'ShowHostnames' => false, 'OverrideHostname' => false, 'DevelopmentWarnings' => false, 'DeprecationReleaseLimit' => false, 'Profiler' => [ ], 'StatsdServer' => false, 'StatsdMetricPrefix' => 'MediaWiki', 'StatsTarget' => null, 'StatsFormat' => null, 'StatsPrefix' => 'mediawiki', 'OpenTelemetryConfig' => null, 'PageInfoTransclusionLimit' => 50, 'EnableJavaScriptTest' => false, 'CachePrefix' => false, 'DebugToolbar' => false, 'ApiClientErrorSampleRate' => 1.0, 'DisableTextSearch' => false, 'AdvancedSearchHighlighting' => false, 'SearchHighlightBoundaries' => '[\\p{Z}\\p{P}\\p{C}]', 'OpenSearchTemplates' => [ 'application/x-suggestions+json' => false, 'application/x-suggestions+xml' => false, ], 'OpenSearchDefaultLimit' => 10, 'OpenSearchDescriptionLength' => 100, 'SearchSuggestCacheExpiry' => 1200, 'DisableSearchUpdate' => false, 'NamespacesToBeSearchedDefault' => [ true, ], 'DisableInternalSearch' => false, 'SearchForwardUrl' => null, 'SitemapNamespaces' => false, 'SitemapNamespacesPriorities' => false, 'SitemapApiConfig' => [ ], 'SpecialSearchFormOptions' => [ ], 'SearchMatchRedirectPreference' => false, 'SearchRunSuggestedQuery' => true, 'Diff3' => '/usr/bin/diff3', 'Diff' => '/usr/bin/diff', 'PreviewOnOpenNamespaces' => [ 14 => true, ], 'UniversalEditButton' => true, 'UseAutomaticEditSummaries' => true, 'CommandLineDarkBg' => false, 'ReadOnly' => null, 'ReadOnlyWatchedItemStore' => false, 'ReadOnlyFile' => false, 'UpgradeKey' => false, 'GitBin' => '/usr/bin/git', 'GitRepositoryViewers' => [ 'https: 'ssh: 'https: 'git@github\\.com:(.*?)(\\.git)?' => 'https: ], 'InstallerInitialPages' => [ [ 'titlemsg' => 'mainpage', 'text' => '{{subst:int:mainpagetext}}{{subst:int:mainpagedocfooter}}', ], ], 'RCMaxAge' => 7776000, 'WatchersMaxAge' => 15552000, 'UnwatchedPageSecret' => 1, 'RCFilterByAge' => false, 'RCLinkLimits' => [ 50, 100, 250, 500, ], 'RCLinkDays' => [ 1, 3, 7, 14, 30, ], 'RCFeeds' => [ ], 'RCWatchCategoryMembership' => false, 'UseRCPatrol' => true, 'StructuredChangeFiltersLiveUpdatePollingRate' => 3, 'UseNPPatrol' => true, 'UseFilePatrol' => true, 'Feed' => true, 'FeedLimit' => 50, 'FeedCacheTimeout' => 60, 'FeedDiffCutoff' => 32768, 'OverrideSiteFeed' => [ ], 'FeedClasses' => [ 'rss' => 'MediaWiki\\Feed\\RSSFeed', 'atom' => 'MediaWiki\\Feed\\AtomFeed', ], 'AdvertisedFeedTypes' => [ 'atom', ], 'RCShowWatchingUsers' => false, 'RCShowChangedSize' => true, 'RCChangedSizeThreshold' => 500, 'ShowUpdatedMarker' => true, 'DisableAnonTalk' => false, 'UseTagFilter' => true, 'SoftwareTags' => [ 'mw-contentmodelchange' => true, 'mw-new-redirect' => true, 'mw-removed-redirect' => true, 'mw-changed-redirect-target' => true, 'mw-blank' => true, 'mw-replace' => true, 'mw-recreated' => true, 'mw-rollback' => true, 'mw-undo' => true, 'mw-manual-revert' => true, 'mw-reverted' => true, 'mw-server-side-upload' => true, 'mw-ipblock-appeal' => true, 'mw-edited-other-users-js' => true, 'mw-edited-other-users-css' => true, ], 'UnwatchedPageThreshold' => false, 'RecentChangesFlags' => [ 'newpage' => [ 'letter' => 'newpageletter', 'title' => 'recentchanges-label-newpage', 'legend' => 'recentchanges-legend-newpage', 'grouping' => 'any', ], 'minor' => [ 'letter' => 'minoreditletter', 'title' => 'recentchanges-label-minor', 'legend' => 'recentchanges-legend-minor', 'class' => 'minoredit', 'grouping' => 'all', ], 'bot' => [ 'letter' => 'boteditletter', 'title' => 'recentchanges-label-bot', 'legend' => 'recentchanges-legend-bot', 'class' => 'botedit', 'grouping' => 'all', ], 'unpatrolled' => [ 'letter' => 'unpatrolledletter', 'title' => 'recentchanges-label-unpatrolled', 'legend' => 'recentchanges-legend-unpatrolled', 'grouping' => 'any', ], ], 'WatchlistExpiry' => false, 'EnableWatchstarPopover' => false, 'EnableWatchlistLabels' => false, 'WatchlistLabelsMaxPerUser' => 100, 'WatchlistPurgeRate' => 0.1, 'WatchlistExpiryMaxDuration' => '1 year', 'EnableChangesListQueryPartitioning' => false, 'RightsPage' => null, 'RightsUrl' => null, 'RightsText' => null, 'RightsIcon' => null, 'UseCopyrightUpload' => false, 'MaxCredits' => 0, 'ShowCreditsIfMax' => true, 'ImportSources' => [ ], 'ImportTargetNamespace' => null, 'ExportAllowHistory' => true, 'ExportMaxHistory' => 0, 'ExportAllowListContributors' => false, 'ExportMaxLinkDepth' => 0, 'ExportFromNamespaces' => false, 'ExportAllowAll' => false, 'ExportPagelistLimit' => 5000, 'XmlDumpSchemaVersion' => '0.11', 'WikiFarmSettingsDirectory' => null, 'WikiFarmSettingsExtension' => 'yaml', 'ExtensionFunctions' => [ ], 'ExtensionMessagesFiles' => [ ], 'MessagesDirs' => [ ], 'TranslationAliasesDirs' => [ ], 'ExtensionEntryPointListFiles' => [ ], 'EnableParserLimitReporting' => true, 'ValidSkinNames' => [ ], 'SpecialPages' => [ ], 'ExtensionCredits' => [ ], 'Hooks' => [ ], 'ServiceWiringFiles' => [ ], 'JobClasses' => [ 'deletePage' => 'MediaWiki\\Page\\DeletePageJob', 'refreshLinks' => 'MediaWiki\\JobQueue\\Jobs\\RefreshLinksJob', 'deleteLinks' => 'MediaWiki\\Page\\DeleteLinksJob', 'htmlCacheUpdate' => 'MediaWiki\\JobQueue\\Jobs\\HTMLCacheUpdateJob', 'sendMail' => [ 'class' => 'MediaWiki\\Mail\\EmaillingJob', 'services' => [ 'Emailer', ], ], 'enotifNotify' => [ 'class' => 'MediaWiki\\RecentChanges\\RecentChangeNotifyJob', 'services' => [ 'RecentChangeLookup', ], ], 'fixDoubleRedirect' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\DoubleRedirectJob', 'services' => [ 'RevisionLookup', 'MagicWordFactory', 'WikiPageFactory', ], 'needsPage' => true, ], 'AssembleUploadChunks' => 'MediaWiki\\JobQueue\\Jobs\\AssembleUploadChunksJob', 'PublishStashedFile' => 'MediaWiki\\JobQueue\\Jobs\\PublishStashedFileJob', 'ThumbnailRender' => 'MediaWiki\\JobQueue\\Jobs\\ThumbnailRenderJob', 'UploadFromUrl' => 'MediaWiki\\JobQueue\\Jobs\\UploadFromUrlJob', 'recentChangesUpdate' => 'MediaWiki\\RecentChanges\\RecentChangesUpdateJob', 'refreshLinksPrioritized' => 'MediaWiki\\JobQueue\\Jobs\\RefreshLinksJob', 'refreshLinksDynamic' => 'MediaWiki\\JobQueue\\Jobs\\RefreshLinksJob', 'activityUpdateJob' => 'MediaWiki\\Watchlist\\ActivityUpdateJob', 'categoryMembershipChange' => [ 'class' => 'MediaWiki\\RecentChanges\\CategoryMembershipChangeJob', 'services' => [ 'RecentChangeFactory', ], ], 'CategoryCountUpdateJob' => [ 'class' => 'MediaWiki\\JobQueue\\Jobs\\CategoryCountUpdateJob', 'services' => [ 'ConnectionProvider', 'NamespaceInfo', ], ], 'clearUserWatchlist' => 'MediaWiki\\Watchlist\\ClearUserWatchlistJob', 'watchlistExpiry' => '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', 'Promise-Non-Write-API-Action', 'Access-Control-Max-Age', 'Authorization', ], 'RestAPIAdditionalRouteFiles' => [ ], 'RestSandboxSpecs' => [ ], 'RestModuleOverrides' => [ ], '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, 'GenerateReqIDFormat' => 'rand24', '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, 'UsePostprocCacheLegacy' => false, 'UsePostprocCacheParsoid' => true, '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', ], 'ThumbnailBuckets' => [ 'array', 'null', ], 'UploadThumbnailRenderMap' => 'object', 'GalleryOptions' => 'object', 'DjvuDump' => [ 'string', 'null', ], 'DjvuRenderer' => [ 'string', 'null', ], 'DjvuTxt' => [ 'string', 'null', ], 'DjvuPostProcessor' => [ 'string', 'null', ], 'SMTP' => [ 'boolean', 'object', ], 'EnotifFromEditor' => 'boolean', 'EmailConfirmationBanner' => 'boolean', 'EnotifRevealEditorAddress' => 'boolean', 'UsersNotifiedOnAllChanges' => 'object', 'DBmwschema' => [ 'string', 'null', ], 'SharedTables' => 'array', 'DBservers' => [ 'boolean', 'array', ], 'LBFactoryConf' => 'object', 'LocalDatabases' => 'array', 'VirtualDomainsMapping' => 'object', 'FileSchemaMigrationStage' => 'integer', 'ExternalLinksDomainGaps' => 'object', 'ContentHandlers' => 'object', 'NamespaceContentModels' => 'object', 'TextModelsToParse' => 'array', 'ExternalStores' => 'array', 'ExternalServers' => 'object', 'DefaultExternalStore' => [ 'array', 'boolean', ], 'RevisionCacheExpiry' => 'integer', 'PageLanguageUseDB' => 'boolean', 'DiffEngine' => [ 'string', 'null', ], 'ExternalDiffEngine' => [ 'string', 'boolean', ], 'Wikidiff2Options' => 'object', 'RequestTimeLimit' => [ 'integer', 'null', ], 'CriticalSectionTimeLimit' => 'number', 'PoolCounterConf' => [ 'object', 'null', ], 'PoolCountClientConf' => 'object', 'MaxUserDBWriteDuration' => [ 'integer', 'boolean', ], 'MaxJobDBWriteDuration' => [ 'integer', 'boolean', ], 'MultiShardSiteStats' => 'boolean', 'ObjectCaches' => 'object', 'WANObjectCache' => 'object', 'MicroStashType' => [ 'string', 'integer', ], 'ParsoidCacheConfig' => 'object', 'ParsoidSelectiveUpdateSampleRate' => 'integer', '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', 'NamespacesWithoutAutoSummaries' => 'array', 'ContentNamespaces' => 'array', 'ShortPagesNamespaceExclusions' => 'array', 'ExtraSignatureNamespaces' => 'array', 'InvalidRedirectTargets' => 'array', 'LocalInterwikis' => 'array', 'InterwikiCache' => [ 'boolean', 'object', ], 'SiteTypes' => 'object', 'UrlProtocols' => 'array', 'TidyConfig' => 'object', 'ParsoidSettings' => 'object', 'ParsoidExperimentalParserFunctionOutput' => 'boolean', 'NoFollowNsExceptions' => 'array', 'NoFollowDomainExceptions' => 'array', 'ExternalLinksIgnoreDomains' => 'array', 'EnableMagicLinks' => 'object', 'ManualRevertSearchRadius' => 'integer', 'RevertedTagMaxDepth' => 'integer', 'CentralIdLookupProviders' => 'object', 'CentralIdLookupProvider' => 'string', 'UserRegistrationProviders' => 'object', 'PasswordPolicy' => 'object', 'AuthManagerConfig' => [ 'object', 'null', ], 'AuthManagerAutoConfig' => 'object', 'RememberMe' => 'string', 'ReauthenticateTime' => 'object', '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', ], 'BotPasswordsLimit' => 'integer', 'CSPHeader' => [ 'boolean', 'object', ], 'CSPReportOnlyHeader' => [ 'boolean', 'object', ], 'CSPUseReportURIDirective' => [ 'boolean', 'object', ], 'CSPFalsePositiveUrls' => 'object', 'AllowCrossOrigin' => 'boolean', 'RestAllowCrossOriginCookieAuth' => 'boolean', 'CookieSameSite' => [ 'string', 'null', ], 'CacheVaryCookies' => 'array', 'TrxProfilerLimits' => 'object', 'DebugLogGroups' => 'object', 'MWLoggerDefaultSpi' => 'object', 'Profiler' => 'object', 'StatsTarget' => [ 'string', 'null', ], 'StatsFormat' => [ 'string', 'null', ], 'StatsPrefix' => 'string', 'OpenTelemetryConfig' => [ 'object', 'null', ], 'OpenSearchTemplates' => 'object', 'NamespacesToBeSearchedDefault' => 'object', 'SitemapNamespaces' => [ 'boolean', 'array', ], 'SitemapNamespacesPriorities' => [ 'boolean', 'object', ], 'SitemapApiConfig' => 'object', 'SpecialSearchFormOptions' => 'object', 'SearchMatchRedirectPreference' => 'boolean', 'SearchRunSuggestedQuery' => 'boolean', 'PreviewOnOpenNamespaces' => 'object', 'ReadOnlyWatchedItemStore' => 'boolean', 'GitRepositoryViewers' => 'object', 'InstallerInitialPages' => 'array', 'RCLinkLimits' => 'array', 'RCLinkDays' => 'array', 'RCFeeds' => 'object', 'OverrideSiteFeed' => 'object', 'FeedClasses' => 'object', 'AdvertisedFeedTypes' => 'array', 'SoftwareTags' => 'object', 'RecentChangesFlags' => 'object', 'WatchlistExpiry' => 'boolean', 'EnableWatchstarPopover' => 'boolean', 'EnableWatchlistLabels' => 'boolean', 'WatchlistLabelsMaxPerUser' => 'integer', 'WatchlistPurgeRate' => 'number', 'WatchlistExpiryMaxDuration' => [ 'string', 'null', ], 'EnableChangesListQueryPartitioning' => 'boolean', 'ImportSources' => 'object', 'ExtensionFunctions' => 'array', 'ExtensionMessagesFiles' => 'object', 'MessagesDirs' => 'object', 'TranslationAliasesDirs' => 'object', 'ExtensionEntryPointListFiles' => 'object', 'ValidSkinNames' => 'object', 'SpecialPages' => 'object', 'ExtensionCredits' => 'object', 'Hooks' => 'object', 'ServiceWiringFiles' => 'array', 'JobClasses' => 'object', 'JobTypesExcludedFromDefaultQueue' => 'array', 'JobBackoffThrottling' => 'object', 'JobTypeConf' => 'object', 'SpecialPageCacheUpdates' => 'object', 'PagePropLinkInvalidations' => 'object', 'TempCategoryCollations' => 'array', 'SortedCategories' => 'boolean', 'TrackingCategories' => 'array', 'LogTypes' => 'array', 'LogRestrictions' => 'object', 'FilterLogTypes' => 'object', 'LogNames' => 'object', 'LogHeaders' => 'object', 'LogActions' => 'object', 'LogActionsHandlers' => 'object', 'ActionFilteredLogs' => 'object', 'RangeContributionsCIDRLimit' => 'object', 'Actions' => 'object', 'NamespaceRobotPolicies' => 'object', 'ArticleRobotPolicies' => 'object', 'ExemptFromUserRobotsControl' => [ 'array', 'null', ], 'APIModules' => 'object', 'APIFormatModules' => 'object', 'APIMetaModules' => 'object', 'APIPropModules' => 'object', 'APIListModules' => 'object', 'APIUselessQueryPages' => 'array', 'CrossSiteAJAXdomains' => 'object', 'CrossSiteAJAXdomainExceptions' => 'object', 'AllowedCorsHeaders' => 'array', 'RestAPIAdditionalRouteFiles' => 'array', 'RestSandboxSpecs' => 'object', 'RestModuleOverrides' => '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', ], 'GenerateReqIDFormat' => 'string', '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', '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', 'RestModuleOverrides' => 'array_replace_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', ], 'file' => [ 'type' => 'string', ], 'msg' => [ 'type' => 'string', 'description' => 'a message key', ], ], ], ], 'RestModuleOverrides' => [ 'additionalProperties' => [ 'type' => 'object', 'properties' => [ 'mode' => [ 'type' => 'string', ], ], 'required' => [ 'mode', ], ], ], '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)