diff --git a/library/Whoops/Exception/ErrorException.php b/library/Whoops/Exception/ErrorException.php
new file mode 100644
index 0000000..d74e823
--- /dev/null
+++ b/library/Whoops/Exception/ErrorException.php
@@ -0,0 +1,17 @@
+
+ */
+
+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
+{
+}
diff --git a/library/Whoops/Exception/Formatter.php b/library/Whoops/Exception/Formatter.php
new file mode 100644
index 0000000..f14b8e2
--- /dev/null
+++ b/library/Whoops/Exception/Formatter.php
@@ -0,0 +1,74 @@
+
+ */
+
+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 = array(
+ 'type' => get_class($exception),
+ 'message' => $exception->getMessage(),
+ 'file' => $exception->getFile(),
+ 'line' => $exception->getLine(),
+ );
+
+ if ($shouldAddTrace) {
+ $frames = $inspector->getFrames();
+ $frameData = array();
+
+ foreach ($frames as $frame) {
+ /** @var Frame $frame */
+ $frameData[] = array(
+ '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;
+ }
+}
diff --git a/library/Whoops/Exception/Frame.php b/library/Whoops/Exception/Frame.php
new file mode 100644
index 0000000..6493f4a
--- /dev/null
+++ b/library/Whoops/Exception/Frame.php
@@ -0,0 +1,269 @@
+
+ */
+
+namespace Whoops\Exception;
+
+use InvalidArgumentException;
+use Serializable;
+
+class Frame implements Serializable
+{
+ /**
+ * @var array
+ */
+ protected $frame;
+
+ /**
+ * @var string
+ */
+ protected $fileContentsCache;
+
+ /**
+ * @var array[]
+ */
+ protected $comments = array();
+
+ /**
+ * @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__))))));
+ $file = str_replace($dirname, "…", $file);
+ $file = str_replace("/", "/", $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'] : array();
+ }
+
+ /**
+ * 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' is passed
+ // this would otherwise raise an exception when
+ // open_basedir is enabled.
+ if ($filePath === "Unknown") {
+ return null;
+ }
+
+ // Return null if the file doesn't actually exist.
+ if (!is_file($filePath)) {
+ 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[] = array(
+ '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 => ' '...', ...)
+ * @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();
+ }
+}
diff --git a/library/Whoops/Exception/FrameCollection.php b/library/Whoops/Exception/FrameCollection.php
new file mode 100644
index 0000000..47fffb6
--- /dev/null
+++ b/library/Whoops/Exception/FrameCollection.php
@@ -0,0 +1,191 @@
+
+ */
+
+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_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);
+ }
+
+ /**
+ * @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;
+ }
+}
diff --git a/library/Whoops/Exception/Inspector.php b/library/Whoops/Exception/Inspector.php
new file mode 100644
index 0000000..5834a22
--- /dev/null
+++ b/library/Whoops/Exception/Inspector.php
@@ -0,0 +1,161 @@
+
+ */
+
+namespace Whoops\Exception;
+
+use Exception;
+
+class Inspector
+{
+ /**
+ * @var Exception
+ */
+ private $exception;
+
+ /**
+ * @var \Whoops\Exception\FrameCollection
+ */
+ private $frames;
+
+ /**
+ * @var \Whoops\Exception\Inspector
+ */
+ private $previousExceptionInspector;
+
+ /**
+ * @param Exception $exception The exception to inspect
+ */
+ public function __construct(Exception $exception)
+ {
+ $this->exception = $exception;
+ }
+
+ /**
+ * @return Exception
+ */
+ public function getException()
+ {
+ return $this->exception;
+ }
+
+ /**
+ * @return string
+ */
+ public function getExceptionName()
+ {
+ return get_class($this->exception);
+ }
+
+ /**
+ * @return string
+ */
+ public function getExceptionMessage()
+ {
+ return $this->exception->getMessage();
+ }
+
+ /**
+ * 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 iterator for the inspected exception's
+ * frames.
+ * @return \Whoops\Exception\FrameCollection
+ */
+ public function getFrames()
+ {
+ if ($this->frames === null) {
+ $frames = $this->exception->getTrace();
+
+ // If we're handling an ErrorException thrown by Whoops,
+ // get rid of the last frame, which matches the handleError method,
+ // and do not add the current exception to trace. We ensure that
+ // the next frame does have a filename / linenumber, though.
+ if ($this->exception instanceof ErrorException && empty($frames[1]['line'])) {
+ $frames = array($this->getFrameFromError($this->exception));
+ } else {
+ $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;
+ }
+
+ /**
+ * Given an exception, generates an array in the format
+ * generated by Exception::getTrace()
+ * @param Exception $exception
+ * @return array
+ */
+ protected function getFrameFromException(Exception $exception)
+ {
+ return array(
+ 'file' => $exception->getFile(),
+ 'line' => $exception->getLine(),
+ 'class' => get_class($exception),
+ 'args' => array(
+ $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 array(
+ 'file' => $exception->getFile(),
+ 'line' => $exception->getLine(),
+ 'class' => null,
+ 'args' => array(),
+ );
+ }
+}
diff --git a/library/Whoops/Handler/CallbackHandler.php b/library/Whoops/Handler/CallbackHandler.php
new file mode 100644
index 0000000..a83c969
--- /dev/null
+++ b/library/Whoops/Handler/CallbackHandler.php
@@ -0,0 +1,49 @@
+
+ */
+
+namespace Whoops\Handler;
+
+use InvalidArgumentException;
+
+/**
+ * Wrapper for Closures passed as handlers. Can be used
+ * directly, or will be instantiated automagically by Whoops\Run
+ * if passed to Run::pushHandler
+ */
+class CallbackHandler extends Handler
+{
+ /**
+ * @var callable
+ */
+ protected $callable;
+
+ /**
+ * @throws InvalidArgumentException If argument is not callable
+ * @param callable $callable
+ */
+ public function __construct($callable)
+ {
+ if (!is_callable($callable)) {
+ throw new InvalidArgumentException(
+ 'Argument to ' . __METHOD__ . ' must be valid callable'
+ );
+ }
+
+ $this->callable = $callable;
+ }
+
+ /**
+ * @return int|null
+ */
+ public function handle()
+ {
+ $exception = $this->getException();
+ $inspector = $this->getInspector();
+ $run = $this->getRun();
+
+ return call_user_func($this->callable, $exception, $inspector, $run);
+ }
+}
diff --git a/library/Whoops/Handler/Handler.php b/library/Whoops/Handler/Handler.php
new file mode 100644
index 0000000..d7e78f1
--- /dev/null
+++ b/library/Whoops/Handler/Handler.php
@@ -0,0 +1,89 @@
+
+ */
+
+namespace Whoops\Handler;
+
+use Exception;
+use Whoops\Exception\Inspector;
+use Whoops\Run;
+
+/**
+ * Abstract implementation of a Handler.
+ */
+abstract class Handler implements HandlerInterface
+{
+ /**
+ * Return constants that can be returned from Handler::handle
+ * to message the handler walker.
+ */
+ const DONE = 0x10; // returning this is optional, only exists for
+ // semantic purposes
+ const LAST_HANDLER = 0x20;
+ const QUIT = 0x30;
+
+ /**
+ * @var Run
+ */
+ private $run;
+
+ /**
+ * @var Inspector $inspector
+ */
+ private $inspector;
+
+ /**
+ * @var Exception $exception
+ */
+ private $exception;
+
+ /**
+ * @param Run $run
+ */
+ public function setRun(Run $run)
+ {
+ $this->run = $run;
+ }
+
+ /**
+ * @return Run
+ */
+ protected function getRun()
+ {
+ return $this->run;
+ }
+
+ /**
+ * @param Inspector $inspector
+ */
+ public function setInspector(Inspector $inspector)
+ {
+ $this->inspector = $inspector;
+ }
+
+ /**
+ * @return Inspector
+ */
+ protected function getInspector()
+ {
+ return $this->inspector;
+ }
+
+ /**
+ * @param Exception $exception
+ */
+ public function setException(Exception $exception)
+ {
+ $this->exception = $exception;
+ }
+
+ /**
+ * @return Exception
+ */
+ protected function getException()
+ {
+ return $this->exception;
+ }
+}
diff --git a/library/Whoops/Handler/HandlerInterface.php b/library/Whoops/Handler/HandlerInterface.php
new file mode 100644
index 0000000..f50bc30
--- /dev/null
+++ b/library/Whoops/Handler/HandlerInterface.php
@@ -0,0 +1,37 @@
+
+ */
+
+namespace Whoops\Handler;
+
+use Exception;
+use Whoops\Exception\Inspector;
+use Whoops\Run;
+
+interface HandlerInterface
+{
+ /**
+ * @return int|null A handler may return nothing, or a Handler::HANDLE_* constant
+ */
+ public function handle();
+
+ /**
+ * @param Run $run
+ * @return void
+ */
+ public function setRun(Run $run);
+
+ /**
+ * @param Exception $exception
+ * @return void
+ */
+ public function setException(Exception $exception);
+
+ /**
+ * @param Inspector $inspector
+ * @return void
+ */
+ public function setInspector(Inspector $inspector);
+}
diff --git a/library/Whoops/Handler/JsonResponseHandler.php b/library/Whoops/Handler/JsonResponseHandler.php
new file mode 100644
index 0000000..b997438
--- /dev/null
+++ b/library/Whoops/Handler/JsonResponseHandler.php
@@ -0,0 +1,90 @@
+
+ */
+
+namespace Whoops\Handler;
+
+use Whoops\Exception\Formatter;
+
+/**
+ * Catches an exception and converts it to a JSON
+ * response. Additionally can also return exception
+ * frames for consumption by an API.
+ */
+class JsonResponseHandler extends Handler
+{
+ /**
+ * @var bool
+ */
+ private $returnFrames = false;
+
+ /**
+ * @var bool
+ */
+ private $onlyForAjaxRequests = false;
+
+ /**
+ * @param bool|null $returnFrames
+ * @return bool|$this
+ */
+ public function addTraceToOutput($returnFrames = null)
+ {
+ if (func_num_args() == 0) {
+ return $this->returnFrames;
+ }
+
+ $this->returnFrames = (bool) $returnFrames;
+ return $this;
+ }
+
+ /**
+ * @param bool|null $onlyForAjaxRequests
+ * @return null|bool
+ */
+ public function onlyForAjaxRequests($onlyForAjaxRequests = null)
+ {
+ if (func_num_args() == 0) {
+ return $this->onlyForAjaxRequests;
+ }
+
+ $this->onlyForAjaxRequests = (bool) $onlyForAjaxRequests;
+ }
+
+ /**
+ * Check, if possible, that this execution was triggered by an AJAX request.
+ *
+ * @return bool
+ */
+ private function isAjaxRequest()
+ {
+ return (
+ !empty($_SERVER['HTTP_X_REQUESTED_WITH'])
+ && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest');
+ }
+
+ /**
+ * @return int
+ */
+ public function handle()
+ {
+ if ($this->onlyForAjaxRequests() && !$this->isAjaxRequest()) {
+ return Handler::DONE;
+ }
+
+ $response = array(
+ 'error' => Formatter::formatExceptionAsDataArray(
+ $this->getInspector(),
+ $this->addTraceToOutput()
+ ),
+ );
+
+ if (\Whoops\Util\Misc::canSendHeaders()) {
+ header('Content-Type: application/json');
+ }
+
+ echo json_encode($response);
+ return Handler::QUIT;
+ }
+}
diff --git a/library/Whoops/Handler/PlainTextHandler.php b/library/Whoops/Handler/PlainTextHandler.php
new file mode 100644
index 0000000..b9af925
--- /dev/null
+++ b/library/Whoops/Handler/PlainTextHandler.php
@@ -0,0 +1,331 @@
+
+* Plaintext handler for command line and logs.
+* @author Pierre-Yves Landuré
+*/
+
+namespace Whoops\Handler;
+
+use InvalidArgumentException;
+use Psr\Log\LoggerInterface;
+use Whoops\Exception\Frame;
+
+/**
+* Handler outputing plaintext error messages. Can be used
+* directly, or will be instantiated automagically by Whoops\Run
+* if passed to Run::pushHandler
+*/
+class PlainTextHandler extends Handler
+{
+ const VAR_DUMP_PREFIX = ' | ';
+
+ /**
+ * @var Psr\Log\LoggerInterface
+ */
+ protected $logger;
+
+ /**
+ * @var bool
+ */
+ private $addTraceToOutput = true;
+
+ /**
+ * @var bool|integer
+ */
+ private $addTraceFunctionArgsToOutput = false;
+
+ /**
+ * @var integer
+ */
+ private $traceFunctionArgsOutputLimit = 1024;
+
+ /**
+ * @var bool
+ */
+ private $onlyForCommandLine = false;
+
+ /**
+ * @var bool
+ */
+ private $outputOnlyIfCommandLine = true;
+
+ /**
+ * @var bool
+ */
+ private $loggerOnly = false;
+
+ /**
+ * Constructor.
+ * @throws InvalidArgumentException If argument is not null or a LoggerInterface
+ * @param Psr\Log\LoggerInterface|null $logger
+ */
+ public function __construct($logger = null)
+ {
+ $this->setLogger($logger);
+ }
+
+ /**
+ * Set the output logger interface.
+ * @throws InvalidArgumentException If argument is not null or a LoggerInterface
+ * @param Psr\Log\LoggerInterface|null $logger
+ */
+ public function setLogger($logger = null)
+ {
+ if (! (is_null($logger)
+ || $logger instanceof LoggerInterface)) {
+ throw new InvalidArgumentException(
+ 'Argument to ' . __METHOD__ .
+ " must be a valid Logger Interface (aka. Monolog), " .
+ get_class($logger) . ' given.'
+ );
+ }
+
+ $this->logger = $logger;
+ }
+
+ /**
+ * @return Psr\Log\LoggerInterface|null
+ */
+ public function getLogger()
+ {
+ return $this->logger;
+ }
+
+ /**
+ * Add error trace to output.
+ * @param bool|null $addTraceToOutput
+ * @return bool|$this
+ */
+ public function addTraceToOutput($addTraceToOutput = null)
+ {
+ if (func_num_args() == 0) {
+ return $this->addTraceToOutput;
+ }
+
+ $this->addTraceToOutput = (bool) $addTraceToOutput;
+ return $this;
+ }
+
+ /**
+ * Add error trace function arguments to output.
+ * Set to True for all frame args, or integer for the n first frame args.
+ * @param bool|integer|null $addTraceFunctionArgsToOutput
+ * @return null|bool|integer
+ */
+ public function addTraceFunctionArgsToOutput($addTraceFunctionArgsToOutput = null)
+ {
+ if (func_num_args() == 0) {
+ return $this->addTraceFunctionArgsToOutput;
+ }
+
+ if (! is_integer($addTraceFunctionArgsToOutput)) {
+ $this->addTraceFunctionArgsToOutput = (bool) $addTraceFunctionArgsToOutput;
+ } else {
+ $this->addTraceFunctionArgsToOutput = $addTraceFunctionArgsToOutput;
+ }
+ }
+
+ /**
+ * Set the size limit in bytes of frame arguments var_dump output.
+ * If the limit is reached, the var_dump output is discarded.
+ * Prevent memory limit errors.
+ * @var integer
+ */
+ public function setTraceFunctionArgsOutputLimit($traceFunctionArgsOutputLimit)
+ {
+ $this->traceFunctionArgsOutputLimit = (integer) $traceFunctionArgsOutputLimit;
+ }
+
+ /**
+ * Get the size limit in bytes of frame arguments var_dump output.
+ * If the limit is reached, the var_dump output is discarded.
+ * Prevent memory limit errors.
+ * @return integer
+ */
+ public function getTraceFunctionArgsOutputLimit()
+ {
+ return $this->traceFunctionArgsOutputLimit;
+ }
+
+ /**
+ * Restrict error handling to command line calls.
+ * @param bool|null $onlyForCommandLine
+ * @return null|bool
+ */
+ public function onlyForCommandLine($onlyForCommandLine = null)
+ {
+ if (func_num_args() == 0) {
+ return $this->onlyForCommandLine;
+ }
+ $this->onlyForCommandLine = (bool) $onlyForCommandLine;
+ }
+
+ /**
+ * Output the error message only if using command line.
+ * else, output to logger if available.
+ * Allow to safely add this handler to web pages.
+ * @param bool|null $outputOnlyIfCommandLine
+ * @return null|bool
+ */
+ public function outputOnlyIfCommandLine($outputOnlyIfCommandLine = null)
+ {
+ if (func_num_args() == 0) {
+ return $this->outputOnlyIfCommandLine;
+ }
+ $this->outputOnlyIfCommandLine = (bool) $outputOnlyIfCommandLine;
+ }
+
+ /**
+ * Only output to logger.
+ * @param bool|null $loggerOnly
+ * @return null|bool
+ */
+ public function loggerOnly($loggerOnly = null)
+ {
+ if (func_num_args() == 0) {
+ return $this->loggerOnly;
+ }
+
+ $this->loggerOnly = (bool) $loggerOnly;
+ }
+
+ /**
+ * Check, if possible, that this execution was triggered by a command line.
+ * @return bool
+ */
+ private function isCommandLine()
+ {
+ return PHP_SAPI == 'cli';
+ }
+
+ /**
+ * Test if handler can process the exception..
+ * @return bool
+ */
+ private function canProcess()
+ {
+ return $this->isCommandLine() || !$this->onlyForCommandLine();
+ }
+
+ /**
+ * Test if handler can output to stdout.
+ * @return bool
+ */
+ private function canOutput()
+ {
+ return ($this->isCommandLine() || ! $this->outputOnlyIfCommandLine())
+ && ! $this->loggerOnly();
+ }
+
+ /**
+ * Get the frame args var_dump.
+ * @param \Whoops\Exception\Frame $frame [description]
+ * @param integer $line [description]
+ * @return string
+ */
+ private function getFrameArgsOutput(Frame $frame, $line)
+ {
+ if ($this->addTraceFunctionArgsToOutput() === false
+ || $this->addTraceFunctionArgsToOutput() < $line) {
+ return '';
+ }
+
+ // Dump the arguments:
+ ob_start();
+ var_dump($frame->getArgs());
+ if (ob_get_length() > $this->getTraceFunctionArgsOutputLimit()) {
+ // The argument var_dump is to big.
+ // Discarded to limit memory usage.
+ ob_clean();
+ return sprintf(
+ "\n%sArguments dump length greater than %d Bytes. Discarded.",
+ self::VAR_DUMP_PREFIX,
+ $this->getTraceFunctionArgsOutputLimit()
+ );
+ }
+
+ return sprintf("\n%s",
+ preg_replace('/^/m', self::VAR_DUMP_PREFIX, ob_get_clean())
+ );
+ }
+
+ /**
+ * Get the exception trace as plain text.
+ * @return string
+ */
+ private function getTraceOutput()
+ {
+ if (! $this->addTraceToOutput()) {
+ return '';
+ }
+ $inspector = $this->getInspector();
+ $frames = $inspector->getFrames();
+
+ $response = "\nStack trace:";
+
+ $line = 1;
+ foreach ($frames as $frame) {
+ /** @var Frame $frame */
+ $class = $frame->getClass();
+
+ $template = "\n%3d. %s->%s() %s:%d%s";
+ if (! $class) {
+ // Remove method arrow (->) from output.
+ $template = "\n%3d. %s%s() %s:%d%s";
+ }
+
+ $response .= sprintf(
+ $template,
+ $line,
+ $class,
+ $frame->getFunction(),
+ $frame->getFile(),
+ $frame->getLine(),
+ $this->getFrameArgsOutput($frame, $line)
+ );
+
+ $line++;
+ }
+
+ return $response;
+ }
+
+ /**
+ * @return int
+ */
+ public function handle()
+ {
+ if (! $this->canProcess()) {
+ return Handler::DONE;
+ }
+
+ $exception = $this->getException();
+
+ $response = sprintf("%s: %s in file %s on line %d%s\n",
+ get_class($exception),
+ $exception->getMessage(),
+ $exception->getFile(),
+ $exception->getLine(),
+ $this->getTraceOutput()
+ );
+
+ if ($this->getLogger()) {
+ $this->getLogger()->error($response);
+ }
+
+ if (! $this->canOutput()) {
+ return Handler::DONE;
+ }
+
+ if (class_exists('\Whoops\Util\Misc')
+ && \Whoops\Util\Misc::canSendHeaders()) {
+ header('Content-Type: text/plain');
+ }
+
+ echo $response;
+
+ return Handler::QUIT;
+ }
+}
diff --git a/library/Whoops/Handler/PrettyPageHandler.php b/library/Whoops/Handler/PrettyPageHandler.php
new file mode 100644
index 0000000..95294ca
--- /dev/null
+++ b/library/Whoops/Handler/PrettyPageHandler.php
@@ -0,0 +1,467 @@
+
+ */
+
+namespace Whoops\Handler;
+
+use InvalidArgumentException;
+use RuntimeException;
+use Whoops\Exception\Formatter;
+use Whoops\Util\Misc;
+use Whoops\Util\TemplateHelper;
+
+class PrettyPageHandler extends Handler
+{
+ /**
+ * Search paths to be scanned for resources, in the reverse
+ * order they're declared.
+ *
+ * @var array
+ */
+ private $searchPaths = array();
+
+ /**
+ * Fast lookup cache for known resource locations.
+ *
+ * @var array
+ */
+ private $resourceCache = array();
+
+ /**
+ * The name of the custom css file.
+ *
+ * @var string
+ */
+ private $customCss = null;
+
+ /**
+ * @var array[]
+ */
+ private $extraTables = array();
+
+ /**
+ * @var bool
+ */
+ private $handleUnconditionally = false;
+
+ /**
+ * @var string
+ */
+ private $pageTitle = "Whoops! There was an error.";
+
+ /**
+ * A string identifier for a known IDE/text editor, 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
+ * "txmt://open?url=%file&line=%line"
+ * @var mixed $editor
+ */
+ protected $editor;
+
+ /**
+ * A list of known editor strings
+ * @var array
+ */
+ protected $editors = array(
+ "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",
+ );
+
+ /**
+ * Constructor.
+ */
+ public function __construct()
+ {
+ if (ini_get('xdebug.file_link_format') || extension_loaded('xdebug')) {
+ // Register editor using xdebug's file_link_format option.
+ $this->editors['xdebug'] = function ($file, $line) {
+ return str_replace(array('%f', '%l'), array($file, $line), ini_get('xdebug.file_link_format'));
+ };
+ }
+
+ // Add the default, local resource search path:
+ $this->searchPaths[] = __DIR__ . "/../Resources";
+ }
+
+ /**
+ * @return int|null
+ */
+ public function handle()
+ {
+ if (!$this->handleUnconditionally()) {
+ // Check conditions for outputting HTML:
+ // @todo: Make this more robust
+ if (php_sapi_name() === '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;
+ }
+ }
+
+ // @todo: Make this more dynamic
+ $helper = new TemplateHelper();
+
+ $templateFile = $this->getResource("views/layout.html.php");
+ $cssFile = $this->getResource("css/whoops.base.css");
+ $zeptoFile = $this->getResource("js/zepto.min.js");
+ $jsFile = $this->getResource("js/whoops.base.js");
+
+ if ($this->customCss) {
+ $customCssFile = $this->getResource($this->customCss);
+ }
+
+ $inspector = $this->getInspector();
+ $frames = $inspector->getFrames();
+
+ $code = $inspector->getException()->getCode();
+
+ if ($inspector->getException() instanceof \ErrorException) {
+ $code = Misc::translateErrorCode($code);
+ }
+
+ // List of variables that will be passed to the layout template.
+ $vars = array(
+ "page_title" => $this->getPageTitle(),
+
+ // @todo: Asset compiler
+ "stylesheet" => file_get_contents($cssFile),
+ "zepto" => file_get_contents($zeptoFile),
+ "javascript" => file_get_contents($jsFile),
+
+ // Template paths:
+ "header" => $this->getResource("views/header.html.php"),
+ "frame_list" => $this->getResource("views/frame_list.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->getException()->getMessage(),
+ "code" => $code,
+ "plain_exception" => Formatter::formatExceptionPlain($inspector),
+ "frames" => $frames,
+ "has_frames" => !!count($frames),
+ "handler" => $this,
+ "handlers" => $this->getRun()->getHandlers(),
+
+ "tables" => array(
+ "Server/Request Data" => $_SERVER,
+ "GET Data" => $_GET,
+ "POST Data" => $_POST,
+ "Files" => $_FILES,
+ "Cookies" => $_COOKIE,
+ "Session" => isset($_SESSION) ? $_SESSION : array(),
+ "Environment Variables" => $_ENV,
+ ),
+ );
+
+ if (isset($customCssFile)) {
+ $vars["stylesheet"] .= file_get_contents($customCssFile);
+ }
+
+ // Add extra entries list of data tables:
+ // @todo: Consolidate addDataTable and addDataTableCallback
+ $extraTables = array_map(function ($table) {
+ return $table instanceof \Closure ? $table() : $table;
+ }, $this->getDataTables());
+ $vars["tables"] = array_merge($extraTables, $vars["tables"]);
+
+ $helper->setVariables($vars);
+ $helper->render($templateFile);
+
+ return Handler::QUIT;
+ }
+
+ /**
+ * 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
+ * @param array $data
+ */
+ public function addDataTable($label, array $data)
+ {
+ $this->extraTables[$label] = $data;
+ }
+
+ /**
+ * 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.
+ *
+ * @throws InvalidArgumentException If $callback is not callable
+ * @param string $label
+ * @param callable $callback Callable returning an associative array
+ */
+ public function addDataTableCallback($label, /* callable */ $callback)
+ {
+ if (!is_callable($callback)) {
+ throw new InvalidArgumentException('Expecting callback argument to be callable');
+ }
+
+ $this->extraTables[$label] = function () use ($callback) {
+ try {
+ $result = call_user_func($callback);
+
+ // Only return the result if it can be iterated over by foreach().
+ return is_array($result) || $result instanceof \Traversable ? $result : array();
+ } catch (\Exception $e) {
+ // Don't allow failure to break the rendering of the original exception.
+ return array();
+ }
+ };
+ }
+
+ /**
+ * 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] : array();
+ }
+
+ return $this->extraTables;
+ }
+
+ /**
+ * 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|null
+ */
+ public function handleUnconditionally($value = null)
+ {
+ if (func_num_args() == 0) {
+ return $this->handleUnconditionally;
+ }
+
+ $this->handleUnconditionally = (bool) $value;
+ }
+
+ /**
+ * Adds an editor resolver, identified by a string
+ * name, and that may be a string path, or a callable
+ * resolver. If the callable returns a string, it will
+ * be set as the file reference's href attribute.
+ *
+ * @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 $resolver
+ */
+ public function addEditor($identifier, $resolver)
+ {
+ $this->editors[$identifier] = $resolver;
+ }
+
+ /**
+ * Set the editor to use to open referenced files, by a string
+ * identifier, or a callable that will be executed for every
+ * file reference, with a $file and $line argument, and should
+ * return a string.
+ *
+ * @example
+ * $run->setEditor(function($file, $line) { return "file:///{$file}"; });
+ * @example
+ * $run->setEditor('sublime');
+ *
+ * @throws InvalidArgumentException If invalid argument identifier provided
+ * @param string|callable $editor
+ */
+ 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;
+ }
+
+ /**
+ * Given a string file path, and an integer file line,
+ * executes the editor resolver and returns, if available,
+ * a string that may be used as the href property for that
+ * file reference.
+ *
+ * @throws InvalidArgumentException If editor resolver does not return a string
+ * @param string $filePath
+ * @param int $line
+ * @return false|string
+ */
+ public function getEditorHref($filePath, $line)
+ {
+ if ($this->editor === null) {
+ return false;
+ }
+
+ $editor = $this->editor;
+ if (is_string($editor)) {
+ $editor = $this->editors[$editor];
+ }
+
+ if (is_callable($editor)) {
+ $editor = call_user_func($editor, $filePath, $line);
+ }
+
+ // Check that the editor is a string, and replace the
+ // %line and %file placeholders:
+ if (!is_string($editor)) {
+ throw new InvalidArgumentException(
+ __METHOD__ . " should always resolve to a string; got something else instead"
+ );
+ }
+
+ $editor = str_replace("%line", rawurlencode($line), $editor);
+ $editor = str_replace("%file", rawurlencode($filePath), $editor);
+
+ return $editor;
+ }
+
+ /**
+ * @param string $title
+ * @return void
+ */
+ public function setPageTitle($title)
+ {
+ $this->pageTitle = (string) $title;
+ }
+
+ /**
+ * @return string
+ */
+ public function getPageTitle()
+ {
+ return $this->pageTitle;
+ }
+
+ /**
+ * Adds a path to the list of paths to be searched for
+ * resources.
+ *
+ * @throws InvalidArgumnetException If $path is not a valid directory
+ *
+ * @param string $path
+ * @return void
+ */
+ public function addResourcePath($path)
+ {
+ if (!is_dir($path)) {
+ throw new InvalidArgumentException(
+ "'$path' is not a valid directory"
+ );
+ }
+
+ array_unshift($this->searchPaths, $path);
+ }
+
+ /**
+ * Adds a custom css file to be loaded.
+ *
+ * @param string $name
+ * @return void
+ */
+ public function addCustomCss($name)
+ {
+ $this->customCss = $name;
+ }
+
+ /**
+ * @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.
+ *
+ * @throws RuntimeException If resource cannot be found in any of the available paths
+ *
+ * @param string $resource
+ * @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 void
+ */
+ public function setResourcesPath($resourcesPath)
+ {
+ $this->addResourcePath($resourcesPath);
+ }
+}
diff --git a/library/Whoops/Handler/SoapResponseHandler.php b/library/Whoops/Handler/SoapResponseHandler.php
new file mode 100644
index 0000000..2173dd7
--- /dev/null
+++ b/library/Whoops/Handler/SoapResponseHandler.php
@@ -0,0 +1,49 @@
+
+ */
+
+namespace Whoops\Handler;
+
+
+/**
+ * Catches an exception and converts it to an Soap XML
+ * response.
+ *
+ * @author Markus Staab
+ */
+class SoapResponseHandler extends Handler
+{
+ /**
+ * @return int
+ */
+ public function handle()
+ {
+ $exception = $this->getException();
+
+ echo $this->toXml($exception);
+
+ return Handler::QUIT;
+ }
+
+ /**
+ * Converts a Exception into a SoapFault XML
+ */
+ private function toXml(\Exception $exception)
+ {
+ $xml = '';
+ $xml .= '';
+ $xml .= '';
+ $xml .= ' ';
+ $xml .= ' ';
+ $xml .= ' '. htmlspecialchars($exception->getCode()) .'';
+ $xml .= ' '. htmlspecialchars($exception->getMessage()) .'';
+ $xml .= ' '. htmlspecialchars($exception->getTraceAsString()) .'';
+ $xml .= ' ';
+ $xml .= ' ';
+ $xml .= '';
+
+ return $xml;
+ }
+}
diff --git a/library/Whoops/Handler/XmlResponseHandler.php b/library/Whoops/Handler/XmlResponseHandler.php
new file mode 100644
index 0000000..61ee86c
--- /dev/null
+++ b/library/Whoops/Handler/XmlResponseHandler.php
@@ -0,0 +1,99 @@
+
+ */
+
+namespace Whoops\Handler;
+
+use SimpleXMLElement;
+use Whoops\Exception\Formatter;
+
+/**
+ * Catches an exception and converts it to an XML
+ * response. Additionally can also return exception
+ * frames for consumption by an API.
+ */
+class XmlResponseHandler extends Handler
+{
+ /**
+ * @var bool
+ */
+ private $returnFrames = false;
+
+ /**
+ * @param bool|null $returnFrames
+ * @return bool|$this
+ */
+ public function addTraceToOutput($returnFrames = null)
+ {
+ if (func_num_args() == 0) {
+ return $this->returnFrames;
+ }
+
+ $this->returnFrames = (bool) $returnFrames;
+ return $this;
+ }
+
+ /**
+ * @return int
+ */
+ public function handle()
+ {
+ $response = array(
+ 'error' => Formatter::formatExceptionAsDataArray(
+ $this->getInspector(),
+ $this->addTraceToOutput()
+ ),
+ );
+
+ echo $this->toXml($response);
+
+ return Handler::QUIT;
+ }
+
+ /**
+ * @param SimpleXMLElement $node Node to append data to, will be modified in place
+ * @param array|Traversable $data
+ * @return SimpleXMLElement The modified node, for chaining
+ */
+ private static function addDataToNode(\SimpleXMLElement $node, $data)
+ {
+ assert('is_array($data) || $node instanceof Traversable');
+
+ foreach ($data as $key => $value) {
+ if (is_numeric($key)) {
+ // Convert the key to a valid string
+ $key = "unknownNode_". (string) $key;
+ }
+
+ // Delete any char not allowed in XML element names
+ $key = preg_replace('/[^a-z0-9\-\_\.\:]/i', '', $key);
+
+ if (is_array($value)) {
+ $child = $node->addChild($key);
+ self::addDataToNode($child, $value);
+ } else {
+ $value = str_replace('&', '&', print_r($value, true));
+ $node->addChild($key, $value);
+ }
+ }
+
+ return $node;
+ }
+
+ /**
+ * The main function for converting to an XML document.
+ *
+ * @param array|Traversable $data
+ * @return string XML
+ */
+ private static function toXml($data)
+ {
+ assert('is_array($data) || $node instanceof Traversable');
+
+ $node = simplexml_load_string("");
+
+ return self::addDataToNode($node, $data)->asXML();
+ }
+}
diff --git a/library/Whoops/Provider/Phalcon/WhoopsServiceProvider.php b/library/Whoops/Provider/Phalcon/WhoopsServiceProvider.php
new file mode 100644
index 0000000..d8d5781
--- /dev/null
+++ b/library/Whoops/Provider/Phalcon/WhoopsServiceProvider.php
@@ -0,0 +1,78 @@
+
+ */
+
+namespace Whoops\Provider\Phalcon;
+
+use Phalcon\DI;
+use Phalcon\DI\Exception;
+use Whoops\Handler\JsonResponseHandler;
+use Whoops\Handler\PrettyPageHandler;
+use Whoops\Run;
+
+class WhoopsServiceProvider
+{
+ /**
+ * @param DI $di
+ */
+ public function __construct(DI $di = null)
+ {
+ if (!$di) {
+ $di = DI::getDefault();
+ }
+
+ // There's only ever going to be one error page...right?
+ $di->setShared('whoops.pretty_page_handler', function () {
+ return new PrettyPageHandler();
+ });
+
+ // There's only ever going to be one error page...right?
+ $di->setShared('whoops.json_response_handler', function () {
+ $jsonHandler = new JsonResponseHandler();
+ $jsonHandler->onlyForAjaxRequests(true);
+ return $jsonHandler;
+ });
+
+ // Retrieves info on the Phalcon environment and ships it off
+ // to the PrettyPageHandler's data tables:
+ // This works by adding a new handler to the stack that runs
+ // before the error page, retrieving the shared page handler
+ // instance, and working with it to add new data tables
+ $phalcon_info_handler = function () use ($di) {
+ try {
+ $request = $di['request'];
+ } catch (Exception $e) {
+ // This error occurred too early in the application's life
+ // and the request instance is not yet available.
+ return;
+ }
+
+ // Request info:
+ $di['whoops.pretty_page_handler']->addDataTable('Phalcon Application (Request)', array(
+ 'URI' => $request->getScheme().'://'.$request->getServer('HTTP_HOST').$request->getServer('REQUEST_URI'),
+ 'Request URI' => $request->getServer('REQUEST_URI'),
+ 'Path Info' => $request->getServer('PATH_INFO'),
+ 'Query String' => $request->getServer('QUERY_STRING') ?: '',
+ 'HTTP Method' => $request->getMethod(),
+ 'Script Name' => $request->getServer('SCRIPT_NAME'),
+ //'Base Path' => $request->getBasePath(),
+ //'Base URL' => $request->getBaseUrl(),
+ 'Scheme' => $request->getScheme(),
+ 'Port' => $request->getServer('SERVER_PORT'),
+ 'Host' => $request->getServerName(),
+ ));
+ };
+
+ $di->setShared('whoops', function () use ($di,$phalcon_info_handler) {
+ $run = new Run();
+ $run->pushHandler($di['whoops.pretty_page_handler']);
+ $run->pushHandler($phalcon_info_handler);
+ $run->pushHandler($di['whoops.json_response_handler']);
+ return $run;
+ });
+
+ $di['whoops']->register();
+ }
+}
diff --git a/library/Whoops/Provider/Silex/WhoopsServiceProvider.php b/library/Whoops/Provider/Silex/WhoopsServiceProvider.php
new file mode 100644
index 0000000..69d01d3
--- /dev/null
+++ b/library/Whoops/Provider/Silex/WhoopsServiceProvider.php
@@ -0,0 +1,111 @@
+
+ */
+
+namespace Whoops\Provider\Silex;
+
+use RuntimeException;
+use Silex\Application;
+use Silex\ServiceProviderInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\HttpException;
+use Whoops\Handler\Handler;
+use Whoops\Handler\PlainTextHandler;
+use Whoops\Handler\PrettyPageHandler;
+use Whoops\Run;
+
+class WhoopsServiceProvider implements ServiceProviderInterface
+{
+ /**
+ * @param Application $app
+ */
+ public function register(Application $app)
+ {
+ // There's only ever going to be one error page...right?
+ $app['whoops.error_page_handler'] = $app->share(function () {
+ if (PHP_SAPI === 'cli') {
+ return new PlainTextHandler();
+ } else {
+ return new PrettyPageHandler();
+ }
+ });
+
+ // Retrieves info on the Silex environment and ships it off
+ // to the PrettyPageHandler's data tables:
+ // This works by adding a new handler to the stack that runs
+ // before the error page, retrieving the shared page handler
+ // instance, and working with it to add new data tables
+ $app['whoops.silex_info_handler'] = $app->protect(function () use ($app) {
+ try {
+ /** @var Request $request */
+ $request = $app['request'];
+ } catch (RuntimeException $e) {
+ // This error occurred too early in the application's life
+ // and the request instance is not yet available.
+ return;
+ }
+
+ /** @var Handler $errorPageHandler */
+ $errorPageHandler = $app["whoops.error_page_handler"];
+
+ if ($errorPageHandler instanceof PrettyPageHandler) {
+ /** @var PrettyPageHandler $errorPageHandler */
+
+ // General application info:
+ $errorPageHandler->addDataTable('Silex Application', array(
+ 'Charset' => $app['charset'],
+ 'Locale' => $app['locale'],
+ 'Route Class' => $app['route_class'],
+ 'Dispatcher Class' => $app['dispatcher_class'],
+ 'Application Class' => get_class($app),
+ ));
+
+ // Request info:
+ $errorPageHandler->addDataTable('Silex Application (Request)', array(
+ 'URI' => $request->getUri(),
+ 'Request URI' => $request->getRequestUri(),
+ 'Path Info' => $request->getPathInfo(),
+ 'Query String' => $request->getQueryString() ?: '',
+ 'HTTP Method' => $request->getMethod(),
+ 'Script Name' => $request->getScriptName(),
+ 'Base Path' => $request->getBasePath(),
+ 'Base URL' => $request->getBaseUrl(),
+ 'Scheme' => $request->getScheme(),
+ 'Port' => $request->getPort(),
+ 'Host' => $request->getHost(),
+ ));
+ }
+ });
+
+ $app['whoops'] = $app->share(function () use ($app) {
+ $run = new Run();
+ $run->allowQuit(false);
+ $run->pushHandler($app['whoops.error_page_handler']);
+ $run->pushHandler($app['whoops.silex_info_handler']);
+ return $run;
+ });
+
+ $app->error(function ($e) use ($app) {
+ $method = Run::EXCEPTION_HANDLER;
+
+ ob_start();
+ $app['whoops']->$method($e);
+ $response = ob_get_clean();
+ $code = $e instanceof HttpException ? $e->getStatusCode() : 500;
+
+ return new Response($response, $code);
+ });
+
+ $app['whoops']->register();
+ }
+
+ /**
+ * @see Silex\ServiceProviderInterface::boot
+ */
+ public function boot(Application $app)
+ {
+ }
+}
diff --git a/library/Whoops/Resources/css/whoops.base.css b/library/Whoops/Resources/css/whoops.base.css
new file mode 100644
index 0000000..16b35a2
--- /dev/null
+++ b/library/Whoops/Resources/css/whoops.base.css
@@ -0,0 +1,337 @@
+.cf:before, .cf:after {content: " ";display: table;} .cf:after {clear: both;} .cf {*zoom: 1;}
+body {
+ font: 14px helvetica, arial, sans-serif;
+ color: #2B2B2B;
+ background-color: #D4D4D4;
+ padding:0;
+ margin: 0;
+ max-height: 100%;
+}
+ a {
+ text-decoration: none;
+ }
+
+.container{
+ height: 100%;
+ width: 100%;
+ position: fixed;
+ margin: 0;
+ padding: 0;
+ left: 0;
+ top: 0;
+}
+
+.branding {
+ position: absolute;
+ top: 10px;
+ right: 20px;
+ color: #777777;
+ font-size: 10px;
+ z-index: 100;
+}
+ .branding a {
+ color: #CD3F3F;
+ }
+
+header {
+ padding: 30px 20px;
+ color: white;
+ background: #272727;
+ box-sizing: border-box;
+ border-left: 5px solid #CD3F3F;
+}
+ .exc-title {
+ margin: 0;
+ color: #616161;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, .1);
+ }
+ .exc-title-primary { color: #CD3F3F; }
+ .exc-message {
+ font-size: 32px;
+ margin: 5px 0;
+ word-wrap: break-word;
+ }
+
+.stack-container {
+ height: 100%;
+ position: relative;
+}
+
+.details-container {
+ height: 100%;
+ overflow: auto;
+ float: right;
+ width: 70%;
+ background: #DADADA;
+}
+ .details {
+ padding: 10px;
+ padding-left: 5px;
+ border-left: 5px solid rgba(0, 0, 0, .1);
+ }
+
+.frames-container {
+ height: 100%;
+ overflow: auto;
+ float: left;
+ width: 30%;
+ background: #FFF;
+}
+ .frame {
+ padding: 14px;
+ background: #F3F3F3;
+ border-right: 1px solid rgba(0, 0, 0, .2);
+ cursor: pointer;
+ }
+ .frame.active {
+ background-color: #4288CE;
+ color: #F3F3F3;
+ box-shadow: inset -2px 0 0 rgba(255, 255, 255, .1);
+ text-shadow: 0 1px 0 rgba(0, 0, 0, .2);
+ }
+
+ .frame:not(.active):hover {
+ background: #BEE9EA;
+ }
+
+ .frame-class, .frame-function, .frame-index {
+ font-weight: bold;
+ }
+
+ .frame-index {
+ font-size: 11px;
+ color: #BDBDBD;
+ }
+
+ .frame-class {
+ color: #4288CE;
+ }
+ .active .frame-class {
+ color: #BEE9EA;
+ }
+
+ .frame-file {
+ font-family: consolas, monospace;
+ word-wrap:break-word;
+ }
+
+ .frame-file .editor-link {
+ color: #272727;
+ }
+
+ .frame-line {
+ font-weight: bold;
+ color: #4288CE;
+ }
+
+ .active .frame-line { color: #BEE9EA; }
+ .frame-line:before {
+ content: ":";
+ }
+
+ .frame-code {
+ padding: 10px;
+ padding-left: 5px;
+ background: #BDBDBD;
+ display: none;
+ border-left: 5px solid #4288CE;
+ }
+
+ .frame-code.active {
+ display: block;
+ }
+
+ .frame-code .frame-file {
+ background: #C6C6C6;
+ color: #525252;
+ text-shadow: 0 1px 0 #E7E7E7;
+ padding: 10px 10px 5px 10px;
+
+ border-top-right-radius: 6px;
+ border-top-left-radius: 6px;
+
+ border: 1px solid rgba(0, 0, 0, .1);
+ border-bottom: none;
+ box-shadow: inset 0 1px 0 #DADADA;
+ }
+
+ .code-block {
+ padding: 10px;
+ margin: 0;
+ box-shadow: inset 0 0 6px rgba(0, 0, 0, .3);
+ }
+
+ .linenums {
+ margin: 0;
+ margin-left: 10px;
+ }
+
+ .frame-comments {
+ box-shadow: inset 0 0 6px rgba(0, 0, 0, .3);
+ border: 1px solid rgba(0, 0, 0, .2);
+ border-top: none;
+
+ border-bottom-right-radius: 6px;
+ border-bottom-left-radius: 6px;
+
+ padding: 5px;
+ font-size: 12px;
+ background: #404040;
+ }
+
+ .frame-comments.empty {
+ padding: 8px 15px;
+ }
+
+ .frame-comments.empty:before {
+ content: "No comments for this stack frame.";
+ font-style: italic;
+ color: #828282;
+ }
+
+ .frame-comment {
+ padding: 10px;
+ color: #D2D2D2;
+ }
+ .frame-comment a {
+ color: #BEE9EA;
+ font-weight: bold;
+ text-decoration: none;
+ }
+ .frame-comment a:hover {
+ color: #4bb1b1;
+ }
+
+ .frame-comment:not(:last-child) {
+ border-bottom: 1px dotted rgba(0, 0, 0, .3);
+ }
+
+ .frame-comment-context {
+ font-size: 10px;
+ font-weight: bold;
+ color: #86D2B6;
+ }
+
+.data-table-container label {
+ font-size: 16px;
+ font-weight: bold;
+ color: #4288CE;
+ margin: 10px 0;
+ padding: 10px 0;
+
+ display: block;
+ margin-bottom: 5px;
+ padding-bottom: 5px;
+ border-bottom: 1px dotted rgba(0, 0, 0, .2);
+}
+ .data-table {
+ width: 100%;
+ margin: 10px 0;
+ }
+
+ .data-table tbody {
+ font: 13px consolas, monospace;
+ }
+
+ .data-table thead {
+ display: none;
+ }
+
+ .data-table tr {
+ padding: 5px 0;
+ }
+
+ .data-table td:first-child {
+ width: 20%;
+ min-width: 130px;
+ overflow: hidden;
+ font-weight: bold;
+ color: #463C54;
+ padding-right: 5px;
+
+ }
+
+ .data-table td:last-child {
+ width: 80%;
+ -ms-word-break: break-all;
+ word-break: break-all;
+ word-break: break-word;
+ -webkit-hyphens: auto;
+ -moz-hyphens: auto;
+ hyphens: auto;
+ }
+
+ .data-table .empty {
+ color: rgba(0, 0, 0, .3);
+ font-style: italic;
+ }
+
+.handler {
+ padding: 10px;
+ font: 14px monospace;
+}
+
+.handler.active {
+ color: #BBBBBB;
+ background: #989898;
+ font-weight: bold;
+}
+
+/* prettify code style
+Uses the Doxy theme as a base */
+pre .str, code .str { color: #BCD42A; } /* string */
+pre .kwd, code .kwd { color: #4bb1b1; font-weight: bold; } /* keyword*/
+pre .com, code .com { color: #888; font-weight: bold; } /* comment */
+pre .typ, code .typ { color: #ef7c61; } /* type */
+pre .lit, code .lit { color: #BCD42A; } /* literal */
+pre .pun, code .pun { color: #fff; font-weight: bold; } /* punctuation */
+pre .pln, code .pln { color: #e9e4e5; } /* plaintext */
+pre .tag, code .tag { color: #4bb1b1; } /* html/xml tag */
+pre .htm, code .htm { color: #dda0dd; } /* html tag */
+pre .xsl, code .xsl { color: #d0a0d0; } /* xslt tag */
+pre .atn, code .atn { color: #ef7c61; font-weight: normal;} /* html/xml attribute name */
+pre .atv, code .atv { color: #bcd42a; } /* html/xml attribute value */
+pre .dec, code .dec { color: #606; } /* decimal */
+pre.prettyprint, code.prettyprint {
+ font-family: 'Source Code Pro', Monaco, Consolas, "Lucida Console", monospace;;
+ background: #333;
+ color: #e9e4e5;
+}
+ pre.prettyprint {
+ white-space: pre-wrap;
+ }
+
+ pre.prettyprint a, code.prettyprint a {
+ text-decoration:none;
+ }
+
+ .linenums li {
+ color: #A5A5A5;
+ }
+
+ .linenums li.current{
+ background: rgba(255, 100, 100, .07);
+ padding-top: 4px;
+ padding-left: 1px;
+ }
+ .linenums li.current.active {
+ background: rgba(255, 100, 100, .17);
+ }
+
+#plain-exception {
+ display: none;
+}
+
+#copy-button {
+ display: none;
+ float: right;
+ cursor: pointer;
+ border: 0;
+}
+
+.clipboard {
+ width: 29px;
+ height: 28px;
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAcCAYAAACdz7SqAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUUByMD0ZSoGQAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAACAklEQVRIx72Wv0sbYRjHP29y1VoXxR9UskjpXTaHoBUcpGgKkS5OZ1Ec/QeKOIhalEghoOCqQsGhFAWbISKYyaFDS1BKKW0TCYrQSdElXSReh9bkksu9iZdLbnrvvee9z/N83+d9nldo2gvjzd5Hxp4246W6J5tJszsxwvxPIbXzwBQDLgABvM1P6JsAwzCkdopl5vqIuWev2K4QpH/4QjjQci/nPCVny3iaNzMcrVUsC1sChFMpwtTu8dTqx7J9dR3a2BngUb0j7Xr+jtjasBR8f+jpNqqqoqoqmqblxjOJq/8GTfhCK8TWhmmykdhRpEIIhBCWMcD51wQXN3KwY3nvYGYgQPbXOMHJKOlMA77QCvsbugXsOFLZ+5+jGULBtyQuFB4PzlrAVSWSGWaptpdbjAcniaZv6RhcIL6VByvVZqsQouBMdutJkrrVrr1/gdjqN4Ze/3DvyBwcnnF9I7N4gC8YYdqNSHP7uD5G/7pdJRrl/ecIva1t9IRcgpolLk6qQic8eB+6GOkdrDjSf/OiTD91CS4r+jXrMqWkrgvUtuDbeVNTKGzw6SRDto5QBc5Yehlg0WbTc8mwHCeld1u+yZSySySlspTHFmZUeIkrgBYvtvPcyBdXkqWKq5OLmbk/luqVYjPOd3lxLXf/J/P7mJ0oCL/fX1Yfs4RO5CxW8C97dLBw2Q3fUwAAAABJRU5ErkJggg==');
+ background-repeat: no-repeat;
+}
\ No newline at end of file
diff --git a/library/Whoops/Resources/js/whoops.base.js b/library/Whoops/Resources/js/whoops.base.js
new file mode 100644
index 0000000..f8197b4
--- /dev/null
+++ b/library/Whoops/Resources/js/whoops.base.js
@@ -0,0 +1,59 @@
+Zepto(function($) {
+ prettyPrint();
+
+ var $frameContainer = $('.frames-container');
+ var $container = $('.details-container');
+ var $activeLine = $frameContainer.find('.frame.active');
+ var $activeFrame = $container.find('.frame-code.active');
+ var headerHeight = $('header').height();
+
+ var highlightCurrentLine = function() {
+ // Highlight the active and neighboring lines for this frame:
+ var activeLineNumber = +($activeLine.find('.frame-line').text());
+ var $lines = $activeFrame.find('.linenums li');
+ var firstLine = +($lines.first().val());
+
+ $($lines[activeLineNumber - firstLine - 1]).addClass('current');
+ $($lines[activeLineNumber - firstLine]).addClass('current active');
+ $($lines[activeLineNumber - firstLine + 1]).addClass('current');
+ }
+
+ // Highlight the active for the first frame:
+ highlightCurrentLine();
+
+ $frameContainer.on('click', '.frame', function() {
+ var $this = $(this);
+ var id = /frame\-line\-([\d]*)/.exec($this.attr('id'))[1];
+ var $codeFrame = $('#frame-code-' + id);
+
+ if ($codeFrame) {
+ $activeLine.removeClass('active');
+ $activeFrame.removeClass('active');
+
+ $this.addClass('active');
+ $codeFrame.addClass('active');
+
+ $activeLine = $this;
+ $activeFrame = $codeFrame;
+
+ highlightCurrentLine();
+
+ $container.scrollTop(headerHeight);
+ }
+ });
+
+ if (typeof ZeroClipboard !== "undefined") {
+ ZeroClipboard.config({
+ moviePath: '//ajax.cdnjs.com/ajax/libs/zeroclipboard/1.3.5/ZeroClipboard.swf',
+ });
+
+ var clipEl = document.getElementById("copy-button");
+ var clip = new ZeroClipboard( clipEl );
+ var $clipEl = $(clipEl);
+
+ // show the button, when swf could be loaded successfully from CDN
+ clip.on("load", function() {
+ $clipEl.show();
+ });
+ }
+});
diff --git a/library/Whoops/Resources/js/zepto.min.js b/library/Whoops/Resources/js/zepto.min.js
new file mode 100644
index 0000000..0b2f97a
--- /dev/null
+++ b/library/Whoops/Resources/js/zepto.min.js
@@ -0,0 +1,2 @@
+/* Zepto v1.1.3 - zepto event ajax form ie - zeptojs.com/license */
+var Zepto=function(){function L(t){return null==t?String(t):j[T.call(t)]||"object"}function Z(t){return"function"==L(t)}function $(t){return null!=t&&t==t.window}function _(t){return null!=t&&t.nodeType==t.DOCUMENT_NODE}function D(t){return"object"==L(t)}function R(t){return D(t)&&!$(t)&&Object.getPrototypeOf(t)==Object.prototype}function M(t){return"number"==typeof t.length}function k(t){return s.call(t,function(t){return null!=t})}function z(t){return t.length>0?n.fn.concat.apply([],t):t}function F(t){return t.replace(/::/g,"/").replace(/([A-Z]+)([A-Z][a-z])/g,"$1_$2").replace(/([a-z\d])([A-Z])/g,"$1_$2").replace(/_/g,"-").toLowerCase()}function q(t){return t in f?f[t]:f[t]=new RegExp("(^|\\s)"+t+"(\\s|$)")}function H(t,e){return"number"!=typeof e||c[F(t)]?e:e+"px"}function I(t){var e,n;return u[t]||(e=a.createElement(t),a.body.appendChild(e),n=getComputedStyle(e,"").getPropertyValue("display"),e.parentNode.removeChild(e),"none"==n&&(n="block"),u[t]=n),u[t]}function V(t){return"children"in t?o.call(t.children):n.map(t.childNodes,function(t){return 1==t.nodeType?t:void 0})}function U(n,i,r){for(e in i)r&&(R(i[e])||A(i[e]))?(R(i[e])&&!R(n[e])&&(n[e]={}),A(i[e])&&!A(n[e])&&(n[e]=[]),U(n[e],i[e],r)):i[e]!==t&&(n[e]=i[e])}function B(t,e){return null==e?n(t):n(t).filter(e)}function J(t,e,n,i){return Z(e)?e.call(t,n,i):e}function X(t,e,n){null==n?t.removeAttribute(e):t.setAttribute(e,n)}function W(e,n){var i=e.className,r=i&&i.baseVal!==t;return n===t?r?i.baseVal:i:void(r?i.baseVal=n:e.className=n)}function Y(t){var e;try{return t?"true"==t||("false"==t?!1:"null"==t?null:/^0/.test(t)||isNaN(e=Number(t))?/^[\[\{]/.test(t)?n.parseJSON(t):t:e):t}catch(i){return t}}function G(t,e){e(t);for(var n in t.childNodes)G(t.childNodes[n],e)}var t,e,n,i,C,N,r=[],o=r.slice,s=r.filter,a=window.document,u={},f={},c={"column-count":1,columns:1,"font-weight":1,"line-height":1,opacity:1,"z-index":1,zoom:1},l=/^\s*<(\w+|!)[^>]*>/,h=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,p=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,d=/^(?:body|html)$/i,m=/([A-Z])/g,g=["val","css","html","text","data","width","height","offset"],v=["after","prepend","before","append"],y=a.createElement("table"),x=a.createElement("tr"),b={tr:a.createElement("tbody"),tbody:y,thead:y,tfoot:y,td:x,th:x,"*":a.createElement("div")},w=/complete|loaded|interactive/,E=/^[\w-]*$/,j={},T=j.toString,S={},O=a.createElement("div"),P={tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},A=Array.isArray||function(t){return t instanceof Array};return S.matches=function(t,e){if(!e||!t||1!==t.nodeType)return!1;var n=t.webkitMatchesSelector||t.mozMatchesSelector||t.oMatchesSelector||t.matchesSelector;if(n)return n.call(t,e);var i,r=t.parentNode,o=!r;return o&&(r=O).appendChild(t),i=~S.qsa(r,e).indexOf(t),o&&O.removeChild(t),i},C=function(t){return t.replace(/-+(.)?/g,function(t,e){return e?e.toUpperCase():""})},N=function(t){return s.call(t,function(e,n){return t.indexOf(e)==n})},S.fragment=function(e,i,r){var s,u,f;return h.test(e)&&(s=n(a.createElement(RegExp.$1))),s||(e.replace&&(e=e.replace(p,"<$1>$2>")),i===t&&(i=l.test(e)&&RegExp.$1),i in b||(i="*"),f=b[i],f.innerHTML=""+e,s=n.each(o.call(f.childNodes),function(){f.removeChild(this)})),R(r)&&(u=n(s),n.each(r,function(t,e){g.indexOf(t)>-1?u[t](e):u.attr(t,e)})),s},S.Z=function(t,e){return t=t||[],t.__proto__=n.fn,t.selector=e||"",t},S.isZ=function(t){return t instanceof S.Z},S.init=function(e,i){var r;if(!e)return S.Z();if("string"==typeof e)if(e=e.trim(),"<"==e[0]&&l.test(e))r=S.fragment(e,RegExp.$1,i),e=null;else{if(i!==t)return n(i).find(e);r=S.qsa(a,e)}else{if(Z(e))return n(a).ready(e);if(S.isZ(e))return e;if(A(e))r=k(e);else if(D(e))r=[e],e=null;else if(l.test(e))r=S.fragment(e.trim(),RegExp.$1,i),e=null;else{if(i!==t)return n(i).find(e);r=S.qsa(a,e)}}return S.Z(r,e)},n=function(t,e){return S.init(t,e)},n.extend=function(t){var e,n=o.call(arguments,1);return"boolean"==typeof t&&(e=t,t=n.shift()),n.forEach(function(n){U(t,n,e)}),t},S.qsa=function(t,e){var n,i="#"==e[0],r=!i&&"."==e[0],s=i||r?e.slice(1):e,a=E.test(s);return _(t)&&a&&i?(n=t.getElementById(s))?[n]:[]:1!==t.nodeType&&9!==t.nodeType?[]:o.call(a&&!i?r?t.getElementsByClassName(s):t.getElementsByTagName(e):t.querySelectorAll(e))},n.contains=function(t,e){return t!==e&&t.contains(e)},n.type=L,n.isFunction=Z,n.isWindow=$,n.isArray=A,n.isPlainObject=R,n.isEmptyObject=function(t){var e;for(e in t)return!1;return!0},n.inArray=function(t,e,n){return r.indexOf.call(e,t,n)},n.camelCase=C,n.trim=function(t){return null==t?"":String.prototype.trim.call(t)},n.uuid=0,n.support={},n.expr={},n.map=function(t,e){var n,r,o,i=[];if(M(t))for(r=0;r=0?e:e+this.length]},toArray:function(){return this.get()},size:function(){return this.length},remove:function(){return this.each(function(){null!=this.parentNode&&this.parentNode.removeChild(this)})},each:function(t){return r.every.call(this,function(e,n){return t.call(e,n,e)!==!1}),this},filter:function(t){return Z(t)?this.not(this.not(t)):n(s.call(this,function(e){return S.matches(e,t)}))},add:function(t,e){return n(N(this.concat(n(t,e))))},is:function(t){return this.length>0&&S.matches(this[0],t)},not:function(e){var i=[];if(Z(e)&&e.call!==t)this.each(function(t){e.call(this,t)||i.push(this)});else{var r="string"==typeof e?this.filter(e):M(e)&&Z(e.item)?o.call(e):n(e);this.forEach(function(t){r.indexOf(t)<0&&i.push(t)})}return n(i)},has:function(t){return this.filter(function(){return D(t)?n.contains(this,t):n(this).find(t).size()})},eq:function(t){return-1===t?this.slice(t):this.slice(t,+t+1)},first:function(){var t=this[0];return t&&!D(t)?t:n(t)},last:function(){var t=this[this.length-1];return t&&!D(t)?t:n(t)},find:function(t){var e,i=this;return e="object"==typeof t?n(t).filter(function(){var t=this;return r.some.call(i,function(e){return n.contains(e,t)})}):1==this.length?n(S.qsa(this[0],t)):this.map(function(){return S.qsa(this,t)})},closest:function(t,e){var i=this[0],r=!1;for("object"==typeof t&&(r=n(t));i&&!(r?r.indexOf(i)>=0:S.matches(i,t));)i=i!==e&&!_(i)&&i.parentNode;return n(i)},parents:function(t){for(var e=[],i=this;i.length>0;)i=n.map(i,function(t){return(t=t.parentNode)&&!_(t)&&e.indexOf(t)<0?(e.push(t),t):void 0});return B(e,t)},parent:function(t){return B(N(this.pluck("parentNode")),t)},children:function(t){return B(this.map(function(){return V(this)}),t)},contents:function(){return this.map(function(){return o.call(this.childNodes)})},siblings:function(t){return B(this.map(function(t,e){return s.call(V(e.parentNode),function(t){return t!==e})}),t)},empty:function(){return this.each(function(){this.innerHTML=""})},pluck:function(t){return n.map(this,function(e){return e[t]})},show:function(){return this.each(function(){"none"==this.style.display&&(this.style.display=""),"none"==getComputedStyle(this,"").getPropertyValue("display")&&(this.style.display=I(this.nodeName))})},replaceWith:function(t){return this.before(t).remove()},wrap:function(t){var e=Z(t);if(this[0]&&!e)var i=n(t).get(0),r=i.parentNode||this.length>1;return this.each(function(o){n(this).wrapAll(e?t.call(this,o):r?i.cloneNode(!0):i)})},wrapAll:function(t){if(this[0]){n(this[0]).before(t=n(t));for(var e;(e=t.children()).length;)t=e.first();n(t).append(this)}return this},wrapInner:function(t){var e=Z(t);return this.each(function(i){var r=n(this),o=r.contents(),s=e?t.call(this,i):t;o.length?o.wrapAll(s):r.append(s)})},unwrap:function(){return this.parent().each(function(){n(this).replaceWith(n(this).children())}),this},clone:function(){return this.map(function(){return this.cloneNode(!0)})},hide:function(){return this.css("display","none")},toggle:function(e){return this.each(function(){var i=n(this);(e===t?"none"==i.css("display"):e)?i.show():i.hide()})},prev:function(t){return n(this.pluck("previousElementSibling")).filter(t||"*")},next:function(t){return n(this.pluck("nextElementSibling")).filter(t||"*")},html:function(t){return 0===arguments.length?this.length>0?this[0].innerHTML:null:this.each(function(e){var i=this.innerHTML;n(this).empty().append(J(this,t,e,i))})},text:function(e){return 0===arguments.length?this.length>0?this[0].textContent:null:this.each(function(){this.textContent=e===t?"":""+e})},attr:function(n,i){var r;return"string"==typeof n&&i===t?0==this.length||1!==this[0].nodeType?t:"value"==n&&"INPUT"==this[0].nodeName?this.val():!(r=this[0].getAttribute(n))&&n in this[0]?this[0][n]:r:this.each(function(t){if(1===this.nodeType)if(D(n))for(e in n)X(this,e,n[e]);else X(this,n,J(this,i,t,this.getAttribute(n)))})},removeAttr:function(t){return this.each(function(){1===this.nodeType&&X(this,t)})},prop:function(e,n){return e=P[e]||e,n===t?this[0]&&this[0][e]:this.each(function(t){this[e]=J(this,n,t,this[e])})},data:function(e,n){var i=this.attr("data-"+e.replace(m,"-$1").toLowerCase(),n);return null!==i?Y(i):t},val:function(t){return 0===arguments.length?this[0]&&(this[0].multiple?n(this[0]).find("option").filter(function(){return this.selected}).pluck("value"):this[0].value):this.each(function(e){this.value=J(this,t,e,this.value)})},offset:function(t){if(t)return this.each(function(e){var i=n(this),r=J(this,t,e,i.offset()),o=i.offsetParent().offset(),s={top:r.top-o.top,left:r.left-o.left};"static"==i.css("position")&&(s.position="relative"),i.css(s)});if(0==this.length)return null;var e=this[0].getBoundingClientRect();return{left:e.left+window.pageXOffset,top:e.top+window.pageYOffset,width:Math.round(e.width),height:Math.round(e.height)}},css:function(t,i){if(arguments.length<2){var r=this[0],o=getComputedStyle(r,"");if(!r)return;if("string"==typeof t)return r.style[C(t)]||o.getPropertyValue(t);if(A(t)){var s={};return n.each(A(t)?t:[t],function(t,e){s[e]=r.style[C(e)]||o.getPropertyValue(e)}),s}}var a="";if("string"==L(t))i||0===i?a=F(t)+":"+H(t,i):this.each(function(){this.style.removeProperty(F(t))});else for(e in t)t[e]||0===t[e]?a+=F(e)+":"+H(e,t[e])+";":this.each(function(){this.style.removeProperty(F(e))});return this.each(function(){this.style.cssText+=";"+a})},index:function(t){return t?this.indexOf(n(t)[0]):this.parent().children().indexOf(this[0])},hasClass:function(t){return t?r.some.call(this,function(t){return this.test(W(t))},q(t)):!1},addClass:function(t){return t?this.each(function(e){i=[];var r=W(this),o=J(this,t,e,r);o.split(/\s+/g).forEach(function(t){n(this).hasClass(t)||i.push(t)},this),i.length&&W(this,r+(r?" ":"")+i.join(" "))}):this},removeClass:function(e){return this.each(function(n){return e===t?W(this,""):(i=W(this),J(this,e,n,i).split(/\s+/g).forEach(function(t){i=i.replace(q(t)," ")}),void W(this,i.trim()))})},toggleClass:function(e,i){return e?this.each(function(r){var o=n(this),s=J(this,e,r,W(this));s.split(/\s+/g).forEach(function(e){(i===t?!o.hasClass(e):i)?o.addClass(e):o.removeClass(e)})}):this},scrollTop:function(e){if(this.length){var n="scrollTop"in this[0];return e===t?n?this[0].scrollTop:this[0].pageYOffset:this.each(n?function(){this.scrollTop=e}:function(){this.scrollTo(this.scrollX,e)})}},scrollLeft:function(e){if(this.length){var n="scrollLeft"in this[0];return e===t?n?this[0].scrollLeft:this[0].pageXOffset:this.each(n?function(){this.scrollLeft=e}:function(){this.scrollTo(e,this.scrollY)})}},position:function(){if(this.length){var t=this[0],e=this.offsetParent(),i=this.offset(),r=d.test(e[0].nodeName)?{top:0,left:0}:e.offset();return i.top-=parseFloat(n(t).css("margin-top"))||0,i.left-=parseFloat(n(t).css("margin-left"))||0,r.top+=parseFloat(n(e[0]).css("border-top-width"))||0,r.left+=parseFloat(n(e[0]).css("border-left-width"))||0,{top:i.top-r.top,left:i.left-r.left}}},offsetParent:function(){return this.map(function(){for(var t=this.offsetParent||a.body;t&&!d.test(t.nodeName)&&"static"==n(t).css("position");)t=t.offsetParent;return t})}},n.fn.detach=n.fn.remove,["width","height"].forEach(function(e){var i=e.replace(/./,function(t){return t[0].toUpperCase()});n.fn[e]=function(r){var o,s=this[0];return r===t?$(s)?s["inner"+i]:_(s)?s.documentElement["scroll"+i]:(o=this.offset())&&o[e]:this.each(function(t){s=n(this),s.css(e,J(this,r,t,s[e]()))})}}),v.forEach(function(t,e){var i=e%2;n.fn[t]=function(){var t,o,r=n.map(arguments,function(e){return t=L(e),"object"==t||"array"==t||null==e?e:S.fragment(e)}),s=this.length>1;return r.length<1?this:this.each(function(t,a){o=i?a:a.parentNode,a=0==e?a.nextSibling:1==e?a.firstChild:2==e?a:null,r.forEach(function(t){if(s)t=t.cloneNode(!0);else if(!o)return n(t).remove();G(o.insertBefore(t,a),function(t){null==t.nodeName||"SCRIPT"!==t.nodeName.toUpperCase()||t.type&&"text/javascript"!==t.type||t.src||window.eval.call(window,t.innerHTML)})})})},n.fn[i?t+"To":"insert"+(e?"Before":"After")]=function(e){return n(e)[t](this),this}}),S.Z.prototype=n.fn,S.uniq=N,S.deserializeValue=Y,n.zepto=S,n}();window.Zepto=Zepto,void 0===window.$&&(window.$=Zepto),function(t){function l(t){return t._zid||(t._zid=e++)}function h(t,e,n,i){if(e=p(e),e.ns)var r=d(e.ns);return(s[l(t)]||[]).filter(function(t){return!(!t||e.e&&t.e!=e.e||e.ns&&!r.test(t.ns)||n&&l(t.fn)!==l(n)||i&&t.sel!=i)})}function p(t){var e=(""+t).split(".");return{e:e[0],ns:e.slice(1).sort().join(" ")}}function d(t){return new RegExp("(?:^| )"+t.replace(" "," .* ?")+"(?: |$)")}function m(t,e){return t.del&&!u&&t.e in f||!!e}function g(t){return c[t]||u&&f[t]||t}function v(e,i,r,o,a,u,f){var h=l(e),d=s[h]||(s[h]=[]);i.split(/\s/).forEach(function(i){if("ready"==i)return t(document).ready(r);var s=p(i);s.fn=r,s.sel=a,s.e in c&&(r=function(e){var n=e.relatedTarget;return!n||n!==this&&!t.contains(this,n)?s.fn.apply(this,arguments):void 0}),s.del=u;var l=u||r;s.proxy=function(t){if(t=j(t),!t.isImmediatePropagationStopped()){t.data=o;var i=l.apply(e,t._args==n?[t]:[t].concat(t._args));return i===!1&&(t.preventDefault(),t.stopPropagation()),i}},s.i=d.length,d.push(s),"addEventListener"in e&&e.addEventListener(g(s.e),s.proxy,m(s,f))})}function y(t,e,n,i,r){var o=l(t);(e||"").split(/\s/).forEach(function(e){h(t,e,n,i).forEach(function(e){delete s[o][e.i],"removeEventListener"in t&&t.removeEventListener(g(e.e),e.proxy,m(e,r))})})}function j(e,i){return(i||!e.isDefaultPrevented)&&(i||(i=e),t.each(E,function(t,n){var r=i[t];e[t]=function(){return this[n]=x,r&&r.apply(i,arguments)},e[n]=b}),(i.defaultPrevented!==n?i.defaultPrevented:"returnValue"in i?i.returnValue===!1:i.getPreventDefault&&i.getPreventDefault())&&(e.isDefaultPrevented=x)),e}function T(t){var e,i={originalEvent:t};for(e in t)w.test(e)||t[e]===n||(i[e]=t[e]);return j(i,t)}var n,e=1,i=Array.prototype.slice,r=t.isFunction,o=function(t){return"string"==typeof t},s={},a={},u="onfocusin"in window,f={focus:"focusin",blur:"focusout"},c={mouseenter:"mouseover",mouseleave:"mouseout"};a.click=a.mousedown=a.mouseup=a.mousemove="MouseEvents",t.event={add:v,remove:y},t.proxy=function(e,n){if(r(e)){var i=function(){return e.apply(n,arguments)};return i._zid=l(e),i}if(o(n))return t.proxy(e[n],e);throw new TypeError("expected function")},t.fn.bind=function(t,e,n){return this.on(t,e,n)},t.fn.unbind=function(t,e){return this.off(t,e)},t.fn.one=function(t,e,n,i){return this.on(t,e,n,i,1)};var x=function(){return!0},b=function(){return!1},w=/^([A-Z]|returnValue$|layer[XY]$)/,E={preventDefault:"isDefaultPrevented",stopImmediatePropagation:"isImmediatePropagationStopped",stopPropagation:"isPropagationStopped"};t.fn.delegate=function(t,e,n){return this.on(e,t,n)},t.fn.undelegate=function(t,e,n){return this.off(e,t,n)},t.fn.live=function(e,n){return t(document.body).delegate(this.selector,e,n),this},t.fn.die=function(e,n){return t(document.body).undelegate(this.selector,e,n),this},t.fn.on=function(e,s,a,u,f){var c,l,h=this;return e&&!o(e)?(t.each(e,function(t,e){h.on(t,s,a,e,f)}),h):(o(s)||r(u)||u===!1||(u=a,a=s,s=n),(r(a)||a===!1)&&(u=a,a=n),u===!1&&(u=b),h.each(function(n,r){f&&(c=function(t){return y(r,t.type,u),u.apply(this,arguments)}),s&&(l=function(e){var n,o=t(e.target).closest(s,r).get(0);return o&&o!==r?(n=t.extend(T(e),{currentTarget:o,liveFired:r}),(c||u).apply(o,[n].concat(i.call(arguments,1)))):void 0}),v(r,e,u,a,s,l||c)}))},t.fn.off=function(e,i,s){var a=this;return e&&!o(e)?(t.each(e,function(t,e){a.off(t,i,e)}),a):(o(i)||r(s)||s===!1||(s=i,i=n),s===!1&&(s=b),a.each(function(){y(this,e,s,i)}))},t.fn.trigger=function(e,n){return e=o(e)||t.isPlainObject(e)?t.Event(e):j(e),e._args=n,this.each(function(){"dispatchEvent"in this?this.dispatchEvent(e):t(this).triggerHandler(e,n)})},t.fn.triggerHandler=function(e,n){var i,r;return this.each(function(s,a){i=T(o(e)?t.Event(e):e),i._args=n,i.target=a,t.each(h(a,e.type||e),function(t,e){return r=e.proxy(i),i.isImmediatePropagationStopped()?!1:void 0})}),r},"focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select keydown keypress keyup error".split(" ").forEach(function(e){t.fn[e]=function(t){return t?this.bind(e,t):this.trigger(e)}}),["focus","blur"].forEach(function(e){t.fn[e]=function(t){return t?this.bind(e,t):this.each(function(){try{this[e]()}catch(t){}}),this}}),t.Event=function(t,e){o(t)||(e=t,t=e.type);var n=document.createEvent(a[t]||"Events"),i=!0;if(e)for(var r in e)"bubbles"==r?i=!!e[r]:n[r]=e[r];return n.initEvent(t,i,!0),j(n)}}(Zepto),function(t){function l(e,n,i){var r=t.Event(n);return t(e).trigger(r,i),!r.isDefaultPrevented()}function h(t,e,i,r){return t.global?l(e||n,i,r):void 0}function p(e){e.global&&0===t.active++&&h(e,null,"ajaxStart")}function d(e){e.global&&!--t.active&&h(e,null,"ajaxStop")}function m(t,e){var n=e.context;return e.beforeSend.call(n,t,e)===!1||h(e,n,"ajaxBeforeSend",[t,e])===!1?!1:void h(e,n,"ajaxSend",[t,e])}function g(t,e,n,i){var r=n.context,o="success";n.success.call(r,t,o,e),i&&i.resolveWith(r,[t,o,e]),h(n,r,"ajaxSuccess",[e,n,t]),y(o,e,n)}function v(t,e,n,i,r){var o=i.context;i.error.call(o,n,e,t),r&&r.rejectWith(o,[n,e,t]),h(i,o,"ajaxError",[n,i,t||e]),y(e,n,i)}function y(t,e,n){var i=n.context;n.complete.call(i,e,t),h(n,i,"ajaxComplete",[e,n]),d(n)}function x(){}function b(t){return t&&(t=t.split(";",2)[0]),t&&(t==f?"html":t==u?"json":s.test(t)?"script":a.test(t)&&"xml")||"text"}function w(t,e){return""==e?t:(t+"&"+e).replace(/[&?]{1,2}/,"?")}function E(e){e.processData&&e.data&&"string"!=t.type(e.data)&&(e.data=t.param(e.data,e.traditional)),!e.data||e.type&&"GET"!=e.type.toUpperCase()||(e.url=w(e.url,e.data),e.data=void 0)}function j(e,n,i,r){return t.isFunction(n)&&(r=i,i=n,n=void 0),t.isFunction(i)||(r=i,i=void 0),{url:e,data:n,success:i,dataType:r}}function S(e,n,i,r){var o,s=t.isArray(n),a=t.isPlainObject(n);t.each(n,function(n,u){o=t.type(u),r&&(n=i?r:r+"["+(a||"object"==o||"array"==o?n:"")+"]"),!r&&s?e.add(u.name,u.value):"array"==o||!i&&"object"==o?S(e,u,i,n):e.add(n,u)})}var i,r,e=0,n=window.document,o=/
+
+
+
+