resolved conflicts

This commit is contained in:
2025-03-31 03:46:05 +03:00
6454 changed files with 1520539 additions and 9 deletions
+160
View File
@@ -0,0 +1,160 @@
# CHANGELOG
## v2.16.0
* Support PHP `8.4`.
* Drop support for PHP older than `7.1`.
## v2.15.4
* Improve link color in comments.
## v2.15.3
* Improve performance of the syntax highlighting (#758).
## v2.15.2
* Fixed missing code highlight, which additionally led to issue with switching tabs, between application and all frames ([#747](https://github.com/filp/whoops/issues/747)).
## v2.15.1
* Fixed bug with PrettyPageHandler "*Calling `getFrameFilters` method on null*" ([#751](https://github.com/filp/whoops/pull/751)).
## v2.15.0
* Add addFrameFilter ([#749](https://github.com/filp/whoops/pull/749))
## v2.14.6
* Upgraded prismJS to version `1.29.0` due to security issue ([#741][i741]).
[i741]: https://github.com/filp/whoops/pull/741
## v2.14.5
* Allow `ArrayAccess` on super globals.
## v2.14.4
* Fix PHP `5.5` support.
* Allow to use psr/log `2` or `3`.
## v2.14.3
* Support PHP `8.1`.
## v2.14.1
* Fix syntax highlighting scrolling too far.
* Improve the way we detect xdebug linkformat.
## v2.14.0
* Switched syntax highlighting to Prism.js.
Avoids licensing issues with prettify, and uses a maintained, modern project.
## v2.13.0
* Add Netbeans editor.
## v2.12.1
* Avoid redirecting away from an error.
## v2.12.0
* Hide non-string values in super globals when requested.
## v2.11.0
* Customize exit code.
## v2.10.0
* Better chaining on handler classes.
## v2.9.2
* Fix copy button styles.
## v2.9.1
* Fix xdebug function crash on PHP `8`.
## v2.9.0
* `JsonResponseHandler` includes the exception code.
## v2.8.0
* Support PHP 8.
## v2.7.3
* `PrettyPageHandler` functionality to hide superglobal keys has a clearer name
(`hideSuperglobalKey`).
## v2.7.2
* `PrettyPageHandler` now accepts custom js files.
* `PrettyPageHandler` and `templateHelper` is now accessible through inheritance.
## v2.7.1
* Fix a PHP warning in some cases with anonymous classes.
## v2.7.0
* Added `removeFirstHandler` and `removeLastHandler`.
## v2.6.0
* Fix 2.4.0 `pushHandler` changing the order of handlers.
## v2.5.1
* Fix error messaging in a rare case.
## v2.5.0
* Automatically configure xdebug if available.
## v2.4.1
* Try harder to close all output buffers.
## v2.4.0
* Allow to prepend and append handlers.
## v2.3.2
* Various fixes from the community.
## v2.3.1
* Prevent exception in Whoops when caught exception frame is not related to real file.
## v2.3.0
* Show previous exception messages.
## v2.2.0
* Support PHP `7.2`.
## v2.1.0
* Add a `SystemFacade` to allow clients to override Whoops behavior.
* Show frame arguments in `PrettyPageHandler`.
* Highlight the line with the error.
* Add icons to search on Google and Stack Overflow.
## v2.0.0
Backwards compatibility breaking changes:
* `Run` class is now `final`. If you inherited from `Run`, please now instead use a custom `SystemFacade` injected into the `Run` constructor, or contribute your changes to our core.
* PHP < 5.5 support dropped.
+45
View File
@@ -0,0 +1,45 @@
{
"name": "filp/whoops",
"license": "MIT",
"description": "php error handling for cool kids",
"keywords": ["library", "error", "handling", "exception", "whoops", "throwable"],
"homepage": "https://filp.github.io/whoops/",
"authors": [
{
"name": "Filipe Dobreira",
"homepage": "https://github.com/filp",
"role": "Developer"
}
],
"scripts": {
"test": "phpunit --testdox tests"
},
"require": {
"php": "^7.1 || ^8.0",
"psr/log": "^1.0.1 || ^2.0 || ^3.0"
},
"require-dev": {
"phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3",
"mockery/mockery": "^1.0",
"symfony/var-dumper": "^4.0 || ^5.0"
},
"suggest": {
"symfony/var-dumper": "Pretty print complex values better with var-dumper available",
"whoops/soap": "Formats errors as SOAP responses"
},
"autoload": {
"psr-4": {
"Whoops\\": "src/Whoops/"
}
},
"autoload-dev": {
"psr-4": {
"Whoops\\": "tests/Whoops/"
}
},
"extra": {
"branch-alias": {
"dev-master": "2.7-dev"
}
}
}
@@ -0,0 +1,832 @@
<?php
/**
* Whoops - php errors for cool kids
* @author Filipe Dobreira <http://github.com/filp>
*/
namespace Whoops\Handler;
use InvalidArgumentException;
use RuntimeException;
use Symfony\Component\VarDumper\Cloner\AbstractCloner;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use UnexpectedValueException;
use Whoops\Exception\Formatter;
use Whoops\Util\Misc;
use Whoops\Util\TemplateHelper;
class PrettyPageHandler extends Handler
{
const EDITOR_SUBLIME = "sublime";
const EDITOR_TEXTMATE = "textmate";
const EDITOR_EMACS = "emacs";
const EDITOR_MACVIM = "macvim";
const EDITOR_PHPSTORM = "phpstorm";
const EDITOR_IDEA = "idea";
const EDITOR_VSCODE = "vscode";
const EDITOR_ATOM = "atom";
const EDITOR_ESPRESSO = "espresso";
const EDITOR_XDEBUG = "xdebug";
const EDITOR_NETBEANS = "netbeans";
/**
* Search paths to be scanned for resources.
*
* Stored in the reverse order they're declared.
*
* @var array
*/
private $searchPaths = [];
/**
* Fast lookup cache for known resource locations.
*
* @var array
*/
private $resourceCache = [];
/**
* The name of the custom css file.
*
* @var string|null
*/
private $customCss = null;
/**
* The name of the custom js file.
*
* @var string|null
*/
private $customJs = null;
/**
* @var array[]
*/
private $extraTables = [];
/**
* @var bool
*/
private $handleUnconditionally = false;
/**
* @var string
*/
private $pageTitle = "Whoops! There was an error.";
/**
* @var array[]
*/
private $applicationPaths;
/**
* @var array[]
*/
private $blacklist = [
'_GET' => [],
'_POST' => [],
'_FILES' => [],
'_COOKIE' => [],
'_SESSION' => [],
'_SERVER' => [],
'_ENV' => [],
];
/**
* An identifier for a known IDE/text editor.
*
* Either a string, or a calalble that resolves a string, that can be used
* to open a given file in an editor. If the string contains the special
* substrings %file or %line, they will be replaced with the correct data.
*
* @example
* "txmt://open?url=%file&line=%line"
*
* @var callable|string $editor
*/
protected $editor;
/**
* A list of known editor strings.
*
* @var array
*/
protected $editors = [
"sublime" => "subl://open?url=file://%file&line=%line",
"textmate" => "txmt://open?url=file://%file&line=%line",
"emacs" => "emacs://open?url=file://%file&line=%line",
"macvim" => "mvim://open/?url=file://%file&line=%line",
"phpstorm" => "phpstorm://open?file=%file&line=%line",
"idea" => "idea://open?file=%file&line=%line",
"vscode" => "vscode://file/%file:%line",
"atom" => "atom://core/open/file?filename=%file&line=%line",
"espresso" => "x-espresso://open?filepath=%file&lines=%line",
"netbeans" => "netbeans://open/?f=%file:%line",
];
/**
* @var TemplateHelper
*/
protected $templateHelper;
/**
* Constructor.
*
* @return void
*/
public function __construct()
{
if (ini_get('xdebug.file_link_format') || get_cfg_var('xdebug.file_link_format')) {
// Register editor using xdebug's file_link_format option.
$this->editors['xdebug'] = function ($file, $line) {
return str_replace(['%f', '%l'], [$file, $line], ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'));
};
// If xdebug is available, use it as default editor.
$this->setEditor('xdebug');
}
// Add the default, local resource search path:
$this->searchPaths[] = __DIR__ . "/../Resources";
// blacklist php provided auth based values
$this->blacklist('_SERVER', 'PHP_AUTH_PW');
$this->templateHelper = new TemplateHelper();
if (class_exists('Symfony\Component\VarDumper\Cloner\VarCloner')) {
$cloner = new VarCloner();
// Only dump object internals if a custom caster exists for performance reasons
// https://github.com/filp/whoops/pull/404
$cloner->addCasters(['*' => function ($obj, $a, $stub, $isNested, $filter = 0) {
$class = $stub->class;
$classes = [$class => $class] + class_parents($obj) + class_implements($obj);
foreach ($classes as $class) {
if (isset(AbstractCloner::$defaultCasters[$class])) {
return $a;
}
}
// Remove all internals
return [];
}]);
$this->templateHelper->setCloner($cloner);
}
}
/**
* @return int|null
*
* @throws \Exception
*/
public function handle()
{
if (!$this->handleUnconditionally()) {
// Check conditions for outputting HTML:
// @todo: Make this more robust
if (PHP_SAPI === 'cli') {
// Help users who have been relying on an internal test value
// fix their code to the proper method
if (isset($_ENV['whoops-test'])) {
throw new \Exception(
'Use handleUnconditionally instead of whoops-test'
.' environment variable'
);
}
return Handler::DONE;
}
}
$templateFile = $this->getResource("views/layout.html.php");
$cssFile = $this->getResource("css/whoops.base.css");
$zeptoFile = $this->getResource("js/zepto.min.js");
$prismJs = $this->getResource("js/prism.js");
$prismCss = $this->getResource("css/prism.css");
$clipboard = $this->getResource("js/clipboard.min.js");
$jsFile = $this->getResource("js/whoops.base.js");
if ($this->customCss) {
$customCssFile = $this->getResource($this->customCss);
}
if ($this->customJs) {
$customJsFile = $this->getResource($this->customJs);
}
$inspector = $this->getInspector();
$frames = $this->getExceptionFrames();
$code = $this->getExceptionCode();
// List of variables that will be passed to the layout template.
$vars = [
"page_title" => $this->getPageTitle(),
// @todo: Asset compiler
"stylesheet" => file_get_contents($cssFile),
"zepto" => file_get_contents($zeptoFile),
"prismJs" => file_get_contents($prismJs),
"prismCss" => file_get_contents($prismCss),
"clipboard" => file_get_contents($clipboard),
"javascript" => file_get_contents($jsFile),
// Template paths:
"header" => $this->getResource("views/header.html.php"),
"header_outer" => $this->getResource("views/header_outer.html.php"),
"frame_list" => $this->getResource("views/frame_list.html.php"),
"frames_description" => $this->getResource("views/frames_description.html.php"),
"frames_container" => $this->getResource("views/frames_container.html.php"),
"panel_details" => $this->getResource("views/panel_details.html.php"),
"panel_details_outer" => $this->getResource("views/panel_details_outer.html.php"),
"panel_left" => $this->getResource("views/panel_left.html.php"),
"panel_left_outer" => $this->getResource("views/panel_left_outer.html.php"),
"frame_code" => $this->getResource("views/frame_code.html.php"),
"env_details" => $this->getResource("views/env_details.html.php"),
"title" => $this->getPageTitle(),
"name" => explode("\\", $inspector->getExceptionName()),
"message" => $inspector->getExceptionMessage(),
"previousMessages" => $inspector->getPreviousExceptionMessages(),
"docref_url" => $inspector->getExceptionDocrefUrl(),
"code" => $code,
"previousCodes" => $inspector->getPreviousExceptionCodes(),
"plain_exception" => Formatter::formatExceptionPlain($inspector),
"frames" => $frames,
"has_frames" => !!count($frames),
"handler" => $this,
"handlers" => $this->getRun()->getHandlers(),
"active_frames_tab" => count($frames) && $frames->offsetGet(0)->isApplication() ? 'application' : 'all',
"has_frames_tabs" => $this->getApplicationPaths(),
"tables" => [
"GET Data" => $this->masked($_GET, '_GET'),
"POST Data" => $this->masked($_POST, '_POST'),
"Files" => isset($_FILES) ? $this->masked($_FILES, '_FILES') : [],
"Cookies" => $this->masked($_COOKIE, '_COOKIE'),
"Session" => isset($_SESSION) ? $this->masked($_SESSION, '_SESSION') : [],
"Server/Request Data" => $this->masked($_SERVER, '_SERVER'),
"Environment Variables" => $this->masked($_ENV, '_ENV'),
],
];
if (isset($customCssFile)) {
$vars["stylesheet"] .= file_get_contents($customCssFile);
}
if (isset($customJsFile)) {
$vars["javascript"] .= file_get_contents($customJsFile);
}
// Add extra entries list of data tables:
// @todo: Consolidate addDataTable and addDataTableCallback
$extraTables = array_map(function ($table) use ($inspector) {
return $table instanceof \Closure ? $table($inspector) : $table;
}, $this->getDataTables());
$vars["tables"] = array_merge($extraTables, $vars["tables"]);
$plainTextHandler = new PlainTextHandler();
$plainTextHandler->setRun($this->getRun());
$plainTextHandler->setException($this->getException());
$plainTextHandler->setInspector($this->getInspector());
$vars["preface"] = "<!--\n\n\n" . $this->templateHelper->escape($plainTextHandler->generateResponse()) . "\n\n\n\n\n\n\n\n\n\n\n-->";
$this->templateHelper->setVariables($vars);
$this->templateHelper->render($templateFile);
return Handler::QUIT;
}
/**
* Get the stack trace frames of the exception currently being handled.
*
* @return \Whoops\Exception\FrameCollection
*/
protected function getExceptionFrames()
{
$frames = $this->getInspector()->getFrames($this->getRun()->getFrameFilters());
if ($this->getApplicationPaths()) {
foreach ($frames as $frame) {
foreach ($this->getApplicationPaths() as $path) {
if (strpos($frame->getFile(), $path) === 0) {
$frame->setApplication(true);
break;
}
}
}
}
return $frames;
}
/**
* Get the code of the exception currently being handled.
*
* @return string
*/
protected function getExceptionCode()
{
$exception = $this->getException();
$code = $exception->getCode();
if ($exception instanceof \ErrorException) {
// ErrorExceptions wrap the php-error types within the 'severity' property
$code = Misc::translateErrorCode($exception->getSeverity());
}
return (string) $code;
}
/**
* @return string
*/
public function contentType()
{
return 'text/html';
}
/**
* Adds an entry to the list of tables displayed in the template.
*
* The expected data is a simple associative array. Any nested arrays
* will be flattened with `print_r`.
*
* @param string $label
*
* @return static
*/
public function addDataTable($label, array $data)
{
$this->extraTables[$label] = $data;
return $this;
}
/**
* Lazily adds an entry to the list of tables displayed in the table.
*
* The supplied callback argument will be called when the error is
* rendered, it should produce a simple associative array. Any nested
* arrays will be flattened with `print_r`.
*
* @param string $label
* @param callable $callback Callable returning an associative array
*
* @throws InvalidArgumentException If $callback is not callable
*
* @return static
*/
public function addDataTableCallback($label, /* callable */ $callback)
{
if (!is_callable($callback)) {
throw new InvalidArgumentException('Expecting callback argument to be callable');
}
$this->extraTables[$label] = function (?\Whoops\Inspector\InspectorInterface $inspector = null) use ($callback) {
try {
$result = call_user_func($callback, $inspector);
// Only return the result if it can be iterated over by foreach().
return is_array($result) || $result instanceof \Traversable ? $result : [];
} catch (\Exception $e) {
// Don't allow failure to break the rendering of the original exception.
return [];
}
};
return $this;
}
/**
* Returns all the extra data tables registered with this handler.
*
* Optionally accepts a 'label' parameter, to only return the data table
* under that label.
*
* @param string|null $label
*
* @return array[]|callable
*/
public function getDataTables($label = null)
{
if ($label !== null) {
return isset($this->extraTables[$label]) ?
$this->extraTables[$label] : [];
}
return $this->extraTables;
}
/**
* Set whether to handle unconditionally.
*
* Allows to disable all attempts to dynamically decide whether to handle
* or return prematurely. Set this to ensure that the handler will perform,
* no matter what.
*
* @param bool|null $value
*
* @return bool|static
*/
public function handleUnconditionally($value = null)
{
if (func_num_args() == 0) {
return $this->handleUnconditionally;
}
$this->handleUnconditionally = (bool) $value;
return $this;
}
/**
* Adds an editor resolver.
*
* Either a string, or a closure that resolves a string, that can be used
* to open a given file in an editor. If the string contains the special
* substrings %file or %line, they will be replaced with the correct data.
*
* @example
* $run->addEditor('macvim', "mvim://open?url=file://%file&line=%line")
* @example
* $run->addEditor('remove-it', function($file, $line) {
* unlink($file);
* return "http://stackoverflow.com";
* });
*
* @param string $identifier
* @param string|callable $resolver
*
* @return static
*/
public function addEditor($identifier, $resolver)
{
$this->editors[$identifier] = $resolver;
return $this;
}
/**
* Set the editor to use to open referenced files.
*
* Pass either the name of a configured editor, or a closure that directly
* resolves an editor string.
*
* @example
* $run->setEditor(function($file, $line) { return "file:///{$file}"; });
* @example
* $run->setEditor('sublime');
*
* @param string|callable $editor
*
* @throws InvalidArgumentException If invalid argument identifier provided
*
* @return static
*/
public function setEditor($editor)
{
if (!is_callable($editor) && !isset($this->editors[$editor])) {
throw new InvalidArgumentException(
"Unknown editor identifier: $editor. Known editors:" .
implode(",", array_keys($this->editors))
);
}
$this->editor = $editor;
return $this;
}
/**
* Get the editor href for a given file and line, if available.
*
* @param string $filePath
* @param int $line
*
* @throws InvalidArgumentException If editor resolver does not return a string
*
* @return string|bool
*/
public function getEditorHref($filePath, $line)
{
$editor = $this->getEditor($filePath, $line);
if (empty($editor)) {
return false;
}
// Check that the editor is a string, and replace the
// %line and %file placeholders:
if (!isset($editor['url']) || !is_string($editor['url'])) {
throw new UnexpectedValueException(
__METHOD__ . " should always resolve to a string or a valid editor array; got something else instead."
);
}
$editor['url'] = str_replace("%line", rawurlencode($line), $editor['url']);
$editor['url'] = str_replace("%file", rawurlencode($filePath), $editor['url']);
return $editor['url'];
}
/**
* Determine if the editor link should act as an Ajax request.
*
* @param string $filePath
* @param int $line
*
* @throws UnexpectedValueException If editor resolver does not return a boolean
*
* @return bool
*/
public function getEditorAjax($filePath, $line)
{
$editor = $this->getEditor($filePath, $line);
// Check that the ajax is a bool
if (!isset($editor['ajax']) || !is_bool($editor['ajax'])) {
throw new UnexpectedValueException(
__METHOD__ . " should always resolve to a bool; got something else instead."
);
}
return $editor['ajax'];
}
/**
* Determines both the editor and if ajax should be used.
*
* @param string $filePath
* @param int $line
*
* @return array
*/
protected function getEditor($filePath, $line)
{
if (!$this->editor || (!is_string($this->editor) && !is_callable($this->editor))) {
return [];
}
if (is_string($this->editor) && isset($this->editors[$this->editor]) && !is_callable($this->editors[$this->editor])) {
return [
'ajax' => false,
'url' => $this->editors[$this->editor],
];
}
if (is_callable($this->editor) || (isset($this->editors[$this->editor]) && is_callable($this->editors[$this->editor]))) {
if (is_callable($this->editor)) {
$callback = call_user_func($this->editor, $filePath, $line);
} else {
$callback = call_user_func($this->editors[$this->editor], $filePath, $line);
}
if (empty($callback)) {
return [];
}
if (is_string($callback)) {
return [
'ajax' => false,
'url' => $callback,
];
}
return [
'ajax' => isset($callback['ajax']) ? $callback['ajax'] : false,
'url' => isset($callback['url']) ? $callback['url'] : $callback,
];
}
return [];
}
/**
* Set the page title.
*
* @param string $title
*
* @return static
*/
public function setPageTitle($title)
{
$this->pageTitle = (string) $title;
return $this;
}
/**
* Get the page title.
*
* @return string
*/
public function getPageTitle()
{
return $this->pageTitle;
}
/**
* Adds a path to the list of paths to be searched for resources.
*
* @param string $path
*
* @throws InvalidArgumentException If $path is not a valid directory
*
* @return static
*/
public function addResourcePath($path)
{
if (!is_dir($path)) {
throw new InvalidArgumentException(
"'$path' is not a valid directory"
);
}
array_unshift($this->searchPaths, $path);
return $this;
}
/**
* Adds a custom css file to be loaded.
*
* @param string|null $name
*
* @return static
*/
public function addCustomCss($name)
{
$this->customCss = $name;
return $this;
}
/**
* Adds a custom js file to be loaded.
*
* @param string|null $name
*
* @return static
*/
public function addCustomJs($name)
{
$this->customJs = $name;
return $this;
}
/**
* @return array
*/
public function getResourcePaths()
{
return $this->searchPaths;
}
/**
* Finds a resource, by its relative path, in all available search paths.
*
* The search is performed starting at the last search path, and all the
* way back to the first, enabling a cascading-type system of overrides for
* all resources.
*
* @param string $resource
*
* @throws RuntimeException If resource cannot be found in any of the available paths
*
* @return string
*/
protected function getResource($resource)
{
// If the resource was found before, we can speed things up
// by caching its absolute, resolved path:
if (isset($this->resourceCache[$resource])) {
return $this->resourceCache[$resource];
}
// Search through available search paths, until we find the
// resource we're after:
foreach ($this->searchPaths as $path) {
$fullPath = $path . "/$resource";
if (is_file($fullPath)) {
// Cache the result:
$this->resourceCache[$resource] = $fullPath;
return $fullPath;
}
}
// If we got this far, nothing was found.
throw new RuntimeException(
"Could not find resource '$resource' in any resource paths."
. "(searched: " . join(", ", $this->searchPaths). ")"
);
}
/**
* @deprecated
*
* @return string
*/
public function getResourcesPath()
{
$allPaths = $this->getResourcePaths();
// Compat: return only the first path added
return end($allPaths) ?: null;
}
/**
* @deprecated
*
* @param string $resourcesPath
*
* @return static
*/
public function setResourcesPath($resourcesPath)
{
$this->addResourcePath($resourcesPath);
return $this;
}
/**
* Return the application paths.
*
* @return array
*/
public function getApplicationPaths()
{
return $this->applicationPaths;
}
/**
* Set the application paths.
*
* @return void
*/
public function setApplicationPaths(array $applicationPaths)
{
$this->applicationPaths = $applicationPaths;
}
/**
* Set the application root path.
*
* @param string $applicationRootPath
*
* @return void
*/
public function setApplicationRootPath($applicationRootPath)
{
$this->templateHelper->setApplicationRootPath($applicationRootPath);
}
/**
* blacklist a sensitive value within one of the superglobal arrays.
* Alias for the hideSuperglobalKey method.
*
* @param string $superGlobalName The name of the superglobal array, e.g. '_GET'
* @param string $key The key within the superglobal
* @see hideSuperglobalKey
*
* @return static
*/
public function blacklist($superGlobalName, $key)
{
$this->blacklist[$superGlobalName][] = $key;
return $this;
}
/**
* Hide a sensitive value within one of the superglobal arrays.
*
* @param string $superGlobalName The name of the superglobal array, e.g. '_GET'
* @param string $key The key within the superglobal
* @return static
*/
public function hideSuperglobalKey($superGlobalName, $key)
{
return $this->blacklist($superGlobalName, $key);
}
/**
* Checks all values within the given superGlobal array.
*
* Blacklisted values will be replaced by a equal length string containing
* only '*' characters for string values.
* Non-string values will be replaced with a fixed asterisk count.
* We intentionally dont rely on $GLOBALS as it depends on the 'auto_globals_jit' php.ini setting.
*
* @param array|\ArrayAccess $superGlobal One of the superglobal arrays
* @param string $superGlobalName The name of the superglobal array, e.g. '_GET'
*
* @return array $values without sensitive data
*/
private function masked($superGlobal, $superGlobalName)
{
$blacklisted = $this->blacklist[$superGlobalName];
$values = $superGlobal;
foreach ($blacklisted as $key) {
if (isset($superGlobal[$key])) {
$values[$key] = str_repeat('*', is_string($superGlobal[$key]) ? strlen($superGlobal[$key]) : 3);
}
}
return $values;
}
}
@@ -0,0 +1,597 @@
<?php
/**
* Whoops - php errors for cool kids
* @author Filipe Dobreira <http://github.com/filp>
*/
namespace Whoops;
use InvalidArgumentException;
use Throwable;
use Whoops\Exception\ErrorException;
use Whoops\Handler\CallbackHandler;
use Whoops\Handler\Handler;
use Whoops\Handler\HandlerInterface;
use Whoops\Inspector\CallableInspectorFactory;
use Whoops\Inspector\InspectorFactory;
use Whoops\Inspector\InspectorFactoryInterface;
use Whoops\Inspector\InspectorInterface;
use Whoops\Util\Misc;
use Whoops\Util\SystemFacade;
final class Run implements RunInterface
{
/**
* @var bool
*/
private $isRegistered;
/**
* @var bool
*/
private $allowQuit = true;
/**
* @var bool
*/
private $sendOutput = true;
/**
* @var integer|false
*/
private $sendHttpCode = 500;
/**
* @var integer|false
*/
private $sendExitCode = 1;
/**
* @var HandlerInterface[]
*/
private $handlerStack = [];
/**
* @var array
* @psalm-var list<array{patterns: string, levels: int}>
*/
private $silencedPatterns = [];
/**
* @var SystemFacade
*/
private $system;
/**
* In certain scenarios, like in shutdown handler, we can not throw exceptions.
*
* @var bool
*/
private $canThrowExceptions = true;
/**
* The inspector factory to create inspectors.
*
* @var InspectorFactoryInterface
*/
private $inspectorFactory;
/**
* @var array<callable>
*/
private $frameFilters = [];
public function __construct(?SystemFacade $system = null)
{
$this->system = $system ?: new SystemFacade;
$this->inspectorFactory = new InspectorFactory();
}
/**
* Explicitly request your handler runs as the last of all currently registered handlers.
*
* @param callable|HandlerInterface $handler
*
* @return Run
*/
public function appendHandler($handler)
{
array_unshift($this->handlerStack, $this->resolveHandler($handler));
return $this;
}
/**
* Explicitly request your handler runs as the first of all currently registered handlers.
*
* @param callable|HandlerInterface $handler
*
* @return Run
*/
public function prependHandler($handler)
{
return $this->pushHandler($handler);
}
/**
* Register your handler as the last of all currently registered handlers (to be executed first).
* Prefer using appendHandler and prependHandler for clarity.
*
* @param callable|HandlerInterface $handler
*
* @return Run
*
* @throws InvalidArgumentException If argument is not callable or instance of HandlerInterface.
*/
public function pushHandler($handler)
{
$this->handlerStack[] = $this->resolveHandler($handler);
return $this;
}
/**
* Removes and returns the last handler pushed to the handler stack.
*
* @see Run::removeFirstHandler(), Run::removeLastHandler()
*
* @return HandlerInterface|null
*/
public function popHandler()
{
return array_pop($this->handlerStack);
}
/**
* Removes the first handler.
*
* @return void
*/
public function removeFirstHandler()
{
array_pop($this->handlerStack);
}
/**
* Removes the last handler.
*
* @return void
*/
public function removeLastHandler()
{
array_shift($this->handlerStack);
}
/**
* Returns an array with all handlers, in the order they were added to the stack.
*
* @return array
*/
public function getHandlers()
{
return $this->handlerStack;
}
/**
* Clears all handlers in the handlerStack, including the default PrettyPage handler.
*
* @return Run
*/
public function clearHandlers()
{
$this->handlerStack = [];
return $this;
}
public function getFrameFilters()
{
return $this->frameFilters;
}
public function clearFrameFilters()
{
$this->frameFilters = [];
return $this;
}
/**
* Registers this instance as an error handler.
*
* @return Run
*/
public function register()
{
if (!$this->isRegistered) {
// Workaround PHP bug 42098
// https://bugs.php.net/bug.php?id=42098
class_exists("\\Whoops\\Exception\\ErrorException");
class_exists("\\Whoops\\Exception\\FrameCollection");
class_exists("\\Whoops\\Exception\\Frame");
class_exists("\\Whoops\\Exception\\Inspector");
class_exists("\\Whoops\\Inspector\\InspectorFactory");
$this->system->setErrorHandler([$this, self::ERROR_HANDLER]);
$this->system->setExceptionHandler([$this, self::EXCEPTION_HANDLER]);
$this->system->registerShutdownFunction([$this, self::SHUTDOWN_HANDLER]);
$this->isRegistered = true;
}
return $this;
}
/**
* Unregisters all handlers registered by this Whoops\Run instance.
*
* @return Run
*/
public function unregister()
{
if ($this->isRegistered) {
$this->system->restoreExceptionHandler();
$this->system->restoreErrorHandler();
$this->isRegistered = false;
}
return $this;
}
/**
* Should Whoops allow Handlers to force the script to quit?
*
* @param bool|int $exit
*
* @return bool
*/
public function allowQuit($exit = null)
{
if (func_num_args() == 0) {
return $this->allowQuit;
}
return $this->allowQuit = (bool) $exit;
}
/**
* Silence particular errors in particular files.
*
* @param array|string $patterns List or a single regex pattern to match.
* @param int $levels Defaults to E_STRICT | E_DEPRECATED.
*
* @return Run
*/
public function silenceErrorsInPaths($patterns, $levels = 10240)
{
$this->silencedPatterns = array_merge(
$this->silencedPatterns,
array_map(
function ($pattern) use ($levels) {
return [
"pattern" => $pattern,
"levels" => $levels,
];
},
(array) $patterns
)
);
return $this;
}
/**
* Returns an array with silent errors in path configuration.
*
* @return array
*/
public function getSilenceErrorsInPaths()
{
return $this->silencedPatterns;
}
/**
* Should Whoops send HTTP error code to the browser if possible?
* Whoops will by default send HTTP code 500, but you may wish to
* use 502, 503, or another 5xx family code.
*
* @param bool|int $code
*
* @return int|false
*
* @throws InvalidArgumentException
*/
public function sendHttpCode($code = null)
{
if (func_num_args() == 0) {
return $this->sendHttpCode;
}
if (!$code) {
return $this->sendHttpCode = false;
}
if ($code === true) {
$code = 500;
}
if ($code < 400 || 600 <= $code) {
throw new InvalidArgumentException(
"Invalid status code '$code', must be 4xx or 5xx"
);
}
return $this->sendHttpCode = $code;
}
/**
* Should Whoops exit with a specific code on the CLI if possible?
* Whoops will exit with 1 by default, but you can specify something else.
*
* @param int $code
*
* @return int
*
* @throws InvalidArgumentException
*/
public function sendExitCode($code = null)
{
if (func_num_args() == 0) {
return $this->sendExitCode;
}
if ($code < 0 || 255 <= $code) {
throw new InvalidArgumentException(
"Invalid status code '$code', must be between 0 and 254"
);
}
return $this->sendExitCode = (int) $code;
}
/**
* Should Whoops push output directly to the client?
* If this is false, output will be returned by handleException.
*
* @param bool|int $send
*
* @return bool
*/
public function writeToOutput($send = null)
{
if (func_num_args() == 0) {
return $this->sendOutput;
}
return $this->sendOutput = (bool) $send;
}
/**
* Handles an exception, ultimately generating a Whoops error page.
*
* @param Throwable $exception
*
* @return string Output generated by handlers.
*/
public function handleException($exception)
{
// Walk the registered handlers in the reverse order
// they were registered, and pass off the exception
$inspector = $this->getInspector($exception);
// Capture output produced while handling the exception,
// we might want to send it straight away to the client,
// or return it silently.
$this->system->startOutputBuffering();
// Just in case there are no handlers:
$handlerResponse = null;
$handlerContentType = null;
try {
foreach (array_reverse($this->handlerStack) as $handler) {
$handler->setRun($this);
$handler->setInspector($inspector);
$handler->setException($exception);
// The HandlerInterface does not require an Exception passed to handle()
// and neither of our bundled handlers use it.
// However, 3rd party handlers may have already relied on this parameter,
// and removing it would be possibly breaking for users.
$handlerResponse = $handler->handle($exception);
// Collect the content type for possible sending in the headers.
$handlerContentType = method_exists($handler, 'contentType') ? $handler->contentType() : null;
if (in_array($handlerResponse, [Handler::LAST_HANDLER, Handler::QUIT])) {
// The Handler has handled the exception in some way, and
// wishes to quit execution (Handler::QUIT), or skip any
// other handlers (Handler::LAST_HANDLER). If $this->allowQuit
// is false, Handler::QUIT behaves like Handler::LAST_HANDLER
break;
}
}
$willQuit = $handlerResponse == Handler::QUIT && $this->allowQuit();
} finally {
$output = $this->system->cleanOutputBuffer();
}
// If we're allowed to, send output generated by handlers directly
// to the output, otherwise, and if the script doesn't quit, return
// it so that it may be used by the caller
if ($this->writeToOutput()) {
// @todo Might be able to clean this up a bit better
if ($willQuit) {
// Cleanup all other output buffers before sending our output:
while ($this->system->getOutputBufferLevel() > 0) {
$this->system->endOutputBuffering();
}
// Send any headers if needed:
if (Misc::canSendHeaders() && $handlerContentType) {
header("Content-Type: {$handlerContentType}");
}
}
$this->writeToOutputNow($output);
}
if ($willQuit) {
// HHVM fix for https://github.com/facebook/hhvm/issues/4055
$this->system->flushOutputBuffer();
$this->system->stopExecution(
$this->sendExitCode()
);
}
return $output;
}
/**
* Converts generic PHP errors to \ErrorException instances, before passing them off to be handled.
*
* This method MUST be compatible with set_error_handler.
*
* @param int $level
* @param string $message
* @param string|null $file
* @param int|null $line
*
* @return bool
*
* @throws ErrorException
*/
public function handleError($level, $message, $file = null, $line = null)
{
if ($level & $this->system->getErrorReportingLevel()) {
foreach ($this->silencedPatterns as $entry) {
$pathMatches = (bool) preg_match($entry["pattern"], $file);
$levelMatches = $level & $entry["levels"];
if ($pathMatches && $levelMatches) {
// Ignore the error, abort handling
// See https://github.com/filp/whoops/issues/418
return true;
}
}
// XXX we pass $level for the "code" param only for BC reasons.
// see https://github.com/filp/whoops/issues/267
$exception = new ErrorException($message, /*code*/ $level, /*severity*/ $level, $file, $line);
if ($this->canThrowExceptions) {
throw $exception;
} else {
$this->handleException($exception);
}
// Do not propagate errors which were already handled by Whoops.
return true;
}
// Propagate error to the next handler, allows error_get_last() to
// work on silenced errors.
return false;
}
/**
* Special case to deal with Fatal errors and the like.
*
* @return void
*/
public function handleShutdown()
{
// If we reached this step, we are in shutdown handler.
// An exception thrown in a shutdown handler will not be propagated
// to the exception handler. Pass that information along.
$this->canThrowExceptions = false;
$error = $this->system->getLastError();
if ($error && Misc::isLevelFatal($error['type'])) {
// If there was a fatal error,
// it was not handled in handleError yet.
$this->allowQuit = false;
$this->handleError(
$error['type'],
$error['message'],
$error['file'],
$error['line']
);
}
}
/**
* @param InspectorFactoryInterface $factory
*
* @return void
*/
public function setInspectorFactory(InspectorFactoryInterface $factory)
{
$this->inspectorFactory = $factory;
}
public function addFrameFilter($filterCallback)
{
if (!is_callable($filterCallback)) {
throw new \InvalidArgumentException(sprintf(
"A frame filter must be of type callable, %s type given.",
gettype($filterCallback)
));
}
$this->frameFilters[] = $filterCallback;
return $this;
}
/**
* @param Throwable $exception
*
* @return InspectorInterface
*/
private function getInspector($exception)
{
return $this->inspectorFactory->create($exception);
}
/**
* Resolves the giving handler.
*
* @param callable|HandlerInterface $handler
*
* @return HandlerInterface
*
* @throws InvalidArgumentException
*/
private function resolveHandler($handler)
{
if (is_callable($handler)) {
$handler = new CallbackHandler($handler);
}
if (!$handler instanceof HandlerInterface) {
throw new InvalidArgumentException(
"Handler must be a callable, or instance of "
. "Whoops\\Handler\\HandlerInterface"
);
}
return $handler;
}
/**
* Echo something to the browser.
*
* @param string $output
*
* @return Run
*/
private function writeToOutputNow($output)
{
if ($this->sendHttpCode() && Misc::canSendHeaders()) {
$this->system->setHttpResponseCode(
$this->sendHttpCode()
);
}
echo $output;
return $this;
}
}
@@ -0,0 +1,349 @@
<?php
/**
* Whoops - php errors for cool kids
* @author Filipe Dobreira <http://github.com/filp>
*/
namespace Whoops\Util;
use Symfony\Component\VarDumper\Caster\Caster;
use Symfony\Component\VarDumper\Cloner\AbstractCloner;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
use Whoops\Exception\Frame;
/**
* Exposes useful tools for working with/in templates
*/
class TemplateHelper
{
/**
* An array of variables to be passed to all templates
* @var array
*/
private $variables = [];
/**
* @var HtmlDumper
*/
private $htmlDumper;
/**
* @var HtmlDumperOutput
*/
private $htmlDumperOutput;
/**
* @var AbstractCloner
*/
private $cloner;
/**
* @var string
*/
private $applicationRootPath;
public function __construct()
{
// root path for ordinary composer projects
$this->applicationRootPath = dirname(dirname(dirname(dirname(dirname(dirname(__DIR__))))));
}
/**
* Escapes a string for output in an HTML document
*
* @param string $raw
* @return string
*/
public function escape($raw)
{
$flags = ENT_QUOTES;
// HHVM has all constants defined, but only ENT_IGNORE
// works at the moment
if (defined("ENT_SUBSTITUTE") && !defined("HHVM_VERSION")) {
$flags |= ENT_SUBSTITUTE;
} else {
// This is for 5.3.
// The documentation warns of a potential security issue,
// but it seems it does not apply in our case, because
// we do not blacklist anything anywhere.
$flags |= ENT_IGNORE;
}
$raw = str_replace(chr(9), ' ', $raw);
return htmlspecialchars($raw, $flags, "UTF-8");
}
/**
* Escapes a string for output in an HTML document, but preserves
* URIs within it, and converts them to clickable anchor elements.
*
* @param string $raw
* @return string
*/
public function escapeButPreserveUris($raw)
{
$escaped = $this->escape($raw);
return preg_replace(
"@([A-z]+?://([-\w\.]+[-\w])+(:\d+)?(/([\w/_\.#-]*(\?\S+)?[^\.\s])?)?)@",
"<a href=\"$1\" target=\"_blank\" rel=\"noreferrer noopener\">$1</a>",
$escaped
);
}
/**
* Makes sure that the given string breaks on the delimiter.
*
* @param string $delimiter
* @param string $s
* @return string
*/
public function breakOnDelimiter($delimiter, $s)
{
$parts = explode($delimiter, $s);
foreach ($parts as &$part) {
$part = '<span class="delimiter">' . $part . '</span>';
}
return implode($delimiter, $parts);
}
/**
* Replace the part of the path that all files have in common.
*
* @param string $path
* @return string
*/
public function shorten($path)
{
if ($this->applicationRootPath != "/") {
$path = str_replace($this->applicationRootPath, '&hellip;', $path);
}
return $path;
}
private function getDumper()
{
if (!$this->htmlDumper && class_exists('Symfony\Component\VarDumper\Cloner\VarCloner')) {
$this->htmlDumperOutput = new HtmlDumperOutput();
// re-use the same var-dumper instance, so it won't re-render the global styles/scripts on each dump.
$this->htmlDumper = new HtmlDumper($this->htmlDumperOutput);
$styles = [
'default' => 'color:#FFFFFF; line-height:normal; font:12px "Inconsolata", "Fira Mono", "Source Code Pro", Monaco, Consolas, "Lucida Console", monospace !important; word-wrap: break-word; white-space: pre-wrap; position:relative; z-index:99999; word-break: normal',
'num' => 'color:#BCD42A',
'const' => 'color: #4bb1b1;',
'str' => 'color:#BCD42A',
'note' => 'color:#ef7c61',
'ref' => 'color:#A0A0A0',
'public' => 'color:#FFFFFF',
'protected' => 'color:#FFFFFF',
'private' => 'color:#FFFFFF',
'meta' => 'color:#FFFFFF',
'key' => 'color:#BCD42A',
'index' => 'color:#ef7c61',
];
$this->htmlDumper->setStyles($styles);
}
return $this->htmlDumper;
}
/**
* Format the given value into a human readable string.
*
* @param mixed $value
* @return string
*/
public function dump($value)
{
$dumper = $this->getDumper();
if ($dumper) {
// re-use the same DumpOutput instance, so it won't re-render the global styles/scripts on each dump.
// exclude verbose information (e.g. exception stack traces)
if (class_exists('Symfony\Component\VarDumper\Caster\Caster')) {
$cloneVar = $this->getCloner()->cloneVar($value, Caster::EXCLUDE_VERBOSE);
// Symfony VarDumper 2.6 Caster class dont exist.
} else {
$cloneVar = $this->getCloner()->cloneVar($value);
}
$dumper->dump(
$cloneVar,
$this->htmlDumperOutput
);
$output = $this->htmlDumperOutput->getOutput();
$this->htmlDumperOutput->clear();
return $output;
}
return htmlspecialchars(print_r($value, true));
}
/**
* Format the args of the given Frame as a human readable html string
*
* @param Frame $frame
* @return string the rendered html
*/
public function dumpArgs(Frame $frame)
{
// we support frame args only when the optional dumper is available
if (!$this->getDumper()) {
return '';
}
$html = '';
$numFrames = count($frame->getArgs());
if ($numFrames > 0) {
$html = '<ol class="linenums">';
foreach ($frame->getArgs() as $j => $frameArg) {
$html .= '<li>'. $this->dump($frameArg) .'</li>';
}
$html .= '</ol>';
}
return $html;
}
/**
* Convert a string to a slug version of itself
*
* @param string $original
* @return string
*/
public function slug($original)
{
$slug = str_replace(" ", "-", $original);
$slug = preg_replace('/[^\w\d\-\_]/i', '', $slug);
return strtolower($slug);
}
/**
* Given a template path, render it within its own scope. This
* method also accepts an array of additional variables to be
* passed to the template.
*
* @param string $template
*/
public function render($template, ?array $additionalVariables = null)
{
$variables = $this->getVariables();
// Pass the helper to the template:
$variables["tpl"] = $this;
if ($additionalVariables !== null) {
$variables = array_replace($variables, $additionalVariables);
}
call_user_func(function () {
extract(func_get_arg(1));
require func_get_arg(0);
}, $template, $variables);
}
/**
* Sets the variables to be passed to all templates rendered
* by this template helper.
*/
public function setVariables(array $variables)
{
$this->variables = $variables;
}
/**
* Sets a single template variable, by its name:
*
* @param string $variableName
* @param mixed $variableValue
*/
public function setVariable($variableName, $variableValue)
{
$this->variables[$variableName] = $variableValue;
}
/**
* Gets a single template variable, by its name, or
* $defaultValue if the variable does not exist
*
* @param string $variableName
* @param mixed $defaultValue
* @return mixed
*/
public function getVariable($variableName, $defaultValue = null)
{
return isset($this->variables[$variableName]) ?
$this->variables[$variableName] : $defaultValue;
}
/**
* Unsets a single template variable, by its name
*
* @param string $variableName
*/
public function delVariable($variableName)
{
unset($this->variables[$variableName]);
}
/**
* Returns all variables for this helper
*
* @return array
*/
public function getVariables()
{
return $this->variables;
}
/**
* Set the cloner used for dumping variables.
*
* @param AbstractCloner $cloner
*/
public function setCloner($cloner)
{
$this->cloner = $cloner;
}
/**
* Get the cloner used for dumping variables.
*
* @return AbstractCloner
*/
public function getCloner()
{
if (!$this->cloner) {
$this->cloner = new VarCloner();
}
return $this->cloner;
}
/**
* Set the application root path.
*
* @param string $applicationRootPath
*/
public function setApplicationRootPath($applicationRootPath)
{
$this->applicationRootPath = $applicationRootPath;
}
/**
* Return the application root path.
*
* @return string
*/
public function getApplicationRootPath()
{
return $this->applicationRootPath;
}
}