DebugBar::isDebugBarRequest()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 6
rs 10
c 0
b 0
f 0
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|false|null
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|null
72
     */
73
    protected static $renderer;
74
75
    /**
76
     * @var bool
77
     */
78
    protected static $showQueries = false;
79
80
    /**
81
     * @var HTTPRequest|null
82
     */
83
    protected static $request;
84
85
    /**
86
     * @var array<string,array<float>>
87
     */
88
    protected static $extraTimes = [];
89
90
    /**
91
     * Get the Debug Bar instance
92
     * @throws Exception
93
     * @global array $databaseConfig
94
     * @return BaseDebugBar|false
95
     */
96
    public static function getDebugBar()
97
    {
98
        if (self::$debugbar !== null) {
99
            return self::$debugbar;
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::debugbar also could return the type boolean which is incompatible with the documented return type DebugBar\DebugBar|false.
Loading history...
100
        }
101
102
        $reasons = self::disabledCriteria();
103
        if (!empty($reasons)) {
104
            self::$debugbar = false; // no need to check again
105
            return false;
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|false.
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;
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::debugbar also could return the type true which is incompatible with the documented return type DebugBar\DebugBar|null.
Loading history...
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($cacheCollector = new CacheCollector);
197
            $cacheCollector->setShowGet(self::config()->cache_collector_show_get);
198
        }
199
200
        // Partial cache
201
        if (self::config()->partial_cache_collector) {
202
            $debugbar->addCollector(new PartialCacheCollector);
203
        }
204
205
        // Email logging
206
        if (self::config()->email_collector) {
207
            $mailer = Injector::inst()->get(MailerInterface::class);
208
            if ($mailer instanceof Mailer) {
209
                $debugbar->addCollector(new SymfonyMailerCollector);
210
            }
211
        }
212
213
        // Since we buffer everything, why not enable all dev options ?
214
        if (self::config()->get('auto_debug')) {
215
            $_REQUEST['debug'] = true;
216
            $_REQUEST['debug_request'] = true;
217
        }
218
219
        if (isset($_REQUEST['debug']) || isset($_REQUEST['debug_request'])) {
220
            self::$bufferingEnabled = true;
221
            ob_start(); // We buffer everything until we have called an action
222
        }
223
224
        return $debugbar;
225
    }
226
227
    /**
228
     * Access a protected property when the api does not allow access
229
     *
230
     * @param object $object
231
     * @param string $property
232
     * @return mixed
233
     */
234
    protected static function getProtectedValue($object, $property)
235
    {
236
        $refObject = new ReflectionObject($object);
237
        $refProperty = $refObject->getProperty($property);
238
        $refProperty->setAccessible(true);
239
        return $refProperty->getValue($object);
240
    }
241
242
    /**
243
     * Set a protected property when the api does not allow access
244
     *
245
     * @param object $object
246
     * @param string $property
247
     * @param mixed $newValue
248
     * @return void
249
     */
250
    protected static function setProtectedValue($object, $property, $newValue)
251
    {
252
        $refObject = new ReflectionObject($object);
253
        $refProperty = $refObject->getProperty($property);
254
        $refProperty->setAccessible(true);
255
        $refProperty->setValue($object, $newValue);
256
    }
257
258
    /**
259
     * Clear the current instance of DebugBar
260
     *
261
     * @return void
262
     */
263
    public static function clearDebugBar()
264
    {
265
        self::$debugbar = null;
266
        self::$bufferingEnabled = false;
267
        self::$renderer = null;
268
        self::$showQueries = false;
269
        self::$request = null;
270
        self::$extraTimes = [];
271
        ProxyDBExtension::resetQueries();
272
    }
273
274
    /**
275
     * @return boolean
276
     */
277
    public static function getShowQueries()
278
    {
279
        return self::$showQueries;
280
    }
281
282
    /**
283
     * Override default showQueries mode
284
     *
285
     * @param boolean $showQueries
286
     * @return void
287
     */
288
    public static function setShowQueries($showQueries)
289
    {
290
        self::$showQueries = $showQueries;
291
    }
292
293
    /**
294
     * Helper to access this module resources
295
     *
296
     * @param string $path
297
     * @return ModuleResource
298
     */
299
    public static function moduleResource($path)
300
    {
301
        return ModuleLoader::getModule('lekoala/silverstripe-debugbar')->getResource($path);
302
    }
303
304
    /**
305
     * @param bool $flag
306
     * @return void
307
     */
308
    public static function suppressJquery($flag = true)
309
    {
310
        $file = "debugbar/assets/vendor/jquery/dist/jquery.min.js";
311
        if ($flag) {
312
            Requirements::block($file);
313
        } else {
314
            Requirements::unblock($file);
315
        }
316
317
        self::$suppressJquery = $flag;
318
    }
319
320
    /**
321
     * Include DebugBar assets using Requirements API
322
     * This needs to be called before the template is rendered otherwise the calls to the Requirements API are ignored
323
     *
324
     * @return bool
325
     */
326
    public static function includeRequirements()
327
    {
328
        $debugbar = self::getDebugBar();
329
330
        if (!$debugbar) {
0 ignored issues
show
introduced by
$debugbar is of type DebugBar\DebugBar, thus it always evaluated to true.
Loading history...
331
            return false;
332
        }
333
334
        // Already called
335
        if (self::$renderer) {
336
            return false;
337
        }
338
339
        $renderer = $debugbar->getJavascriptRenderer();
340
341
        // We don't need the true path since we are going to use Requirements API that appends the BASE_PATH
342
        $assetsResource = self::moduleResource('assets');
343
        $renderer->setBasePath($assetsResource->getRelativePath());
344
        $renderer->setBaseUrl(Director::makeRelative($assetsResource->getURL()));
345
346
        $includeJquery = self::config()->get('include_jquery');
347
        // In CMS, jQuery is already included
348
        if (self::isAdminController()) {
349
            $includeJquery = false;
350
        }
351
        // If jQuery is already included, set to false
352
        $js = Requirements::backend()->getJavascript();
353
        foreach ($js as $url => $args) {
354
            $name = basename($url);
355
            if ($name == 'jquery.js' || $name == 'jquery.min.js') {
356
                $includeJquery = false;
357
                break;
358
            }
359
        }
360
361
        if ($includeJquery) {
362
            $renderer->setEnableJqueryNoConflict(true);
363
        } else {
364
            $renderer->disableVendor('jquery');
365
            $renderer->setEnableJqueryNoConflict(false);
366
        }
367
368
        if (DebugBar::config()->get('enable_storage')) {
369
            $renderer->setOpenHandlerUrl('__debugbar');
370
        }
371
372
        foreach ($renderer->getAssets('css') as $cssFile) {
373
            Requirements::css(self::replaceAssetPath($cssFile));
374
        }
375
376
        foreach ($renderer->getAssets('js') as $jsFile) {
377
            Requirements::javascript(self::replaceAssetPath($jsFile), [
378
                'type' => 'application/javascript'
379
            ]);
380
        }
381
382
        self::$renderer = $renderer;
383
384
        return true;
385
    }
386
387
    /**
388
     * @param string $file
389
     * @return string
390
     */
391
    protected static function replaceAssetPath($file)
392
    {
393
        return Director::makeRelative(str_replace('\\', '/', ltrim($file, '/')));
394
    }
395
396
    /**
397
     * Returns the script to display the DebugBar
398
     *
399
     * @return string
400
     */
401
    public static function renderDebugBar()
402
    {
403
        if (!self::$renderer) {
404
            return '';
405
        }
406
407
        // If we have any extra time pending, add it
408
        if (!empty(self::$extraTimes)) {
409
            foreach (self::$extraTimes as $extraTime => $extraTimeData) {
410
                self::trackTime($extraTime);
411
            }
412
        }
413
414
        // Requirements may have been cleared (CMS iframes...) or not set
415
        $js = Requirements::backend()->getJavascript();
416
        $debugBarResource = self::moduleResource('assets/debugbar.js');
417
        $path = $debugBarResource->getRelativePath();
418
419
        // Url in getJavascript has a / slash, so fix if necessary
420
        $path = str_replace("\\", "/", $path);
421
        if (!array_key_exists($path, $js)) {
422
            return '';
423
        }
424
        $initialize = true;
425
        if (Director::is_ajax()) {
426
            $initialize = false;
427
        }
428
429
        // Normally deprecation notices are output in a shutdown function, which runs well after debugbar has rendered.
430
        // This ensures the deprecation notices which have been noted up to this point are logged out and collected by
431
        // the MonologCollector.
432
        Deprecation::outputNotices();
433
434
        $script = self::$renderer->render($initialize);
435
436
        return $script;
437
    }
438
439
    /**
440
     * Get all criteria why the DebugBar could be disabled
441
     *
442
     * @return array<string>
443
     */
444
    public static function disabledCriteria()
445
    {
446
        $reasons = array();
447
        if (!Director::isDev() && !self::allowAllEnvironments()) {
448
            $reasons[] = 'Not in dev mode';
449
        }
450
        if (self::isDisabled()) {
451
            $reasons[] = 'Disabled by a constant or configuration';
452
        }
453
        if (self::vendorNotInstalled()) {
454
            $reasons[] = 'DebugBar is not installed in vendors';
455
        }
456
        if (self::notLocalIp()) {
457
            $reasons[] = 'Not a local ip';
458
        }
459
        if (Director::is_cli()) {
460
            $reasons[] = 'In CLI mode';
461
        }
462
        if (self::isDevUrl()) {
463
            $reasons[] = 'Dev tools';
464
        }
465
        if (self::isAdminUrl() && !self::config()->get('enabled_in_admin')) {
466
            $reasons[] = 'In admin';
467
        }
468
        if (isset($_GET['CMSPreview'])) {
469
            $reasons[] = 'CMS Preview';
470
        }
471
        if (self::isExcludedRoute()) {
472
            $reasons[] = 'Route excluded';
473
        }
474
        return $reasons;
475
    }
476
477
    /**
478
     * Determine why DebugBar is disabled
479
     *
480
     * Deprecated in favor of disabledCriteria
481
     *
482
     * @return string
483
     */
484
    public static function whyDisabled()
485
    {
486
        $reasons = self::disabledCriteria();
487
        if (!empty($reasons)) {
488
            return $reasons[0];
489
        }
490
        return "I don't know why";
491
    }
492
493
    /**
494
     * @return bool
495
     */
496
    public static function vendorNotInstalled()
497
    {
498
        return !class_exists('DebugBar\\StandardDebugBar');
499
    }
500
501
    /**
502
     * @return bool
503
     */
504
    public static function notLocalIp()
505
    {
506
        if (!self::config()->get('check_local_ip')) {
507
            return false;
508
        }
509
        if (isset($_SERVER['REMOTE_ADDR'])) {
510
            return !in_array($_SERVER['REMOTE_ADDR'], array('127.0.0.1', '::1', '1'));
511
        }
512
        return false;
513
    }
514
515
    /**
516
     * @return bool
517
     */
518
    public static function allowAllEnvironments()
519
    {
520
        // You will also need to add a debugbar-live config
521
        if (Environment::getEnv('DEBUGBAR_ALLOW_ALL_ENV')) {
522
            return true;
523
        }
524
        return false;
525
    }
526
527
    /**
528
     * @return bool
529
     */
530
    public static function isDisabled()
531
    {
532
        if (Environment::getEnv('DEBUGBAR_DISABLE') || static::config()->get('disabled')) {
533
            return true;
534
        }
535
        return false;
536
    }
537
538
    /**
539
     * @return bool
540
     */
541
    public static function isDevUrl()
542
    {
543
        return strpos(self::getRequestUrl(), '/dev/') === 0;
544
    }
545
546
    /**
547
     * @return bool
548
     */
549
    public static function isAdminUrl()
550
    {
551
        $baseUrl = rtrim(BASE_URL, '/');
552
        if (class_exists(AdminRootController::class)) {
553
            $adminUrl = AdminRootController::config()->get('url_base');
554
        } else {
555
            $adminUrl = 'admin';
556
        }
557
558
        return strpos(self::getRequestUrl(), $baseUrl . '/' . $adminUrl . '/') === 0;
559
    }
560
561
    /**
562
     * @return bool
563
     */
564
    public static function isExcludedRoute()
565
    {
566
        $isExcluded = false;
567
        $excludedRoutes = self::config()->get('excluded_routes');
568
        if (!empty($excludedRoutes)) {
569
            $url = self::getRequestUrl();
570
            foreach ($excludedRoutes as $excludedRoute) {
571
                if (strpos($url, $excludedRoute) === 0) {
572
                    $isExcluded = true;
573
                    break;
574
                }
575
            }
576
        }
577
        return $isExcluded;
578
    }
579
580
    /**
581
     * @return bool
582
     */
583
    public static function isAdminController()
584
    {
585
        if (Controller::has_curr()) {
586
            return Controller::curr() instanceof LeftAndMain;
587
        }
588
        return self::isAdminUrl();
589
    }
590
591
    /**
592
     * Avoid triggering data collection for open handler
593
     *
594
     * @return boolean
595
     */
596
    public static function isDebugBarRequest()
597
    {
598
        if ($url = self::getRequestUrl()) {
599
            return strpos($url, '/__debugbar') === 0;
600
        }
601
        return true;
602
    }
603
604
    /**
605
     * Get request url
606
     *
607
     * @return string
608
     */
609
    public static function getRequestUrl()
610
    {
611
        if (isset($_REQUEST['url'])) {
612
            return $_REQUEST['url'];
613
        }
614
        if (isset($_SERVER['REQUEST_URI'])) {
615
            return $_SERVER['REQUEST_URI'];
616
        }
617
        return '';
618
    }
619
620
    /**
621
     * Helper to make code cleaner
622
     *
623
     * @param callable $callback
624
     * @return void
625
     */
626
    public static function withDebugBar($callback)
627
    {
628
        if (self::getDebugBar() && !self::isDebugBarRequest()) {
629
            $callback(self::getDebugBar());
630
        }
631
    }
632
633
    /**
634
     * Set the current request. Is provided by the DebugBarMiddleware.
635
     *
636
     * @param HTTPRequest $request
637
     * @return void
638
     */
639
    public static function setRequest(HTTPRequest $request)
640
    {
641
        self::$request = $request;
642
    }
643
644
    /**
645
     * Get the current request
646
     *
647
     * @return HTTPRequest|null
648
     */
649
    public static function getRequest()
650
    {
651
        if (self::$request) {
652
            return self::$request;
653
        }
654
        // Fall back to trying from the global state
655
        if (Controller::has_curr()) {
656
            return Controller::curr()->getRequest();
657
        }
658
        return null;
659
    }
660
661
    /**
662
     * @return TimeDataCollector|false
663
     */
664
    public static function getTimeCollector()
665
    {
666
        $debugbar = self::getDebugBar();
667
        if (!$debugbar) {
0 ignored issues
show
introduced by
$debugbar is of type DebugBar\DebugBar, thus it always evaluated to true.
Loading history...
668
            return false;
669
        }
670
        //@phpstan-ignore-next-line
671
        return $debugbar->getCollector('time');
672
    }
673
674
    /**
675
     * @return MessagesCollector|false
676
     */
677
    public static function getMessageCollector()
678
    {
679
        $debugbar = self::getDebugBar();
680
        if (!$debugbar) {
0 ignored issues
show
introduced by
$debugbar is of type DebugBar\DebugBar, thus it always evaluated to true.
Loading history...
681
            return false;
682
        }
683
        //@phpstan-ignore-next-line
684
        return  $debugbar->getCollector('messages');
685
    }
686
687
    /**
688
     * Start/stop time tracking (also before init)
689
     *
690
     * @param string $label
691
     * @return void
692
     */
693
    public static function trackTime($label)
694
    {
695
        if (!isset(self::$extraTimes[$label])) {
696
            self::$extraTimes[$label] = [microtime(true)];
697
        } else {
698
            self::$extraTimes[$label][] = microtime(true);
699
700
            // If we have the debugbar instance, add the measure
701
            if (self::$debugbar) {
702
                $timeData = self::getTimeCollector();
703
                if (!$timeData) {
0 ignored issues
show
introduced by
$timeData is of type LeKoala\DebugBar\Collector\TimeDataCollector, thus it always evaluated to true.
Loading history...
704
                    return;
705
                }
706
                $values = self::$extraTimes[$label];
707
                $timeData->addMeasure(
708
                    $label,
709
                    $values[0],
710
                    $values[1]
711
                );
712
                unset(self::$extraTimes[$label]);
713
            }
714
        }
715
    }
716
717
    /**
718
     * Close any open extra time record
719
     *
720
     * @return void
721
     */
722
    public static function closeExtraTime()
723
    {
724
        foreach (self::$extraTimes as $label => $values) {
725
            if (!isset($values[1])) {
726
                self::$extraTimes[$label][] = microtime(true);
727
            }
728
        }
729
    }
730
731
    /**
732
     * Add extra time to time collector
733
     * @return void
734
     */
735
    public static function measureExtraTime()
736
    {
737
        $timeData = self::getTimeCollector();
738
        if (!$timeData) {
0 ignored issues
show
introduced by
$timeData is of type LeKoala\DebugBar\Collector\TimeDataCollector, thus it always evaluated to true.
Loading history...
739
            return;
740
        }
741
        foreach (self::$extraTimes as $label => $values) {
742
            if (!isset($values[1])) {
743
                continue; // unfinished measure
744
            }
745
            $timeData->addMeasure(
746
                $label,
747
                $values[0],
748
                $values[1]
749
            );
750
            unset(self::$extraTimes[$label]);
751
        }
752
    }
753
}
754