Completed
Pull Request — master (#482)
by Markus
63:57
created

PrettyPageHandler::addDataTableCallback()   B

Complexity

Conditions 5
Paths 2

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 5.9256

Importance

Changes 0
Metric Value
dl 0
loc 18
c 0
b 0
f 0
ccs 6
cts 9
cp 0.6667
rs 8.8571
cc 5
eloc 9
nc 2
nop 2
crap 5.9256
1
<?php
2
/**
3
 * Whoops - php errors for cool kids
4
 * @author Filipe Dobreira <http://github.com/filp>
5
 */
6
7
namespace Whoops\Handler;
8
9
use InvalidArgumentException;
10
use RuntimeException;
11
use Symfony\Component\VarDumper\Cloner\AbstractCloner;
12
use Symfony\Component\VarDumper\Cloner\VarCloner;
13
use UnexpectedValueException;
14
use Whoops\Exception\Formatter;
15
use Whoops\Util\Misc;
16
use Whoops\Util\TemplateHelper;
17
18
class PrettyPageHandler extends Handler
19
{
20
    /**
21
     * Search paths to be scanned for resources, in the reverse
22
     * order they're declared.
23
     *
24
     * @var array
25
     */
26
    private $searchPaths = [];
27
28
    /**
29
     * Fast lookup cache for known resource locations.
30
     *
31
     * @var array
32
     */
33
    private $resourceCache = [];
34
35
    /**
36
     * The name of the custom css file.
37
     *
38
     * @var string
39
     */
40
    private $customCss = null;
41
42
    /**
43
     * @var array[]
44
     */
45
    private $extraTables = [];
46
47
    /**
48
     * @var bool
49
     */
50
    private $handleUnconditionally = false;
51
52
    /**
53
     * @var string
54
     */
55
    private $pageTitle = "Whoops! There was an error.";
56
57
    /**
58
     * @var array[]
59
     */
60
    private $applicationPaths;
61
62
    /**
63
     * @var array[]
64
     */
65
    private $blacklist = [
66
        '_GET' => [],
67
        '_POST' => [],
68
        '_FILES' => [],
69
        '_COOKIE' => [],
70
        '_SESSION' => [],
71
        '_SERVER' => [],
72
        '_ENV' => [],
73
    ];
74
75
    /**
76
     * A string identifier for a known IDE/text editor, or a closure
77
     * that resolves a string that can be used to open a given file
78
     * in an editor. If the string contains the special substrings
79
     * %file or %line, they will be replaced with the correct data.
80
     *
81
     * @example
82
     *  "txmt://open?url=%file&line=%line"
83
     * @var mixed $editor
84
     */
85
    protected $editor;
86
87
    /**
88
     * A list of known editor strings
89
     * @var array
90 1
     */
91
    protected $editors = [
92 1
        "sublime"  => "subl://open?url=file://%file&line=%line",
93
        "textmate" => "txmt://open?url=file://%file&line=%line",
94
        "emacs"    => "emacs://open?url=file://%file&line=%line",
95 1
        "macvim"   => "mvim://open/?url=file://%file&line=%line",
96
        "phpstorm" => "phpstorm://open?file=%file&line=%line",
97 1
        "idea"     => "idea://open?file=%file&line=%line",
98
    ];
99
100 1
    /**
101 1
     * Constructor.
102
     */
103
    public function __construct()
104
    {
105
        if (ini_get('xdebug.file_link_format') || extension_loaded('xdebug')) {
106 1
            // Register editor using xdebug's file_link_format option.
107
            $this->editors['xdebug'] = function ($file, $line) {
108 1
                return str_replace(['%f', '%l'], [$file, $line], ini_get('xdebug.file_link_format'));
109
            };
110
        }
111 1
112
        // Add the default, local resource search path:
113
        $this->searchPaths[] = __DIR__ . "/../Resources";
114 1
115
        // blacklist php provided auth based values
116
        $this->blacklist('_SERVER', 'PHP_AUTH_PW');
117
    }
118
119
    /**
120
     * @return int|null
121 1
     */
122
    public function handle()
123
    {
124
        if (!$this->handleUnconditionally()) {
125
            // Check conditions for outputting HTML:
126
            // @todo: Make this more robust
127
            if (php_sapi_name() === 'cli') {
128
                // Help users who have been relying on an internal test value
129
                // fix their code to the proper method
130
                if (isset($_ENV['whoops-test'])) {
131
                    throw new \Exception(
132
                        'Use handleUnconditionally instead of whoops-test'
133
                        .' environment variable'
134
                    );
135
                }
136
137
                return Handler::DONE;
138
            }
139
        }
140
141
        // @todo: Make this more dynamic
142
        $helper = new TemplateHelper();
143
144
        if (class_exists('Symfony\Component\VarDumper\Cloner\VarCloner')) {
145
            $cloner = new VarCloner();
146
            // Only dump object internals if a custom caster exists.
147
            $cloner->addCasters(['*' => function ($obj, $a, $stub, $isNested, $filter = 0) {
148
                $class = $stub->class;
149
                $classes = [$class => $class] + class_parents($class) + class_implements($class);
150
151
                foreach ($classes as $class) {
152
                    if (isset(AbstractCloner::$defaultCasters[$class])) {
153
                        return $a;
154
                    }
155
                }
156
157
                // Remove all internals
158
                return [];
159
            }]);
160
            $helper->setCloner($cloner);
161
        }
162
163
        $templateFile = $this->getResource("views/layout.html.php");
164
        $cssFile      = $this->getResource("css/whoops.base.css");
165
        $zeptoFile    = $this->getResource("js/zepto.min.js");
166
        $clipboard    = $this->getResource("js/clipboard.min.js");
167
        $jsFile       = $this->getResource("js/whoops.base.js");
168
169
        if ($this->customCss) {
170
            $customCssFile = $this->getResource($this->customCss);
171
        }
172
173
        $inspector = $this->getInspector();
174
        $frames    = $inspector->getFrames();
175
176
        $code = $inspector->getException()->getCode();
177
178
        if ($inspector->getException() instanceof \ErrorException) {
179
            // ErrorExceptions wrap the php-error types within the "severity" property
180
            $code = Misc::translateErrorCode($inspector->getException()->getSeverity());
181
        }
182
183
        // Detect frames that belong to the application.
184
        if ($this->applicationPaths) {
185
            /* @var \Whoops\Exception\Frame $frame */
186
            foreach ($frames as $frame) {
187
                foreach ($this->applicationPaths as $path) {
188
                    if (substr($frame->getFile(), 0, strlen($path)) === $path) {
189
                        $frame->setApplication(true);
190
                        break;
191
                    }
192
                }
193
            }
194
        }
195
196
        // List of variables that will be passed to the layout template.
197
        $vars = [
198
            "page_title" => $this->getPageTitle(),
199
200
            // @todo: Asset compiler
201
            "stylesheet" => file_get_contents($cssFile),
202
            "zepto"      => file_get_contents($zeptoFile),
203
            "clipboard"  => file_get_contents($clipboard),
204
            "javascript" => file_get_contents($jsFile),
205
206
            // Template paths:
207
            "header"                     => $this->getResource("views/header.html.php"),
208
            "header_outer"               => $this->getResource("views/header_outer.html.php"),
209
            "frame_list"                 => $this->getResource("views/frame_list.html.php"),
210
            "frames_description"         => $this->getResource("views/frames_description.html.php"),
211
            "frames_container"           => $this->getResource("views/frames_container.html.php"),
212
            "panel_details"              => $this->getResource("views/panel_details.html.php"),
213
            "panel_details_outer"        => $this->getResource("views/panel_details_outer.html.php"),
214
            "panel_left"                 => $this->getResource("views/panel_left.html.php"),
215
            "panel_left_outer"           => $this->getResource("views/panel_left_outer.html.php"),
216
            "frame_code"                 => $this->getResource("views/frame_code.html.php"),
217
            "env_details"                => $this->getResource("views/env_details.html.php"),
218
219
            "title"          => $this->getPageTitle(),
220
            "name"           => explode("\\", $inspector->getExceptionName()),
221
            "message"        => $inspector->getException()->getMessage(),
222
            "code"           => $code,
223
            "plain_exception" => Formatter::formatExceptionPlain($inspector),
224
            "frames"         => $frames,
225
            "has_frames"     => !!count($frames),
226
            "handler"        => $this,
227
            "handlers"       => $this->getRun()->getHandlers(),
228
229
            "active_frames_tab" => count($frames) && $frames->offsetGet(0)->isApplication() ?  'application' : 'all',
230
            "has_frames_tabs"   => $this->getApplicationPaths(),
231
232
            "tables"      => [
233
                "GET Data"              => $this->masked('_GET'),
234
                "POST Data"             => $this->masked('_POST'),
235
                "Files"                 => $this->masked('_FILES'),
236
                "Cookies"               => $this->masked('_COOKIE'),
237
                "Session"               => isset($_SESSION) ? $this->masked('_SESSION') :  [],
238
                "Server/Request Data"   => $this->masked('_SERVER'),
239
                "Environment Variables" => $this->masked('_ENV'),
240
            ],
241
        ];
242
243
        if (isset($customCssFile)) {
244
            $vars["stylesheet"] .= file_get_contents($customCssFile);
245
        }
246
247
        // Add extra entries list of data tables:
248
        // @todo: Consolidate addDataTable and addDataTableCallback
249
        $extraTables = array_map(function ($table) use ($inspector) {
250
            return $table instanceof \Closure ? $table($inspector) : $table;
251
        }, $this->getDataTables());
252
        $vars["tables"] = array_merge($extraTables, $vars["tables"]);
253
254
        $plainTextHandler = new PlainTextHandler();
255
        $plainTextHandler->setException($this->getException());
256
        $plainTextHandler->setInspector($this->getInspector());
257
        $vars["preface"] = "<!--\n\n\n" . $plainTextHandler->generateResponse() . "\n\n\n\n\n\n\n\n\n\n\n-->";
258
259
        $helper->setVariables($vars);
260
        $helper->render($templateFile);
261
262
        return Handler::QUIT;
263
    }
264 1
265
    /**
266 1
     * @return string
267 1
     */
268
    public function contentType()
269
    {
270
        return 'text/html';
271
    }
272
273
    /**
274
     * Adds an entry to the list of tables displayed in the template.
275
     * The expected data is a simple associative array. Any nested arrays
276
     * will be flattened with print_r
277
     * @param string $label
278
     * @param array  $data
279 1
     */
280
    public function addDataTable($label, array $data)
281 1
    {
282
        $this->extraTables[$label] = $data;
283
    }
284
285 1
    /**
286
     * Lazily adds an entry to the list of tables displayed in the table.
287 1
     * The supplied callback argument will be called when the error is rendered,
288
     * it should produce a simple associative array. Any nested arrays will
289
     * be flattened with print_r.
290 1
     *
291
     * @throws InvalidArgumentException If $callback is not callable
292
     * @param  string                   $label
293
     * @param  callable                 $callback Callable returning an associative array
294
     */
295
    public function addDataTableCallback($label, /* callable */ $callback)
296 1
    {
297
        if (!is_callable($callback)) {
298
            throw new InvalidArgumentException('Expecting callback argument to be callable');
299
        }
300
301
        $this->extraTables[$label] = function (\Whoops\Exception\Inspector $inspector = null) use ($callback) {
302
            try {
303
                $result = call_user_func($callback, $inspector);
304
305 2
                // Only return the result if it can be iterated over by foreach().
306
                return is_array($result) || $result instanceof \Traversable ? $result : [];
307 2
            } catch (\Exception $e) {
308 2
                // Don't allow failure to break the rendering of the original exception.
309 2
                return [];
310
            }
311
        };
312 2
    }
313
314
    /**
315
     * Returns all the extra data tables registered with this handler.
316
     * Optionally accepts a 'label' parameter, to only return the data
317
     * table under that label.
318
     * @param  string|null      $label
319
     * @return array[]|callable
320
     */
321
    public function getDataTables($label = null)
322 1
    {
323
        if ($label !== null) {
324 1
            return isset($this->extraTables[$label]) ?
325 1
                   $this->extraTables[$label] : [];
326
        }
327
328
        return $this->extraTables;
329
    }
330
331
    /**
332
     * Allows to disable all attempts to dynamically decide whether to
333
     * handle or return prematurely.
334
     * Set this to ensure that the handler will perform no matter what.
335
     * @param  bool|null $value
336
     * @return bool|null
337
     */
338
    public function handleUnconditionally($value = null)
339
    {
340
        if (func_num_args() == 0) {
341
            return $this->handleUnconditionally;
342
        }
343
344
        $this->handleUnconditionally = (bool) $value;
345
    }
346
347 1
    /**
348
     * Adds an editor resolver, identified by a string
349 1
     * name, and that may be a string path, or a callable
350 1
     * resolver. If the callable returns a string, it will
351
     * be set as the file reference's href attribute.
352
     *
353
     * @example
354
     *  $run->addEditor('macvim', "mvim://open?url=file://%file&line=%line")
355
     * @example
356
     *   $run->addEditor('remove-it', function($file, $line) {
357
     *       unlink($file);
358
     *       return "http://stackoverflow.com";
359
     *   });
360
     * @param string $identifier
361
     * @param string $resolver
362
     */
363
    public function addEditor($identifier, $resolver)
364
    {
365
        $this->editors[$identifier] = $resolver;
366 4
    }
367
368 4
    /**
369
     * Set the editor to use to open referenced files, by a string
370
     * identifier, or a callable that will be executed for every
371
     * file reference, with a $file and $line argument, and should
372
     * return a string.
373
     *
374
     * @example
375 4
     *   $run->setEditor(function($file, $line) { return "file:///{$file}"; });
376 4
     * @example
377
     *   $run->setEditor('sublime');
378
     *
379
     * @throws InvalidArgumentException If invalid argument identifier provided
380
     * @param  string|callable          $editor
381
     */
382
    public function setEditor($editor)
383
    {
384
        if (!is_callable($editor) && !isset($this->editors[$editor])) {
385
            throw new InvalidArgumentException(
386
                "Unknown editor identifier: $editor. Known editors:" .
387
                implode(",", array_keys($this->editors))
388
            );
389 4
        }
390
391 4
        $this->editor = $editor;
392
    }
393 4
394
    /**
395
     * Given a string file path, and an integer file line,
396
     * executes the editor resolver and returns, if available,
397
     * a string that may be used as the href property for that
398
     * file reference.
399 4
     *
400
     * @throws InvalidArgumentException If editor resolver does not return a string
401
     * @param  string                   $filePath
402
     * @param  int                      $line
403
     * @return string|bool
404
     */
405 4
    public function getEditorHref($filePath, $line)
406 4
    {
407
        $editor = $this->getEditor($filePath, $line);
408 4
409
        if (empty($editor)) {
410
            return false;
411
        }
412
413
        // Check that the editor is a string, and replace the
414
        // %line and %file placeholders:
415
        if (!isset($editor['url']) || !is_string($editor['url'])) {
416
            throw new UnexpectedValueException(
417
                __METHOD__ . " should always resolve to a string or a valid editor array; got something else instead."
418
            );
419
        }
420
421 1
        $editor['url'] = str_replace("%line", rawurlencode($line), $editor['url']);
422
        $editor['url'] = str_replace("%file", rawurlencode($filePath), $editor['url']);
423 1
424
        return $editor['url'];
425
    }
426 1
427
    /**
428
     * Given a boolean if the editor link should
429
     * act as an Ajax request. The editor must be a
430
     * valid callable function/closure
431 1
     *
432
     * @throws UnexpectedValueException  If editor resolver does not return a boolean
433
     * @param  string                   $filePath
434
     * @param  int                      $line
435
     * @return bool
436
     */
437
    public function getEditorAjax($filePath, $line)
438
    {
439
        $editor = $this->getEditor($filePath, $line);
440
441
        // Check that the ajax is a bool
442
        if (!isset($editor['ajax']) || !is_bool($editor['ajax'])) {
443 1
            throw new UnexpectedValueException(
444
                __METHOD__ . " should always resolve to a bool; got something else instead."
445 1
            );
446
        }
447
        return $editor['ajax'];
448
    }
449 1
450
    /**
451
     * Given a boolean if the editor link should
452
     * act as an Ajax request. The editor must be a
453
     * valid callable function/closure
454
     *
455
     * @param  string $filePath
456 1
     * @param  int    $line
457 1
     * @return array
458
     */
459
    protected function getEditor($filePath, $line)
460 1
    {
461
        if (!$this->editor || (!is_string($this->editor) && !is_callable($this->editor))) {
462
            return [];
463 1
        }
464
465 1
        if (is_string($this->editor) && isset($this->editors[$this->editor]) && !is_callable($this->editors[$this->editor])) {
466 1
           return [
467 1
                'ajax' => false,
468
                'url' => $this->editors[$this->editor],
469
            ];
470
        }
471
472
        if (is_callable($this->editor) || (isset($this->editors[$this->editor]) && is_callable($this->editors[$this->editor]))) {
473
            if (is_callable($this->editor)) {
474
                $callback = call_user_func($this->editor, $filePath, $line);
475
            } else {
476
                $callback = call_user_func($this->editors[$this->editor], $filePath, $line);
477
            }
478
479
            if (is_string($callback)) {
480
                return [
481
                    'ajax' => false,
482
                    'url' => $callback,
483 1
                ];
484
            }
485 1
486 1
            return [
487
                'ajax' => isset($callback['ajax']) ? $callback['ajax'] : false,
488
                'url' => isset($callback['url']) ? $callback['url'] : $callback,
489
            ];
490
        }
491 1
492
        return [];
493 1
    }
494
495
    /**
496
     * @param  string $title
497
     * @return void
498
     */
499
    public function setPageTitle($title)
500
    {
501
        $this->pageTitle = (string) $title;
502
    }
503
504
    /**
505 2
     * @return string
506
     */
507 2
    public function getPageTitle()
508 1
    {
509 1
        return $this->pageTitle;
510 1
    }
511
512
    /**
513 1
     * Adds a path to the list of paths to be searched for
514 1
     * resources.
515
     *
516
     * @throws InvalidArgumentException If $path is not a valid directory
517
     *
518
     * @param  string $path
519
     * @return void
520
     */
521
    public function addResourcePath($path)
522
    {
523
        if (!is_dir($path)) {
524
            throw new InvalidArgumentException(
525
                "'$path' is not a valid directory"
526
            );
527
        }
528
529
        array_unshift($this->searchPaths, $path);
530 1
    }
531
532 1
    /**
533
     * Adds a custom css file to be loaded.
534
     *
535
     * @param  string $name
536
     * @return void
537
     */
538
    public function addCustomCss($name)
539
    {
540
        $this->customCss = $name;
541
    }
542
543
    /**
544
     * @return array
545
     */
546
    public function getResourcePaths()
547
    {
548
        return $this->searchPaths;
549
    }
550
551
    /**
552
     * Finds a resource, by its relative path, in all available search paths.
553
     * The search is performed starting at the last search path, and all the
554
     * way back to the first, enabling a cascading-type system of overrides
555
     * for all resources.
556
     *
557
     * @throws RuntimeException If resource cannot be found in any of the available paths
558
     *
559
     * @param  string $resource
560
     * @return string
561
     */
562
    protected function getResource($resource)
563
    {
564
        // If the resource was found before, we can speed things up
565
        // by caching its absolute, resolved path:
566
        if (isset($this->resourceCache[$resource])) {
567
            return $this->resourceCache[$resource];
568
        }
569
570
        // Search through available search paths, until we find the
571
        // resource we're after:
572
        foreach ($this->searchPaths as $path) {
573
            $fullPath = $path . "/$resource";
574
575
            if (is_file($fullPath)) {
576
                // Cache the result:
577
                $this->resourceCache[$resource] = $fullPath;
578
                return $fullPath;
579
            }
580
        }
581
582
        // If we got this far, nothing was found.
583
        throw new RuntimeException(
584
            "Could not find resource '$resource' in any resource paths."
585
            . "(searched: " . join(", ", $this->searchPaths). ")"
586
        );
587
    }
588
589
    /**
590
     * @deprecated
591
     *
592
     * @return string
593
     */
594
    public function getResourcesPath()
595
    {
596
        $allPaths = $this->getResourcePaths();
597
598
        // Compat: return only the first path added
599
        return end($allPaths) ?: null;
600
    }
601
602
    /**
603
     * @deprecated
604
     *
605
     * @param  string $resourcesPath
606
     * @return void
607
     */
608
    public function setResourcesPath($resourcesPath)
609
    {
610
        $this->addResourcePath($resourcesPath);
611
    }
612
613
    /**
614
     * Return the application paths.
615
     *
616
     * @return array
617
     */
618
    public function getApplicationPaths()
619
    {
620
        return $this->applicationPaths;
621
    }
622
623
    /**
624
     * Set the application paths.
625
     *
626
     * @param array $applicationPaths
627
     */
628
    public function setApplicationPaths($applicationPaths)
629
    {
630
        $this->applicationPaths = $applicationPaths;
631
    }
632
633
    /**
634
     * blacklist a sensitive value within one of the superglobal arrays.
635
     *
636
     * @param $superGlobalName string the name of the superglobal array, e.g. '_GET'
637
     * @param $key string the key within the superglobal
638
     */
639
    public function blacklist($superGlobalName, $key) {
640
        $this->blacklist[$superGlobalName][] = $key;
641
    }
642
643
    /**
644
     * Checks all values identified by the given superGlobalName within GLOBALS.
645
     * Blacklisted values will be replaced by a equal length string cointaining only '*' characters.
646
     *
647
     * @param $superGlobalName string the name of the superglobal array, e.g. '_GET'
648
     * @return array $values without sensitive data
649
     */
650
    private function masked($superGlobalName) {
651
        $blacklisted = $this->blacklist[$superGlobalName]
652
        $values = $GLOBALS[$superGlobalName];
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected T_VARIABLE
Loading history...
653
654
        foreach($blacklisted as $key) {
655
            if (isset($values[$key])) {
656
                $values[$key] = str_repeat('*', strlen($values[$key]));
657
            }
658
        }
659
        return $values;
660
    }
661
}
662