Passed
Push — master ( e37dfe...becd39 )
by MusikAnimal
05:00
created

AppExtension::intuitionMessageExists()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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