Passed
Push — master ( c88656...e5f428 )
by MusikAnimal
08:08
created

AppExtension::generateYear()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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