Completed
Push — master ( 5c8b1a...3af732 )
by MusikAnimal
02:29
created

AppExtension::replag()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 36
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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