first version

This commit is contained in:
Bastian Allgeier
2019-01-13 23:17:34 +01:00
commit 01277f79f2
595 changed files with 82913 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
<?php
/**
* Whoops - php errors for cool kids
* @author Filipe Dobreira <http://github.com/filp>
*/
namespace Whoops\Exception;
use ErrorException as BaseErrorException;
/**
* Wraps ErrorException; mostly used for typing (at least now)
* to easily cleanup the stack trace of redundant info.
*/
class ErrorException extends BaseErrorException
{
}

View File

@@ -0,0 +1,73 @@
<?php
/**
* Whoops - php errors for cool kids
* @author Filipe Dobreira <http://github.com/filp>
*/
namespace Whoops\Exception;
class Formatter
{
/**
* Returns all basic information about the exception in a simple array
* for further convertion to other languages
* @param Inspector $inspector
* @param bool $shouldAddTrace
* @return array
*/
public static function formatExceptionAsDataArray(Inspector $inspector, $shouldAddTrace)
{
$exception = $inspector->getException();
$response = [
'type' => get_class($exception),
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
];
if ($shouldAddTrace) {
$frames = $inspector->getFrames();
$frameData = [];
foreach ($frames as $frame) {
/** @var Frame $frame */
$frameData[] = [
'file' => $frame->getFile(),
'line' => $frame->getLine(),
'function' => $frame->getFunction(),
'class' => $frame->getClass(),
'args' => $frame->getArgs(),
];
}
$response['trace'] = $frameData;
}
return $response;
}
public static function formatExceptionPlain(Inspector $inspector)
{
$message = $inspector->getException()->getMessage();
$frames = $inspector->getFrames();
$plain = $inspector->getExceptionName();
$plain .= ' thrown with message "';
$plain .= $message;
$plain .= '"'."\n\n";
$plain .= "Stacktrace:\n";
foreach ($frames as $i => $frame) {
$plain .= "#". (count($frames) - $i - 1). " ";
$plain .= $frame->getClass() ?: '';
$plain .= $frame->getClass() && $frame->getFunction() ? ":" : "";
$plain .= $frame->getFunction() ?: '';
$plain .= ' in ';
$plain .= ($frame->getFile() ?: '<#unknown>');
$plain .= ':';
$plain .= (int) $frame->getLine(). "\n";
}
return $plain;
}
}

View File

@@ -0,0 +1,291 @@
<?php
/**
* Whoops - php errors for cool kids
* @author Filipe Dobreira <http://github.com/filp>
*/
namespace Whoops\Exception;
use InvalidArgumentException;
use Serializable;
class Frame implements Serializable
{
/**
* @var array
*/
protected $frame;
/**
* @var string
*/
protected $fileContentsCache;
/**
* @var array[]
*/
protected $comments = [];
/**
* @var bool
*/
protected $application;
/**
* @param array[]
*/
public function __construct(array $frame)
{
$this->frame = $frame;
}
/**
* @param bool $shortened
* @return string|null
*/
public function getFile($shortened = false)
{
if (empty($this->frame['file'])) {
return null;
}
$file = $this->frame['file'];
// Check if this frame occurred within an eval().
// @todo: This can be made more reliable by checking if we've entered
// eval() in a previous trace, but will need some more work on the upper
// trace collector(s).
if (preg_match('/^(.*)\((\d+)\) : (?:eval\(\)\'d|assert) code$/', $file, $matches)) {
$file = $this->frame['file'] = $matches[1];
$this->frame['line'] = (int) $matches[2];
}
if ($shortened && is_string($file)) {
// Replace the part of the path that all frames have in common, and add 'soft hyphens' for smoother line-breaks.
$dirname = dirname(dirname(dirname(dirname(dirname(dirname(__DIR__))))));
if ($dirname !== '/') {
$file = str_replace($dirname, "&hellip;", $file);
}
$file = str_replace("/", "/&shy;", $file);
}
return $file;
}
/**
* @return int|null
*/
public function getLine()
{
return isset($this->frame['line']) ? $this->frame['line'] : null;
}
/**
* @return string|null
*/
public function getClass()
{
return isset($this->frame['class']) ? $this->frame['class'] : null;
}
/**
* @return string|null
*/
public function getFunction()
{
return isset($this->frame['function']) ? $this->frame['function'] : null;
}
/**
* @return array
*/
public function getArgs()
{
return isset($this->frame['args']) ? (array) $this->frame['args'] : [];
}
/**
* Returns the full contents of the file for this frame,
* if it's known.
* @return string|null
*/
public function getFileContents()
{
if ($this->fileContentsCache === null && $filePath = $this->getFile()) {
// Leave the stage early when 'Unknown' or '[internal]' is passed
// this would otherwise raise an exception when
// open_basedir is enabled.
if ($filePath === "Unknown" || $filePath === '[internal]') {
return null;
}
$this->fileContentsCache = file_get_contents($filePath);
}
return $this->fileContentsCache;
}
/**
* Adds a comment to this frame, that can be received and
* used by other handlers. For example, the PrettyPage handler
* can attach these comments under the code for each frame.
*
* An interesting use for this would be, for example, code analysis
* & annotations.
*
* @param string $comment
* @param string $context Optional string identifying the origin of the comment
*/
public function addComment($comment, $context = 'global')
{
$this->comments[] = [
'comment' => $comment,
'context' => $context,
];
}
/**
* Returns all comments for this frame. Optionally allows
* a filter to only retrieve comments from a specific
* context.
*
* @param string $filter
* @return array[]
*/
public function getComments($filter = null)
{
$comments = $this->comments;
if ($filter !== null) {
$comments = array_filter($comments, function ($c) use ($filter) {
return $c['context'] == $filter;
});
}
return $comments;
}
/**
* Returns the array containing the raw frame data from which
* this Frame object was built
*
* @return array
*/
public function getRawFrame()
{
return $this->frame;
}
/**
* Returns the contents of the file for this frame as an
* array of lines, and optionally as a clamped range of lines.
*
* NOTE: lines are 0-indexed
*
* @example
* Get all lines for this file
* $frame->getFileLines(); // => array( 0 => '<?php', 1 => '...', ...)
* @example
* Get one line for this file, starting at line 10 (zero-indexed, remember!)
* $frame->getFileLines(9, 1); // array( 10 => '...', 11 => '...')
*
* @throws InvalidArgumentException if $length is less than or equal to 0
* @param int $start
* @param int $length
* @return string[]|null
*/
public function getFileLines($start = 0, $length = null)
{
if (null !== ($contents = $this->getFileContents())) {
$lines = explode("\n", $contents);
// Get a subset of lines from $start to $end
if ($length !== null) {
$start = (int) $start;
$length = (int) $length;
if ($start < 0) {
$start = 0;
}
if ($length <= 0) {
throw new InvalidArgumentException(
"\$length($length) cannot be lower or equal to 0"
);
}
$lines = array_slice($lines, $start, $length, true);
}
return $lines;
}
}
/**
* Implements the Serializable interface, with special
* steps to also save the existing comments.
*
* @see Serializable::serialize
* @return string
*/
public function serialize()
{
$frame = $this->frame;
if (!empty($this->comments)) {
$frame['_comments'] = $this->comments;
}
return serialize($frame);
}
/**
* Unserializes the frame data, while also preserving
* any existing comment data.
*
* @see Serializable::unserialize
* @param string $serializedFrame
*/
public function unserialize($serializedFrame)
{
$frame = unserialize($serializedFrame);
if (!empty($frame['_comments'])) {
$this->comments = $frame['_comments'];
unset($frame['_comments']);
}
$this->frame = $frame;
}
/**
* Compares Frame against one another
* @param Frame $frame
* @return bool
*/
public function equals(Frame $frame)
{
if (!$this->getFile() || $this->getFile() === 'Unknown' || !$this->getLine()) {
return false;
}
return $frame->getFile() === $this->getFile() && $frame->getLine() === $this->getLine();
}
/**
* Returns whether this frame belongs to the application or not.
*
* @return boolean
*/
public function isApplication()
{
return $this->application;
}
/**
* Mark as an frame belonging to the application.
*
* @param boolean $application
*/
public function setApplication($application)
{
$this->application = $application;
}
}

View File

@@ -0,0 +1,203 @@
<?php
/**
* Whoops - php errors for cool kids
* @author Filipe Dobreira <http://github.com/filp>
*/
namespace Whoops\Exception;
use ArrayAccess;
use ArrayIterator;
use Countable;
use IteratorAggregate;
use Serializable;
use UnexpectedValueException;
/**
* Exposes a fluent interface for dealing with an ordered list
* of stack-trace frames.
*/
class FrameCollection implements ArrayAccess, IteratorAggregate, Serializable, Countable
{
/**
* @var array[]
*/
private $frames;
/**
* @param array $frames
*/
public function __construct(array $frames)
{
$this->frames = array_map(function ($frame) {
return new Frame($frame);
}, $frames);
}
/**
* Filters frames using a callable, returns the same FrameCollection
*
* @param callable $callable
* @return FrameCollection
*/
public function filter($callable)
{
$this->frames = array_values(array_filter($this->frames, $callable));
return $this;
}
/**
* Map the collection of frames
*
* @param callable $callable
* @return FrameCollection
*/
public function map($callable)
{
// Contain the map within a higher-order callable
// that enforces type-correctness for the $callable
$this->frames = array_map(function ($frame) use ($callable) {
$frame = call_user_func($callable, $frame);
if (!$frame instanceof Frame) {
throw new UnexpectedValueException(
"Callable to " . __METHOD__ . " must return a Frame object"
);
}
return $frame;
}, $this->frames);
return $this;
}
/**
* Returns an array with all frames, does not affect
* the internal array.
*
* @todo If this gets any more complex than this,
* have getIterator use this method.
* @see FrameCollection::getIterator
* @return array
*/
public function getArray()
{
return $this->frames;
}
/**
* @see IteratorAggregate::getIterator
* @return ArrayIterator
*/
public function getIterator()
{
return new ArrayIterator($this->frames);
}
/**
* @see ArrayAccess::offsetExists
* @param int $offset
*/
public function offsetExists($offset)
{
return isset($this->frames[$offset]);
}
/**
* @see ArrayAccess::offsetGet
* @param int $offset
*/
public function offsetGet($offset)
{
return $this->frames[$offset];
}
/**
* @see ArrayAccess::offsetSet
* @param int $offset
*/
public function offsetSet($offset, $value)
{
throw new \Exception(__CLASS__ . ' is read only');
}
/**
* @see ArrayAccess::offsetUnset
* @param int $offset
*/
public function offsetUnset($offset)
{
throw new \Exception(__CLASS__ . ' is read only');
}
/**
* @see Countable::count
* @return int
*/
public function count()
{
return count($this->frames);
}
/**
* Count the frames that belongs to the application.
*
* @return int
*/
public function countIsApplication()
{
return count(array_filter($this->frames, function (Frame $f) {
return $f->isApplication();
}));
}
/**
* @see Serializable::serialize
* @return string
*/
public function serialize()
{
return serialize($this->frames);
}
/**
* @see Serializable::unserialize
* @param string $serializedFrames
*/
public function unserialize($serializedFrames)
{
$this->frames = unserialize($serializedFrames);
}
/**
* @param Frame[] $frames Array of Frame instances, usually from $e->getPrevious()
*/
public function prependFrames(array $frames)
{
$this->frames = array_merge($frames, $this->frames);
}
/**
* Gets the innermost part of stack trace that is not the same as that of outer exception
*
* @param FrameCollection $parentFrames Outer exception frames to compare tail against
* @return Frame[]
*/
public function topDiff(FrameCollection $parentFrames)
{
$diff = $this->frames;
$parentFrames = $parentFrames->getArray();
$p = count($parentFrames)-1;
for ($i = count($diff)-1; $i >= 0 && $p >= 0; $i--) {
/** @var Frame $tailFrame */
$tailFrame = $diff[$i];
if ($tailFrame->equals($parentFrames[$p])) {
unset($diff[$i]);
}
$p--;
}
return $diff;
}
}

View File

@@ -0,0 +1,323 @@
<?php
/**
* Whoops - php errors for cool kids
* @author Filipe Dobreira <http://github.com/filp>
*/
namespace Whoops\Exception;
use Whoops\Util\Misc;
class Inspector
{
/**
* @var \Throwable
*/
private $exception;
/**
* @var \Whoops\Exception\FrameCollection
*/
private $frames;
/**
* @var \Whoops\Exception\Inspector
*/
private $previousExceptionInspector;
/**
* @var \Throwable[]
*/
private $previousExceptions;
/**
* @param \Throwable $exception The exception to inspect
*/
public function __construct($exception)
{
$this->exception = $exception;
}
/**
* @return \Throwable
*/
public function getException()
{
return $this->exception;
}
/**
* @return string
*/
public function getExceptionName()
{
return get_class($this->exception);
}
/**
* @return string
*/
public function getExceptionMessage()
{
return $this->extractDocrefUrl($this->exception->getMessage())['message'];
}
/**
* @return string[]
*/
public function getPreviousExceptionMessages()
{
return array_map(function ($prev) {
/** @var \Throwable $prev */
return $this->extractDocrefUrl($prev->getMessage())['message'];
}, $this->getPreviousExceptions());
}
/**
* @return int[]
*/
public function getPreviousExceptionCodes()
{
return array_map(function ($prev) {
/** @var \Throwable $prev */
return $prev->getCode();
}, $this->getPreviousExceptions());
}
/**
* Returns a url to the php-manual related to the underlying error - when available.
*
* @return string|null
*/
public function getExceptionDocrefUrl()
{
return $this->extractDocrefUrl($this->exception->getMessage())['url'];
}
private function extractDocrefUrl($message)
{
$docref = [
'message' => $message,
'url' => null,
];
// php embbeds urls to the manual into the Exception message with the following ini-settings defined
// http://php.net/manual/en/errorfunc.configuration.php#ini.docref-root
if (!ini_get('html_errors') || !ini_get('docref_root')) {
return $docref;
}
$pattern = "/\[<a href='([^']+)'>(?:[^<]+)<\/a>\]/";
if (preg_match($pattern, $message, $matches)) {
// -> strip those automatically generated links from the exception message
$docref['message'] = preg_replace($pattern, '', $message, 1);
$docref['url'] = $matches[1];
}
return $docref;
}
/**
* Does the wrapped Exception has a previous Exception?
* @return bool
*/
public function hasPreviousException()
{
return $this->previousExceptionInspector || $this->exception->getPrevious();
}
/**
* Returns an Inspector for a previous Exception, if any.
* @todo Clean this up a bit, cache stuff a bit better.
* @return Inspector
*/
public function getPreviousExceptionInspector()
{
if ($this->previousExceptionInspector === null) {
$previousException = $this->exception->getPrevious();
if ($previousException) {
$this->previousExceptionInspector = new Inspector($previousException);
}
}
return $this->previousExceptionInspector;
}
/**
* Returns an array of all previous exceptions for this inspector's exception
* @return \Throwable[]
*/
public function getPreviousExceptions()
{
if ($this->previousExceptions === null) {
$this->previousExceptions = [];
$prev = $this->exception->getPrevious();
while ($prev !== null) {
$this->previousExceptions[] = $prev;
$prev = $prev->getPrevious();
}
}
return $this->previousExceptions;
}
/**
* Returns an iterator for the inspected exception's
* frames.
* @return \Whoops\Exception\FrameCollection
*/
public function getFrames()
{
if ($this->frames === null) {
$frames = $this->getTrace($this->exception);
// Fill empty line/file info for call_user_func_array usages (PHP Bug #44428)
foreach ($frames as $k => $frame) {
if (empty($frame['file'])) {
// Default values when file and line are missing
$file = '[internal]';
$line = 0;
$next_frame = !empty($frames[$k + 1]) ? $frames[$k + 1] : [];
if ($this->isValidNextFrame($next_frame)) {
$file = $next_frame['file'];
$line = $next_frame['line'];
}
$frames[$k]['file'] = $file;
$frames[$k]['line'] = $line;
}
}
// Find latest non-error handling frame index ($i) used to remove error handling frames
$i = 0;
foreach ($frames as $k => $frame) {
if ($frame['file'] == $this->exception->getFile() && $frame['line'] == $this->exception->getLine()) {
$i = $k;
}
}
// Remove error handling frames
if ($i > 0) {
array_splice($frames, 0, $i);
}
$firstFrame = $this->getFrameFromException($this->exception);
array_unshift($frames, $firstFrame);
$this->frames = new FrameCollection($frames);
if ($previousInspector = $this->getPreviousExceptionInspector()) {
// Keep outer frame on top of the inner one
$outerFrames = $this->frames;
$newFrames = clone $previousInspector->getFrames();
// I assume it will always be set, but let's be safe
if (isset($newFrames[0])) {
$newFrames[0]->addComment(
$previousInspector->getExceptionMessage(),
'Exception message:'
);
}
$newFrames->prependFrames($outerFrames->topDiff($newFrames));
$this->frames = $newFrames;
}
}
return $this->frames;
}
/**
* Gets the backtrace from an exception.
*
* If xdebug is installed
*
* @param \Throwable $e
* @return array
*/
protected function getTrace($e)
{
$traces = $e->getTrace();
// Get trace from xdebug if enabled, failure exceptions only trace to the shutdown handler by default
if (!$e instanceof \ErrorException) {
return $traces;
}
if (!Misc::isLevelFatal($e->getSeverity())) {
return $traces;
}
if (!extension_loaded('xdebug') || !xdebug_is_enabled()) {
return [];
}
// Use xdebug to get the full stack trace and remove the shutdown handler stack trace
$stack = array_reverse(xdebug_get_function_stack());
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$traces = array_diff_key($stack, $trace);
return $traces;
}
/**
* Given an exception, generates an array in the format
* generated by Exception::getTrace()
* @param \Throwable $exception
* @return array
*/
protected function getFrameFromException($exception)
{
return [
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'class' => get_class($exception),
'args' => [
$exception->getMessage(),
],
];
}
/**
* Given an error, generates an array in the format
* generated by ErrorException
* @param ErrorException $exception
* @return array
*/
protected function getFrameFromError(ErrorException $exception)
{
return [
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'class' => null,
'args' => [],
];
}
/**
* Determine if the frame can be used to fill in previous frame's missing info
* happens for call_user_func and call_user_func_array usages (PHP Bug #44428)
*
* @param array $frame
* @return bool
*/
protected function isValidNextFrame(array $frame)
{
if (empty($frame['file'])) {
return false;
}
if (empty($frame['line'])) {
return false;
}
if (empty($frame['function']) || !stristr($frame['function'], 'call_user_func')) {
return false;
}
return true;
}
}