Completed
Pull Request — master (#353)
by Vincent
08:43 queued 04:35
created

PrettyPageHandler::addResourcePath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2
Metric Value
dl 0
loc 10
ccs 7
cts 7
cp 1
rs 9.4286
cc 2
eloc 5
nc 2
nop 1
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 UnexpectedValueException;
12
use Whoops\Exception\Formatter;
13
use Whoops\Util\Misc;
14
use Whoops\Util\TemplateHelper;
15
16
class PrettyPageHandler extends Handler
17
{
18
    /**
19
     * Search paths to be scanned for resources, in the reverse
20
     * order they're declared.
21
     *
22
     * @var array
23
     */
24
    private $searchPaths = array();
25
26
    /**
27
     * Fast lookup cache for known resource locations.
28
     *
29
     * @var array
30
     */
31
    private $resourceCache = array();
32
33
    /**
34
     * The name of the custom css file.
35
     *
36
     * @var string
37
     */
38
    private $customCss = null;
39
40
    /**
41
     * @var array[]
42
     */
43
    private $extraTables = array();
44
45
    /**
46
     * @var bool
47
     */
48
    private $handleUnconditionally = false;
49
50
    /**
51
     * @var string
52
     */
53
    private $pageTitle = "Whoops! There was an error.";
54
55
    /**
56
     * A string identifier for a known IDE/text editor, or a closure
57
     * that resolves a string that can be used to open a given file
58
     * in an editor. If the string contains the special substrings
59
     * %file or %line, they will be replaced with the correct data.
60
     *
61
     * @example
62
     *  "txmt://open?url=%file&line=%line"
63
     * @var mixed $editor
64
     */
65
    protected $editor;
66
67
    /**
68
     * A list of known editor strings
69
     * @var array
70
     */
71
    protected $editors = array(
72
        "sublime"  => "subl://open?url=file://%file&line=%line",
73
        "textmate" => "txmt://open?url=file://%file&line=%line",
74
        "emacs"    => "emacs://open?url=file://%file&line=%line",
75
        "macvim"   => "mvim://open/?url=file://%file&line=%line",
76
        "phpstorm" => "phpstorm://open?file=%file&line=%line",
77
    );
78
79
    /**
80
     * Constructor.
81
     */
82 1
    public function __construct()
83
    {
84 1
        if (ini_get('xdebug.file_link_format') || extension_loaded('xdebug')) {
85
            // Register editor using xdebug's file_link_format option.
86
            $this->editors['xdebug'] = function ($file, $line) {
87 1
                return str_replace(array('%f', '%l'), array($file, $line), ini_get('xdebug.file_link_format'));
88
            };
89 1
        }
90
91
        // Add the default, local resource search path:
92 1
        $this->searchPaths[] = __DIR__ . "/../Resources";
93 1
    }
94
95
    /**
96
     * @return int|null
97
     */
98 1
    public function handle()
7 ignored issues
show
Coding Style introduced by
handle uses the super-global variable $_ENV which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
Coding Style introduced by
handle uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
Coding Style introduced by
handle uses the super-global variable $_POST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
Coding Style introduced by
handle uses the super-global variable $_FILES which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
Coding Style introduced by
handle uses the super-global variable $_COOKIE which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
Coding Style introduced by
handle uses the super-global variable $_SESSION which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
Coding Style introduced by
handle uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
99
    {
100 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...
101
            // Check conditions for outputting HTML:
102
            // @todo: Make this more robust
103 1
            if (php_sapi_name() === 'cli') {
104
                // Help users who have been relying on an internal test value
105
                // fix their code to the proper method
106 1
                if (isset($_ENV['whoops-test'])) {
107
                    throw new \Exception(
108
                        'Use handleUnconditionally instead of whoops-test'
109
                        .' environment variable'
110
                    );
111
                }
112
113 1
                return Handler::DONE;
114
            }
115
        }
116
117
        // @todo: Make this more dynamic
118
        $helper = new TemplateHelper();
119
120
        $templateFile = $this->getResource("views/layout.html.php");
121
        $cssFile      = $this->getResource("css/whoops.base.css");
122
        $zeptoFile    = $this->getResource("js/zepto.min.js");
123
        $clipboard    = $this->getResource("js/clipboard.min.js");
124
        $jsFile       = $this->getResource("js/whoops.base.js");
125
126
        if ($this->customCss) {
127
            $customCssFile = $this->getResource($this->customCss);
128
        }
129
130
        $inspector = $this->getInspector();
131
        $frames    = $inspector->getFrames();
132
133
        $code = $inspector->getException()->getCode();
134
135
        if ($inspector->getException() instanceof \ErrorException) {
136
            // ErrorExceptions wrap the php-error types within the "severity" property
137
            $code = Misc::translateErrorCode($inspector->getException()->getSeverity());
138
        }
139
140
        // List of variables that will be passed to the layout template.
141
        $vars = array(
142
            "page_title" => $this->getPageTitle(),
143
144
            // @todo: Asset compiler
145
            "stylesheet" => file_get_contents($cssFile),
146
            "zepto"      => file_get_contents($zeptoFile),
147
            "clipboard"  => file_get_contents($clipboard),
148
            "javascript" => file_get_contents($jsFile),
149
150
            // Template paths:
151
            "header"      => $this->getResource("views/header.html.php"),
152
            "frame_list"  => $this->getResource("views/frame_list.html.php"),
153
            "frame_code"  => $this->getResource("views/frame_code.html.php"),
154
            "env_details" => $this->getResource("views/env_details.html.php"),
155
156
            "title"          => $this->getPageTitle(),
157
            "name"           => explode("\\", $inspector->getExceptionName()),
158
            "message"        => $inspector->getException()->getMessage(),
159
            "code"           => $code,
160
            "plain_exception" => Formatter::formatExceptionPlain($inspector),
161
            "frames"         => $frames,
162
            "has_frames"     => !!count($frames),
163
            "handler"        => $this,
164
            "handlers"       => $this->getRun()->getHandlers(),
165
166
            "tables"      => array(
167
                "GET Data"              => $_GET,
168
                "POST Data"             => $_POST,
169
                "Files"                 => $_FILES,
170
                "Cookies"               => $_COOKIE,
171
                "Session"               => isset($_SESSION) ? $_SESSION :  array(),
172
                "Server/Request Data"   => $_SERVER,
173
                "Environment Variables" => $_ENV,
174
            ),
175
        );
176
177
        if (isset($customCssFile)) {
178
            $vars["stylesheet"] .= file_get_contents($customCssFile);
179
        }
180
181
        // Add extra entries list of data tables:
182
        // @todo: Consolidate addDataTable and addDataTableCallback
183
        $extraTables = array_map(function ($table) {
184
            return $table instanceof \Closure ? $table() : $table;
185
        }, $this->getDataTables());
186
        $vars["tables"] = array_merge($extraTables, $vars["tables"]);
187
188
        if (\Whoops\Util\Misc::canSendHeaders()) {
189
            header('Content-Type: text/html');
190
        }
191
192
        $helper->setVariables($vars);
193
        $helper->render($templateFile);
194
195
        return Handler::QUIT;
196
    }
197
198
    /**
199
     * Adds an entry to the list of tables displayed in the template.
200
     * The expected data is a simple associative array. Any nested arrays
201
     * will be flattened with print_r
202
     * @param string $label
203
     * @param array  $data
204
     */
205 1
    public function addDataTable($label, array $data)
206
    {
207 1
        $this->extraTables[$label] = $data;
208 1
    }
209
210
    /**
211
     * Lazily adds an entry to the list of tables displayed in the table.
212
     * The supplied callback argument will be called when the error is rendered,
213
     * it should produce a simple associative array. Any nested arrays will
214
     * be flattened with print_r.
215
     *
216
     * @throws InvalidArgumentException If $callback is not callable
217
     * @param  string                   $label
218
     * @param  callable                 $callback Callable returning an associative array
219
     */
220 1
    public function addDataTableCallback($label, /* callable */ $callback)
221
    {
222 1
        if (!is_callable($callback)) {
223
            throw new InvalidArgumentException('Expecting callback argument to be callable');
224
        }
225
226 1
        $this->extraTables[$label] = function () use ($callback) {
227
            try {
228 1
                $result = call_user_func($callback);
229
230
                // Only return the result if it can be iterated over by foreach().
231 1
                return is_array($result) || $result instanceof \Traversable ? $result : array();
232
            } catch (\Exception $e) {
233
                // Don't allow failure to break the rendering of the original exception.
234
                return array();
235
            }
236
        };
237 1
    }
238
239
    /**
240
     * Returns all the extra data tables registered with this handler.
241
     * Optionally accepts a 'label' parameter, to only return the data
242
     * table under that label.
243
     * @param  string|null      $label
244
     * @return array[]|callable
245
     */
246 2
    public function getDataTables($label = null)
247
    {
248 2
        if ($label !== null) {
249 2
            return isset($this->extraTables[$label]) ?
250 2
                   $this->extraTables[$label] : array();
251
        }
252
253 2
        return $this->extraTables;
254
    }
255
256
    /**
257
     * Allows to disable all attempts to dynamically decide whether to
258
     * handle or return prematurely.
259
     * Set this to ensure that the handler will perform no matter what.
260
     * @param  bool|null $value
261
     * @return bool|null
262
     */
263 1
    public function handleUnconditionally($value = null)
264
    {
265 1
        if (func_num_args() == 0) {
266 1
            return $this->handleUnconditionally;
267
        }
268
269
        $this->handleUnconditionally = (bool) $value;
270
    }
271
272
    /**
273
     * Adds an editor resolver, identified by a string
274
     * name, and that may be a string path, or a callable
275
     * resolver. If the callable returns a string, it will
276
     * be set as the file reference's href attribute.
277
     *
278
     * @example
279
     *  $run->addEditor('macvim', "mvim://open?url=file://%file&line=%line")
280
     * @example
281
     *   $run->addEditor('remove-it', function($file, $line) {
282
     *       unlink($file);
283
     *       return "http://stackoverflow.com";
284
     *   });
285
     * @param string $identifier
286
     * @param string $resolver
287
     */
288 1
    public function addEditor($identifier, $resolver)
289
    {
290 1
        $this->editors[$identifier] = $resolver;
291 1
    }
292
293
    /**
294
     * Set the editor to use to open referenced files, by a string
295
     * identifier, or a callable that will be executed for every
296
     * file reference, with a $file and $line argument, and should
297
     * return a string.
298
     *
299
     * @example
300
     *   $run->setEditor(function($file, $line) { return "file:///{$file}"; });
301
     * @example
302
     *   $run->setEditor('sublime');
303
     *
304
     * @throws InvalidArgumentException If invalid argument identifier provided
305
     * @param  string|callable          $editor
306
     */
307 4
    public function setEditor($editor)
308
    {
309 4
        if (!is_callable($editor) && !isset($this->editors[$editor])) {
310
            throw new InvalidArgumentException(
311
                "Unknown editor identifier: $editor. Known editors:" .
312
                implode(",", array_keys($this->editors))
313
            );
314
        }
315
316 4
        $this->editor = $editor;
317 4
    }
318
319
    /**
320
     * Given a string file path, and an integer file line,
321
     * executes the editor resolver and returns, if available,
322
     * a string that may be used as the href property for that
323
     * file reference.
324
     *
325
     * @throws InvalidArgumentException If editor resolver does not return a string
326
     * @param  string                   $filePath
327
     * @param  int                      $line
328
     * @return string|bool
329
     */
330 4
    public function getEditorHref($filePath, $line)
331
    {
332 4
        $editor = $this->getEditor($filePath, $line);
333
334 4
        if (!$editor) {
335
            return false;
336
        }
337
338
        // Check that the editor is a string, and replace the
339
        // %line and %file placeholders:
340 4
        if (!isset($editor['url']) || !is_string($editor['url'])) {
341
            throw new UnexpectedValueException(
342
                __METHOD__ . " should always resolve to a string or a valid editor array; got something else instead."
343
            );
344
        }
345
346 4
        $editor['url'] = str_replace("%line", rawurlencode($line), $editor['url']);
347 4
        $editor['url'] = str_replace("%file", rawurlencode($filePath), $editor['url']);
348
349 4
        return $editor['url'];
350
    }
351
352
    /**
353
     * Given a boolean if the editor link should
354
     * act as an Ajax request. The editor must be a
355
     * valid callable function/closure
356
     *
357
     * @throws UnexpectedValueException  If editor resolver does not return a boolean
358
     * @param  string                   $filePath
359
     * @param  int                      $line
360
     * @return bool
361
     */
362 1
    public function getEditorAjax($filePath, $line)
363
    {
364 1
        $editor = $this->getEditor($filePath, $line);
365
366
        // Check that the ajax is a bool
367 1
        if (!isset($editor['ajax']) || !is_bool($editor['ajax'])) {
368
            throw new UnexpectedValueException(
369
                __METHOD__ . " should always resolve to a bool; got something else instead."
370
            );
371
        }
372 1
        return $editor['ajax'];
373
    }
374
375
    /**
376
     * Given a boolean if the editor link should
377
     * act as an Ajax request. The editor must be a
378
     * valid callable function/closure
379
     *
380
     * @throws UnexpectedValueException  If editor resolver does not return a boolean
381
     * @param  string                   $filePath
382
     * @param  int                      $line
383
     * @return mixed
384
     */
385 1
    protected function getEditor($filePath, $line)
386
    {
387 1
        if ($this->editor === null && !is_string($this->editor) && !is_callable($this->editor))
388 1
        {
389
            return false;
390
        }
391 1
        else if(is_string($this->editor) && isset($this->editors[$this->editor]) && !is_callable($this->editors[$this->editor]))
392 1
        {
393
           return array(
394
                'ajax' => false,
395
                'url' => $this->editors[$this->editor],
396
            );
397
        }
398 1
        else if(is_callable($this->editor) || (isset($this->editors[$this->editor]) && is_callable($this->editors[$this->editor])))
399 1
        {
400 1
            if(is_callable($this->editor))
401 1
            {
402
                $callback = call_user_func($this->editor, $filePath, $line);
403
            }
404
            else
405
            {
406 1
                $callback = call_user_func($this->editors[$this->editor], $filePath, $line);
407
            }
408
409
            return array(
410 1
                'ajax' => isset($callback['ajax']) ? $callback['ajax'] : false,
411 1
                'url' => (is_array($callback) ? $callback['url'] : $callback),
412 1
            );
413
        }
414
415
        return false;
416
    }
417
418
    /**
419
     * @param  string $title
420
     * @return void
421
     */
422 1
    public function setPageTitle($title)
423
    {
424 1
        $this->pageTitle = (string) $title;
425 1
    }
426
427
    /**
428
     * @return string
429
     */
430 1
    public function getPageTitle()
431
    {
432 1
        return $this->pageTitle;
433
    }
434
435
    /**
436
     * Adds a path to the list of paths to be searched for
437
     * resources.
438
     *
439
     * @throws InvalidArgumnetException If $path is not a valid directory
440
     *
441
     * @param  string $path
442
     * @return void
443
     */
444 2
    public function addResourcePath($path)
445
    {
446 2
        if (!is_dir($path)) {
447 1
            throw new InvalidArgumentException(
448 1
                "'$path' is not a valid directory"
449 1
            );
450
        }
451
452 1
        array_unshift($this->searchPaths, $path);
453 1
    }
454
455
    /**
456
     * Adds a custom css file to be loaded.
457
     *
458
     * @param  string $name
459
     * @return void
460
     */
461
    public function addCustomCss($name)
462
    {
463
        $this->customCss = $name;
464
    }
465
466
    /**
467
     * @return array
468
     */
469 1
    public function getResourcePaths()
470
    {
471 1
        return $this->searchPaths;
472
    }
473
474
    /**
475
     * Finds a resource, by its relative path, in all available search paths.
476
     * The search is performed starting at the last search path, and all the
477
     * way back to the first, enabling a cascading-type system of overrides
478
     * for all resources.
479
     *
480
     * @throws RuntimeException If resource cannot be found in any of the available paths
481
     *
482
     * @param  string $resource
483
     * @return string
484
     */
485
    protected function getResource($resource)
486
    {
487
        // If the resource was found before, we can speed things up
488
        // by caching its absolute, resolved path:
489
        if (isset($this->resourceCache[$resource])) {
490
            return $this->resourceCache[$resource];
491
        }
492
493
        // Search through available search paths, until we find the
494
        // resource we're after:
495
        foreach ($this->searchPaths as $path) {
496
            $fullPath = $path . "/$resource";
497
498
            if (is_file($fullPath)) {
499
                // Cache the result:
500
                $this->resourceCache[$resource] = $fullPath;
501
                return $fullPath;
502
            }
503
        }
504
505
        // If we got this far, nothing was found.
506
        throw new RuntimeException(
507
            "Could not find resource '$resource' in any resource paths."
508
            . "(searched: " . join(", ", $this->searchPaths). ")"
509
        );
510
    }
511
512
    /**
513
     * @deprecated
514
     *
515
     * @return string
516
     */
517
    public function getResourcesPath()
518
    {
519
        $allPaths = $this->getResourcePaths();
520
521
        // Compat: return only the first path added
522
        return end($allPaths) ?: null;
523
    }
524
525
    /**
526
     * @deprecated
527
     *
528
     * @param  string $resourcesPath
529
     * @return void
530
     */
531
    public function setResourcesPath($resourcesPath)
532
    {
533
        $this->addResourcePath($resourcesPath);
534
    }
535
}
536