Passed
Push — master ( 60e77e...4fa0f7 )
by Thomas
02:04
created

DebugBar::suppressJquery()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 10
rs 10
1
<?php
2
3
namespace LeKoala\DebugBar;
4
5
use Exception;
6
use Monolog\Logger;
7
use ReflectionObject;
8
use Psr\Log\LoggerInterface;
9
use SilverStripe\Core\Kernel;
10
use DebugBar\JavascriptRenderer;
11
use DebugBar\Storage\FileStorage;
12
use SilverStripe\Dev\Deprecation;
13
use SilverStripe\Control\Director;
14
use SilverStripe\Core\Environment;
15
use SilverStripe\Admin\LeftAndMain;
16
use SilverStripe\View\Requirements;
17
use SilverStripe\Control\Controller;
18
use Symfony\Component\Mailer\Mailer;
19
use SilverStripe\Control\HTTPRequest;
20
use DebugBar\DebugBar as BaseDebugBar;
21
use SilverStripe\Core\Injector\Injector;
22
use SilverStripe\Core\Config\ConfigLoader;
23
use SilverStripe\Core\Config\Configurable;
24
use SilverStripe\Core\Injector\Injectable;
25
use DebugBar\DataCollector\MemoryCollector;
26
use LeKoala\DebugBar\Messages\LogFormatter;
27
use SilverStripe\Admin\AdminRootController;
28
use SilverStripe\Core\Manifest\ModuleLoader;
29
use DebugBar\DataCollector\MessagesCollector;
30
use LeKoala\DebugBar\Bridge\MonologCollector;
31
use Symfony\Component\Mailer\MailerInterface;
32
use LeKoala\DebugBar\Collector\CacheCollector;
33
use SilverStripe\Core\Manifest\ModuleResource;
34
use LeKoala\DebugBar\Collector\ConfigCollector;
35
use LeKoala\DebugBar\Proxy\ConfigManifestProxy;
36
use LeKoala\DebugBar\Collector\PhpInfoCollector;
37
use LeKoala\DebugBar\Extension\ProxyDBExtension;
38
use LeKoala\DebugBar\Collector\DatabaseCollector;
39
use LeKoala\DebugBar\Collector\TimeDataCollector;
40
use LeKoala\DebugBar\Proxy\DeltaConfigManifestProxy;
41
use LeKoala\DebugBar\Collector\PartialCacheCollector;
42
use LeKoala\DebugBar\Collector\SilverStripeCollector;
43
use SilverStripe\Config\Collections\DeltaConfigCollection;
44
use SilverStripe\Config\Collections\CachedConfigCollection;
45
use LeKoala\DebugBar\Bridge\SymfonyMailer\SymfonyMailerCollector;
46
47
/**
48
 * A simple helper
49
 */
50
class DebugBar
51
{
52
    use Configurable;
53
    use Injectable;
54
55
    /**
56
     * @var BaseDebugBar
57
     */
58
    protected static $debugbar;
59
60
    /**
61
     * @var bool
62
     */
63
    public static $bufferingEnabled = false;
64
65
    /**
66
     * @var bool
67
     */
68
    public static $suppressJquery = false;
69
70
    /**
71
     * @var JavascriptRenderer
72
     */
73
    protected static $renderer;
74
75
    /**
76
     * @var bool
77
     */
78
    protected static $showQueries = false;
79
80
    /**
81
     * @var HTTPRequest
82
     */
83
    protected static $request;
84
85
    /**
86
     * @var array
87
     */
88
    protected static $extraTimes = [];
89
90
    /**
91
     * Get the Debug Bar instance
92
     * @throws Exception
93
     * @global array $databaseConfig
94
     * @return BaseDebugBar
95
     */
96
    public static function getDebugBar()
97
    {
98
        if (self::$debugbar !== null) {
99
            return self::$debugbar;
100
        }
101
102
        $reasons = self::disabledCriteria();
103
        if (!empty($reasons)) {
104
            self::$debugbar = false; // no need to check again
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type DebugBar\DebugBar of property $debugbar.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
105
            return;
106
        }
107
108
        self::initDebugBar();
109
110
        if (!self::$debugbar) {
111
            throw new Exception("Failed to initialize the DebugBar");
112
        }
113
114
        return self::$debugbar;
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::debugbar returns the type void which is incompatible with the documented return type DebugBar\DebugBar.
Loading history...
115
    }
116
117
    /**
118
     * Init the debugbar instance
119
     *
120
     * @global array $databaseConfig
121
     * @return BaseDebugBar|null
122
     */
123
    public static function initDebugBar()
124
    {
125
        // Prevent multiple inits
126
        if (self::$debugbar) {
127
            return self::$debugbar;
128
        }
129
130
        self::$debugbar = $debugbar = new BaseDebugBar();
131
132
        if (isset($_REQUEST['showqueries']) && Director::isDev()) {
133
            self::setShowQueries(true);
134
            unset($_REQUEST['showqueries']);
135
        }
136
137
        $debugbar->addCollector(new PhpInfoCollector());
138
        $debugbar->addCollector(new TimeDataCollector());
139
        self::measureExtraTime();
140
        $debugbar->addCollector(new MemoryCollector());
141
142
        // Add config proxy replacing the core config manifest
143
        if (self::config()->config_collector) {
144
            /** @var ConfigLoader $configLoader */
145
            $configLoader = Injector::inst()->get(Kernel::class)->getConfigLoader();
146
            // There is no getManifests method on ConfigLoader
147
            $manifests = self::getProtectedValue($configLoader, 'manifests');
148
            foreach ($manifests as $manifestIdx => $manifest) {
149
                if ($manifest instanceof CachedConfigCollection) {
150
                    $manifest = new ConfigManifestProxy($manifest);
151
                    $manifests[$manifestIdx] = $manifest;
152
                }
153
                if ($manifest instanceof DeltaConfigCollection) {
154
                    $manifest = DeltaConfigManifestProxy::createFromOriginal($manifest);
155
                    $manifests[$manifestIdx] = $manifest;
156
                }
157
            }
158
            // Don't push as it may change stack order
159
            self::setProtectedValue($configLoader, 'manifests', $manifests);
160
        }
161
162
        if (self::config()->db_collector) {
163
            $debugbar->addCollector(new DatabaseCollector);
164
        }
165
166
        // Add message collector last so other collectors can send messages to the console using it
167
        $debugbar->addCollector(new MessagesCollector());
168
169
        // Aggregate monolog into messages
170
        $logger = Injector::inst()->get(LoggerInterface::class);
171
        if ($logger instanceof Logger) {
172
            $logCollector = new MonologCollector($logger);
173
            $logCollector->setFormatter(new LogFormatter);
174
            $debugbar['messages']->aggregate($logCollector);
0 ignored issues
show
Bug introduced by
The method aggregate() does not exist on DebugBar\DataCollector\DataCollectorInterface. It seems like you code against a sub-type of DebugBar\DataCollector\DataCollectorInterface such as DebugBar\DataCollector\MessagesCollector. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

174
            $debugbar['messages']->/** @scrutinizer ignore-call */ 
175
                                   aggregate($logCollector);
Loading history...
175
        }
176
177
        // Add some SilverStripe specific infos
178
        $debugbar->addCollector(new SilverStripeCollector);
179
180
        if (self::config()->get('enable_storage')) {
181
            $debugBarTempFolder = TEMP_FOLDER . '/debugbar';
182
            $debugbar->setStorage($fileStorage = new FileStorage($debugBarTempFolder));
183
            if (isset($_GET['flush']) && is_dir($debugBarTempFolder)) {
184
                // FileStorage::clear() is implemented with \DirectoryIterator which throws UnexpectedValueException if dir can not be opened
185
                $fileStorage->clear();
186
            }
187
        }
188
189
        if (self::config()->config_collector) {
190
            // Add the config collector
191
            $debugbar->addCollector(new ConfigCollector);
192
        }
193
194
        // Cache
195
        if (self::config()->cache_collector) {
196
            $debugbar->addCollector(new CacheCollector);
197
        }
198
199
        // Partial cache
200
        if (self::config()->partial_cache_collector) {
201
            $debugbar->addCollector(new PartialCacheCollector);
202
        }
203
204
        // Email logging
205
        if (self::config()->email_collector) {
206
            $mailer = Injector::inst()->get(MailerInterface::class);
207
            if ($mailer instanceof Mailer) {
208
                $debugbar->addCollector(new SymfonyMailerCollector($mailer));
0 ignored issues
show
Unused Code introduced by
The call to LeKoala\DebugBar\Bridge\...ollector::__construct() has too many arguments starting with $mailer. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

208
                $debugbar->addCollector(/** @scrutinizer ignore-call */ new SymfonyMailerCollector($mailer));

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
209
            }
210
        }
211
212
        // Since we buffer everything, why not enable all dev options ?
213
        if (self::config()->get('auto_debug')) {
214
            $_REQUEST['debug'] = true;
215
            $_REQUEST['debug_request'] = true;
216
        }
217
218
        if (isset($_REQUEST['debug']) || isset($_REQUEST['debug_request'])) {
219
            self::$bufferingEnabled = true;
220
            ob_start(); // We buffer everything until we have called an action
221
        }
222
223
        return $debugbar;
224
    }
225
226
    /**
227
     * Access a protected property when the api does not allow access
228
     *
229
     * @param object $object
230
     * @param string $property
231
     * @return mixed
232
     */
233
    protected static function getProtectedValue($object, $property)
234
    {
235
        $refObject = new ReflectionObject($object);
236
        $refProperty = $refObject->getProperty($property);
237
        $refProperty->setAccessible(true);
238
        return $refProperty->getValue($object);
239
    }
240
241
    /**
242
     * Set a protected property when the api does not allow access
243
     *
244
     * @param object $object
245
     * @param string $property
246
     * @param mixed $newValue
247
     * @return void
248
     */
249
    protected static function setProtectedValue($object, $property, $newValue)
250
    {
251
        $refObject = new ReflectionObject($object);
252
        $refProperty = $refObject->getProperty($property);
253
        $refProperty->setAccessible(true);
254
        return $refProperty->setValue($object, $newValue);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $refProperty->setValue($object, $newValue) targeting ReflectionProperty::setValue() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
255
    }
256
257
    /**
258
     * Clear the current instance of DebugBar
259
     *
260
     * @return void
261
     */
262
    public static function clearDebugBar()
263
    {
264
        self::$debugbar = null;
265
        self::$bufferingEnabled = false;
266
        self::$renderer = null;
267
        self::$showQueries = false;
268
        self::$request = null;
269
        self::$extraTimes = [];
270
        ProxyDBExtension::resetQueries();
271
    }
272
273
    /**
274
     * @return boolean
275
     */
276
    public static function getShowQueries()
277
    {
278
        return self::$showQueries;
279
    }
280
281
    /**
282
     * Override default showQueries mode
283
     *
284
     * @param boolean $showQueries
285
     * @return void
286
     */
287
    public static function setShowQueries($showQueries)
288
    {
289
        self::$showQueries = $showQueries;
290
    }
291
292
    /**
293
     * Helper to access this module resources
294
     *
295
     * @param string $path
296
     * @return ModuleResource
297
     */
298
    public static function moduleResource($path)
299
    {
300
        return ModuleLoader::getModule('lekoala/silverstripe-debugbar')->getResource($path);
301
    }
302
303
    public static function suppressJquery($flag = true)
304
    {
305
        $file = "debugbar/assets/vendor/jquery/dist/jquery.min.js";
306
        if ($flag) {
307
            Requirements::block($file);
308
        } else {
309
            Requirements::unblock($file);
310
        }
311
312
        self::$suppressJquery = $flag;
313
    }
314
315
    /**
316
     * Include DebugBar assets using Requirements API
317
     * This needs to be called before the template is rendered otherwise the calls to the Requirements API are ignored
318
     *
319
     * @return bool
320
     */
321
    public static function includeRequirements()
322
    {
323
        $debugbar = self::getDebugBar();
324
325
        if (!$debugbar) {
0 ignored issues
show
introduced by
$debugbar is of type DebugBar\DebugBar, thus it always evaluated to true.
Loading history...
326
            return false;
327
        }
328
329
        // Already called
330
        if (self::$renderer) {
331
            return false;
332
        }
333
334
        $renderer = $debugbar->getJavascriptRenderer();
335
336
        // We don't need the true path since we are going to use Requirements API that appends the BASE_PATH
337
        $assetsResource = self::moduleResource('assets');
338
        $renderer->setBasePath($assetsResource->getRelativePath());
339
        $renderer->setBaseUrl(Director::makeRelative($assetsResource->getURL()));
340
341
        $includeJquery = self::config()->get('include_jquery');
342
        // In CMS, jQuery is already included
343
        if (self::isAdminController()) {
344
            $includeJquery = false;
345
        }
346
        // If jQuery is already included, set to false
347
        $js = Requirements::backend()->getJavascript();
348
        foreach ($js as $url => $args) {
349
            $name = basename($url);
350
            if ($name == 'jquery.js' || $name == 'jquery.min.js') {
351
                $includeJquery = false;
352
                break;
353
            }
354
        }
355
356
        if ($includeJquery) {
357
            $renderer->setEnableJqueryNoConflict(true);
358
        } else {
359
            $renderer->disableVendor('jquery');
360
            $renderer->setEnableJqueryNoConflict(false);
361
        }
362
363
        if (DebugBar::config()->get('enable_storage')) {
364
            $renderer->setOpenHandlerUrl('__debugbar');
365
        }
366
367
        foreach ($renderer->getAssets('css') as $cssFile) {
368
            Requirements::css(self::replaceAssetPath($cssFile));
369
        }
370
371
        foreach ($renderer->getAssets('js') as $jsFile) {
372
            Requirements::javascript(self::replaceAssetPath($jsFile), [
373
                'type' => 'application/javascript'
374
            ]);
375
        }
376
377
        self::$renderer = $renderer;
378
379
        return true;
380
    }
381
382
    protected static function replaceAssetPath($file)
383
    {
384
        return Director::makeRelative(str_replace('\\', '/', ltrim($file, '/')));
385
    }
386
387
    /**
388
     * Returns the script to display the DebugBar
389
     *
390
     * @return string
391
     */
392
    public static function renderDebugBar()
393
    {
394
        if (!self::$renderer) {
395
            return;
396
        }
397
398
        // If we have any extra time pending, add it
399
        if (!empty(self::$extraTimes)) {
400
            foreach (self::$extraTimes as $extraTime => $extraTimeData) {
401
                self::trackTime($extraTime);
402
            }
403
        }
404
405
        // Requirements may have been cleared (CMS iframes...) or not set
406
        $js = Requirements::backend()->getJavascript();
407
        $debugBarResource = self::moduleResource('assets/debugbar.js');
408
        $path = $debugBarResource->getRelativePath();
409
410
        // Url in getJavascript has a / slash, so fix if necessary
411
        $path = str_replace("\\", "/", $path);
412
        if (!array_key_exists($path, $js)) {
413
            return;
414
        }
415
        $initialize = true;
416
        if (Director::is_ajax()) {
417
            $initialize = false;
418
        }
419
420
        // Normally deprecation notices are output in a shutdown function, which runs well after debugbar has rendered.
421
        // This ensures the deprecation notices which have been noted up to this point are logged out and collected by
422
        // the MonologCollector.
423
        Deprecation::outputNotices();
424
425
        $script = self::$renderer->render($initialize);
426
427
        return $script;
428
    }
429
430
    /**
431
     * Get all criteria why the DebugBar could be disabled
432
     *
433
     * @return array
434
     */
435
    public static function disabledCriteria()
436
    {
437
        $reasons = array();
438
        if (!Director::isDev() && !self::allowAllEnvironments()) {
439
            $reasons[] = 'Not in dev mode';
440
        }
441
        if (self::isDisabled()) {
442
            $reasons[] = 'Disabled by a constant or configuration';
443
        }
444
        if (self::vendorNotInstalled()) {
445
            $reasons[] = 'DebugBar is not installed in vendors';
446
        }
447
        if (self::notLocalIp()) {
448
            $reasons[] = 'Not a local ip';
449
        }
450
        if (Director::is_cli()) {
451
            $reasons[] = 'In CLI mode';
452
        }
453
        if (self::isDevUrl()) {
454
            $reasons[] = 'Dev tools';
455
        }
456
        if (self::isAdminUrl() && !self::config()->get('enabled_in_admin')) {
457
            $reasons[] = 'In admin';
458
        }
459
        if (isset($_GET['CMSPreview'])) {
460
            $reasons[] = 'CMS Preview';
461
        }
462
        if (self::isExcludedRoute()) {
463
            $reasons[] = 'Route excluded';
464
        }
465
        return $reasons;
466
    }
467
468
    /**
469
     * Determine why DebugBar is disabled
470
     *
471
     * Deprecated in favor of disabledCriteria
472
     *
473
     * @return string
474
     */
475
    public static function whyDisabled()
476
    {
477
        $reasons = self::disabledCriteria();
478
        if (!empty($reasons)) {
479
            return $reasons[0];
480
        }
481
        return "I don't know why";
482
    }
483
484
    public static function vendorNotInstalled()
485
    {
486
        return !class_exists('DebugBar\\StandardDebugBar');
487
    }
488
489
    public static function notLocalIp()
490
    {
491
        if (!self::config()->get('check_local_ip')) {
492
            return false;
493
        }
494
        if (isset($_SERVER['REMOTE_ADDR'])) {
495
            return !in_array($_SERVER['REMOTE_ADDR'], array('127.0.0.1', '::1', '1'));
496
        }
497
        return false;
498
    }
499
500
    public static function allowAllEnvironments()
501
    {
502
        // You will also need to add a debugbar-live config
503
        if (Environment::getEnv('DEBUGBAR_ALLOW_ALL_ENV')) {
504
            return true;
505
        }
506
        return false;
507
    }
508
509
    public static function isDisabled()
510
    {
511
        if (Environment::getEnv('DEBUGBAR_DISABLE') || static::config()->get('disabled')) {
512
            return true;
513
        }
514
        return false;
515
    }
516
517
    public static function isDevUrl()
518
    {
519
        return strpos(self::getRequestUrl(), '/dev/') === 0;
520
    }
521
522
    public static function isAdminUrl()
523
    {
524
        $baseUrl = rtrim(BASE_URL, '/');
525
        if (class_exists(AdminRootController::class)) {
526
            $adminUrl = AdminRootController::config()->get('url_base');
527
        } else {
528
            $adminUrl = 'admin';
529
        }
530
531
        return strpos(self::getRequestUrl(), $baseUrl . '/' . $adminUrl . '/') === 0;
532
    }
533
534
    public static function isExcludedRoute()
535
    {
536
        $isExcluded = false;
537
        $excludedRoutes = self::config()->get('excluded_routes');
538
        if (!empty($excludedRoutes)) {
539
            $url = self::getRequestUrl();
540
            foreach ($excludedRoutes as $excludedRoute) {
541
                if (strpos($url, $excludedRoute) === 0) {
542
                    $isExcluded = true;
543
                    break;
544
                }
545
            }
546
        }
547
        return $isExcluded;
548
    }
549
550
    public static function isAdminController()
551
    {
552
        if (Controller::curr()) {
553
            return Controller::curr() instanceof LeftAndMain;
554
        }
555
        return self::isAdminUrl();
556
    }
557
558
    /**
559
     * Avoid triggering data collection for open handler
560
     *
561
     * @return boolean
562
     */
563
    public static function isDebugBarRequest()
564
    {
565
        if ($url = self::getRequestUrl()) {
566
            return strpos($url, '/__debugbar') === 0;
567
        }
568
        return true;
569
    }
570
571
    /**
572
     * Get request url
573
     *
574
     * @return string
575
     */
576
    public static function getRequestUrl()
577
    {
578
        if (isset($_REQUEST['url'])) {
579
            return $_REQUEST['url'];
580
        }
581
        if (isset($_SERVER['REQUEST_URI'])) {
582
            return $_SERVER['REQUEST_URI'];
583
        }
584
        return '';
585
    }
586
587
    /**
588
     * Helper to make code cleaner
589
     *
590
     * @param callable $callback
591
     */
592
    public static function withDebugBar($callback)
593
    {
594
        if (self::getDebugBar() && !self::isDebugBarRequest()) {
595
            $callback(self::getDebugBar());
596
        }
597
    }
598
599
    /**
600
     * Set the current request. Is provided by the DebugBarMiddleware.
601
     *
602
     * @param HTTPRequest $request
603
     */
604
    public static function setRequest(HTTPRequest $request)
605
    {
606
        self::$request = $request;
607
    }
608
609
    /**
610
     * Get the current request
611
     *
612
     * @return HTTPRequest
613
     */
614
    public static function getRequest()
615
    {
616
        if (self::$request) {
617
            return self::$request;
618
        }
619
        // Fall back to trying from the global state
620
        if (Controller::has_curr()) {
621
            return Controller::curr()->getRequest();
622
        }
623
    }
624
625
    /**
626
     * @return TimeDataCollector|false
627
     */
628
    public static function getTimeCollector()
629
    {
630
        $debugbar = self::getDebugBar();
631
        if (!$debugbar) {
0 ignored issues
show
introduced by
$debugbar is of type DebugBar\DebugBar, thus it always evaluated to true.
Loading history...
632
            return false;
633
        }
634
        return $debugbar->getCollector('time');
635
    }
636
637
    /**
638
     * @return MessagesCollector|false
639
     */
640
    public static function getMessageCollector()
641
    {
642
        $debugbar = self::getDebugBar();
643
        if (!$debugbar) {
0 ignored issues
show
introduced by
$debugbar is of type DebugBar\DebugBar, thus it always evaluated to true.
Loading history...
644
            return false;
645
        }
646
        return  $debugbar->getCollector('messages');
647
    }
648
649
    /**
650
     * Start/stop time tracking (also before init)
651
     *
652
     * @param string $label
653
     * @return void
654
     */
655
    public static function trackTime($label)
656
    {
657
        if (!isset(self::$extraTimes[$label])) {
658
            self::$extraTimes[$label] = [microtime(true)];
659
        } else {
660
            self::$extraTimes[$label][] = microtime(true);
661
662
            // If we have the debugbar instance, add the measure
663
            if (self::$debugbar) {
664
                $timeData = self::getTimeCollector();
665
                if (!$timeData) {
0 ignored issues
show
introduced by
$timeData is of type LeKoala\DebugBar\Collector\TimeDataCollector, thus it always evaluated to true.
Loading history...
666
                    return;
667
                }
668
                $values = self::$extraTimes[$label];
669
                $timeData->addMeasure(
670
                    $label,
671
                    $values[0],
672
                    $values[1]
673
                );
674
                unset(self::$extraTimes[$label]);
675
            }
676
        }
677
    }
678
679
    /**
680
     * Close any open extra time record
681
     *
682
     * @return void
683
     */
684
    public static function closeExtraTime()
685
    {
686
        foreach (self::$extraTimes as $label => $values) {
687
            if (!isset($values[1])) {
688
                self::$extraTimes[$label][] = microtime(true);
689
            }
690
        }
691
    }
692
693
    /**
694
     * Add extra time to time collector
695
     */
696
    public static function measureExtraTime()
697
    {
698
        $timeData = self::getTimeCollector();
699
        if (!$timeData) {
0 ignored issues
show
introduced by
$timeData is of type LeKoala\DebugBar\Collector\TimeDataCollector, thus it always evaluated to true.
Loading history...
700
            return;
701
        }
702
        foreach (self::$extraTimes as $label => $values) {
703
            if (!isset($values[1])) {
704
                continue; // unfinished measure
705
            }
706
            $timeData->addMeasure(
707
                $label,
708
                $values[0],
709
                $values[1]
710
            );
711
            unset(self::$extraTimes[$label]);
712
        }
713
    }
714
}
715