Completed
Push — master ( 77d109...dc7aae )
by Sam
05:53 queued 03:04
created

AppExtension::formatDuration()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 28
rs 8.439
c 0
b 0
f 0
cc 5
eloc 16
nc 8
nop 2
1
<?php
2
/**
3
 * This file contains only the AppExtension class.
4
 */
5
6
namespace AppBundle\Twig;
7
8
use Xtools\User;
9
10
/**
11
 * Twig functions and filters for XTools.
12
 */
13
class AppExtension extends Extension
14
{
15
16
    /**
17
     * Get the name of this extension.
18
     * @return string
19
     */
20
    public function getName()
21
    {
22
        return 'app_extension';
23
    }
24
25
    /*********************************** FUNCTIONS ***********************************/
26
27
    /**
28
     * Get all functions that this class provides.
29
     * @return array
30
     */
31
    public function getFunctions()
32
    {
33
        $options = ['is_safe' => ['html']];
34
        return [
35
            new \Twig_SimpleFunction('request_time', [ $this, 'requestTime' ], $options),
36
            new \Twig_SimpleFunction('memory_usage', [ $this, 'requestMemory' ], $options),
37
            new \Twig_SimpleFunction('year', [ $this, 'generateYear' ], $options),
38
            new \Twig_SimpleFunction('msgPrintExists', [ $this, 'intuitionMessagePrintExists' ], $options),
39
            new \Twig_SimpleFunction('msgExists', [ $this, 'intuitionMessageExists' ], $options),
40
            new \Twig_SimpleFunction('msg', [ $this, 'intuitionMessage' ], $options),
41
            new \Twig_SimpleFunction('lang', [ $this, 'getLang' ], $options),
42
            new \Twig_SimpleFunction('langName', [ $this, 'getLangName' ], $options),
43
            new \Twig_SimpleFunction('allLangs', [ $this, 'getAllLangs' ]),
44
            new \Twig_SimpleFunction('isRTL', [ $this, 'intuitionIsRTL' ]),
45
            new \Twig_SimpleFunction('isRTLLang', [ $this, 'intuitionIsRTLLang' ]),
46
            new \Twig_SimpleFunction('shortHash', [ $this, 'gitShortHash' ]),
47
            new \Twig_SimpleFunction('hash', [ $this, 'gitHash' ]),
48
            new \Twig_SimpleFunction('enabled', [ $this, 'tabEnabled' ]),
49
            new \Twig_SimpleFunction('tools', [ $this, 'allTools' ]),
50
            new \Twig_SimpleFunction('color', [ $this, 'getColorList' ]),
51
            new \Twig_SimpleFunction('chartColor', [ $this, 'chartColor' ]),
52
            new \Twig_SimpleFunction('isWMFLabs', [ $this, 'isWMFLabs' ]),
53
            new \Twig_SimpleFunction('isSingleWiki', [ $this, 'isSingleWiki' ]),
54
            new \Twig_SimpleFunction('getReplagThreshold', [ $this, 'getReplagThreshold' ]),
55
            new \Twig_SimpleFunction('loadStylesheetsFromCDN', [ $this, 'loadStylesheetsFromCDN' ]),
56
            new \Twig_SimpleFunction('isWMFLabs', [ $this, 'isWMFLabs' ]),
57
            new \Twig_SimpleFunction('replag', [ $this, 'replag' ]),
58
            new \Twig_SimpleFunction('link', [ $this, 'link' ]),
59
            new \Twig_SimpleFunction('quote', [ $this, 'quote' ]),
60
            new \Twig_SimpleFunction('bugReportURL', [ $this, 'bugReportURL' ]),
61
            new \Twig_SimpleFunction('logged_in_user', [$this, 'functionLoggedInUser']),
62
            new \Twig_SimpleFunction('isUserAnon', [$this, 'isUserAnon']),
63
            new \Twig_SimpleFunction('nsName', [$this, 'nsName']),
64
            new \Twig_SimpleFunction('formatDuration', [$this, 'formatDuration']),
65
        ];
66
    }
67
68
    /**
69
     * Get the duration of the current HTTP request in microseconds.
70
     * @param int $decimals
71
     * @return string
72
     */
73
    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...
74
    {
75
        return number_format(microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'], $decimals);
76
    }
77
78
    /**
79
     * Get the formatted real memory usage.
80
     * @return float
81
     */
82
    public function requestMemory()
83
    {
84
        $mem = memory_get_usage(false);
85
        $div = pow(1024, 2);
86
        $mem = $mem / $div;
87
88
        return round($mem, 2);
89
    }
90
91
    /**
92
     * Get the current year.
93
     * @return string
94
     */
95
    public function generateYear()
96
    {
97
        return date('Y');
98
    }
99
100
    /**
101
     * See if a given i18n message exists.
102
     * @TODO: refactor all intuition stuff so it can be used anywhere
103
     * @param string $message The message.
104
     * @return bool
105
     */
106
    public function intuitionMessageExists($message = "")
107
    {
108
        return $this->getIntuition()->msgExists($message, [ "domain" => "xtools" ]);
109
    }
110
111
    /**
112
     * Get an i18n message if it exists, otherwise just get the message key.
113
     * @param string $message
114
     * @param array $vars
115
     * @return mixed|null|string
116
     */
117
    public function intuitionMessagePrintExists($message = "", $vars = [])
118
    {
119
        if (is_array($message)) {
120
            $vars = $message;
121
            $message = $message[0];
122
            $vars = array_slice($vars, 1);
123
        }
124
        if ($this->intuitionMessageExists($message)) {
125
            return $this->intuitionMessage($message, $vars);
126
        } else {
127
            return $message;
128
        }
129
    }
130
131
    /**
132
     * Get an i18n message.
133
     * @param string $message
134
     * @param array $vars
135
     * @return mixed|null|string
136
     */
137
    public function intuitionMessage($message = "", $vars = [])
138
    {
139
        return $this->getIntuition()->msg($message, [ "domain" => "xtools", "variables" => $vars ]);
140
    }
141
142
    /**
143
     * Get the current language code.
144
     * @return string
145
     */
146
    public function getLang()
147
    {
148
        return $this->getIntuition()->getLang();
149
    }
150
151
    /**
152
     * Get the current language name (defaults to 'English').
153
     * @return string
154
     */
155
    public function getLangName()
156
    {
157
        return in_array(ucfirst($this->getIntuition()->getLangName()), $this->getAllLangs())
158
            ? $this->getIntuition()->getLangName()
159
            : 'English';
160
    }
161
162
    /**
163
     * Get all available languages in the i18n directory
164
     * @return array Associative array of langKey => langName
165
     */
166
    public function getAllLangs()
167
    {
168
        $messageFiles = glob($this->container->getParameter("kernel.root_dir") . '/../i18n/*.json');
169
170
        $languages = array_values(array_unique(array_map(
171
            function ($filename) {
172
                return basename($filename, '.json');
173
            },
174
            $messageFiles
175
        )));
176
177
        $availableLanguages = [];
178
179
        foreach ($languages as $lang) {
180
            $availableLanguages[$lang] = ucfirst($this->getIntuition()->getLangName($lang));
181
        }
182
        asort($availableLanguages);
183
184
        return $availableLanguages;
185
    }
186
187
    /**
188
     * Whether the current language is right-to-left.
189
     * @return bool
190
     */
191
    public function intuitionIsRTL()
192
    {
193
        return $this->getIntuition()->isRTL($this->getIntuition()->getLang());
194
    }
195
196
    /**
197
     * Whether the given language is right-to-left.
198
     * @param string $lang The language code.
199
     * @return bool
200
     */
201
    public function intuitionIsRTLLang($lang)
202
    {
203
        return $this->getIntuition()->isRTL($lang);
204
    }
205
206
    /**
207
     * Get the short hash of the currently checked-out Git commit.
208
     * @return string
209
     */
210
    public function gitShortHash()
211
    {
212
        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
    public function gitHash()
220
    {
221
        return exec("git rev-parse HEAD");
222
    }
223
224
    /**
225
     * Check whether a given tool is enabled.
226
     * @param string $tool The short name of the tool.
227
     * @return bool
228
     */
229 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...
230
    {
231
        $param = false;
232
        if ($this->container->hasParameter("enable.$tool")) {
233
            $param = boolval($this->container->getParameter("enable.$tool"));
234
        }
235
        return $param;
236
    }
237
238
    /**
239
     * Get a list of the short names of all tools.
240
     * @return string[]
241
     */
242
    public function allTools()
243
    {
244
        $retVal = [];
245
        if ($this->container->hasParameter("tools")) {
246
            $retVal = $this->container->getParameter("tools");
247
        }
248
        return $retVal;
249
    }
250
251
    /**
252
     * Get a list of namespace colours (one or all).
253
     * @param bool $num The NS ID to get.
254
     * @return string[]|string Indexed by namespace ID.
255
     */
256
    public static function getColorList($num = false)
257
    {
258
        $colors = [
259
            0 => '#FF5555',
260
            1 => '#55FF55',
261
            2 => '#FFEE22',
262
            3 => '#FF55FF',
263
            4 => '#5555FF',
264
            5 => '#55FFFF',
265
            6 => '#C00000',
266
            7 => '#0000C0',
267
            8 => '#008800',
268
            9 => '#00C0C0',
269
            10 => '#FFAFAF',
270
            11 => '#808080',
271
            12 => '#00C000',
272
            13 => '#404040',
273
            14 => '#C0C000',
274
            15 => '#C000C0',
275
            90 => '#991100',
276
            91 => '#99FF00',
277
            92 => '#000000',
278
            93 => '#777777',
279
            100 => '#75A3D1',
280
            101 => '#A679D2',
281
            102 => '#660000',
282
            103 => '#000066',
283
            104 => '#FAFFAF',
284
            105 => '#408345',
285
            106 => '#5c8d20',
286
            107 => '#e1711d',
287
            108 => '#94ef2b',
288
            109 => '#756a4a',
289
            110 => '#6f1dab',
290
            111 => '#301e30',
291
            112 => '#5c9d96',
292
            113 => '#a8cd8c',
293
            114 => '#f2b3f1',
294
            115 => '#9b5828',
295
            116 => '#002288',
296
            117 => '#0000CC',
297
            118 => '#99FFFF',
298
            119 => '#99BBFF',
299
            120 => '#FF99FF',
300
            121 => '#CCFFFF',
301
            122 => '#CCFF00',
302
            123 => '#CCFFCC',
303
            200 => '#33FF00',
304
            201 => '#669900',
305
            202 => '#666666',
306
            203 => '#999999',
307
            204 => '#FFFFCC',
308
            205 => '#FF00CC',
309
            206 => '#FFFF00',
310
            207 => '#FFCC00',
311
            208 => '#FF0000',
312
            209 => '#FF6600',
313
            250 => '#6633CC',
314
            251 => '#6611AA',
315
            252 => '#66FF99',
316
            253 => '#66FF66',
317
            446 => '#06DCFB',
318
            447 => '#892EE4',
319
            460 => '#99FF66',
320
            461 => '#99CC66',
321
            470 => '#CCCC33',
322
            471 => '#CCFF33',
323
            480 => '#6699FF',
324
            481 => '#66FFFF',
325
            484 => '#07C8D6',
326
            485 => '#2AF1FF',
327
            486 => '#79CB21',
328
            487 => '#80D822',
329
            490 => '#995500',
330
            491 => '#998800',
331
            710 => '#FFCECE',
332
            711 => '#FFC8F2',
333
            828 => '#F7DE00',
334
            829 => '#BABA21',
335
            866 => '#FFFFFF',
336
            867 => '#FFCCFF',
337
            1198 => '#FF34B3',
338
            1199 => '#8B1C62',
339
            2300 => '#A900B8',
340
            2301 => '#C93ED6',
341
            2302 => '#8A09C1',
342
            2303 => '#974AB8',
343
            2600 => '#000000',
344
        ];
345
346
        if ($num === false) {
347
            return $colors;
348
        } elseif (isset($colors[$num])) {
349
            return $colors[$num];
350
        } else {
351
            // Default to grey.
352
            return '#CCC';
353
        }
354
    }
355
356
    /**
357
     * Get color-blind friendly colors for use in charts
358
     * @param  Integer $num Index of color
359
     * @return String RGBA color (so you can more easily adjust the opacity)
360
     */
361
    public function chartColor($num)
362
    {
363
        $colors = [
364
            'rgba(171, 212, 235, 1)',
365
            'rgba(178, 223, 138, 1)',
366
            'rgba(251, 154, 153, 1)',
367
            'rgba(253, 191, 111, 1)',
368
            'rgba(202, 178, 214, 1)',
369
            'rgba(207, 182, 128, 1)',
370
            'rgba(141, 211, 199, 1)',
371
            'rgba(252, 205, 229, 1)',
372
            'rgba(255, 247, 161, 1)',
373
            'rgba(217, 217, 217, 1)',
374
        ];
375
376
        return $colors[$num % count($colors)];
377
    }
378
379
    /**
380
     * Whether XTools is running in single-project mode.
381
     * @return bool
382
     */
383 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...
384
    {
385
        $param = true;
386
        if ($this->container->hasParameter('app.single_wiki')) {
387
            $param = boolval($this->container->getParameter('app.single_wiki'));
388
        }
389
        return $param;
390
    }
391
392
    /**
393
     * Get the database replication-lag threshold.
394
     * @return int
395
     */
396
    public function getReplagThreshold()
397
    {
398
        $param = 30;
399
        if ($this->container->hasParameter('app.replag_threshold')) {
400
            $param = $this->container->getParameter('app.replag_threshold');
401
        };
402
        return $param;
403
    }
404
405
    /**
406
     * Whether we should load stylesheets from external CDNs or not.
407
     * @return bool
408
     */
409 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...
410
    {
411
        $param = false;
412
        if ($this->container->hasParameter('app.load_stylesheets_from_cdn')) {
413
            $param = boolval($this->container->getParameter('app.load_stylesheets_from_cdn'));
414
        }
415
        return $param;
416
    }
417
418
    /**
419
     * Whether XTools is running in WMF Labs mode.
420
     * @return bool
421
     */
422 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...
423
    {
424
        $param = false;
425
        if ($this->container->hasParameter('app.is_labs')) {
426
            $param = boolval($this->container->getParameter('app.is_labs'));
427
        }
428
        return $param;
429
    }
430
431
    /**
432
     * The current replication lag.
433
     * @return int
434
     */
435
    public function replag()
436
    {
437
        $retVal = 0;
438
439
        if ($this->isWMFLabs()) {
440
            $project = $this->container->get('request_stack')->getCurrentRequest()->get('project');
441
442
            if (!isset($project)) {
443
                $project = 'enwiki';
444
            }
445
446
            $stmt = "SELECT lag FROM `heartbeat_p`.`heartbeat` h
447
            RIGHT JOIN `meta_p`.`wiki` w ON concat(h.shard, \".labsdb\")=w.slice
448
            WHERE dbname LIKE :project OR name LIKE :project OR url LIKE :project LIMIT 1";
449
450
            $conn = $this->container->get('doctrine')->getManager('replicas')->getConnection();
451
452
            // Prepare the query and execute
453
            $resultQuery = $conn->prepare($stmt);
454
            $resultQuery->bindParam('project', $project);
455
            $resultQuery->execute();
456
457
            if ($resultQuery->errorCode() == 0) {
458
                $results = $resultQuery->fetchAll();
459
460
                if (isset($results[0]['lag'])) {
461
                    $retVal = $results[0]['lag'];
462
                }
463
            }
464
        }
465
466
        return $retVal;
467
    }
468
469
    /**
470
     * Generate an XTools URL.
471
     * @deprecated Use path() instead.
472
     * @param string $path The path.
473
     * @return string
474
     */
475
    public function link($path = "/")
476
    {
477
        $base_path = $this->container->getParameter("app.base_path");
478
        $retVal = $path;
479
480
        if (isset($base_path)) {
481
            $retVal = "$base_path/$path";
482
        }
483
484
        $retVal = str_replace("//", "/", $retVal);
485
486
        return $retVal;
487
    }
488
489
    /**
490
     * Get a random quote for the footer
491
     * @return string
492
     */
493
    public function quote()
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