Completed
Push — master ( 765575...5c8b1a )
by MusikAnimal
02:22
created

AppExtension::xtApiUrl()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 7
nc 2
nop 0
1
<?php
2
/**
3
 * This file contains only the AppExtension class.
4
 */
5
6
namespace AppBundle\Twig;
7
8
use Xtools\ProjectRepository;
9
use Xtools\User;
10
11
/**
12
 * Twig functions and filters for XTools.
13
 */
14
class AppExtension extends Extension
15
{
16
17
    /**
18
     * Get the name of this extension.
19
     * @return string
20
     */
21
    public function getName()
22
    {
23
        return 'app_extension';
24
    }
25
26
    /*********************************** FUNCTIONS ***********************************/
27
28
    /**
29
     * Get all functions that this class provides.
30
     * @return array
31
     */
32
    public function getFunctions()
33
    {
34
        $options = ['is_safe' => ['html']];
35
        return [
36
            new \Twig_SimpleFunction('request_time', [ $this, 'requestTime' ], $options),
37
            new \Twig_SimpleFunction('memory_usage', [ $this, 'requestMemory' ], $options),
38
            new \Twig_SimpleFunction('year', [ $this, 'generateYear' ], $options),
39
            new \Twig_SimpleFunction('msgPrintExists', [ $this, 'intuitionMessagePrintExists' ], $options),
40
            new \Twig_SimpleFunction('msgExists', [ $this, 'intuitionMessageExists' ], $options),
41
            new \Twig_SimpleFunction('msg', [ $this, 'intuitionMessage' ], $options),
42
            new \Twig_SimpleFunction('lang', [ $this, 'getLang' ], $options),
43
            new \Twig_SimpleFunction('langName', [ $this, 'getLangName' ], $options),
44
            new \Twig_SimpleFunction('allLangs', [ $this, 'getAllLangs' ]),
45
            new \Twig_SimpleFunction('isRTL', [ $this, 'intuitionIsRTL' ]),
46
            new \Twig_SimpleFunction('isRTLLang', [ $this, 'intuitionIsRTLLang' ]),
47
            new \Twig_SimpleFunction('shortHash', [ $this, 'gitShortHash' ]),
48
            new \Twig_SimpleFunction('hash', [ $this, 'gitHash' ]),
49
            new \Twig_SimpleFunction('releaseDate', [ $this, 'gitDate' ]),
50
            new \Twig_SimpleFunction('enabled', [ $this, 'tabEnabled' ]),
51
            new \Twig_SimpleFunction('tools', [ $this, 'allTools' ]),
52
            new \Twig_SimpleFunction('color', [ $this, 'getColorList' ]),
53
            new \Twig_SimpleFunction('chartColor', [ $this, 'chartColor' ]),
54
            new \Twig_SimpleFunction('xtApiUrl', [ $this, 'xtApiUrl' ]),
55
            new \Twig_SimpleFunction('isSingleWiki', [ $this, 'isSingleWiki' ]),
56
            new \Twig_SimpleFunction('getReplagThreshold', [ $this, 'getReplagThreshold' ]),
57
            new \Twig_SimpleFunction('loadStylesheetsFromCDN', [ $this, 'loadStylesheetsFromCDN' ]),
58
            new \Twig_SimpleFunction('isWMFLabs', [ $this, 'isWMFLabs' ]),
59
            new \Twig_SimpleFunction('replag', [ $this, 'replag' ]),
60
            new \Twig_SimpleFunction('link', [ $this, 'link' ]),
61
            new \Twig_SimpleFunction('quote', [ $this, 'quote' ]),
62
            new \Twig_SimpleFunction('bugReportURL', [ $this, 'bugReportURL' ]),
63
            new \Twig_SimpleFunction('logged_in_user', [$this, 'functionLoggedInUser']),
64
            new \Twig_SimpleFunction('isUserAnon', [$this, 'isUserAnon']),
65
            new \Twig_SimpleFunction('nsName', [$this, 'nsName']),
66
            new \Twig_SimpleFunction('formatDuration', [$this, 'formatDuration']),
67
        ];
68
    }
69
70
    /**
71
     * Get the duration of the current HTTP request in microseconds.
72
     * @param int $decimals
73
     * @return string
74
     */
75
    public function requestTime($decimals = 3)
0 ignored issues
show
Coding Style introduced by
requestTime uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
76
    {
77
        return number_format(microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'], $decimals);
78
    }
79
80
    /**
81
     * Get the formatted real memory usage.
82
     * @return float
83
     */
84
    public function requestMemory()
85
    {
86
        $mem = memory_get_usage(false);
87
        $div = pow(1024, 2);
88
        $mem = $mem / $div;
89
90
        return round($mem, 2);
91
    }
92
93
    /**
94
     * Get the current year.
95
     * @return string
96
     */
97
    public function generateYear()
98
    {
99
        return date('Y');
100
    }
101
102
    /**
103
     * See if a given i18n message exists.
104
     * @TODO: refactor all intuition stuff so it can be used anywhere
105
     * @param string $message The message.
106
     * @return bool
107
     */
108
    public function intuitionMessageExists($message = "")
109
    {
110
        return $this->getIntuition()->msgExists($message, [ "domain" => "xtools" ]);
111
    }
112
113
    /**
114
     * Get an i18n message if it exists, otherwise just get the message key.
115
     * @param string $message
116
     * @param array $vars
117
     * @return mixed|null|string
118
     */
119
    public function intuitionMessagePrintExists($message = "", $vars = [])
120
    {
121
        if (is_array($message)) {
122
            $vars = $message;
123
            $message = $message[0];
124
            $vars = array_slice($vars, 1);
125
        }
126
        if ($this->intuitionMessageExists($message)) {
127
            return $this->intuitionMessage($message, $vars);
128
        } else {
129
            return $message;
130
        }
131
    }
132
133
    /**
134
     * Get an i18n message.
135
     * @param string $message
136
     * @param array $vars
137
     * @return mixed|null|string
138
     */
139
    public function intuitionMessage($message = "", $vars = [])
140
    {
141
        return $this->getIntuition()->msg($message, [ "domain" => "xtools", "variables" => $vars ]);
142
    }
143
144
    /**
145
     * Get the current language code.
146
     * @return string
147
     */
148
    public function getLang()
149
    {
150
        return $this->getIntuition()->getLang();
151
    }
152
153
    /**
154
     * Get the current language name (defaults to 'English').
155
     * @return string
156
     */
157
    public function getLangName()
158
    {
159
        return in_array(ucfirst($this->getIntuition()->getLangName()), $this->getAllLangs())
160
            ? $this->getIntuition()->getLangName()
161
            : 'English';
162
    }
163
164
    /**
165
     * Get all available languages in the i18n directory
166
     * @return array Associative array of langKey => langName
167
     */
168
    public function getAllLangs()
169
    {
170
        $messageFiles = glob($this->container->getParameter("kernel.root_dir") . '/../i18n/*.json');
171
172
        $languages = array_values(array_unique(array_map(
173
            function ($filename) {
174
                return basename($filename, '.json');
175
            },
176
            $messageFiles
177
        )));
178
179
        $availableLanguages = [];
180
181
        foreach ($languages as $lang) {
182
            $availableLanguages[$lang] = ucfirst($this->getIntuition()->getLangName($lang));
183
        }
184
        asort($availableLanguages);
185
186
        return $availableLanguages;
187
    }
188
189
    /**
190
     * Whether the current language is right-to-left.
191
     * @return bool
192
     */
193
    public function intuitionIsRTL()
194
    {
195
        return $this->getIntuition()->isRTL($this->getIntuition()->getLang());
196
    }
197
198
    /**
199
     * Whether the given language is right-to-left.
200
     * @param string $lang The language code.
201
     * @return bool
202
     */
203
    public function intuitionIsRTLLang($lang)
204
    {
205
        return $this->getIntuition()->isRTL($lang);
206
    }
207
208
    /**
209
     * Get the short hash of the currently checked-out Git commit.
210
     * @return string
211
     */
212
    public function gitShortHash()
213
    {
214
        return exec("git rev-parse --short HEAD");
215
    }
216
217
    /**
218
     * Get the full hash of the currently checkout-out Git commit.
219
     * @return string
220
     */
221
    public function gitHash()
222
    {
223
        return exec("git rev-parse HEAD");
224
    }
225
226
    /**
227
     * Get the date of the HEAD commit.
228
     * @return string
229
     */
230
    public function gitDate()
231
    {
232
        $date = new \DateTime(exec('git show -s --format=%ci'));
233
        return $date->format('Y-m-d');
234
    }
235
236
    /**
237
     * Check whether a given tool is enabled.
238
     * @param string $tool The short name of the tool.
239
     * @return bool
240
     */
241 View Code Duplication
    public function tabEnabled($tool = "index")
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
242
    {
243
        $param = false;
244
        if ($this->container->hasParameter("enable.$tool")) {
245
            $param = boolval($this->container->getParameter("enable.$tool"));
246
        }
247
        return $param;
248
    }
249
250
    /**
251
     * Get a list of the short names of all tools.
252
     * @return string[]
253
     */
254
    public function allTools()
255
    {
256
        $retVal = [];
257
        if ($this->container->hasParameter("tools")) {
258
            $retVal = $this->container->getParameter("tools");
259
        }
260
        return $retVal;
261
    }
262
263
    /**
264
     * Get a list of namespace colours (one or all).
265
     * @param bool $num The NS ID to get.
266
     * @return string[]|string Indexed by namespace ID.
267
     */
268
    public static function getColorList($num = false)
269
    {
270
        $colors = [
271
            0 => '#FF5555',
272
            1 => '#55FF55',
273
            2 => '#FFEE22',
274
            3 => '#FF55FF',
275
            4 => '#5555FF',
276
            5 => '#55FFFF',
277
            6 => '#C00000',
278
            7 => '#0000C0',
279
            8 => '#008800',
280
            9 => '#00C0C0',
281
            10 => '#FFAFAF',
282
            11 => '#808080',
283
            12 => '#00C000',
284
            13 => '#404040',
285
            14 => '#C0C000',
286
            15 => '#C000C0',
287
            90 => '#991100',
288
            91 => '#99FF00',
289
            92 => '#000000',
290
            93 => '#777777',
291
            100 => '#75A3D1',
292
            101 => '#A679D2',
293
            102 => '#660000',
294
            103 => '#000066',
295
            104 => '#FAFFAF',
296
            105 => '#408345',
297
            106 => '#5c8d20',
298
            107 => '#e1711d',
299
            108 => '#94ef2b',
300
            109 => '#756a4a',
301
            110 => '#6f1dab',
302
            111 => '#301e30',
303
            112 => '#5c9d96',
304
            113 => '#a8cd8c',
305
            114 => '#f2b3f1',
306
            115 => '#9b5828',
307
            116 => '#002288',
308
            117 => '#0000CC',
309
            118 => '#99FFFF',
310
            119 => '#99BBFF',
311
            120 => '#FF99FF',
312
            121 => '#CCFFFF',
313
            122 => '#CCFF00',
314
            123 => '#CCFFCC',
315
            200 => '#33FF00',
316
            201 => '#669900',
317
            202 => '#666666',
318
            203 => '#999999',
319
            204 => '#FFFFCC',
320
            205 => '#FF00CC',
321
            206 => '#FFFF00',
322
            207 => '#FFCC00',
323
            208 => '#FF0000',
324
            209 => '#FF6600',
325
            250 => '#6633CC',
326
            251 => '#6611AA',
327
            252 => '#66FF99',
328
            253 => '#66FF66',
329
            446 => '#06DCFB',
330
            447 => '#892EE4',
331
            460 => '#99FF66',
332
            461 => '#99CC66',
333
            470 => '#CCCC33',
334
            471 => '#CCFF33',
335
            480 => '#6699FF',
336
            481 => '#66FFFF',
337
            484 => '#07C8D6',
338
            485 => '#2AF1FF',
339
            486 => '#79CB21',
340
            487 => '#80D822',
341
            490 => '#995500',
342
            491 => '#998800',
343
            710 => '#FFCECE',
344
            711 => '#FFC8F2',
345
            828 => '#F7DE00',
346
            829 => '#BABA21',
347
            866 => '#FFFFFF',
348
            867 => '#FFCCFF',
349
            1198 => '#FF34B3',
350
            1199 => '#8B1C62',
351
            2300 => '#A900B8',
352
            2301 => '#C93ED6',
353
            2302 => '#8A09C1',
354
            2303 => '#974AB8',
355
            2600 => '#000000',
356
        ];
357
358
        if ($num === false) {
359
            return $colors;
360
        } elseif (isset($colors[$num])) {
361
            return $colors[$num];
362
        } else {
363
            // Default to grey.
364
            return '#CCC';
365
        }
366
    }
367
368
    /**
369
     * Get color-blind friendly colors for use in charts
370
     * @param  Integer $num Index of color
371
     * @return String RGBA color (so you can more easily adjust the opacity)
372
     */
373
    public function chartColor($num)
374
    {
375
        $colors = [
376
            'rgba(171, 212, 235, 1)',
377
            'rgba(178, 223, 138, 1)',
378
            'rgba(251, 154, 153, 1)',
379
            'rgba(253, 191, 111, 1)',
380
            'rgba(202, 178, 214, 1)',
381
            'rgba(207, 182, 128, 1)',
382
            'rgba(141, 211, 199, 1)',
383
            'rgba(252, 205, 229, 1)',
384
            'rgba(255, 247, 161, 1)',
385
            'rgba(217, 217, 217, 1)',
386
        ];
387
388
        return $colors[$num % count($colors)];
389
    }
390
391
    /**
392
     * Whether XTools is running in single-project mode.
393
     * @return bool
394
     */
395 View Code Duplication
    public function isSingleWiki()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
396
    {
397
        $param = true;
398
        if ($this->container->hasParameter('app.single_wiki')) {
399
            $param = boolval($this->container->getParameter('app.single_wiki'));
400
        }
401
        return $param;
402
    }
403
404
    /**
405
     * Get the database replication-lag threshold.
406
     * @return int
407
     */
408
    public function getReplagThreshold()
409
    {
410
        $param = 30;
411
        if ($this->container->hasParameter('app.replag_threshold')) {
412
            $param = $this->container->getParameter('app.replag_threshold');
413
        };
414
        return $param;
415
    }
416
417
    /**
418
     * Whether we should load stylesheets from external CDNs or not.
419
     * @return bool
420
     */
421 View Code Duplication
    public function loadStylesheetsFromCDN()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
422
    {
423
        $param = false;
424
        if ($this->container->hasParameter('app.load_stylesheets_from_cdn')) {
425
            $param = boolval($this->container->getParameter('app.load_stylesheets_from_cdn'));
426
        }
427
        return $param;
428
    }
429
430
    /**
431
     * Whether XTools is running in WMF Labs mode.
432
     * @return bool
433
     */
434 View Code Duplication
    public function isWMFLabs()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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
     * What URL to use as the internal API, based on configuration.
445
     * @return string
446
     */
447
    public function xtApiUrl()
448
    {
449
        if ($this->container->hasParameter('app.multithread.enable') &&
450
            (bool) $this->container->getParameter('app.multithread.enable')
451
        ) {
452
            return $this->container->getParameter('app.multithread.api_url');
453
        } else {
454
            // Use root URL of application.
455
            $router = $this->container->get('router');
456
            return $router->generate('homepage', [], true);
457
        }
458
    }
459
460
    /**
461
     * The current replication lag.
462
     * @return int
463
     */
464
    public function replag()
465
    {
466
        $retVal = 0;
467
468
        if ($this->isWMFLabs()) {
469
            $project = $this->container->get('request_stack')->getCurrentRequest()->get('project');
470
471
            if (!isset($project)) {
472
                $project = 'enwiki';
473
            }
474
475
            $dbName = ProjectRepository::getProject($project, $this->container)
476
                ->getDatabaseName();
477
478
            $stmt = "SELECT lag FROM `heartbeat_p`.`heartbeat` h
479
            RIGHT JOIN `meta_p`.`wiki` w ON concat(h.shard, \".labsdb\")=w.slice
480
            WHERE dbname LIKE :project LIMIT 1";
481
482
            $conn = $this->container->get('doctrine')->getManager('replicas')->getConnection();
483
484
            // Prepare the query and execute
485
            $resultQuery = $conn->prepare($stmt);
486
            $resultQuery->bindParam('project', $dbName);
487
            $resultQuery->execute();
488
489
            if ($resultQuery->errorCode() == 0) {
490
                $results = $resultQuery->fetchAll();
491
492
                if (isset($results[0]['lag'])) {
493
                    $retVal = $results[0]['lag'];
494
                }
495
            }
496
        }
497
498
        return $retVal;
499
    }
500
501
    /**
502
     * Get a random quote for the footer
503
     * @return string
504
     */
505
    public function quote()
506
    {
507
        // Don't show if bash is turned off, but always show for Labs
508
        // (so quote is in footer but not in nav).
509
        $isLabs = $this->container->getParameter('app.is_labs');
510
        if (!$isLabs && !$this->container->getParameter('enable.bash')) {
511
            return '';
512
        }
513
        $quotes = $this->container->getParameter('quotes');
514
        $id = array_rand($quotes);
515
        return $quotes[$id];
516
    }
517
518
    /**
519
     * Get the currently logged in user's details.
520
     * @return string[]
521
     */
522
    public function functionLoggedInUser()
523
    {
524
        return $this->container->get('session')->get('logged_in_user');
525
    }
526
527
528
    /*********************************** FILTERS ***********************************/
529
530
    /**
531
     * Get all filters for this extension.
532
     * @return array
533
     */
534
    public function getFilters()
535
    {
536
        return [
537
            new \Twig_SimpleFilter('capitalize_first', [ $this, 'capitalizeFirst' ]),
538
            new \Twig_SimpleFilter('percent_format', [ $this, 'percentFormat' ]),
539
            new \Twig_SimpleFilter('diff_format', [ $this, 'diffFormat' ], [ 'is_safe' => [ 'html' ] ]),
540
        ];
541
    }
542
543
    /**
544
     * Mysteriously missing Twig helper to capitalize only the first character.
545
     * E.g. used for table headings for translated messages
546
     * @param  string $str The string
547
     * @return string      The string, capitalized
548
     */
549
    public function capitalizeFirst($str)
550
    {
551
        return ucfirst($str);
552
    }
553
554
    /**
555
     * Format a given number or fraction as a percentage
556
     * @param  number  $numerator     Numerator or single fraction if denominator is ommitted
557
     * @param  number  [$denominator] Denominator
558
     * @param  integer [$precision]   Number of decimal places to show
559
     * @return string                 Formatted percentage
560
     */
561
    public function percentFormat($numerator, $denominator = null, $precision = 1)
562
    {
563
        if (!$denominator) {
564
            $quotient = $numerator;
565
        } else {
566
            $quotient = ( $numerator / $denominator ) * 100;
567
        }
568
569
        return round($quotient, $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
    public function isUserAnon($user)
578
    {
579
        if ($user instanceof User) {
580
            $username = $user.username;
581
        } else {
582
            $username = $user;
583
        }
584
585
        return 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->getIntuition()->msg('all');
599
        } elseif ($namespace === '0' || $namespace === 0 || $namespace === 'Main') {
600
            return $this->getIntuition()->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
    public function diffFormat($size)
612
    {
613
        if ($size < 0) {
614
            $class = 'diff-neg';
615
        } elseif ($size > 0) {
616
            $class = 'diff-pos';
617
        } else {
618
            $class = 'diff-zero';
619
        }
620
621
        $size = number_format($size);
622
623
        return "<span class='$class'>$size</span>";
624
    }
625
626
    /**
627
     * Format a time duration as humanized string.
628
     * @param int $seconds Number of seconds
629
     * @param bool $translate Used for unit testing. Set to false to return
630
     *   the value and i18n key, instead of the actual translation.
631
     * @return string|array Examples: '30 seconds', '2 minutes', '15 hours', '500 days',
632
     *   or [30, 'num-seconds'] (etc.) if $translate is true
633
     */
634
    public function formatDuration($seconds, $translate = true)
635
    {
636
        /** @var int Value to show in message */
637
        $val = $seconds;
638
639
        /** @var string Unit of time, used in the key for the i18n message */
640
        $key = 'seconds';
641
642
        if ($seconds >= 86400) {
643
            // Over a day
644
            $val = (int) floor($seconds / 86400);
645
            $key = 'days';
646
        } elseif ($seconds >= 3600) {
647
            // Over an hour, less than a day
648
            $val = (int) floor($seconds / 3600);
649
            $key = 'hours';
650
        } elseif ($seconds >= 60) {
651
            // Over a minute, less than an hour
652
            $val = (int) floor($seconds / 60);
653
            $key = 'minutes';
654
        }
655
656
        if ($translate) {
657
            return number_format($val) . ' ' . $this->intuitionMessage("num-$key", [$val]);
658
        } else {
659
            return [number_format($val), "num-$key"];
660
        }
661
    }
662
}
663