Completed
Push — master ( 4bbb33...2abce9 )
by Denis
11s
created

PrettyPageHandler::contentType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 2
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
     * A string identifier for a known IDE/text editor, or a closure
64
     * that resolves a string that can be used to open a given file
65
     * in an editor. If the string contains the special substrings
66
     * %file or %line, they will be replaced with the correct data.
67
     *
68
     * @example
69
     *  "txmt://open?url=%file&line=%line"
70
     * @var mixed $editor
71
     */
72
    protected $editor;
73
74
    /**
75
     * A list of known editor strings
76
     * @var array
77
     */
78
    protected $editors = [
79
        "sublime"  => "subl://open?url=file://%file&line=%line",
80
        "textmate" => "txmt://open?url=file://%file&line=%line",
81
        "emacs"    => "emacs://open?url=file://%file&line=%line",
82
        "macvim"   => "mvim://open/?url=file://%file&line=%line",
83
        "phpstorm" => "phpstorm://open?file=%file&line=%line",
84
        "idea"     => "idea://open?file=%file&line=%line",
85
    ];
86
87
    /**
88
     * Constructor.
89
     */
90 1
    public function __construct()
91
    {
92 1
        if (ini_get('xdebug.file_link_format') || extension_loaded('xdebug')) {
93
            // Register editor using xdebug's file_link_format option.
94
            $this->editors['xdebug'] = function ($file, $line) {
95 1
                return str_replace(['%f', '%l'], [$file, $line], ini_get('xdebug.file_link_format'));
96
            };
97 1
        }
98
99
        // Add the default, local resource search path:
100 1
        $this->searchPaths[] = __DIR__ . "/../Resources";
101 1
    }
102
103
    /**
104
     * @return int|null
105
     */
106 1
    public function handle()
107
    {
108 1
        if (!$this->handleUnconditionally()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->handleUnconditionally() of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
109
            // Check conditions for outputting HTML:
110
            // @todo: Make this more robust
111 1
            if (php_sapi_name() === 'cli') {
112
                // Help users who have been relying on an internal test value
113
                // fix their code to the proper method
114 1
                if (isset($_ENV['whoops-test'])) {
115
                    throw new \Exception(
116
                        'Use handleUnconditionally instead of whoops-test'
117
                        .' environment variable'
118
                    );
119
                }
120
121 1
                return Handler::DONE;
122
            }
123
        }
124
125
        // @todo: Make this more dynamic
126
        $helper = new TemplateHelper();
127
128
        if (class_exists('Symfony\Component\VarDumper\Cloner\VarCloner')) {
129
            $cloner = new VarCloner();
130
            // Only dump object internals if a custom caster exists.
131
            $cloner->addCasters(['*' => function ($obj, $a, $stub, $isNested, $filter = 0) {
0 ignored issues
show
Unused Code introduced by
The parameter $isNested is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $filter is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
132
                $class = $stub->class;
133
                $classes = [$class => $class] + class_parents($class) + class_implements($class);
134
135
                foreach ($classes as $class) {
136
                    if (isset(AbstractCloner::$defaultCasters[$class])) {
137
                        return $a;
138
                    }
139
                }
140
141
                // Remove all internals
142
                return [];
143
            }]);
144
            $helper->setCloner($cloner);
145
        }
146
147
        $templateFile = $this->getResource("views/layout.html.php");
148
        $cssFile      = $this->getResource("css/whoops.base.css");
149
        $zeptoFile    = $this->getResource("js/zepto.min.js");
150
        $clipboard    = $this->getResource("js/clipboard.min.js");
151
        $jsFile       = $this->getResource("js/whoops.base.js");
152
153
        if ($this->customCss) {
154
            $customCssFile = $this->getResource($this->customCss);
155
        }
156
157
        $inspector = $this->getInspector();
158
        $frames    = $inspector->getFrames();
159
160
        $code = $inspector->getException()->getCode();
161
162
        if ($inspector->getException() instanceof \ErrorException) {
163
            // ErrorExceptions wrap the php-error types within the "severity" property
164
            $code = Misc::translateErrorCode($inspector->getException()->getSeverity());
165
        }
166
167
        // Detect frames that belong to the application.
168
        if ($this->applicationPaths) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->applicationPaths of type array[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
169
            /* @var \Whoops\Exception\Frame $frame */
170
            foreach ($frames as $frame) {
171
                foreach ($this->applicationPaths as $path) {
172
                    if (substr($frame->getFile(), 0, strlen($path)) === $path) {
173
                        $frame->setApplication(true);
174
                        break;
175
                    }
176
                }
177
            }
178
        }
179
180
        // List of variables that will be passed to the layout template.
181
        $vars = [
182
            "page_title" => $this->getPageTitle(),
183
184
            // @todo: Asset compiler
185
            "stylesheet" => file_get_contents($cssFile),
186
            "zepto"      => file_get_contents($zeptoFile),
187
            "clipboard"  => file_get_contents($clipboard),
188
            "javascript" => file_get_contents($jsFile),
189
190
            // Template paths:
191
            "header"                     => $this->getResource("views/header.html.php"),
192
            "header_outer"               => $this->getResource("views/header_outer.html.php"),
193
            "frame_list"                 => $this->getResource("views/frame_list.html.php"),
194
            "frames_description"         => $this->getResource("views/frames_description.html.php"),
195
            "frames_container"           => $this->getResource("views/frames_container.html.php"),
196
            "panel_details"              => $this->getResource("views/panel_details.html.php"),
197
            "panel_details_outer"        => $this->getResource("views/panel_details_outer.html.php"),
198
            "panel_left"                 => $this->getResource("views/panel_left.html.php"),
199
            "panel_left_outer"           => $this->getResource("views/panel_left_outer.html.php"),
200
            "frame_code"                 => $this->getResource("views/frame_code.html.php"),
201
            "env_details"                => $this->getResource("views/env_details.html.php"),
202
203
            "title"          => $this->getPageTitle(),
204
            "name"           => explode("\\", $inspector->getExceptionName()),
205
            "message"        => $inspector->getException()->getMessage(),
206
            "code"           => $code,
207
            "plain_exception" => Formatter::formatExceptionPlain($inspector),
208
            "frames"         => $frames,
209
            "has_frames"     => !!count($frames),
210
            "handler"        => $this,
211
            "handlers"       => $this->getRun()->getHandlers(),
212
213
            "active_frames_tab" => count($frames) && $frames->offsetGet(0)->isApplication() ?  'application' : 'all',
0 ignored issues
show
Bug introduced by
The method isApplication cannot be called on $frames->offsetGet(0) (of type array).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
214
            "has_frames_tabs"   => $this->getApplicationPaths(),
215
216
            "tables"      => [
217
                "GET Data"              => $_GET,
218
                "POST Data"             => $_POST,
219
                "Files"                 => $_FILES,
220
                "Cookies"               => $_COOKIE,
221
                "Session"               => isset($_SESSION) ? $_SESSION :  [],
222
                "Server/Request Data"   => $_SERVER,
223
                "Environment Variables" => $_ENV,
224
            ],
225
        ];
226
227
        if (isset($customCssFile)) {
228
            $vars["stylesheet"] .= file_get_contents($customCssFile);
229
        }
230
231
        // Add extra entries list of data tables:
232
        // @todo: Consolidate addDataTable and addDataTableCallback
233
        $extraTables = array_map(function ($table) use ($inspector) {
234
            return $table instanceof \Closure ? $table($inspector) : $table;
235
        }, $this->getDataTables());
236
        $vars["tables"] = array_merge($extraTables, $vars["tables"]);
237
238
        $plainTextHandler = new PlainTextHandler();
239
        $plainTextHandler->setException($this->getException());
240
        $plainTextHandler->setInspector($this->getInspector());
241
        $vars["preface"] = "<!--\n\n\n" . $plainTextHandler->generateResponse() . "\n\n\n\n\n\n\n\n\n\n\n-->";
242
243
        $helper->setVariables($vars);
244
        $helper->render($templateFile);
245
246
        return Handler::QUIT;
247
    }
248
249
    /**
250
     * @return string
251
     */
252
    public function contentType()
253
    {
254
        return 'text/html';
255
    }
256
257
    /**
258
     * Adds an entry to the list of tables displayed in the template.
259
     * The expected data is a simple associative array. Any nested arrays
260
     * will be flattened with print_r
261
     * @param string $label
262
     * @param array  $data
263
     */
264 1
    public function addDataTable($label, array $data)
265
    {
266 1
        $this->extraTables[$label] = $data;
267 1
    }
268
269
    /**
270
     * Lazily adds an entry to the list of tables displayed in the table.
271
     * The supplied callback argument will be called when the error is rendered,
272
     * it should produce a simple associative array. Any nested arrays will
273
     * be flattened with print_r.
274
     *
275
     * @throws InvalidArgumentException If $callback is not callable
276
     * @param  string                   $label
277
     * @param  callable                 $callback Callable returning an associative array
278
     */
279 1
    public function addDataTableCallback($label, /* callable */ $callback)
280
    {
281 1
        if (!is_callable($callback)) {
282
            throw new InvalidArgumentException('Expecting callback argument to be callable');
283
        }
284
285 1
        $this->extraTables[$label] = function (\Whoops\Exception\Inspector $inspector = null) use ($callback) {
286
            try {
287 1
                $result = call_user_func($callback, $inspector);
288
289
                // Only return the result if it can be iterated over by foreach().
290 1
                return is_array($result) || $result instanceof \Traversable ? $result : [];
291
            } catch (\Exception $e) {
292
                // Don't allow failure to break the rendering of the original exception.
293
                return [];
294
            }
295
        };
296 1
    }
297
298
    /**
299
     * Returns all the extra data tables registered with this handler.
300
     * Optionally accepts a 'label' parameter, to only return the data
301
     * table under that label.
302
     * @param  string|null      $label
303
     * @return array[]|callable
304
     */
305 2
    public function getDataTables($label = null)
306
    {
307 2
        if ($label !== null) {
308 2
            return isset($this->extraTables[$label]) ?
309 2
                   $this->extraTables[$label] : [];
310
        }
311
312 2
        return $this->extraTables;
313
    }
314
315
    /**
316
     * Allows to disable all attempts to dynamically decide whether to
317
     * handle or return prematurely.
318
     * Set this to ensure that the handler will perform no matter what.
319
     * @param  bool|null $value
320
     * @return bool|null
321
     */
322 1
    public function handleUnconditionally($value = null)
323
    {
324 1
        if (func_num_args() == 0) {
325 1
            return $this->handleUnconditionally;
326
        }
327
328
        $this->handleUnconditionally = (bool) $value;
329
    }
330
331
    /**
332
     * Adds an editor resolver, identified by a string
333
     * name, and that may be a string path, or a callable
334
     * resolver. If the callable returns a string, it will
335
     * be set as the file reference's href attribute.
336
     *
337
     * @example
338
     *  $run->addEditor('macvim', "mvim://open?url=file://%file&line=%line")
339
     * @example
340
     *   $run->addEditor('remove-it', function($file, $line) {
341
     *       unlink($file);
342
     *       return "http://stackoverflow.com";
343
     *   });
344
     * @param string $identifier
345
     * @param string $resolver
346
     */
347 1
    public function addEditor($identifier, $resolver)
348
    {
349 1
        $this->editors[$identifier] = $resolver;
350 1
    }
351
352
    /**
353
     * Set the editor to use to open referenced files, by a string
354
     * identifier, or a callable that will be executed for every
355
     * file reference, with a $file and $line argument, and should
356
     * return a string.
357
     *
358
     * @example
359
     *   $run->setEditor(function($file, $line) { return "file:///{$file}"; });
360
     * @example
361
     *   $run->setEditor('sublime');
362
     *
363
     * @throws InvalidArgumentException If invalid argument identifier provided
364
     * @param  string|callable          $editor
365
     */
366 4
    public function setEditor($editor)
367
    {
368 4
        if (!is_callable($editor) && !isset($this->editors[$editor])) {
369
            throw new InvalidArgumentException(
370
                "Unknown editor identifier: $editor. Known editors:" .
371
                implode(",", array_keys($this->editors))
372
            );
373
        }
374
375 4
        $this->editor = $editor;
376 4
    }
377
378
    /**
379
     * Given a string file path, and an integer file line,
380
     * executes the editor resolver and returns, if available,
381
     * a string that may be used as the href property for that
382
     * file reference.
383
     *
384
     * @throws InvalidArgumentException If editor resolver does not return a string
385
     * @param  string                   $filePath
386
     * @param  int                      $line
387
     * @return string|bool
388
     */
389 4
    public function getEditorHref($filePath, $line)
390
    {
391 4
        $editor = $this->getEditor($filePath, $line);
392
393 4
        if (!$editor) {
394
            return false;
395
        }
396
397
        // Check that the editor is a string, and replace the
398
        // %line and %file placeholders:
399 4
        if (!isset($editor['url']) || !is_string($editor['url'])) {
400
            throw new UnexpectedValueException(
401
                __METHOD__ . " should always resolve to a string or a valid editor array; got something else instead."
402
            );
403
        }
404
405 4
        $editor['url'] = str_replace("%line", rawurlencode($line), $editor['url']);
406 4
        $editor['url'] = str_replace("%file", rawurlencode($filePath), $editor['url']);
407
408 4
        return $editor['url'];
409
    }
410
411
    /**
412
     * Given a boolean if the editor link should
413
     * act as an Ajax request. The editor must be a
414
     * valid callable function/closure
415
     *
416
     * @throws UnexpectedValueException  If editor resolver does not return a boolean
417
     * @param  string                   $filePath
418
     * @param  int                      $line
419
     * @return bool
420
     */
421 1
    public function getEditorAjax($filePath, $line)
422
    {
423 1
        $editor = $this->getEditor($filePath, $line);
424
425
        // Check that the ajax is a bool
426 1
        if (!isset($editor['ajax']) || !is_bool($editor['ajax'])) {
427
            throw new UnexpectedValueException(
428
                __METHOD__ . " should always resolve to a bool; got something else instead."
429
            );
430
        }
431 1
        return $editor['ajax'];
432
    }
433
434
    /**
435
     * Given a boolean if the editor link should
436
     * act as an Ajax request. The editor must be a
437
     * valid callable function/closure
438
     *
439
     * @throws UnexpectedValueException  If editor resolver does not return a boolean
440
     * @param  string                   $filePath
441
     * @param  int                      $line
442
     * @return mixed
443
     */
444 1
    protected function getEditor($filePath, $line)
445
    {
446 1
        if ($this->editor === null && !is_string($this->editor) && !is_callable($this->editor))
447 1
        {
448
            return false;
449
        }
450 1
        else if(is_string($this->editor) && isset($this->editors[$this->editor]) && !is_callable($this->editors[$this->editor]))
451 1
        {
452
           return [
453
                'ajax' => false,
454
                'url' => $this->editors[$this->editor],
455
            ];
456
        }
457 1
        else if(is_callable($this->editor) || (isset($this->editors[$this->editor]) && is_callable($this->editors[$this->editor])))
458 1
        {
459 1
            if(is_callable($this->editor))
460 1
            {
461
                $callback = call_user_func($this->editor, $filePath, $line);
462
            }
463
            else
464
            {
465 1
                $callback = call_user_func($this->editors[$this->editor], $filePath, $line);
466
            }
467
468
            return [
469 1
                'ajax' => isset($callback['ajax']) ? $callback['ajax'] : false,
470 1
                'url' => (is_array($callback) ? $callback['url'] : $callback),
471 1
            ];
472
        }
473
474
        return false;
475
    }
476
477
    /**
478
     * @param  string $title
479
     * @return void
480
     */
481 1
    public function setPageTitle($title)
482
    {
483 1
        $this->pageTitle = (string) $title;
484 1
    }
485
486
    /**
487
     * @return string
488
     */
489 1
    public function getPageTitle()
490
    {
491 1
        return $this->pageTitle;
492
    }
493
494
    /**
495
     * Adds a path to the list of paths to be searched for
496
     * resources.
497
     *
498
     * @throws InvalidArgumnetException If $path is not a valid directory
499
     *
500
     * @param  string $path
501
     * @return void
502
     */
503 2
    public function addResourcePath($path)
504
    {
505 2
        if (!is_dir($path)) {
506 1
            throw new InvalidArgumentException(
507 1
                "'$path' is not a valid directory"
508 1
            );
509
        }
510
511 1
        array_unshift($this->searchPaths, $path);
512 1
    }
513
514
    /**
515
     * Adds a custom css file to be loaded.
516
     *
517
     * @param  string $name
518
     * @return void
519
     */
520
    public function addCustomCss($name)
521
    {
522
        $this->customCss = $name;
523
    }
524
525
    /**
526
     * @return array
527
     */
528 1
    public function getResourcePaths()
529
    {
530 1
        return $this->searchPaths;
531
    }
532
533
    /**
534
     * Finds a resource, by its relative path, in all available search paths.
535
     * The search is performed starting at the last search path, and all the
536
     * way back to the first, enabling a cascading-type system of overrides
537
     * for all resources.
538
     *
539
     * @throws RuntimeException If resource cannot be found in any of the available paths
540
     *
541
     * @param  string $resource
542
     * @return string
543
     */
544
    protected function getResource($resource)
545
    {
546
        // If the resource was found before, we can speed things up
547
        // by caching its absolute, resolved path:
548
        if (isset($this->resourceCache[$resource])) {
549
            return $this->resourceCache[$resource];
550
        }
551
552
        // Search through available search paths, until we find the
553
        // resource we're after:
554
        foreach ($this->searchPaths as $path) {
555
            $fullPath = $path . "/$resource";
556
557
            if (is_file($fullPath)) {
558
                // Cache the result:
559
                $this->resourceCache[$resource] = $fullPath;
560
                return $fullPath;
561
            }
562
        }
563
564
        // If we got this far, nothing was found.
565
        throw new RuntimeException(
566
            "Could not find resource '$resource' in any resource paths."
567
            . "(searched: " . join(", ", $this->searchPaths). ")"
568
        );
569
    }
570
571
    /**
572
     * @deprecated
573
     *
574
     * @return string
575
     */
576
    public function getResourcesPath()
577
    {
578
        $allPaths = $this->getResourcePaths();
579
580
        // Compat: return only the first path added
581
        return end($allPaths) ?: null;
582
    }
583
584
    /**
585
     * @deprecated
586
     *
587
     * @param  string $resourcesPath
588
     * @return void
589
     */
590
    public function setResourcesPath($resourcesPath)
591
    {
592
        $this->addResourcePath($resourcesPath);
593
    }
594
595
    /**
596
     * Return the application paths.
597
     *
598
     * @return array
599
     */
600
    public function getApplicationPaths()
601
    {
602
        return $this->applicationPaths;
603
    }
604
605
    /**
606
     * Set the application paths.
607
     *
608
     * @param array $applicationPaths
609
     */
610
    public function setApplicationPaths($applicationPaths)
611
    {
612
        $this->applicationPaths = $applicationPaths;
613
    }
614
}
615