1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* This file contains only the AppExtension class. |
4
|
|
|
*/ |
5
|
|
|
|
6
|
|
|
declare(strict_types = 1); |
7
|
|
|
|
8
|
|
|
namespace App\Twig; |
9
|
|
|
|
10
|
|
|
use App\Helper\I18nHelper; |
11
|
|
|
use App\Model\Edit; |
12
|
|
|
use App\Model\Project; |
13
|
|
|
use App\Model\User; |
14
|
|
|
use App\Repository\ProjectRepository; |
15
|
|
|
use DateTime; |
16
|
|
|
use Symfony\Component\DependencyInjection\ContainerInterface; |
17
|
|
|
use Symfony\Component\HttpFoundation\Request; |
18
|
|
|
use Symfony\Component\HttpFoundation\RequestStack; |
19
|
|
|
use Symfony\Component\HttpFoundation\Session\SessionInterface; |
20
|
|
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; |
21
|
|
|
use Twig\Extension\AbstractExtension; |
22
|
|
|
use Twig\TwigFilter; |
23
|
|
|
use Twig\TwigFunction; |
24
|
|
|
use Wikimedia\IPUtils; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* Twig functions and filters for XTools. |
28
|
|
|
*/ |
29
|
|
|
class AppExtension extends AbstractExtension |
30
|
|
|
{ |
31
|
|
|
/** @var ContainerInterface The application's container interface. */ |
32
|
|
|
protected $container; |
33
|
|
|
|
34
|
|
|
/** @var RequestStack The request stack. */ |
35
|
|
|
protected $requestStack; |
36
|
|
|
|
37
|
|
|
/** @var SessionInterface User's current session. */ |
38
|
|
|
protected $session; |
39
|
|
|
|
40
|
|
|
/** @var I18nHelper For i18n and l10n. */ |
41
|
|
|
protected $i18n; |
42
|
|
|
|
43
|
|
|
/** @var float Duration of the current HTTP request in seconds. */ |
44
|
|
|
protected $requestTime; |
45
|
|
|
|
46
|
|
|
/** @var UrlGeneratorInterface */ |
47
|
|
|
private $urlGenerator; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* Constructor, with the I18nHelper through dependency injection. |
51
|
|
|
* @param ContainerInterface $container |
52
|
|
|
* @param RequestStack $requestStack |
53
|
|
|
* @param SessionInterface $session |
54
|
|
|
* @param I18nHelper $i18n |
55
|
|
|
* @param UrlGeneratorInterface $generator |
56
|
|
|
*/ |
57
|
|
|
public function __construct( |
58
|
|
|
ContainerInterface $container, |
59
|
|
|
RequestStack $requestStack, |
60
|
|
|
SessionInterface $session, |
61
|
|
|
I18nHelper $i18n, |
62
|
|
|
UrlGeneratorInterface $generator |
63
|
|
|
) { |
64
|
|
|
$this->container = $container; |
65
|
|
|
$this->requestStack = $requestStack; |
66
|
|
|
$this->session = $session; |
67
|
|
|
$this->i18n = $i18n; |
68
|
|
|
$this->urlGenerator = $generator; |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
/*********************************** FUNCTIONS ***********************************/ |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Get all functions that this class provides. |
75
|
|
|
* @return TwigFunction[] |
76
|
|
|
* @codeCoverageIgnore |
77
|
|
|
*/ |
78
|
|
|
public function getFunctions(): array |
79
|
|
|
{ |
80
|
|
|
return [ |
81
|
|
|
new TwigFunction('request_time', [$this, 'requestTime']), |
82
|
|
|
new TwigFunction('memory_usage', [$this, 'requestMemory']), |
83
|
|
|
new TwigFunction('msgIfExists', [$this, 'msgIfExists'], ['is_safe' => ['html']]), |
84
|
|
|
new TwigFunction('msgExists', [$this, 'msgExists'], ['is_safe' => ['html']]), |
85
|
|
|
new TwigFunction('msg', [$this, 'msg'], ['is_safe' => ['html']]), |
86
|
|
|
new TwigFunction('lang', [$this, 'getLang']), |
87
|
|
|
new TwigFunction('langName', [$this, 'getLangName']), |
88
|
|
|
new TwigFunction('fallbackLangs', [$this, 'getFallbackLangs']), |
89
|
|
|
new TwigFunction('allLangs', [$this, 'getAllLangs']), |
90
|
|
|
new TwigFunction('isRTL', [$this, 'isRTL']), |
91
|
|
|
new TwigFunction('shortHash', [$this, 'gitShortHash']), |
92
|
|
|
new TwigFunction('hash', [$this, 'gitHash']), |
93
|
|
|
new TwigFunction('releaseDate', [$this, 'gitDate']), |
94
|
|
|
new TwigFunction('enabled', [$this, 'toolEnabled']), |
95
|
|
|
new TwigFunction('tools', [$this, 'tools']), |
96
|
|
|
new TwigFunction('color', [$this, 'getColorList']), |
97
|
|
|
new TwigFunction('chartColor', [$this, 'chartColor']), |
98
|
|
|
new TwigFunction('isSingleWiki', [$this, 'isSingleWiki']), |
99
|
|
|
new TwigFunction('getReplagThreshold', [$this, 'getReplagThreshold']), |
100
|
|
|
new TwigFunction('isWMFLabs', [$this, 'isWMFLabs']), |
101
|
|
|
new TwigFunction('replag', [$this, 'replag']), |
102
|
|
|
new TwigFunction('quote', [$this, 'quote']), |
103
|
|
|
new TwigFunction('bugReportURL', [$this, 'bugReportURL']), |
104
|
|
|
new TwigFunction('logged_in_user', [$this, 'loggedInUser']), |
105
|
|
|
new TwigFunction('isUserAnon', [$this, 'isUserAnon']), |
106
|
|
|
new TwigFunction('nsName', [$this, 'nsName']), |
107
|
|
|
new TwigFunction('titleWithNs', [$this, 'titleWithNs']), |
108
|
|
|
new TwigFunction('formatDuration', [$this, 'formatDuration']), |
109
|
|
|
new TwigFunction('numberFormat', [$this, 'numberFormat']), |
110
|
|
|
new TwigFunction('buildQuery', [$this, 'buildQuery']), |
111
|
|
|
new TwigFunction('login_url', [$this, 'loginUrl']), |
112
|
|
|
]; |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
/** |
116
|
|
|
* Get the duration of the current HTTP request in seconds. |
117
|
|
|
* @return float |
118
|
|
|
* Untestable since there is no request stack in the tests. |
119
|
|
|
* @codeCoverageIgnore |
120
|
|
|
*/ |
121
|
|
|
public function requestTime(): float |
122
|
|
|
{ |
123
|
|
|
if (!isset($this->requestTime)) { |
124
|
|
|
$this->requestTime = microtime(true) - $this->getRequest()->server->get('REQUEST_TIME_FLOAT'); |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
return $this->requestTime; |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
/** |
131
|
|
|
* Get the formatted real memory usage. |
132
|
|
|
* @return float |
133
|
|
|
*/ |
134
|
|
|
public function requestMemory(): float |
135
|
|
|
{ |
136
|
|
|
$mem = memory_get_usage(false); |
137
|
|
|
$div = pow(1024, 2); |
138
|
|
|
return $mem / $div; |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
/** |
142
|
|
|
* Get an i18n message. |
143
|
|
|
* @param string $message |
144
|
|
|
* @param string[] $vars |
145
|
|
|
* @return string|null |
146
|
|
|
*/ |
147
|
|
|
public function msg(string $message = '', array $vars = []): ?string |
148
|
|
|
{ |
149
|
|
|
return $this->i18n->msg($message, $vars); |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* See if a given i18n message exists. |
154
|
|
|
* @param string $message The message. |
155
|
|
|
* @param string[] $vars |
156
|
|
|
* @return bool |
157
|
|
|
*/ |
158
|
|
|
public function msgExists(?string $message, array $vars = []): bool |
159
|
|
|
{ |
160
|
|
|
return $this->i18n->msgExists($message, $vars); |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* Get an i18n message if it exists, otherwise just get the message key. |
165
|
|
|
* @param string $message |
166
|
|
|
* @param string[] $vars |
167
|
|
|
* @return string |
168
|
|
|
*/ |
169
|
|
|
public function msgIfExists(?string $message, array $vars = []): string |
170
|
|
|
{ |
171
|
|
|
return $this->i18n->msgIfExists($message, $vars); |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
/** |
175
|
|
|
* Get the current language code. |
176
|
|
|
* @return string |
177
|
|
|
*/ |
178
|
|
|
public function getLang(): string |
179
|
|
|
{ |
180
|
|
|
return $this->i18n->getLang(); |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
/** |
184
|
|
|
* Get the current language name (defaults to 'English'). |
185
|
|
|
* @return string |
186
|
|
|
*/ |
187
|
|
|
public function getLangName(): string |
188
|
|
|
{ |
189
|
|
|
return $this->i18n->getLangName(); |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* Get the fallback languages for the current language, so we know what to load with jQuery.i18n. |
194
|
|
|
* @return string[] |
195
|
|
|
*/ |
196
|
|
|
public function getFallbackLangs(): array |
197
|
|
|
{ |
198
|
|
|
return $this->i18n->getFallbacks(); |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
/** |
202
|
|
|
* Get all available languages in the i18n directory |
203
|
|
|
* @return string[] Associative array of langKey => langName |
204
|
|
|
*/ |
205
|
|
|
public function getAllLangs(): array |
206
|
|
|
{ |
207
|
|
|
return $this->i18n->getAllLangs(); |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* Whether the current language is right-to-left. |
212
|
|
|
* @param string|null $lang Optionally provide a specific lanuage code. |
213
|
|
|
* @return bool |
214
|
|
|
*/ |
215
|
|
|
public function isRTL(?string $lang = null): bool |
216
|
|
|
{ |
217
|
|
|
return $this->i18n->isRTL($lang); |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* Get the short hash of the currently checked-out Git commit. |
222
|
|
|
* @return string |
223
|
|
|
*/ |
224
|
|
|
public function gitShortHash(): string |
225
|
|
|
{ |
226
|
|
|
return exec('git rev-parse --short HEAD'); |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
/** |
230
|
|
|
* Get the full hash of the currently checkout-out Git commit. |
231
|
|
|
* @return string |
232
|
|
|
*/ |
233
|
|
|
public function gitHash(): string |
234
|
|
|
{ |
235
|
|
|
return exec('git rev-parse HEAD'); |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
/** |
239
|
|
|
* Get the date of the HEAD commit. |
240
|
|
|
* @return string |
241
|
|
|
*/ |
242
|
|
|
public function gitDate(): string |
243
|
|
|
{ |
244
|
|
|
$date = new DateTime(exec('git show -s --format=%ci')); |
245
|
|
|
return $this->dateFormat($date, 'yyyy-MM-dd'); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* Check whether a given tool is enabled. |
250
|
|
|
* @param string $tool The short name of the tool. |
251
|
|
|
* @return bool |
252
|
|
|
*/ |
253
|
|
|
public function toolEnabled(string $tool = 'index'): bool |
254
|
|
|
{ |
255
|
|
|
$param = false; |
256
|
|
|
if ($this->container->hasParameter("enable.$tool")) { |
257
|
|
|
$param = boolval($this->container->getParameter("enable.$tool")); |
258
|
|
|
} |
259
|
|
|
return $param; |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
/** |
263
|
|
|
* Get a list of the short names of all tools. |
264
|
|
|
* @return string[] |
265
|
|
|
*/ |
266
|
|
|
public function tools(): array |
267
|
|
|
{ |
268
|
|
|
$retVal = []; |
269
|
|
|
if ($this->container->hasParameter('tools')) { |
270
|
|
|
$retVal = $this->container->getParameter('tools'); |
271
|
|
|
} |
272
|
|
|
return $retVal; |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* Get a list of namespace colours (one or all). |
277
|
|
|
* @param int|false $num The NS ID to get. False to get the full list. |
278
|
|
|
* @return string|string[] Color or all all colors indexed by namespace ID. |
279
|
|
|
*/ |
280
|
|
|
public static function getColorList($num = false) |
281
|
|
|
{ |
282
|
|
|
$colors = [ |
283
|
|
|
0 => '#FF5555', |
284
|
|
|
1 => '#55FF55', |
285
|
|
|
2 => '#FFEE22', |
286
|
|
|
3 => '#FF55FF', |
287
|
|
|
4 => '#5555FF', |
288
|
|
|
5 => '#55FFFF', |
289
|
|
|
6 => '#C00000', |
290
|
|
|
7 => '#0000C0', |
291
|
|
|
8 => '#008800', |
292
|
|
|
9 => '#00C0C0', |
293
|
|
|
10 => '#FFAFAF', |
294
|
|
|
11 => '#808080', |
295
|
|
|
12 => '#00C000', |
296
|
|
|
13 => '#404040', |
297
|
|
|
14 => '#C0C000', |
298
|
|
|
15 => '#C000C0', |
299
|
|
|
90 => '#991100', |
300
|
|
|
91 => '#99FF00', |
301
|
|
|
92 => '#000000', |
302
|
|
|
93 => '#777777', |
303
|
|
|
100 => '#75A3D1', |
304
|
|
|
101 => '#A679D2', |
305
|
|
|
102 => '#660000', |
306
|
|
|
103 => '#000066', |
307
|
|
|
104 => '#FAFFAF', |
308
|
|
|
105 => '#408345', |
309
|
|
|
106 => '#5c8d20', |
310
|
|
|
107 => '#e1711d', |
311
|
|
|
108 => '#94ef2b', |
312
|
|
|
109 => '#756a4a', |
313
|
|
|
110 => '#6f1dab', |
314
|
|
|
111 => '#301e30', |
315
|
|
|
112 => '#5c9d96', |
316
|
|
|
113 => '#a8cd8c', |
317
|
|
|
114 => '#f2b3f1', |
318
|
|
|
115 => '#9b5828', |
319
|
|
|
116 => '#002288', |
320
|
|
|
117 => '#0000CC', |
321
|
|
|
118 => '#99FFFF', |
322
|
|
|
119 => '#99BBFF', |
323
|
|
|
120 => '#FF99FF', |
324
|
|
|
121 => '#CCFFFF', |
325
|
|
|
122 => '#CCFF00', |
326
|
|
|
123 => '#CCFFCC', |
327
|
|
|
200 => '#33FF00', |
328
|
|
|
201 => '#669900', |
329
|
|
|
202 => '#666666', |
330
|
|
|
203 => '#999999', |
331
|
|
|
204 => '#FFFFCC', |
332
|
|
|
205 => '#FF00CC', |
333
|
|
|
206 => '#FFFF00', |
334
|
|
|
207 => '#FFCC00', |
335
|
|
|
208 => '#FF0000', |
336
|
|
|
209 => '#FF6600', |
337
|
|
|
250 => '#6633CC', |
338
|
|
|
251 => '#6611AA', |
339
|
|
|
252 => '#66FF99', |
340
|
|
|
253 => '#66FF66', |
341
|
|
|
446 => '#06DCFB', |
342
|
|
|
447 => '#892EE4', |
343
|
|
|
460 => '#99FF66', |
344
|
|
|
461 => '#99CC66', |
345
|
|
|
470 => '#CCCC33', |
346
|
|
|
471 => '#CCFF33', |
347
|
|
|
480 => '#6699FF', |
348
|
|
|
481 => '#66FFFF', |
349
|
|
|
484 => '#07C8D6', |
350
|
|
|
485 => '#2AF1FF', |
351
|
|
|
486 => '#79CB21', |
352
|
|
|
487 => '#80D822', |
353
|
|
|
490 => '#995500', |
354
|
|
|
491 => '#998800', |
355
|
|
|
710 => '#FFCECE', |
356
|
|
|
711 => '#FFC8F2', |
357
|
|
|
828 => '#F7DE00', |
358
|
|
|
829 => '#BABA21', |
359
|
|
|
866 => '#FFFFFF', |
360
|
|
|
867 => '#FFCCFF', |
361
|
|
|
1198 => '#FF34B3', |
362
|
|
|
1199 => '#8B1C62', |
363
|
|
|
2300 => '#A900B8', |
364
|
|
|
2301 => '#C93ED6', |
365
|
|
|
2302 => '#8A09C1', |
366
|
|
|
2303 => '#974AB8', |
367
|
|
|
2600 => '#000000', |
368
|
|
|
]; |
369
|
|
|
|
370
|
|
|
if (false === $num) { |
371
|
|
|
return $colors; |
372
|
|
|
} elseif (isset($colors[$num])) { |
373
|
|
|
return $colors[$num]; |
374
|
|
|
} else { |
375
|
|
|
// Default to grey. |
376
|
|
|
return '#CCC'; |
377
|
|
|
} |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
/** |
381
|
|
|
* Get color-blind friendly colors for use in charts |
382
|
|
|
* @param int $num Index of color |
383
|
|
|
* @return string RGBA color (so you can more easily adjust the opacity) |
384
|
|
|
*/ |
385
|
|
|
public function chartColor(int $num): string |
386
|
|
|
{ |
387
|
|
|
$colors = [ |
388
|
|
|
'rgba(171, 212, 235, 1)', |
389
|
|
|
'rgba(178, 223, 138, 1)', |
390
|
|
|
'rgba(251, 154, 153, 1)', |
391
|
|
|
'rgba(253, 191, 111, 1)', |
392
|
|
|
'rgba(202, 178, 214, 1)', |
393
|
|
|
'rgba(207, 182, 128, 1)', |
394
|
|
|
'rgba(141, 211, 199, 1)', |
395
|
|
|
'rgba(252, 205, 229, 1)', |
396
|
|
|
'rgba(255, 247, 161, 1)', |
397
|
|
|
'rgba(252, 146, 114, 1)', |
398
|
|
|
'rgba(217, 217, 217, 1)', |
399
|
|
|
]; |
400
|
|
|
|
401
|
|
|
return $colors[$num % count($colors)]; |
402
|
|
|
} |
403
|
|
|
|
404
|
|
|
/** |
405
|
|
|
* Whether XTools is running in single-project mode. |
406
|
|
|
* @return bool |
407
|
|
|
*/ |
408
|
|
|
public function isSingleWiki(): bool |
409
|
|
|
{ |
410
|
|
|
$param = true; |
411
|
|
|
if ($this->container->hasParameter('app.single_wiki')) { |
412
|
|
|
$param = boolval($this->container->getParameter('app.single_wiki')); |
413
|
|
|
} |
414
|
|
|
return $param; |
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
/** |
418
|
|
|
* Get the database replication-lag threshold. |
419
|
|
|
* @return int |
420
|
|
|
*/ |
421
|
|
|
public function getReplagThreshold(): int |
422
|
|
|
{ |
423
|
|
|
$param = 30; |
424
|
|
|
if ($this->container->hasParameter('app.replag_threshold')) { |
425
|
|
|
$param = $this->container->getParameter('app.replag_threshold'); |
426
|
|
|
} |
427
|
|
|
return $param; |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
/** |
431
|
|
|
* Whether XTools is running in WMF Labs mode. |
432
|
|
|
* @return bool |
433
|
|
|
*/ |
434
|
|
|
public function isWMFLabs(): bool |
435
|
|
|
{ |
436
|
|
|
$param = false; |
437
|
|
|
if ($this->container->hasParameter('app.is_labs')) { |
438
|
|
|
$param = boolval($this->container->getParameter('app.is_labs')); |
439
|
|
|
} |
440
|
|
|
return $param; |
441
|
|
|
} |
442
|
|
|
|
443
|
|
|
/** |
444
|
|
|
* The current replication lag. |
445
|
|
|
* @return int |
446
|
|
|
* @codeCoverageIgnore |
447
|
|
|
*/ |
448
|
|
|
public function replag(): int |
449
|
|
|
{ |
450
|
|
|
$projectIdent = $this->getRequest()->get('project', 'enwiki'); |
451
|
|
|
$project = ProjectRepository::getProject($projectIdent, $this->container); |
452
|
|
|
$dbName = $project->getDatabaseName(); |
453
|
|
|
$sql = "SELECT lag FROM `heartbeat_p`.`heartbeat`"; |
454
|
|
|
return (int)$project->getRepository()->executeProjectsQuery($project, $sql, [ |
|
|
|
|
455
|
|
|
'project' => $dbName, |
456
|
|
|
])->fetchColumn(); |
457
|
|
|
} |
458
|
|
|
|
459
|
|
|
/** |
460
|
|
|
* Get a random quote for the footer |
461
|
|
|
* @return string |
462
|
|
|
*/ |
463
|
|
|
public function quote(): string |
464
|
|
|
{ |
465
|
|
|
// Don't show if Quote is turned off, but always show for Labs |
466
|
|
|
// (so quote is in footer but not in nav). |
467
|
|
|
$isLabs = $this->container->getParameter('app.is_labs'); |
468
|
|
|
if (!$isLabs && !$this->container->getParameter('enable.Quote')) { |
469
|
|
|
return ''; |
470
|
|
|
} |
471
|
|
|
$quotes = $this->container->getParameter('quotes'); |
472
|
|
|
$id = array_rand($quotes); |
473
|
|
|
return $quotes[$id]; |
474
|
|
|
} |
475
|
|
|
|
476
|
|
|
/** |
477
|
|
|
* Get the currently logged in user's details. |
478
|
|
|
* @return string[]|object|null |
479
|
|
|
*/ |
480
|
|
|
public function loggedInUser() |
481
|
|
|
{ |
482
|
|
|
return $this->container->get('session')->get('logged_in_user'); |
483
|
|
|
} |
484
|
|
|
|
485
|
|
|
/** |
486
|
|
|
* Get a URL to the login route with parameters to redirect back to the current page after logging in. |
487
|
|
|
* @param Request $request |
488
|
|
|
* @return string |
489
|
|
|
*/ |
490
|
|
|
public function loginUrl(Request $request): string |
491
|
|
|
{ |
492
|
|
|
return $this->urlGenerator->generate('login', [ |
493
|
|
|
'callback' => $this->urlGenerator->generate( |
494
|
|
|
'oauth_callback', |
495
|
|
|
['redirect' => $request->getUri()], |
496
|
|
|
UrlGeneratorInterface::ABSOLUTE_URL |
497
|
|
|
), |
498
|
|
|
], UrlGeneratorInterface::ABSOLUTE_URL); |
499
|
|
|
} |
500
|
|
|
|
501
|
|
|
/*********************************** FILTERS ***********************************/ |
502
|
|
|
|
503
|
|
|
/** |
504
|
|
|
* Get all filters for this extension. |
505
|
|
|
* @return TwigFilter[] |
506
|
|
|
* @codeCoverageIgnore |
507
|
|
|
*/ |
508
|
|
|
public function getFilters(): array |
509
|
|
|
{ |
510
|
|
|
return [ |
511
|
|
|
new TwigFilter('ucfirst', [$this, 'capitalizeFirst']), |
512
|
|
|
new TwigFilter('percent_format', [$this, 'percentFormat']), |
513
|
|
|
new TwigFilter('diff_format', [$this, 'diffFormat'], ['is_safe' => ['html']]), |
514
|
|
|
new TwigFilter('num_format', [$this, 'numberFormat']), |
515
|
|
|
new TwigFilter('size_format', [$this, 'sizeFormat']), |
516
|
|
|
new TwigFilter('date_format', [$this, 'dateFormat']), |
517
|
|
|
new TwigFilter('wikify', [$this, 'wikify']), |
518
|
|
|
]; |
519
|
|
|
} |
520
|
|
|
|
521
|
|
|
/** |
522
|
|
|
* Format a number based on language settings. |
523
|
|
|
* @param int|float $number |
524
|
|
|
* @param int $decimals Number of decimals to format to. |
525
|
|
|
* @return string |
526
|
|
|
*/ |
527
|
|
|
public function numberFormat($number, int $decimals = 0): string |
528
|
|
|
{ |
529
|
|
|
return $this->i18n->numberFormat($number, $decimals); |
530
|
|
|
} |
531
|
|
|
|
532
|
|
|
/** |
533
|
|
|
* Format the given size (in bytes) as KB, MB, GB, or TB. |
534
|
|
|
* Some code courtesy of Leo, CC BY-SA 4.0 |
535
|
|
|
* @see https://stackoverflow.com/a/2510459/604142 |
536
|
|
|
* @param int $bytes |
537
|
|
|
* @param int $precision |
538
|
|
|
* @return string |
539
|
|
|
*/ |
540
|
|
|
public function sizeFormat(int $bytes, int $precision = 2): string |
541
|
|
|
{ |
542
|
|
|
$base = log($bytes, 1024); |
543
|
|
|
$suffixes = ['', 'kilobytes', 'megabytes', 'gigabytes', 'terabytes']; |
544
|
|
|
|
545
|
|
|
$index = floor($base); |
546
|
|
|
|
547
|
|
|
if (0 === (int)$index) { |
548
|
|
|
return $this->numberFormat($bytes); |
549
|
|
|
} |
550
|
|
|
|
551
|
|
|
$sizeMessage = $this->numberFormat( |
552
|
|
|
pow(1024, $base - floor($base)), |
553
|
|
|
$precision |
554
|
|
|
); |
555
|
|
|
|
556
|
|
|
return $this->i18n->msg('size-'.$suffixes[floor($base)], [$sizeMessage]); |
557
|
|
|
} |
558
|
|
|
|
559
|
|
|
/** |
560
|
|
|
* Localize the given date based on language settings. |
561
|
|
|
* @param string|int|DateTime $datetime |
562
|
|
|
* @param string $pattern Format according to this ICU date format. |
563
|
|
|
* @see http://userguide.icu-project.org/formatparse/datetime |
564
|
|
|
* @return string |
565
|
|
|
*/ |
566
|
|
|
public function dateFormat($datetime, $pattern = 'yyyy-MM-dd HH:mm'): string |
567
|
|
|
{ |
568
|
|
|
return $this->i18n->dateFormat($datetime, $pattern); |
569
|
|
|
} |
570
|
|
|
|
571
|
|
|
/** |
572
|
|
|
* Convert raw wikitext to HTML-formatted string. |
573
|
|
|
* @param string $str |
574
|
|
|
* @param Project $project |
575
|
|
|
* @return string |
576
|
|
|
*/ |
577
|
|
|
public function wikify(string $str, Project $project): string |
578
|
|
|
{ |
579
|
|
|
return Edit::wikifyString($str, $project); |
580
|
|
|
} |
581
|
|
|
|
582
|
|
|
/** |
583
|
|
|
* Mysteriously missing Twig helper to capitalize only the first character. |
584
|
|
|
* E.g. used for table headings for translated messages |
585
|
|
|
* @param string $str The string |
586
|
|
|
* @return string The string, capitalized |
587
|
|
|
*/ |
588
|
|
|
public function capitalizeFirst(string $str): string |
589
|
|
|
{ |
590
|
|
|
return ucfirst($str); |
591
|
|
|
} |
592
|
|
|
|
593
|
|
|
/** |
594
|
|
|
* Format a given number or fraction as a percentage. |
595
|
|
|
* @param int|float $numerator Numerator or single fraction if denominator is ommitted. |
596
|
|
|
* @param int $denominator Denominator. |
597
|
|
|
* @param integer $precision Number of decimal places to show. |
598
|
|
|
* @return string Formatted percentage. |
599
|
|
|
*/ |
600
|
|
|
public function percentFormat($numerator, ?int $denominator = null, int $precision = 1): string |
601
|
|
|
{ |
602
|
|
|
return $this->i18n->percentFormat($numerator, $denominator, $precision); |
603
|
|
|
} |
604
|
|
|
|
605
|
|
|
/** |
606
|
|
|
* Helper to return whether the given user is an anonymous (logged out) user. |
607
|
|
|
* @param User|string $user User object or username as a string. |
608
|
|
|
* @return bool |
609
|
|
|
*/ |
610
|
|
|
public function isUserAnon($user): bool |
611
|
|
|
{ |
612
|
|
|
if ($user instanceof User) { |
613
|
|
|
$username = $user->getUsername(); |
614
|
|
|
} else { |
615
|
|
|
$username = $user; |
616
|
|
|
} |
617
|
|
|
|
618
|
|
|
return IPUtils::isIPAddress($username); |
619
|
|
|
} |
620
|
|
|
|
621
|
|
|
/** |
622
|
|
|
* Helper to properly translate a namespace name. |
623
|
|
|
* @param int|string $namespace Namespace key as a string or ID. |
624
|
|
|
* @param string[] $namespaces List of available namespaces as retrieved from Project::getNamespaces(). |
625
|
|
|
* @return string Namespace name |
626
|
|
|
*/ |
627
|
|
|
public function nsName($namespace, array $namespaces): string |
628
|
|
|
{ |
629
|
|
|
if ('all' === $namespace) { |
630
|
|
|
return $this->i18n->msg('all'); |
631
|
|
|
} elseif ('0' === $namespace || 0 === $namespace || 'Main' === $namespace) { |
632
|
|
|
return $this->i18n->msg('mainspace'); |
633
|
|
|
} else { |
634
|
|
|
return $namespaces[$namespace] ?? $this->i18n->msg('unknown'); |
635
|
|
|
} |
636
|
|
|
} |
637
|
|
|
|
638
|
|
|
/** |
639
|
|
|
* Given a page title and namespace, generate the full page title. |
640
|
|
|
* @param string $title |
641
|
|
|
* @param int $namespace |
642
|
|
|
* @param array $namespaces |
643
|
|
|
* @return string |
644
|
|
|
*/ |
645
|
|
|
public function titleWithNs(string $title, int $namespace, array $namespaces): string |
646
|
|
|
{ |
647
|
|
|
if (0 === $namespace) { |
648
|
|
|
return $title; |
649
|
|
|
} |
650
|
|
|
return $this->nsName($namespace, $namespaces).':'.$title; |
651
|
|
|
} |
652
|
|
|
|
653
|
|
|
/** |
654
|
|
|
* Format a given number as a diff, colouring it green if it's positive, red if negative, gary if zero |
655
|
|
|
* @param int $size Diff size |
656
|
|
|
* @return string Markup with formatted number |
657
|
|
|
*/ |
658
|
|
|
public function diffFormat(int $size): string |
659
|
|
|
{ |
660
|
|
|
if ($size < 0) { |
661
|
|
|
$class = 'diff-neg'; |
662
|
|
|
} elseif ($size > 0) { |
663
|
|
|
$class = 'diff-pos'; |
664
|
|
|
} else { |
665
|
|
|
$class = 'diff-zero'; |
666
|
|
|
} |
667
|
|
|
|
668
|
|
|
$size = $this->numberFormat($size); |
669
|
|
|
|
670
|
|
|
return "<span class='$class'". |
671
|
|
|
($this->i18n->isRTL() ? " dir='rtl'" : ''). |
672
|
|
|
">$size</span>"; |
673
|
|
|
} |
674
|
|
|
|
675
|
|
|
/** |
676
|
|
|
* Format a time duration as humanized string. |
677
|
|
|
* @param int $seconds Number of seconds. |
678
|
|
|
* @param bool $translate Used for unit testing. Set to false to return |
679
|
|
|
* the value and i18n key, instead of the actual translation. |
680
|
|
|
* @return string|mixed[] Examples: '30 seconds', '2 minutes', '15 hours', '500 days', |
681
|
|
|
* or [30, 'num-seconds'] (etc.) if $translate is false. |
682
|
|
|
*/ |
683
|
|
|
public function formatDuration(int $seconds, bool $translate = true) |
684
|
|
|
{ |
685
|
|
|
[$val, $key] = $this->getDurationMessageKey($seconds); |
686
|
|
|
|
687
|
|
|
if ($translate) { |
688
|
|
|
return $this->numberFormat($val).' '.$this->i18n->msg("num-$key", [$val]); |
689
|
|
|
} else { |
690
|
|
|
return [$this->numberFormat($val), "num-$key"]; |
691
|
|
|
} |
692
|
|
|
} |
693
|
|
|
|
694
|
|
|
/** |
695
|
|
|
* Given a time duration in seconds, generate a i18n message key and value. |
696
|
|
|
* @param int $seconds Number of seconds. |
697
|
|
|
* @return array<integer|string> [int - message value, string - message key] |
698
|
|
|
*/ |
699
|
|
|
private function getDurationMessageKey(int $seconds) |
700
|
|
|
{ |
701
|
|
|
/** @var int $val Value to show in message */ |
702
|
|
|
$val = $seconds; |
703
|
|
|
|
704
|
|
|
/** @var string $key Unit of time, used in the key for the i18n message */ |
705
|
|
|
$key = 'seconds'; |
706
|
|
|
|
707
|
|
|
if ($seconds >= 86400) { |
708
|
|
|
// Over a day |
709
|
|
|
$val = (int) floor($seconds / 86400); |
710
|
|
|
$key = 'days'; |
711
|
|
|
} elseif ($seconds >= 3600) { |
712
|
|
|
// Over an hour, less than a day |
713
|
|
|
$val = (int) floor($seconds / 3600); |
714
|
|
|
$key = 'hours'; |
715
|
|
|
} elseif ($seconds >= 60) { |
716
|
|
|
// Over a minute, less than an hour |
717
|
|
|
$val = (int) floor($seconds / 60); |
718
|
|
|
$key = 'minutes'; |
719
|
|
|
} |
720
|
|
|
|
721
|
|
|
return [$val, $key]; |
722
|
|
|
} |
723
|
|
|
|
724
|
|
|
/** |
725
|
|
|
* Build URL query string from given params. |
726
|
|
|
* @param string[] $params |
727
|
|
|
* @return string |
728
|
|
|
*/ |
729
|
|
|
public function buildQuery(array $params): string |
730
|
|
|
{ |
731
|
|
|
return is_array($params) ? http_build_query($params) : ''; |
|
|
|
|
732
|
|
|
} |
733
|
|
|
|
734
|
|
|
/** |
735
|
|
|
* Shorthand to get the current request from the request stack. |
736
|
|
|
* @return \Symfony\Component\HttpFoundation\Request |
737
|
|
|
* There is no request stack in the tests. |
738
|
|
|
* @codeCoverageIgnore |
739
|
|
|
*/ |
740
|
|
|
private function getRequest(): \Symfony\Component\HttpFoundation\Request |
741
|
|
|
{ |
742
|
|
|
return $this->container->get('request_stack')->getCurrentRequest(); |
743
|
|
|
} |
744
|
|
|
} |
745
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.