Issues (59)

code/DebugBar.php (5 issues)

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